Resume replay can surface a stale assistant final message after restart from archived/removed subagent history

Open 💬 3 comments Opened Apr 11, 2026 by nos1609
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

codex-cli 0.120.0

What subscription do you have?

Pro

Which model were you using?

gpt-5.4

What platform is your computer?

Linux 6.12.0-124.47.1.el10_1.x86_64 x86_64 unknown

What terminal emulator and version are you using (if applicable)?

VSCode

What issue are you seeing?

After reopening Codex, an old assistant message kept reappearing as the visible final message for the current thread, even after I removed the obvious stale text from local session storage.

I verified that:

  • the exact phrase was removed from ~/.codex
  • the child subagent thread was archived in ~/.codex/state_5.sqlite
  • the parent->child spawn edge was removed
  • the child rollout JSONL was moved out of ~/.codex/sessions/
  • the parent rollout JSONL was edited to remove the exact stale phrase

Despite that, after restarting the window, Codex still rendered that old assistant final message.

This looks like replay/resume is reconstructing the stale final message from persisted history sources rather than only from the latest valid turn state.

What steps can reproduce the bug?

Thread id involved:

  • parent thread: 019d6838-2c9c-76c3-a02d-96a0cde9a4b7
  • child subagent thread: 019d7def-2e30-7970-bf17-012e7ec95c90

What I observed locally suggests this repro shape:

  1. Start a main thread.
  2. Spawn one or more subagents from it.
  3. Let one subagent produce a visible assistant final message.
  4. Continue working in the parent thread so newer messages exist.
  5. Restart/reopen Codex and resume the parent thread.
  6. Observe that an older assistant final message can reappear as the visible final message for the parent thread.

I also tested local cleanup after the fact:

  1. Archive the child thread in ~/.codex/state_5.sqlite.
  2. Remove the corresponding row from thread_spawn_edges.
  3. Move the child rollout JSONL out of ~/.codex/sessions/.
  4. Remove the exact stale phrase from the parent rollout JSONL.
  5. Restart the client.

Actual result:

  • the stale assistant final message can still reappear.

Relevant local files during investigation:

  • parent rollout:

~/.codex/sessions/2026/04/07/rollout-2026-04-07T16-53-25-019d6838-2c9c-76c3-a02d-96a0cde9a4b7.jsonl

  • child rollout:

~/.codex/sessions/2026/04/11/rollout-2026-04-11T22-05-20-019d7def-2e30-7970-bf17-012e7ec95c90.jsonl

What is the expected behavior?

On resume/replay, Codex should only show the latest valid assistant state for the resumed thread.

Archived or removed child-thread history should not be able to resurrect an older assistant final message in the parent thread UI.

Replay should not treat stale historical AgentMessage / TurnComplete.last_agent_message / compacted history as a fresh final assistant response for the current thread.

Additional information

I inspected the latest main branch locally and found a likely replay path issue.

Suspect areas:

  • codex-rs/tui/src/chatwidget.rs
  • replayed EventMsg::AgentMessage(...) still records markdown into last_agent_markdown
  • on_task_complete(last_agent_message, from_replay) still emits Notification::AgentTurnComplete even when from_replay == true
  • codex-rs/rollout/src/recorder.rs
  • load_rollout_items() and get_rollout_history() replay EventMsg and Compacted items as-is

That combination seems capable of reviving stale final-message sources on resume.

A plausible fix direction:

  1. Do not emit AgentTurnComplete notifications during replay.
  2. Restrict replay so only the latest valid turn contributes final-message state.
  3. Add a regression test covering:
  • parent thread
  • spawned child thread
  • archived/removed child
  • replay with legacy AgentMessage
  • TurnComplete.last_agent_message
  • compacted history

View original on GitHub ↗

3 Comments

github-actions[bot] contributor · 4 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #17301
  • #16861

Powered by Codex Action

leventov · 15 days ago

I can confirm a narrower, repeatable manifestation of this on codex-cli 0.147.0 on Linux with Ghostty.

The trigger does not require restarting or resuming Codex:

  1. Let a parent thread complete a turn with final message A.
  2. Spawn a subagent whose thread inherits/forks the parent history.
  3. Continue far enough that newer text exists in the parent and/or child.
  4. Use /agent to switch from the parent to the child, or back and forth.
  5. Codex emits a terminal AgentTurnComplete notification containing old message A at thread-selection time. With the OSC 9 backend this appears as a stale desktop popup; forcing the BEL backend merely turns it into a stale tab bell.

There is a second visible symptom in the same setup: a live completion from an unselected background subagent can also produce a desktop notification even though that child is not the foreground /agent thread. That may be a separate notification-policy issue, but it makes the replayed completion especially confusing.

Local rollout evidence for the switch case:

  • The parent actually completed message A several minutes before the switch.
  • At each child thread's creation time, the child rollout begins with one or more inherited historical task_complete events from the parent, before the child's newer content.
  • Selecting each child causes those historical completion events to be replayed and the old completion notification to be emitted.
  • This was reproduced with two different child threads created seconds apart.
  • The same inherited-at-child-creation event shape exists in older local 0.146.0 rollouts, so this does not appear to have been introduced specifically by 0.147.0.

The current source path matches the analysis already in this issue:

  • handle_turn_completed_notification passes replay_kind.is_some() into on_task_complete(..., from_replay).
  • on_task_complete correctly uses !from_replay for several side effects, but the final terminal notification block is still only guarded by queued-follow-up/goal state:

``rust
if !follow_up_started && !active_goal_continuing {
self.notify(Notification::AgentTurnComplete { ... });
}
``

  • That remains true on current main.

Expected invariant: replay -- whether caused by resume or by selecting another agent thread -- may reconstruct transcript/UI state, but must never emit a fresh external completion notification. The notification guard should include !from_replay, with coverage for /agent selection of a child containing inherited historical task_complete events.

As a mitigation, tui.notification_method = "bel" makes the bug less disruptive, but does not prevent the stale completion event.

leventov · 15 days ago

I opened #38192 as the focused report for the narrower terminal-notification defect described above. It has a minimal /agent-switch reproduction, the missing replay guard diagnosis, and a regression-test direction. This issue remains relevant for the broader stale final-message replay behavior.