Repository navigation
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 #75043
Description
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
- [Bug] Subagent results route to root teammate instead of spawning teammate #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.
- [Bug] Nested subagents deliver conclusions to main agent instead of parent subagent #69249 (closed as duplicate of the above).
Activity
mof086999-code commentedon Jul 7, 2026
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.
mof086999-code commentedon Jul 7, 2026
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.rpelevin commentedon Jul 7, 2026
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.
kcarriedo commentedon Jul 7, 2026
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.
kenmulford commentedon Jul 7, 2026
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
taminomara commentedon Jul 7, 2026
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).rpelevin commentedon Jul 7, 2026
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.
taminomara commentedon Jul 8, 2026
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:
joseortiz3 commentedon Jul 13, 2026
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).
0xbrainkid commentedon Jul 13, 2026
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:
{ "child_run_id": "...", "parent_run_id": "...", "completion_route": "...", "stop_authority": "...", "intent": "foreground|detached", "artifact_contract": "optional path/hash/schema" }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.
vstratful commentedon Jul 19, 2026
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.1 remaining item
mof086999-code commentedon Jul 19, 2026
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.fix(#361): delete the worker layer, dispatch by stage from the orches…
feat(ca77y-engineering): convert the lead into a main-session skill r…
bcherny commentedon Aug 15, 2026
Reproduced on v2.1.233 (macOS) — partially. Of the three symptoms, symptom 3 (
TaskStopownership error) reproduces and is a real bug; symptom 1 (nested child runs in the background withoutrun_in_background) is now the documented default; symptom 2 (child completion never reaches the spawning subagent) did not reproduce on v2.1.233 in a depth-1 test.Steps
claude(v2.1.233), fresh project directory.run_in_background: true, whose prompt is: spawn one child via the Agent tool without settingrun_in_background, then write the Agent tool's raw result verbatim to a file.sleep 120) and immediately callsTaskStopon the returned agent id, writing the result to a file.Observed
Async agent launched successfully. ... You will be notified automatically when it completes.— the child's answer was not in the tool result (symptom 1).<task-notification>was delivered to the orchestrator, which was resumed and processed it. Symptom 2 did not reproduce in this simple case.TaskStopfrom the orchestrator on the child it had just spawned (no resume involved) failed withTask a35e...779 is owned by a35e...779; agent a275...00b cannot stop it.and the child kept running (symptom 3, matching @taminomara's no-resume observation).Expected
Assessment:
TaskStopapplies to a subagent compares the caller against the task's own agent id, which for a spawned agent is the child itself — so the spawning subagent is never considered the owner, resume or not. This is why the error reads "owned by ". The runtime does record who spawned the child (that's what the subagent panel tree uses), it just isn't consulted byTaskStop. Not a regression: this never worked for nested agents since nesting was introduced. Workaround until fixed: have the main conversation stop the child (it can stop any task by id), or use the report-file pattern from this thread.🤖 Generated with Claude Code
bcherny commentedon Aug 17, 2026
Thanks for the detailed report — I re-tested this on 2.1.233 (Linux, CLI) with a nested orchestrator setup mirroring your repro.
What I found:
run_in_background: falsenow blocks and returns its result synchronously in the Agent tool result. The changelog shows nested spawning was reworked after your version — disabled by default in 2.1.217, then re-enabled to depth 3 in 2.1.219 (see the CHANGELOG);CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTHcontrols it.Keeping this open for item 2. If you can re-test on 2.1.233+, that would confirm whether items 1 and 3 are resolved for your workflow.
🤖 Generated with Claude Code
vstratful commentedon Aug 18, 2026
Re-tested all three on 2.1.234 (Linux/WSL2, interactive CLI). Setup was main session → agent → one child of that agent.
Short version: items 1 and 3 are still broken here, item 2 works fine here.
Item 1 — the fix isn't working for us. The agent spawned its child with
run_in_background: falseand the child still ran in the background: the Agent call returned launch metadata immediately, the child ran 37s, and its result came back later as a task-notification. Nothing blocked.I think the flag isn't reachable from there.
run_in_backgroundisn't in the Agent tool's schema as the agent sees it (description, isolation, mode, model, name, prompt, subagent_type, team_name), and the description says "Subagents run in the background; you'll be notified when one completes." Passing it anyway gave no error, no warning, and changed nothing — though the schema declaresadditionalProperties: false, so by its own contract it should have been rejected rather than dropped. Either way the caller gets no signal it did nothing.Item 2 — can't reproduce, routing looks right here. Ran it twice, with
run_in_background: falseandtrue. Both times the child's notification went to the agent that spawned it with the full result attached, and that agent was auto-resumed without me messaging it. The main session only got notifications for agents it spawned directly. One run per variant, so small sample.Item 3 — still reproduces, deterministically, with no resume involved. The agent spawns a child that runs ~80s, then calls TaskStop on it while it's still alive:
Called twice, byte-identical both times, so it's the ownership gate rather than a race. The task's owner is reported as its own id, which matches your diagnosis — the spawner can never satisfy it. The child then ran to completion: status
completed,duration_ms81006, all 20 ticks printed. The stop signal never reached it, so the tokens weren't reclaimable.That's probably the difference from your run — you noted the child had already been reaped by the time the stop was attempted. Mine was still mid-loop.
Env in case item 2 is environmental:
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1,CLAUDE_CODE_CHILD_SESSION=1,CLAUDE_CODE_FORK_SUBAGENTunset, interactive not headless.taminomara commentedon Aug 18, 2026
Can confirm that item 2 look fixed, and 3 is not. Tested using v2.1.226 Linux.
The exact prompts I used for repro are below.
Item 2:
Item 3:
Zorast commentedon Aug 26, 2026
Yes, I feel exactly the same way. I can confirm that. The error is just incredibly annoying. There’s no other way to put it. And we’re working with the AI that might one day manage to implement its own software without bugs.
mof086999-code commentedon Aug 31, 2026
Re-tested all three on v2.1.241 (macOS, same Darwin version as the original report — 27.0.0), following up on the re-test request above. Beyond a straight re-run of @taminomara's procedures, we added discriminating runs that localize item 2's failure more precisely than "misrouting". Evidence below is from the session JSONLs (
queue-operationentries and sub-agent transcripts), not agent self-reports alone; line numbers are 1-indexed.Sessions used (relevant given the conflicting item-2 results in this thread): (i) one SDK/bridge session — the worker process runs with
--print --input-format stream-jsonunder aclaude remote-controlparent,CLAUDE_CODE_ENTRYPOINT=sdk-cli,CLAUDE_CODE_CHILD_SESSION=1,CLAUDE_CODE_ENVIRONMENT_KIND=bridge,CLAUDE_CODE_FORK_SUBAGENT/CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMSunset; and (ii) two dedicated headlessclaude -psessions (one per item-2 variant, nothing else running in them). The bridge session's main conversation and itsgeneral-purposesub-agents ran on Fable 5 (inherited); the twoclaude -psessions and their sub-agents ran on Sonnet 5 (so the item-2 result below replicates across two models); plus one schema check on Haiku 4.5. Run-isolation disclosure: the first three bridge runs ran concurrently; the two discriminating runs (active parent, resumed parent) were serial; a later batch (TaskStop trials 2–3, foreground trials 2–3, schema check) ran concurrently; the root stop-control ran after it. The twoclaude -psessions were single-purpose, isolated sessions but overlapped each other in wall-clock time. We could not test an interactive TUI session; all data below is from non-interactive session kinds.Short version: item 1 worked in 3/3 trials here (schema drift gone); item 3 still reproduces (3/3 distinct parents; root-level control succeeds). Item 2's round-1 symptom reproduces in 4/4 yield-and-wait runs across both session kinds — but two additional runs show it is not blanket misrouting: a sub-agent that is mid-turn when its child completes DOES receive the notification, and so does one resumed via SendMessage. In all four yielded-parent runs, no parent was auto-resumed by its child's completion; the child's notification was consumed by the root conversation instead.
Item 1 — foreground nesting: worked in 3/3 trials; schema restored for these agents
run_in_backgroundagain. Parameter lists as reported by two sub-agents on different models (Fable 5-inherited and Haiku 4.5) were identical:description, isolation, model, prompt, run_in_background, subagent_type(namestill absent). Both reports quote the parameter description verbatim as: "Agents run in the background by default; you will be notified when one completes. Set to false only when your very next action depends on this agent's result …". The 2.1.215/2.1.234 drift @vstratful reported ("only synchronous subagents" while detaching everything) was not present for these agents.run_in_background: falsewas honored in all 3 trials: each Agent call was accepted (no validation error), blocked for the child's full duration (childduration_ms56387 / 41466 / 39417; e.g. trial 1's tool_use at 23:45:16.016Z → tool_result at 23:46:15.793Z), and returned the child's actual final text inline (unique marker present) — not launch metadata.Item 2 — round-1 symptom reproduces: 0/4 yielded parents auto-resumed; 2/2 active-turn parents received notifications
Baseline (@taminomara's item-2 procedure — spawn child, record metadata, yield with a no-tool-call message, rely on the promised notification): 4 parents across 3 sessions — bridge session: one child spawned with no
run_in_backgroundkey (input keys["description", "prompt", "subagent_type"]in the transcript) and one withtrue; then oneclaude -psession per variant. Result identical in all 4:acc8bb3294655e72d), L124 @ 23:48:18.741Z (childa08eaa2de809e307d), each with the full result payload. Same pattern in bothclaude -psessions (e.g. grandchild enqueue @ 01:42:21.570Z, removed at the root's next tool boundary @ 01:44:27.706Z).claude -psessions ran to session end).completedtask-notification for the parent itself (bridge JSONL L91 @ 23:45:25.104Z, L96 @ 23:45:36.589Z) — i.e. a parent that yields to wait is immediately treated as a stopped agent.Discriminating runs (bridge session, serial):
duration_ms). One run.Across these six runs the pattern was consistent: both parents that had an active turn when their child completed received the notification at their next tool boundary; all four yielded parents did not — we observed no child-triggered auto-resume of a yielded sub-agent — and their children's notifications were consumed by the root conversation instead (in one case starting a new root turn; in the others attaching at the root's next tool boundary while it happened to be active). The launch metadata's promise to the spawning parent — "You will be notified automatically when it completes" — was kept only when the parent was still mid-turn when the child finished.
This may also explain the conflicting reports in this thread: @vstratful (2.1.234, WSL2) explicitly observed yielded parents auto-resumed in an interactive CLI session, @taminomara (2.1.226, Linux) observed successful auto-resume as well (session kind not stated), while @bcherny saw item 2 fixed in one 2.1.233 test and reproducing in another. Both of our session kinds are non-interactive (remote-control bridge worker, headless
claude -p). A session-kind-dependent auto-resume path for stopped sub-agents is one concrete candidate worth checking; we can't rule out other environmental factors (custom agent roster, hooks) and did not test an interactive TUI session.Two notes for whoever debugs this: the session JSONL's
queue-operationentries (operation: enqueue|dequeue|removewith timestamps) make the destination analysis mechanical; and when a sub-agent receives a notification mid-turn, no standalone task-notification entry is written to its transcript (the hold command's tool_result stays untouched; the only serialized copy is the parent's later quotation of it). Receipt is supported by the queue-removal timestamp landing exactly at the parent's tool boundary, plus the parent quoting fields (tool-use-id,duration_ms, status, result) that match the enqueue record.Item 3 — TaskStop ownership: still reproduces (3/3 parents); root-level control succeeds
Three distinct first-level parents (all in the bridge session; the later two ran concurrently with each other) each spawned a background child and called TaskStop while the child was verifiably mid-run (the two later trials confirmed a start-marker file on disk and the absence of a done-marker immediately before stopping). All three were rejected with the same error shape (verbatim, ids per trial):
The first parent called TaskStop twice, 4s apart, with byte-identical results (within-task repeat; no resume involved in any trial). All three children were unaffected and ran to completion (
duration_ms196760 / 99609 / 100602).Positive control: the root conversation spawned its own background child and stopped it mid-run: TaskStop returned
Successfully stopped task: …and was effective — the child was killed (statuskilled) and its 90s loop never wrote its final marker. So the stop machinery itself works; the ownership gate specifically excludes the spawning sub-agent, consistent with @bcherny's diagnosis (the check compares the caller against the task's own agent id), and the 2.1.203 fix does not cover this path.As before, happy to provide JSONL slices around any cited line or the exact repro prompts used.
mof086999-code commentedon Aug 31, 2026
Follow-up: a same-machine interactive-TUI control supports the session-kind hypothesis.
We ran the same yield-and-wait protocol (the no-
run_in_background-key nested-child variant) in an interactive TUI session (claudein a terminal) on the same machine and build (v2.1.241, macOS; main, parent and child all on Sonnet 5 — the same model as the two headlessclaude -psessions above, so this is not a model difference). Protocol differences from the headless runs were minor: the child's loop duration (40×3s here vs 34×3s) and probe filename, and the root→parent spawn omittedrun_in_backgroundhere (background by default) where the headless runs set it explicitly totrue. Result: the yielded parent WAS auto-resumed by its child's completion (1/1).One more discriminating detail: in the TUI session, no "parent completed" notification was enqueued when the parent yielded — the parent's only completion event fired at 04:01:06.617Z, after it had processed the child's result and genuinely finished. In both non-interactive session kinds, the parent's
completednotification was enqueued almost immediately after its yield (~1.6s in the bridge session, ~23–33ms in the headlessclaude -psessions).On one machine and one build, the observed outcomes aligned with session kind in these runs:
claude -p(4/4 runs)completedwithin ~2sOne TUI run plus the minor invocation differences above can't establish causation by itself, but combined with the 4/4 non-interactive runs it points at a concrete place to look: whether a sub-agent that ends its turn with children outstanding is finalized immediately (non-interactive) or kept resumable (interactive). Transcripts available as before.
mof086999-code commentedon Aug 31, 2026
Addendum: more controls, including a fork-gate 2×2 with raw schema capture. Result: the
CLAUDE_CODE_FORK_SUBAGENTgate moves the Agent tool schema, but the yielded-parent lifecycle did not follow it — outcomes grouped by session kind in every run (interactive 4/4 resumed, non-interactive 0/6).All runs below are on the same pinned build (the CLI had meanwhile auto-updated to 2.1.251, so we launched the 2.1.241 versioned binary directly; every session JSONL records
version: 2.1.241).TUI runs 2 and 3 (parent→child prompt byte-identical to the headless F1 run; outer spawn carried an explicit
run_in_backgroundkey both times): the yielded parent was auto-resumed both times (run 2: yield @ 04:25:44.394Z, child notification enqueued @ 04:27:30.528Z, injected into the parent @ 04:27:30.543Z; run 3: yield @ 04:39:19.395Z, enqueue @ 04:41:06.892Z, injected @ 04:41:06.906Z). Both runs serialized the key's value as the string"true"rather than the booleantrueused in the four earlier failing non-interactive runs (run 3 despite an explicit requirement to emit the unquoted boolean); a schema-introspection run (run 4) suggested why — the default interactive root's Agent schema reports norun_in_backgroundparameter at all — and the fork-gate runs below corroborated the gate/schema relationship from raw request bodies for the arms they tested.Fork-gate 2×2. Static inspection of the 2.1.241 binary suggested
CLAUDE_CODE_FORK_SUBAGENTis parsed as a tri-state boolean whose explicit values override the session-kind default, and that it participates in Agent schema construction. We added three fresh serial arms — headless fork-off, headless fork-on, and TUI fork-off. Their root→parent calls all omittedrun_in_backgroundand used the same 34×3s yield-and-wait protocol; only arm labels and probe filenames differed. The fourth cell uses the prior default-TUI runs 1–3 for behavior and run 4 for schema introspection (runs 2–3 included the undeclared string key). For the three new arms we captured the root's actualtoolsarray from the constructed API request bodies viaOTEL_LOG_RAW_API_BODIES=file:…(raw bodies kept private; only the Agent schema entry was inspected):CLAUDE_CODE_FORK_SUBAGENT=0=1/ defaultclaude -prun_in_background(raw capture); yielded parent not resumedRepresentative timestamps — headless fork-on (the arm that would have flipped if the gate governed the lifecycle): parent yielded @ 05:06:02.403Z; child finished @ 05:07:42Z; both notifications were consumed at the root's tool boundary @ 05:09:51.032Z; the parent transcript contains zero notification entries. Interactive fork-off (the reverse direction): parent yielded @ 05:12:48.529Z; child notification enqueued @ 05:14:35.545Z and injected into the parent @ 05:14:35.563Z; the parent resumed and reported.
Reading. Headless fork-on omitted the parameter in its raw request; default-TUI introspection also reported it absent. Fork-off raw captures declared it in both tested session kinds. So the gate demonstrably controls which Agent tool surface the root gets — but flipping it changed the yielded-parent lifecycle in neither direction. Across these runs, outcomes remained grouped by session kind: interactive 4/4 resumed (runs 1–3 default, run 5 fork-off) and non-interactive 0/6 did not (bridge ×2, headless default ×2, headless fork-off, headless fork-on). Neither fork-gate setting nor outer-key presence changed that grouping. In the new arms the gate changed the schema without an observed lifecycle flip, ruling it out as a sufficient explanation here; session kind bundles other execution-path differences, so the internal cause remains unknown — but the boundary is now drawn tightly enough that the responsible branch should be findable directly.
Method note for whoever picks this up:
OTEL_LOG_RAW_API_BODIES=file:<dir>turned out to be an effective way to capture the actual per-session tool schemas without relying on model self-report (the bodies contain full prompts, so treat them as sensitive). Transcripts available as before.mof086999-code commentedon Sep 2, 2026
Independent reproduction of item 2 on 2.1.250 / 2.1.251: #91140 — headless
claude -p, 0 wakes in 18 valid trials, with a matched interactive control that woke in 2–6 s. Its queue journal shows the child's notification removed withreason: absorbed_mid_turnby the root in headless runs vsdelivered_to_agentinteractively, i.e. the same session-kind split as our 2.1.241 runs above. That report also covers the own-background-Bash channel (as in @taminomara's Linux confirmation) and ran withCLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1, whereas ours ran with it unset — so the flag does not appear to matter either way.cwj2001 commentedon Sep 16, 2026
Related non-interactive streamed-input postmortem.
Environment
claude-agent-sdk==0.2.1402.1.235--output-format stream-jsonObserved ordered lifecycle
A dynamically supplied
local_agenttask starts inside one parent run. Its durable ordered journal then records:system/task_progress/TaskProgressMessageapproximately every 6–12 seconds for more than 45 minutes;TaskProgressmetadata reportinglast_tool_name=TaskStoprepeatedly during the later loop;ResultMessageand no terminal parent run state.The host's model token-usage and request counters do not change during the repeated-progress interval. This excludes a currently active model request as the source of the repeated progress loop, although it does not establish that every custom-provider interaction is irrelevant.
Process and host evidence
claudeOS process is the parent query CLI for this SDK session; it is alive and waits inepoll_waitbetween progress notifications.TaskProgressMessagein its run journal. It does not receive a finalResultMessage.The same task id had an earlier completed invocation and was then reused by a later invocation with a distinct
tool_use_id. Our task projection retains the earlier terminal state, so its summary can show a contradictory completed/running view. The ordered journal separates the invocations and shows the later task continuing to emit running progress. This projection defect is an observability issue; it does not generate the SDK messages or the loop.A local no-provider replay of the normal SDK sequence (
TaskStarted -> task completion -> parent ResultMessage) converges correctly in the host. The host therefore handles the ordinary terminal sequence; the live run has not delivered that sequence.Conclusion
This is a native task-lifecycle non-convergence in a non-interactive stream-json host. The observed failure is localized to the Claude Code CLI / Agent SDK transport boundary: native task progress continues indefinitely while the parent receives no terminal result. It is not an execd command stall, and it is not a currently active model-request stall.
The available capture begins after SDK parsing, so it cannot further divide responsibility between CLI emission/order and SDK transport/parser behavior. Raw CLI stdout was not captured for this incident.
This matches the non-interactive nested task completion/wake failure family in #75043 and #91140, while adding a distinct signature: repeated
task_progresswithlast_tool_name=TaskStopand no parentResultMessage.Set spawn depth 1, land agent roles through installers, document roles