Skip to content

fix(sidecar): actually resume the main agent after a background subagent finishes (#891)#904

Open
passion729 wants to merge 1 commit into
dohooo:mainfrom
passion729:passion729/subagent-command-consistency
Open

fix(sidecar): actually resume the main agent after a background subagent finishes (#891)#904
passion729 wants to merge 1 commit into
dohooo:mainfrom
passion729:passion729/subagent-command-consistency

Conversation

@passion729

Copy link
Copy Markdown
Contributor

Summary

Fixes #891 for real. Background Task/Agent subagents now finish and the main agent resumes to report back, instead of the conversation stalling. Also makes a backgrounded run legible in the chat.

#891 was previously marked fixed by #897, but #897 does not resolve it — it wired up the wrong SDK signal (details below).

Root cause

With @anthropic-ai/claude-agent-sdk (tested on 0.3.191; same on the current 0.3.195), when the main agent launches a background subagent and then finishes its message, the SDK emits a normal terminal result (terminal_reason: "completed", stop_reason: "end_turn") while the subagent is still running. The sidecar's event loop treats the first terminal result as end-of-turn → emitter.end() + query.close(), tearing down the still-live subagent ("interrupted, state lost"). The later task_notification then has no live query to land on, so the main agent never resumes.

The subagent's own tool calls actually stream inside the main query's event stream (interleaved task_progress + the subagent's tool_uses), so the query just needs to stay open until the subagent is done.

Why #897 didn't work

#897 keys on terminal_reason === "background_requested". That is a real TerminalReason, but per the SDK's own type doc it means the whole query loop was backgrounded — not a Task run_in_background subagent. In the subagent flow the main turn still ends with terminal_reason: "completed" (confirmed in session logs), so #897's filter is never hit for this issue. Upgrading the SDK (0.3.191 → 0.3.195) doesn't change this — the TerminalReason semantics are the same.

Fix

In sidecar/src/claude/session-manager.ts:

  • Track outstanding background subagents by task_id: a system task_started with task_type === "local_agent" adds; task_notification removes (local_bash / local_workflow are ignored).
  • When a terminal result arrives while any subagent is still outstanding, hold the turn open (record usage, keep the result out of the pipeline, don't end, don't close the input) and keep draining — the subagents finish, emit their task_notifications, and the main agent resumes. End exactly once when a genuinely-terminal result arrives with nothing outstanding.
  • Safety: an idle keep-alive timeout (reset on every event) gracefully closes the input if a subagent wedges; a naturally-ending generator self-heals to the normal end; user Stop/abort is unchanged.

The existing background_requested path is kept (harmless) for the whole-query-backgrounded case it was actually meant for.

UI (same change set)

  • Fold a run's task_* lifecycle into one evolving status card (running / completed / failed / interrupted) under the spawning Task card; relabel a process-restart interruption as "interrupted" rather than a misleading "failed".
  • Render the Task/Agent block as a collapsed-by-default <details> so the thread doesn't jump while a subagent streams.
  • Never leak the internal "Async agent launched…" launch acknowledgment; backgrounded agents read as "running in background".

Tests

  • sidecar/test/background-subagent-hold.test.ts: unit tests for the task-id tracking + hold decision, plus an integration test (controllable stream) proving the turn holds open through completed and ends exactly once after the subagent reports.
  • Pipeline snapshot scenarios (lifecycle fold, interrupt relabel, genuine failure, parallel isolation); frontend component tests (status card, collapsible block, ack suppression).
  • bun run test (frontend/sidecar/rust), bun run lint (biome + clippy), typecheck — all green.

closes #891

Background Task/Agent subagents now finish and report back instead of
stranding the conversation, and their progress renders as a stable,
collapsible block.

Sidecar (auto-resume):
- claude-agent-sdk 0.3.x doesn't emit a `background_requested` pause; it
  reports `end_turn` while subagents are still running, so closing the
  query killed them ("interrupted, state lost"). Track outstanding
  `local_agent` tasks by task_id and hold the turn open on a terminal
  result until they finish, then end exactly once. Idle-timeout backstop
  + generator-end self-heal guard against hangs.

Pipeline + UI (visibility):
- Fold a subagent run's repeated task_* events into one evolving
  MessagePart::Subagent (running/completed/failed/interrupted), folded
  under the spawning Task card; relabel process-restart interrupts as
  "interrupted" rather than "failed".
- Render the Task/Agent block as a collapsed-by-default <details> so the
  thread doesn't jump while a subagent streams; expand to see nested work.
- Never leak the internal "Async agent launched…" launch acknowledgment;
  backgrounded agents read as "running in background".

Tests: sidecar hold/resume unit + integration; pipeline snapshot scenarios
(lifecycle fold, interrupt relabel, genuine failure, parallel isolation);
frontend SubagentStatusCard + collapsible block + ack-suppression.
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

@passion729 is attempting to deploy a commit to the Caspian's Team Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backgrounded subagents never resume the main Claude agent — turn ends prematurely

1 participant