Skip to content

fix(agent-chat): stop labeling a live run as interrupted after a workspace switch#164

Open
Zeus-Deus wants to merge 1 commit into
mainfrom
codex-queue-interrupt
Open

fix(agent-chat): stop labeling a live run as interrupted after a workspace switch#164
Zeus-Deus wants to merge 1 commit into
mainfrom
codex-queue-interrupt

Conversation

@Zeus-Deus

Copy link
Copy Markdown
Owner

Problem

Switching workspaces unmounts the inactive workspace's chat pane while its run keeps going in the backend. On switch-back, the remount hydrate recomputes the interrupted flag purely from the persisted transcript: a healthy mid-flight run has no turn_completed after its last user turn yet, so lastTurnUnsettled flags it and the pane shows a false "Run interrupted" divider + Continue run chip over a run that is actually alive (it then "continues by itself" once the next live event clears the flag).

Subagent-heavy runs hit this reliably: their item_completed rows grow the disk transcript past the frozen in-memory slice, which is exactly the condition that lets the remount hydrate guard pass.

Fix

Give hydrate an authoritative liveness signal instead of the heuristic alone:

  • AgentProvider::turn_active — new default trait method: true iff a live (non-dead) session is bound to the thread and its active_turn is set. Cheap in-memory check, never touches the subprocess. Implemented for Claude and Codex via the sessions map, and for OpenCode via the SSE event-context flag behind the same absent/dead guards.
  • agent_chat_turn_active — new read-only command exposing it (no auto-resume).
  • The remount hydrate effect probes it after fetching payloads (if the turn settles between the two calls, the settling turn_completed row is already in the payloads — a stale "alive" answer can't mislabel a finished run) and threads { runLive } through hydrateThreadreplayPayloads. runLive: true forces interrupted: false and sets streaming: true so the streaming marker shows instead of the divider. A probe error degrades to false (previous behavior).
  • Dev mock implements the command from its in-flight turn set; docs/features/agent-chat.md documents the new guard and command.

The genuine cases keep working: dead-run detection (child-exit watchdog, app-died-mid-turn hydrate) still surfaces the divider and Continue chip.

Verification

  • npm run verify green: cargo check + cargo test, tsc, vitest (171 files / 2522 tests).
  • New tests: Rust adapter coverage for unknown-thread / in-flight / settled / dead-session on Claude and Codex; hydrate unit tests for the runLive override (incl. overriding a replay-observed child_exited); pane tests for probe-true, probe-rejected fallback, and arg routing.
  • Exercised end to end in the browser against the dev mock: the probe flips false → true → false across a streamed turn; switching away and back mid-run no longer surfaces the divider; a genuine child_exited interrupt still renders the divider + Continue chip.

…space switch

Switching workspaces unmounts the inactive pane while its run keeps
going and keeps persisting rows. On switch-back the remount hydrate
recomputed interrupted purely from the persisted tail, and a healthy
mid-flight run has no turn_completed after its last user turn yet, so
lastTurnUnsettled flagged it and the pane showed a false "Run
interrupted" divider plus a Continue chip until the next live event
cleared them. Subagent-heavy runs hit this reliably because their
item_completed rows grow the disk transcript past the frozen slice,
which is exactly what lets the remount hydrate guard pass.

Give hydrate an authoritative signal instead of the heuristic alone:

- New AgentProvider::turn_active default trait method: true iff a live
  (non-dead) session is bound to the thread and its active_turn is set.
  Implemented for Claude and Codex as a cheap in-memory sessions map
  check, and for OpenCode via the SSE event context turn_active flag
  behind the same absent/dead guards. Never touches the subprocess.
- New agent_chat_turn_active command exposing it, read-only, no
  auto-resume; registered alongside the other agent_chat commands.
- The remount hydrate effect probes it after fetching payloads (if the
  turn settles between the two calls the turn_completed row is already
  in the payloads, so a stale "alive" answer cannot mislabel a finished
  run) and threads hydrateThread(threadId, payloads, { runLive })
  through to replayPayloads. runLive true forces interrupted false,
  suppressing both the unsettled-tail heuristic and a replay-observed
  child_exited, and sets streaming true so the streaming marker shows
  instead of the divider. A probe error degrades to false, keeping the
  previous behavior.
- Dev mock implements the command from its in-flight turn set; docs
  cover the new guard and command.

Verified with npm run verify (cargo check + cargo test, tsc, vitest:
171 files / 2522 tests) plus new coverage: Rust adapter tests for
unknown thread, in-flight, settled, and dead-session cases on Claude
and Codex; hydrate unit tests for the runLive override; pane tests for
probe-true, probe-rejects, and fallback routing. Exercised end to end
in the browser against the dev mock: the probe flips false/true/false
across a streamed turn, switching away and back mid-run no longer
surfaces the divider, and a genuine child_exited interrupt still
renders the divider and Continue chip.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant