[BUG] Nested background subagent never re-woken by its own children — completion notifications route to the top-level session (2.1.224)

Status Open
Reported on v2.1.224
Maintainer reply None cached
Activity 4 comments · opened Aug 15, 2026

Description

A background subagent (a /code-review skill running as a forked background agent) spawned its own background children (8 "finder" agents plus run_in_background Bash commands). When those children finished, their completion notifications were delivered to the top-level session instead of re-waking the parent subagent that spawned them. The parent also was not re-woken when its own background Bash command (sleep 120 used as a wait timer) completed. Result: the parent stalls indefinitely mid-orchestration ("Waiting for the last three finder agents") until the top-level session manually resumes it via SendMessage. After resuming, it stalls again the same way on the next wait, so a multi-stage orchestration needs a manual nudge per stage.

Possibly the same root cause as #68065 (parent-child association / notification routing for nested background agents), but the symptom differs: here the children's notifications carry their own correct task IDs — they are just delivered one level too high (to the main session), while the level that is blocked on them (the parent subagent) never gets woken.

Environment

  • Claude Code 2.1.224 (current stable at time of filing), CLI entrypoint
  • Linux WSL2 (6.6.87.2-microsoft-standard-WSL2), bash
  • Model: claude-fable-5 (main session and subagents)

Sequence observed (from session transcripts, single occurrence, timestamps local)

  1. Main session invokes the code-review skill → runs as forked background subagent (task a9961e3668c6acc7e).
  2. Reviewer spawns ~8 background finder subagents; five finish by ~19:39.
  3. 19:42:28 — reviewer launches Bash {command: "sleep 120", run_in_background: true} to wait for the last three finders, then stops with text "Waiting for the last three finder agents."
  4. The reviewer's stop fires a <task-notification> with status: completed to the main session — even though it is logically mid-task with pending children.
  5. The last finders finish by 19:41–19:44 (transcripts complete). No notification re-wakes the reviewer. The sleep 120 completion (~19:44:29) also does not re-wake it.
  6. The finders' completion notifications arrive at the main session (correct task IDs, wrong recipient level — the main session did not spawn them).
  7. Main session sends the reviewer a SendMessage → reviewer resumes, re-resumes finders/verifiers, and stalls again the same way ("I'll consolidate once their notifications arrive"), with the next round of child notifications again delivered to the main session.
  8. Main session ultimately collected the children's results from their transcript files and finished consolidation itself.

Expected

A background subagent that spawns background children (agents or Bash commands) should be re-invoked when those children complete, the way the main session is. Its "completed" notification to its own parent should not fire while it is deliberately waiting on tracked background children.

Impact

Any skill or subagent that orchestrates its own background fan-out stalls at every join point unless a human (or the top-level session) notices and manually resumes it. Silent — the top-level session just sees a premature "completed" notification with an in-progress-sounding result text.

Notes

  • The task-notification's own note text says "A task-notification fires each time this agent stops with no live background children of its own" — but the reviewer stopped while it did have a live background child (the sleep), and the children finishing later never woke it, so the documented model doesn't match observed behavior at nesting depth ≥ 2.
  • Happy to provide redacted transcript excerpts if useful.

View original on GitHub ↗

3 Comments

kcarriedo · 2 months ago

Ran into the same routing gap while building an orchestration harness where background subagents spawn their own children.

The core problem seems to be that completion notifications carry the spawning session's ID statically at spawn time, so when a second-level subagent finishes, the message routes to whoever the first-level subagent knew as "parent" at creation -- which is the top-level session, not the intermediate subagent that's currently waiting.

A few things that helped us partially work around it while this gets fixed upstream:

  1. Instead of relying on completion notifications to re-wake the parent, poll a shared state file from the parent's main loop. Ugly but deterministic -- the parent checks every N seconds whether its child IDs appear in a "done" registry rather than waiting for a push notification that may land in the wrong place.
  1. Use a flat coordination structure where possible -- have the top-level session be the actual orchestrator and subagents return results rather than spawning grandchildren. This avoids the routing ambiguity entirely at the cost of some parallelism.
  1. If you need the multi-level tree, log child IDs to disk at spawn time and have the top-level session act as a forwarding relay -- it sees all the notifications anyway, checks if any belong to a waiting subagent, and sends a follow-up message to re-wake it.

None of these are good substitutes for correct notification routing. But option 1 in particular was reliable enough for us to ship with while waiting on a fix. Worth filing this as a regression check for the Agent Teams feature too -- the nested completion routing is exactly what makes multi-level coordination fail silently rather than loudly.

BlackWhiteYellow · 15 days ago

Adding a data point from a different platform, plus a concrete request, because this limitation shapes how we can build multi-level agent trees today.

Environment

  • Claude Code 2.1.285, VS Code extension, Windows 10 x64 (build 19045)
  • Model: Opus 5.5 (1M context)
  • CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=4; custom subagent types that have the Agent tool

What we run

A main conversation spawns several "head" subagents (depth 1), and each head spawns its own children (depth 2–4) for a sub-stream of work. The heads write the children's briefs, accept their results, and hand a synthesis up to main.

What the docs say today

From https://code.claude.com/docs/en/sub-agents:

"In an interactive session, only the top-level subagent's summary returns to you and the intermediate output stays out of your main conversation: a subagent that launches background subagents waits for their results before it finishes." "In non-interactive mode and the Agent SDK, the launching subagent doesn't wait, so a nested background subagent that finishes after its launcher has ended reports to your main conversation instead."

We can't tell which branch applies to conversations started by the VS Code extension. The 2.1.281 release notes list the extension together with --input-format stream-json sessions. That suggests the "doesn't wait" branch, but we have not confirmed it.

Why it hurts multi-level trees

Background children are unsafe for us because their results can land in the main conversation instead of the head that owns them. So our heads spawn children in the foreground only. Measured on one of our runs (29.09, 2.1.28x):

  • a head that is blocked on a foreground child gets no turn at all: heads waited 30.8, 35.7 and 47.7 minutes, which is 57–64% of their lifetime;
  • messages to a blocked head arrive only on its next turn. Median delivery was 507 s to heads (max 2,899 s), against 14 s to agents that weren't blocked;
  • 6 of 6 child → head messages were read together with the same child's final result, so they carried no extra value. The heads could not answer the children's forks ("no answer in 5 minutes — I proceed this way");
  • while blocked, a head's prompt cache (5-minute TTL for subagents) expires, so every resume re-writes the head's context. We haven't measured this cost yet.

What would help (any one of these)

  1. Documented, predictable ownership. A background child's completion always goes to its launcher, or to the launcher's nearest live ancestor if the launcher has ended — never straight to the top-level session.
  2. Keep the launcher alive. An option on the Agent call or a setting such as waitForBackgroundChildren. With it, a launching subagent does not finish, and does not send its own completion upward, while it still has tracked background children. This is the interactive behavior quoted above, made available to IDE and SDK sessions.
  3. Re-wake the launcher, the way the main session is re-woken, when one of its background children completes. This is the expectation stated in the original report above.
  4. Say which branch applies to VS Code extension conversations ("interactive" or not), with the version it changed in.
  5. Messages to a parent blocked in a foreground Agent call. Deliver SendMessage at the child's next tool boundary, or let the parent receive messages while it waits. Mid-flight facts from children could then change the parent's decisions. Today they arrive only after the child has already finished.

Minimal scenario

  1. From the main conversation, spawn subagent H of a custom type that has the Agent tool, in the background.
  2. H spawns child C with run_in_background: true. C runs sleep 120 in Bash and then writes a marker file.
  3. Variant A: H ends right after spawning C. Variant B: H keeps polling for the marker with short foreground Bash calls (for example sleep 20 in a loop) until it appears.
  4. Observe where C's completion notification lands in each variant: in H or in the main conversation.

Expected, per the requests above: in variant B, H receives C's completion, and H's own completion reaches main only after that. In variant A, C's completion goes to H's caller, not to an unrelated top-level turn.

Possibly related: #75043.

coygeek · 7 days ago

Data point on 2.1.295 (current latest), macOS 27.0.1, Agent SDK / --input-format stream-json session driven by a third-party host, Opus 5.5. It answers the variant A/B question in the comment above and narrows what is still broken.

Shape: main → 5 background general-purpose subagents (depth 1) → each launches background workers with run_in_background: true (depth 2; each worker's meta.json has parentAgentId set to its depth-1 parent and spawnDepth: 2).

1. A stopped depth-1 subagent is re-woken by its own background Bash task, but not by its own background Agent child. Two depth-1 subagents each started a background Agent worker plus a background Bash watcher, then ended their turn. Each was later resumed by the Bash task's [SYSTEM NOTIFICATION], delivered into its own transcript. Their Agent workers' completion notices went only to the main session (the worker's task-id appears 0 times in the parent transcript and 2 times in the main one), and neither parent ran again after that. After its Bash wake, one parent said "The worker's final report has not arrived yet; I'll resume on its notification." Its worker had finished a minute earlier.

2. Variant B (parent still mid-turn) works; variant A (parent already stopped) loses the notice. Two depth-1 subagents stayed in their turn with foreground Bash polls. Each received its worker's completion as a queued_command attachment at the next tool boundary and finished normally; the same notice was also enqueued in the main session. Three depth-1 subagents that had ended their turn before their worker finished never received the notice and never resumed on their own.

3. The parent's completion note counts Bash children but not Agent children. One depth-1 subagent stopped twice while its Agent worker was running:

  • First stop, with a background Bash watcher also live: "This agent stopped with background work of its own still running. It may resume on its own when that work completes or reports…" It was resumed only when the Bash watcher finished.
  • Second stop, after the Bash watcher had finished but with the Agent worker still running: "A task-notification fires each time this agent stops with no live background children of its own." The worker finished 24 seconds later; the parent never resumed.

4. Item 1 of #75043 looks fixed here. A depth-1 subagent's Agent call with run_in_background: false blocked and returned the child's hand-back as its tool result.

The bare case also reproduces with claude -p --model sonnet on 2.1.295 in 2 of 2 attempts: a background middle agent launches a background grandchild and ends its turn; the grandchild's notice lands in the main session and the middle agent never resumes.

The docs sentence "In non-interactive mode and the Agent SDK, the launching subagent doesn't wait" covers routing to main. However, the Agent tool result inside the subagent still says "You will be notified automatically when it completes", and the note in item 3 says the parent has no live children while one is running. Since a stopped subagent is already re-woken for its own Bash tasks, delivering its Agent children's notices the same way would match both messages.

Showing cached comments. Read the full discussion on GitHub ↗