Skip to content

fix(agent-chat): stale "subagents running" indicators — settle non-terminal subagents in the transcript layer#158

Merged
Zeus-Deus merged 1 commit into
mainfrom
fix/subagent-indicator
Jul 10, 2026
Merged

fix(agent-chat): stale "subagents running" indicators — settle non-terminal subagents in the transcript layer#158
Zeus-Deus merged 1 commit into
mainfrom
fix/subagent-indicator

Conversation

@Zeus-Deus

Copy link
Copy Markdown
Owner

Fixes #153

Problem

The frontend transcript layer had no settlement path for non-terminal subagents: the only running→terminal transition was a terminal subagent_updated snapshot. When the Claude adapter's demux loses track (sidecar restart/resume), the spawning tool's raw parent-scoped tool_result flows through and no terminal snapshot ever arrives — so the SubagentActivityBar / SubagentsCard showed "N subagents running" forever, the thread persisted that way, and reopening the workspace resurrected running spinners on a dead session. Provider rate-limit stops were invisible (runtime_warningconsole.warn only).

Fix (frontend-only — the backend SubagentTracker already handles the sidebar dot)

  1. tool_result derivation — a parent-scoped tool_result whose tool_use_id matches a running subagent's demux id or its parentItemId (spawning call id, now merged from SubagentSnapshot.parent_item_id) settles it (completed/failed). The Workflow tool's own tool_result likewise settles the run + its in-flight phase agents. Because tool_results are persisted, existing stuck threads self-heal on hydrate replay — no migration.
  2. Forced settlesession_state_changed{closed|error} and a new user turn (mirror of SubagentTracker::clear_thread; queued follow-ups behind an active turn are left alone) interrupt running subagents and stop running workflows, even when streaming was already false.
  3. Interrupted-on-hydrate — new view-only interrupted status (SubagentViewStatus, never on the wire): muted-amber non-spinning glyph, excluded from runningSubagentEntries. Post-replay reconciliation force-settles anything still running — a dead session never hydrates a spinner. A pending_approval workflow is preserved (a legitimate resting state, not a spinner).
  4. Revivable assumed settles — every inferred settlement stamps statusAssumed; a later real running snapshot (Claude background task_progress ticks) revives the row, a later real terminal snapshot confirms and clears the flag. Self-healing on live streams and on replay.
  5. runtime_warning visibility — a conservative classifier (runtime-notice.ts) promotes user-facing warnings (rate-limit status: "rejected", enumerated assistant error: …) to an inline amber runtime_notice transcript row; SDK debug noise stays console-only.
  6. Hardening found in review — a status-less snapshot (Rust serde-defaults status to pending; hand-built payloads can omit it) leaked undefined into the view and crashed SubagentRow at render. mergeStatus now treats missing/unknown incoming status as pending.

Verification

  • npm run check clean, npm run test 2415/2415 (new: settlement/revive/classifier/hydrate-reconciliation coverage; dev-fixture tests deliberately updated — the hydrated seed's still-running subagent now correctly settles to interrupted).
  • Visually verified in the dev mock (browser pane): the hydrated seed shows the muted-amber ⊘ interrupted row with no stale docked bar; a live streamSubagents() run still shows the running bar, then settles green end-to-end.
  • Rust untouched (should_persist_event already persists everything needed).

The transcript layer's only running→terminal transition was a terminal
subagent_updated snapshot, so a subagent whose terminal snapshot never
arrived (adapter demux lost track after a sidecar restart/resume) showed
'N subagents running' forever — persisted that way and resurrected on
hydrate. Frontend-only counterpart of the backend SubagentTracker:

- Derive settlement from a parent-scoped tool_result whose tool_use_id
  matches the subagent's demux id or parentItemId (spawn call id) —
  existing stuck threads self-heal on hydrate replay, no migration.
  The Workflow tool's own tool_result likewise settles the run + its
  in-flight phase agents.
- Force-settle on session_state_changed{closed|error} and on a new user
  turn (queued follow-ups behind an active turn are left alone), even
  when streaming was already false.
- New view-only 'interrupted' subagent status (muted amber, non-spinning
  glyph, excluded from runningSubagentEntries) for forced settles and
  the post-hydrate reconciliation — a dead session never hydrates a
  spinner. Every inferred settlement is stamped statusAssumed so a later
  real running snapshot (Claude background task_progress ticks) revives
  the row, and a real terminal snapshot confirms it.
- Surface user-facing runtime_warnings (provider rate-limit rejection,
  enumerated assistant errors) as an inline amber runtime_notice row;
  debug warnings stay console-only.
- Harden mergeStatus against a missing/unknown incoming status (treated
  as pending) — a status-less snapshot used to leak undefined into the
  view and crash SubagentRow at render.

Fixes #153
@Zeus-Deus Zeus-Deus merged commit 0511bd0 into main Jul 10, 2026
4 checks passed
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.

bug(agent-chat): stale "subagents running" indicators — transcript layer never settles non-terminal subagents (incl. after hydrate)

1 participant