/agent thread switching emits stale completion notifications during replay

Open 💬 1 comment Opened Aug 12, 2026 by leventov

Codex version

codex-cli 0.147.0

Platform

Linux, Ghostty, Codex running in long-lived dtach sessions.

What issue are you seeing?

Selecting a parent or child thread with /agent can emit a fresh terminal AgentTurnComplete notification for a completion that happened earlier and is merely being replayed.

If the terminal notification method is OSC 9, this appears as a stale desktop popup containing an old final message. With the BEL backend, it becomes a stale tab bell instead. Changing the backend therefore mitigates the disruption but does not suppress the erroneous event.

This is distinct from a real completion occurring while the selected thread is in the foreground: the notification is emitted at thread-selection/replay time, even when newer text already exists below the historical completion and the user just selected the thread manually.

Steps to reproduce

  1. Start a parent thread and let a turn finish with final message A.
  2. Spawn a subagent whose thread inherits or forks the parent history.
  3. Continue until newer content exists in the parent and/or child.
  4. Use /agent to switch from the parent to the child, or back and forth.
  5. Observe a terminal completion notification containing old message A at thread-selection time.

This was reproduced with multiple child threads.

Expected behavior

Replay, whether caused by resume or by selecting another agent thread, may reconstruct transcript and UI state but must not emit a fresh external completion notification.

Actual behavior

Historical task_complete events replayed while selecting a thread cause Notification::AgentTurnComplete to be emitted again.

Source-level diagnosis

The current source path passes replay state into task completion handling:

  • handle_turn_completed_notification passes replay_kind.is_some() to on_task_complete(..., from_replay).
  • on_task_complete guards several replay-only side effects with !from_replay.
  • The terminal completion notification is still emitted without that guard:
if !follow_up_started && !active_goal_continuing {
    self.notify(Notification::AgentTurnComplete { ... });
}

A likely minimal fix is to include !from_replay in this notification guard. A regression test should cover /agent selection of a child whose inherited history contains an earlier task_complete.

Local rollout evidence

At child creation time, the child rollout begins with inherited historical task_complete events from the parent before the child's newer content. Selecting the child replays those events and emits the old notification. The same inherited event shape exists in rollouts created by 0.146.0, suggesting that the unguarded replay side effect predates 0.147.0.

Related upstream activity

  • Related issue: #17487 discusses stale final-message state during resume/replay. This report is narrower: an external terminal notification is emitted directly by replay during /agent selection.
  • PR #35693 added background discovery of descendants absent from the current TUI navigation state. That appears to make this latent replay bug much easier to encounter after upgrading to 0.147.0, but the PR is likely an exposure amplifier rather than the root defect.

Possibly separate symptom

Live completion of an unselected background subagent can also produce a terminal notification. That may be a separate notification-policy issue and is not required to reproduce the replay bug described here.

View original on GitHub ↗

1 Comment

jdcodes1 · 9 days ago

Confirmed on main @ 1f41cc5d92, and it's a one-condition bug: the turn-complete handler receives a from_replay flag and uses it to suppress other replay side effects — the plan-implementation prompt and the per-turn transcript flag are both gated on !from_replay (tui/src/chatwidget/turn_runtime.rs#L195-L207) — but the AgentTurnComplete notification a few lines down is only gated on follow-up/goal state, not on from_replay:

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

(#L218-L222). So /agent thread switching, which replays the selected thread's historical events through this handler, re-fires the terminal notification for every replayed completion — OSC 9 popup or BEL depending on backend, exactly as you observed, and backend choice can only change the flavor.

Fix: add !from_replay to that condition. The neighboring gates show the pattern was intended and this call was simply missed; a replay-path test asserting zero notifications during thread-selection replay would pin it.