Nested subagents: children spawned by a subagent are always async (regardless of run_in_background), completion notifications never reach the subagent parent, and TaskStop fails with ownership errors after resume
Environment
- Claude Code v2.1.201, macOS (Darwin 27.0.0), CLI + desktop session
- Reproduced across models (Opus 4.8 and Fable 5 orchestrators, Sonnet children)
Summary
When an agent spawned via the Agent tool (an "orchestrator" subagent) spawns its own children via the Agent tool, three related failures occur reliably. We ran a controlled experiment (10+ orchestrator runs over two days) and can share reproduction data.
1. Children are effectively async regardless of run_in_background
Even when the orchestrator's Agent call does not set run_in_background (or sets it to false/null), the child runs detached: the orchestrator's turn does not block on it, no completion notification is delivered to the orchestrator, and the orchestrator is never auto-resumed. Transcript inspection shows run_in_background: null on the calls, yet the parent proceeded and later ended its turn "waiting for the child's completion notification" — which never arrives. Result: the orchestrator stalls permanently unless externally poked.
- Frequency: in our first experiment round, all 4 of 4 orchestrator runs (2 models × 2 tasks) stalled at least once with "I'll wait for the implementer's completion notification".
- Docs mismatch: the docs suggest the top-level subagent's summary returns to the caller; for nested spawns nothing returns to the intermediate parent.
2. Child completion notifications are misdelivered to the main conversation
When the child finishes, its completion notification (task-notification) is delivered to the main conversation that spawned the orchestrator — not to the orchestrator that actually spawned the child. We observed 6+ instances. The orchestrator meanwhile believes the child is still running.
3. TaskStop fails with ownership errors after a SendMessage resume
If the main conversation resumes a stalled orchestrator via SendMessage ("was stopped; resumed it"), the resumed orchestrator can no longer stop children it spawned before the resume: TaskStop fails with an ownership/permission error (observed in 3 of 4 runs that attempted it). The un-stoppable child keeps writing to the shared working tree, racing the parent.
Minimal reproduction sketch
- Main conversation: spawn orchestrator O via
Agent(run_in_background: true). - O's prompt: "delegate an implementation task to a child via the Agent tool (foreground), then verify its result".
- O spawns child C without
run_in_background. - Observe: O's turn continues immediately (C is detached); O ends its turn stating it will wait for C.
- C completes → notification arrives in the main conversation, not in O. O never resumes.
- Main resumes O via SendMessage → O attempts
TaskStopon C → ownership error.
Workaround that eliminated the stalls for us
Prompting orchestrators with a "delegation set" removed the stalls completely (0 stalls in 4 subsequent runs, vs 4/4 + 3 more in control runs):
- children must write their report to an agreed absolute path (write to a temp name, then
mvto the final name for atomicity), - the parent runs a bounded foreground wait loop in the same turn (
for i in $(seq 1 60); do [ -f REPORT ] && break; sleep 15; done), - never end a turn depending on a child's completion; on timeout, checkpoint and continue solo (children can't be stopped anyway).
This works but burns a foreground Bash slot for polling; native completion delivery to the spawning parent would remove the need.
What we can provide
Timestamped transcripts (JSONL) for all runs showing: run_in_background: null spawns behaving async, stall messages, misdelivered notifications (with origin.kind metadata), and TaskStop ownership errors. Happy to share extracted excerpts.
Related issues
- #69212 (open) — overlaps with symptom 2 here (notification misrouting). This issue adds symptoms 1 (always-async nested children) and 3 (TaskStop ownership errors after resume), plus controlled-experiment frequency data and a prompting workaround.
- #69249 (closed as duplicate of the above).
Showing cached comments. Read the full discussion on GitHub ↗
13 Comments
As promised, here are extracted transcript excerpts (Claude Code v2.1.201, macOS). Notation: orchestrator transcripts are the subagent JSONL files under
~/.claude/projects/<proj>/<session-id>/subagents/agent-<id>.jsonl;L<n>= 1-indexed JSONL line. Usernames and project paths redacted, delegation prompt bodies omitted; timestamps, agent IDs and tool-use IDs preserved. Japanese assistant text is translated in brackets.The two orchestrator runs below are independent (different models), and the agent IDs cross-link across all three symptoms.
Symptom 1 — child spawned without
run_in_backgroundruns detachedOrchestrator A (
agent-a215a67fb6edbaff4.jsonl):Same in orchestrator B (
agent-af983b9cb6f00497c.jsonlL57/L58: input keys["description", "model", "prompt", "subagent_type"], identical "Async agent launched" result). Note the tool result itself promises "You will be notified automatically when it completes" — for a subagent parent, that notification never arrives (see Symptom 2 for where it goes instead).The stall this produces, orchestrator A:
Orchestrator B stalls identically (L60, 04:30:28.982Z: "[translated] Once I receive the completion notification, next is independent verification by the verifier...").
Symptom 2 — the child's completion notification is delivered to the main conversation instead
The main conversation transcript (the session that spawned the orchestrators — neither child was spawned by it) contains
queue-operationentries for both orchestrators' children:Neither orchestrator received anything; both remained stalled until externally resumed via
SendMessage.Symptom 3 —
TaskStopownership error after aSendMessageresumeAfter each stalled orchestrator was resumed via
SendMessage, it attempted to stop the child it had spawned before the resume:Two details worth noting for whoever debugs this:
owned by a93e408d6c7fde8ac= the task's own ID), not by any parent — so after the resume, the spawning parent's ownership link appears to be gone entirely.Editold_string mismatches and re-ran its test suite to reconcile).We reproduced the misdelivery (Symptom 2) twice more in a later experiment round with different orchestrators/tasks; happy to extract those as well if useful.
Follow-up with the remaining excerpts we promised — the other two runs from round 1, the
origin.kindmetadata, and the round-2 misdelivery data (which turned out to be more systematic than the "2 more instances" mentioned above). Same notation and redaction rules as the previous comment.One correction from re-checking the raw files: the reproduction transcripts all record
version: 2.1.197. The "v2.1.201" in the original post was the CLI version installed when the report was written (now 2.1.202); the runs themselves were on 2.1.197. Sorry for the imprecision.Round 1, runs 3 and 4 — same spawn/stall pattern
Orchestrator C (
agent-a442cc0acd7386a2c.jsonl, ~49 min run):run_in_backgroundkeyTaskStop: never called in this run.Orchestrator D (
agent-a864dcd4e3829da4b.jsonl, ~1h52m run): 1 spawn (norun_in_backgroundkey), 1 stall statement, then the TaskStop sequence below.origin.kindmetadata on external resume messagesWhen we externally resumed a stalled orchestrator, the poke is recorded in its transcript as a
type: "user"entry with explicit metadata (orchestrator D, L28):Useful for whoever debugs this: external resumes are machine-identifiable in transcripts, so stall→resume→TaskStop sequences can be reconstructed from the JSONL alone.
TaskStop detail: fails on the pre-resume child agent, succeeds on own Bash tasks
Orchestrator D, after its resume:
We can't fully disentangle whether the discriminator is agent-vs-shell-task or spawned-before-vs-after-resume (both differ here), but combined with the two runs in the previous comment (both failed on pre-resume child agents with the identical
owned by <child-id>message), "child agent spawned before the resume" is the consistent failure case across all three observations.Round 2 — misdelivery is systematic and depth-independent
In a later round (same session; two fresh orchestrators
af8dc684d96381e0aanda23628cdd6cd809d0; 19 Agent spawns between them, all 19 with norun_in_backgroundkey), the main-session transcript logs 15queue-operationtask-notification entries for 13 distinct child agents between 14:02:56Z and 15:58:01Z on 2026-07-06 — the children's completion notifications arrived at the root conversation throughout the window. Representative entries:12 of the 13 IDs appear in exactly one of the two orchestrators' transcripts (their Agent tool results), confirming which orchestrator spawned them. The 13th —
a9106d2e366a9fa2dabove — appears in neither: it only appears in the transcript ofa6ff161e1def7af5b, which is itself a child of orchestrator A'. In other words, a grandchild (depth-2 spawn) notification also routed to the root conversation — delivery appears to target the root session regardless of spawning depth.One nuance for accuracy: unlike round 1 (orchestrators received nothing; 4/4 stalled), both round-2 orchestrators did observe some child-stop notifications mid-run — but with unusable payloads:
So delivery to the spawning parent is inconsistent — absent in round 1, partial/unusable in round 2 — while the root session receives the full stream in parallel. Both round-2 orchestrators recovered without any external intervention using the report-file + bounded-polling pattern from the original post; orchestrator B' had declared the risk upfront:
That is everything we committed to providing. If raw JSONL slices around any cited line would help, the line numbers are 1-indexed into the named
agent-<id>.jsonlfiles (under~/.claude/projects/<proj>/<session-id>/subagents/) and we're happy to paste specific ranges.I would split this into ownership receipts, not only foreground/background scheduling.
The nested child has three authorities that should be explicit:
Regression cases:
The important invariant is that parentage, notification routing, and stop authority are one lifecycle contract. Once a child is accepted, every later event should be attributable to the parent that requested it, and a failed stop should be visible as a no-effect authorization outcome rather than a silent ownership drift.
Good controlled experiment. The three failure modes you have documented -- forced async regardless of run_in_background, completion notifications not reaching the parent, and TaskStop ownership errors after resume -- look like symptoms of a single missing primitive: the subagent call stack is not persisted anywhere the runtime can interrogate after a context compaction or resume.
From what I have seen experimenting with nested agent patterns, the issue compounds when the parent (orchestrator) subagent itself gets resumed: the resumed agent has no knowledge of child agents it previously spawned because that spawning state lived in the pre-compaction transcript. So the parent either ignores the children (and eventually ends its turn "successfully" while grandchildren still run) or, if it notices the gap, it re-spawns -- creating duplicates.
The ownership error on TaskStop is the most actionable signal here. If TaskStop enforces that only the spawning session can stop a task, but the spawning entity is a subagent whose session is now "ended" from the parent's perspective, then stopped tasks are effectively orphaned. A two-level stop -- stop task, stop owning subagent -- with the right error codes would at least make the failure observable.
One workaround that has been somewhat reliable: have the orchestrator subagent write its child agent IDs to a shared file immediately on spawn, and have a top-level session hook read that file and log or alert when children are still running after the orchestrator reports done. Not a fix, but it surfaces the orphan state faster.
The three-way split your last commenter describes (ownership, scheduling, notification) is the right decomposition for the spec.
Confirming this affects a different call site:
/code-review's own internal multi-agent fan-out — so this isn't limited to custom orchestrator patterns, it also surfaces inside an Anthropic-shipped plugin.Environment: Claude Code v2.1.202, macOS 26.5.1 (Darwin, build 25F80), CLI session, Sonnet 5 orchestrator.
Context: a
milestone-driver:solve-issue --workerbackground agent (dispatched from asolve-milestonerun) invoked/code-reviewon its own diff./code-reviewinternally fans out to ~6 independent review-angle sub-agents (a nestedAgent-tool dispatch, one tier below the worker). One angle — a line-by-line scan — hit exactly the failure mode described here: it spawned, the intermediate parent (/code-review's own dispatch logic) never received a completion notification for it, and the worker's transcript shows it idled until the harness's own 600-second watchdog killed the stalled angle outright (not a graceful timeout with a result, just a kill).Outcome: the worker itself degraded gracefully — it noticed the missing angle, proceeded with the other 5 completed angles plus its own direct line-by-line read of the changed files, and noted the gap explicitly in its output. So the outer task wasn't blocked, but one of
/code-review's six intended review angles silently never completed, and nothing surfaced this as an error — it only became visible because the worker happened to narrate it.This matches failure mode 1 and 2 from the original report exactly (forced-async child, notification misdelivered/never delivered to the spawning parent) — just discovered via a shipped plugin's own fan-out rather than a hand-rolled orchestrator, which suggests the blast radius is broader than custom multi-agent scripts.
---
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirming on Linux / v2.1.195 (Opus 4.8 orchestrator,
general-purposechildren) — so this isn't macOS/2.1.201-specific and is present at least back to 2.1.195. I ran a controlled set of nested-spawn experiments and independently verified each step from the top-level session viapsand the filesystem, not just the agents' self-reports. Two additions to your report:1. Symptom 3 (
TaskStopownership error) reproduces with noSendMessageresumeYour writeup ties the ownership error to a resumed orchestrator. I hit it on the first, direct
TaskStopcall, no resume involved:Agent,run_in_background: true) running a 120-iteration marked loop that appends to a file once per second.TaskStop(<child agentId>). Verbatim response:Task a6ae1d8eeb3b63f7c is owned by a6ae1d8eeb3b63f7c; agent a82cf85b039339bf8 cannot stop it.
(
a82cf85…= the sub-agent that spawned the child;a6ae1d8…= the child.)psafterTaskStopreturned.So the ownership guard looks unconditional — a spawning sub-agent is never the owner of the child's task, resume or not.
2. Symptom 1 reproduces with a plain background Bash command — no nested
Agentat allMinimal isolation of "the spawning parent is never resumed":
Bash(run_in_background: true):sleep 60 && echo done > /tmp/marker.txt, then yields to await it (relying on the documented "you'll be notified when it completes").completedto its parent at ~51s — before its ownsleep 60had finished — and its "final" result is its pre-yield message: "…I am now yielding and waiting to be auto-notified when the background task exits. I have not yet read MARKER, so I cannot yet report its contents."Symptom 2, confirmed directly (nested-
Agentcase)When a sub-agent's background child (a
sleep 300 && echo done > sentinelworker) finished, itstask-notificationwas delivered to the top-level / main conversation — an agent that never spawned it — while the spawning sub-agent had already been reportedcompleted~102s in, with a"I will wait for the completion notification … before proceeding to Step 3"stall message, even though the child and two of the sub-agent's own poll loops were still live inps.Minor corroboration of your workaround note
A sub-agent has no
TaskGet/TaskOutput/TaskList/ScheduleWakeuptools at all (onlyAgent+run_in_background;Monitor/TaskStopare loadable viaToolSearch), so it genuinely cannot fetch a child's output. Consistent with that, I watched sub-agents spontaneously and unprompted spawnuntil test -f <artifact>; do sleep 2; donebackground Bash loops to watch for a child's file — the exact foreground-poll workaround you describe, but self-inflicted (and a foregroundsleepto wait is blocked, which pushes them toward it).With the later Linux, plugin-internal fan-out, and plain background Bash confirmations, I would make the acceptance matrix task-class based, not only Agent-child based.
For every work unit spawned below the root conversation, the runtime should record the same three refs before any detach:
Then I would test the matrix across Agent child, plugin-internal fan-out, and background shell task, each in foreground-intent and detached-intent modes, before and after resume/compaction.
Expected outcomes:
That keeps the invariant small: scheduling, notification, and stop/cancel authority are one child-work lifecycle contract. If the runtime cannot preserve that contract across nesting and resume, it should fail closed with an auditable no_effect outcome rather than orphaning work or letting the root conversation absorb ownership implicitly.
Boundary: architecture and regression-test feedback only; no claim about Anthropic/Claude Code implementation correctness, official alignment, adoption, integration, customer interest, partnership, or Neura usage.
Can confirm that workaround from this ticket works. The following prompt resulted in a sub-agent performing a clean run of multi-agent code-review skill:
<img width="641" height="122.5" alt="Image" src="https://github.com/user-attachments/assets/22240ea4-55e8-4f00-8462-961e4d512fb9" />
Confirming on Windows — Claude Code v2.1.207 (Windows 11 Pro, desktop app). Together with the OP (macOS, 2.1.197–2.1.202) and the Linux 2.1.195 report above, this is now reproduced on all three platforms, and still present as of 2.1.207.
I ran the same minimal repro as the OP on 2026-07-13: an orchestrator subagent spawns two trivial background children via the Agent tool, then ends its turn. Results:
The orchestrator never received either child's completion. It was not resumed automatically when the children finished; every SendMessage resume found it idle with nothing new in its context, and when asked directly it confirmed that nothing had arrived from either child. (Total absence, matching the OP's round-1 results — not the partial/unusable payloads seen in round 2.)
Both task-notifications were delivered to the main conversation, once per child.
One detail to add to the routing picture: the notifications arrived at the main conversation while the orchestrator was resumed and mid-turn, actively checking on its own children. So the misrouting does not look like a fallback to "whichever conversation is live at delivery time" — the orchestrator that spawned the children was live at that moment. Delivery appears to target the root conversation unconditionally, which is consistent with the OP's round-2 observation that the behavior is depth-independent.
We've settled on the same operational conclusion as the rest of the thread: either avoid intermediate orchestrators entirely (the main conversation spawns and collects all children itself), or use the OP's delegation-set workaround (children write report files to agreed absolute paths; the orchestrator waits on those files in a bounded foreground loop and never ends its turn while children are outstanding).
This looks like one lifecycle contract split across three runtime surfaces:
The bug shows up when those three are recorded separately, or not durably enough. A child can be accepted by an orchestrator, but the terminal receipt routes to root, stop authority points at the child itself, and the parent is left with only a narrative that it is "waiting." That is exactly the failure mode multi-agent systems hit when coordination state is chat-local instead of receipt-bound.
A useful invariant might be: every spawned work unit gets a stable parent receipt at creation time:
Then the regression matrix can be small and mechanical: Agent child, plugin fan-out, and background shell task each preserve the same receipt across nested spawn, resume, compaction, timeout, and cancellation. If the runtime cannot preserve the receipt, it should fail closed with a terminal
no_effect/orphanedoutcome rather than letting work continue against the shared tree with ambiguous authority.This is also the layer where systems like AgentFolio/SATP can eventually score coordination quality: not by reading private transcripts, but by evaluating whether a run produced verifiable receipts for parentage, artifact handoff, and terminal outcome. The important part here is not reputation, though — it is making the lifecycle boundary explicit enough that a parent cannot lose its child while the child keeps acting.
Still present in v2.1.215 (Linux/WSL2) — with a schema regression: the subagent's Agent tool now explicitly claims "only synchronous subagents" while detaching everything. Also independently confirming @taminomara's bounded-poll workaround.
Environment: Claude Code v2.1.215, Linux 6.6.87.2 (WSL2), CLI. Main session on Fable 5; subagent worker on Opus 4.8; children on Haiku 4.5.
We hit the OP's failure modes during orchestrated delivery (main session → card-worker subagents → their review/research children), then ran a controlled experiment:
Schema drift (new detail). Our worker's Agent tool schema omits
run_in_backgroundandnameentirely and its description states verbatim: "run_in_backgroundandnameare unavailable here — only synchronous subagents." @taminomara's report (2.1.195) hadrun_in_backgroundpresent in the subagent schema; by 2.1.215 the schema instead asserts synchronous behavior — while the runtime still detaches every child. So the contract and the behavior have moved further apart: the tool now actively promises what the bug prevents.Behavior (matches OP findings 1–2). Two parallel Agent calls with no flags: both children detached ("Async agent launched successfully… You will be notified automatically when it completes" + agentId + output_file), no result or notification ever reached the spawning worker, and both children's completion notifications — full
<result>payloads — were delivered to the main conversation. Passingrun_in_background: true(absent from the schema) was silently accepted with no validation error and behaved identically.Workaround confirmation. @taminomara's report-file + bounded foreground poll works on this build too: child writes its result atomically (
> F.tmp && mv F.tmp F) as its last act; the parent, in the same turn, runsfor i in $(seq 1 90); do [ -f F ] && break; sleep 2; done; cat F || echo TIMEOUTwith a generous Bash tool timeout. The parent collected the result in-turn without parking. One nuance worth recording: the Bash tool blocks a bare foregroundsleep, but asleepinside a bounded loop is permitted — which is presumably why agents spontaneously converge on this pattern.output_file caveat. The spawn metadata's
output_fileis the child's full JSONL transcript; the final text is extractable from the last assistanttextblock, so it's a recovery path for short children, but for real workloads (our review children ran 70–85k tokens) reading it would blow the parent's context, as the launch metadata itself warns.@vstratful I've also observed that Write tool blocks files named "report" or similar in subagents, worth knowing if you're gonna use my workaround.
Following up on @taminomara's report that the Write tool blocks report-ish filenames in subagents — we measured the blocking behavior on v2.1.215 (macOS): three subagent rounds covering 33 distinct filenames, plus main-conversation, cross-model, same-path, and depth-2 controls, every tool result cross-checked against
lson disk. In everything we tested, the block occurred only in subagent contexts, only via the Write tool, and only for filenames whose stem starts with one of a small set of report-ish keywords combined with a.mdextension — and the error message steers nested children toward the same parent-handoff failure this issue describes.Blocked (17 names; identical verbatim error on every rejection):
report.md,REPORT.md,reports.md,report-2.md,reportage.md,rEpOrTs.mD,summary.md,Summary.md,SUMMARY.MD,summary-1.md,summary_notes.md,findings.md,findings-1.md,Findings.MD,analysis.md,analysis-notes.md,analysisXYZ.mdAllowed (16 names):
report.txt,report.markdown,summary.txt,summarize.md,analyses.md,analysis.markdown,findings.txt,final-report.md,test-report.md,my-summary.md,RESULT.md,RESULT-example.md,RESULT-content-test.md,results.md,notes.md,output.mdVerbatim error, identical across all rejections:
Observed properties (stated within tested scope):
report.md,summary.md,reports.md,summary-1.md,findings.md,analysis.md) all succeed at depth 0 in the main conversation, and all six are rejected in Sonnet 5 subagent runs; two of them (report.md,summary-1.md) were also rejected in a subagent running the same model as the main conversation, ruling out model identity as the sole explanation. A same-path cross-check — a subagent's Write ofreport.mdrejected at an exact path, then the main conversation successfully writing the identical path immediately afterward — rules out the tested target path as the sole explanation as well. The block also reproduces at depth 2: a child spawned by a subagent orchestrator hadreport.mdandsummary.mdrejected with the same verbatim error (its results were retrieved via an allowed-name report file plus a bounded foreground poll, since — per this issue — the child's completion never reached its spawning parent)..mdfilter. All 33 tested names fit: stem starts with one of at leastreport,summary,findings,analysis(a lower bound from finite testing, not an enumeration), gated on a case-insensitive.mdextension. Discriminating results:reports.md,summary-1.md,reportage.md,analysisXYZ.mdblocked (rules out exact-basename and word-boundary matching);final-report.md,my-summary.mdallowed (rules out substring matching);summarize.md,analyses.mdallowed (the keyword must be a prefix of the stem — "summary" is not a prefix of "summarize", "analysis" is not a prefix of "analyses");report.txt,report.markdown,analysis.markdown,findings.txtallowed (extension-gated;.markdownis not matched). This is a model fitted to the tested sample — the actual implementation may differ outside it.mv x.tmp report.md, Bashprintf ... > report.md,cptoanalysis.md, and the Edit tool modifying an existingreport.mdall succeeded; rejected Writes leave nothing on disk (no partial writes).summary.mdwas rejected with four different payloads (empty, prose, code snippet, report-style markdown), while the same report-style payload written toRESULT-content-test.mdsucceeded.Why this matters for this issue specifically: per the reports in this thread (OP round 1, and the Linux / Windows / WSL2 confirmations), a nested child's final response does not reliably reach its spawning parent. So a nested child that obeys this error's guidance — "include this content in your final response instead" — risks losing its deliverable. For handoffs that Write a report-ish
.mdname directly, this can leave the parent seeing a missing file or a poll timeout rather than the child's Write error. The file-based handoff used as the workaround in this thread is the very pattern this filter discourages — and we confirmed the block applies at depth 2, where the file handoff is the reliable workaround demonstrated in this thread.Practical refinement for the workaround: pick report filenames whose stem doesn't start with one of the blocked keywords — e.g.
RESULT-<slug>.md(RESULT-example.mdverified via direct Write in a subagent) — or have the child write to a neutral temp name and rename as its last act; the write-tmp-then-mvpattern already recommended in this thread is unaffected in our tests, since the Write tool only ever sees the temp name.Method note: each filename in the direct-Write matrices was attempted exactly once per run. The content-variation set wrote
summary.mdonce per payload (four in total) plus one allowed-name control, and the Bash/cp/Edit route tests were a separate, explicitly instructed set — not retries of rejected Writes. Agents were instructed not to retry, rephrase, or route around rejections within the matrices and to report per-name tool results verbatim; every reported outcome was cross-checked againstls(blocked names absent on disk, allowed names present). Run 2 re-tested run 1's names without being given run 1's results, with full agreement on the overlap.