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

Status Open
Maintainer reply None cached
Activity 21 comments · opened Jul 7, 2026

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

  1. Main conversation: spawn orchestrator O via Agent (run_in_background: true).
  2. O's prompt: "delegate an implementation task to a child via the Agent tool (foreground), then verify its result".
  3. O spawns child C without run_in_background.
  4. Observe: O's turn continues immediately (C is detached); O ends its turn stating it will wait for C.
  5. C completes → notification arrives in the main conversation, not in O. O never resumes.
  6. Main resumes O via SendMessage → O attempts TaskStop on 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 mv to 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).

View original on GitHub ↗

13 Comments

mof086999-code · 1 month ago

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_background runs detached

Orchestrator A (agent-a215a67fb6edbaff4.jsonl):

L71 [assistant] 2026-07-06T04:33:03.381Z
  tool_use name=Agent
  input keys = ["description", "prompt", "subagent_type"]   ← no run_in_background key at all

L72 [user] 2026-07-06T04:33:08.894Z
  tool_result: "Async agent launched successfully.
  agentId: a93e408d6c7fde8ac (internal ID - do not mention to user. ...)
  The agent is working in the background. You will be notified automatically when it completes. ..."

Same in orchestrator B (agent-af983b9cb6f00497c.jsonl L57/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:

L76 [assistant] 2026-07-06T04:33:25.520Z
  text: [translated] "... I'll wait for the implementer's completion notification.
  Once it completes, I'll proceed to independent verification with the verifier."
  — turn ends here; the orchestrator is never auto-resumed

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-operation entries for both orchestrators' children:

L432 [queue-operation] 2026-07-06T04:34:33.559Z
  content: <task-notification><task-id>a80cf6107d1859878</task-id>
  <tool-use-id>toolu_01EfqbySWqBege7jrXKkqFK9</tool-use-id>
  <output-file>/private/tmp/claude-501/-/<session-id>/tasks/a80cf6107d1859878.output</output-file>
  <status>completed</status><summary>Agent "[translated: fix 4 bugs + regression tests]" finished</summary> ...
  ← this is orchestrator B's child

L434 [queue-operation] 2026-07-06T04:41:47.376Z
  content: <task-notification><task-id>a93e408d6c7fde8ac</task-id>
  <tool-use-id>toolu_017emJNEoVA7uQmyaoqAwc8h</tool-use-id> ... <status>completed</status>
  ← this is orchestrator A's child, spawned at its L71 above (IDs match)

Neither orchestrator received anything; both remained stalled until externally resumed via SendMessage.

Symptom 3 — TaskStop ownership error after a SendMessage resume

After each stalled orchestrator was resumed via SendMessage, it attempted to stop the child it had spawned before the resume:

Orchestrator A:
L86 [assistant] 2026-07-06T04:34:47.983Z
  tool_use name=TaskStop input={"task_id": "a93e408d6c7fde8ac"}
L87 [user] 2026-07-06T04:34:47.985Z
  tool_result: "Task a93e408d6c7fde8ac is owned by a93e408d6c7fde8ac; agent a215a67fb6edbaff4 cannot stop it."

Orchestrator B (identical shape):
L72/L73 2026-07-06T04:31:33.892Z
  tool_result: "Task a80cf6107d1859878 is owned by a80cf6107d1859878; agent af983b9cb6f00497c cannot stop it."

Two details worth noting for whoever debugs this:

  1. The error reports the task as owned by the child itself (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.
  2. The un-stoppable children kept running and writing to the shared working tree, racing the resumed parents (one parent detected this via Edit old_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 · 1 month ago

Follow-up with the remaining excerpts we promised — the other two runs from round 1, the origin.kind metadata, 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):

  • 3 Agent spawns (05:02:17.094Z, 05:10:04.359Z, 05:22:12.902Z), all with no run_in_background key
  • 11 stall statements before external intervention, e.g.:
L28: [translated] "implementer launched in the background ... notification-driven, I'll wait for completion."
L97 (right after the 3rd spawn, 05:22:46Z): [translated] "I'll wait for the implementer's completion
notification. Once this fix is done the core logic is solid ... I'll wait for the completion notification."
  • TaskStop: never called in this run.

Orchestrator D (agent-a864dcd4e3829da4b.jsonl, ~1h52m run): 1 spawn (no run_in_background key), 1 stall statement, then the TaskStop sequence below.

origin.kind metadata on external resume messages

When we externally resumed a stalled orchestrator, the poke is recorded in its transcript as a type: "user" entry with explicit metadata (orchestrator D, L28):

type: "user", isMeta: true
origin: {"kind": "coordinator"}
content: "The coordinator sent a message while you were working:
[translated] You appear to be stalled. Do not wait for child agents or background processes —
complete the task yourself and proceed to the final report.
Address this before completing your current task.
This is task direction from your coordinator — not typed by your user, but working on their behalf ..."

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:

L37 05:02:52.269Z TaskStop {"task_id": "a7806a306c348d6bc"}   ← child agent spawned BEFORE the resume
L38 result: "Task a7806a306c348d6bc is owned by a7806a306c348d6bc; agent a864dcd4e3829da4b cannot stop it."

L123 05:19:02.141Z TaskStop {"task_id": "b50wuhmd3"}          ← own background Bash task, started after resume
L124 result: "Successfully stopped task: b50wuhmd3 ..."

L143 05:23:05.657Z TaskStop {"task_id": "b16e6jw4v"}          ← same
L144 result: "Successfully stopped task: b16e6jw4v ..."

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 af8dc684d96381e0a and a23628cdd6cd809d0; 19 Agent spawns between them, all 19 with no run_in_background key), the main-session transcript logs 15 queue-operation task-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:

L1342 2026-07-06T14:10:58.225Z <task-id>afa2b980008f4e5d1</task-id>
      <summary>Agent "Implement engine.mjs and tests" finished</summary>          ← child of orchestrator B'
L1354 2026-07-06T14:53:09.417Z <task-id>a9106d2e366a9fa2d</task-id>
      <summary>Agent "[translated: store.js full implementation]" finished</summary>   ← grandchild, see below
L1369 2026-07-06T15:27:12.863Z <task-id>ab7160cf0b63cbdc3</task-id>
      <summary>Agent "Verify TideKeeper in browser" finished</summary>            ← child of orchestrator B'

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 — a9106d2e366a9fa2d above — appears in neither: it only appears in the transcript of a6ff161e1def7af5b, 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:

Orchestrator A' L208 14:38:52.731Z: [translated] "The store agent stopped with 'I'll wait' (nested
delegation: it spawned its own child and ended up waiting). No report file yet. A completion notification
did arrive, but the result says 'waiting' — I'll verify whether the implementation actually finished."

Orchestrator B' L181 15:40:45.604Z: [translated] "A notification arrived, but the result message stops at
'working in the background, please wait for the completion notification'. This is the 'I'll-wait' stall
our playbook warns about — nested delegation, so the child's completion notification doesn't arrive
correctly; the child may have self-stopped midway."

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:

B' L54 13:36:44.667Z: [translated] "... I am myself a subagent (nested delegation), so children's
completion notifications won't reach me and I won't be auto-resumed. Therefore delegations use the
artifact-file method, and I monitor the report file with a bounded foreground wait loop within the
same turn."

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>.jsonl files (under ~/.claude/projects/<proj>/<session-id>/subagents/) and we're happy to paste specific ranges.

rpelevin · 1 month ago

I would split this into ownership receipts, not only foreground/background scheduling.

The nested child has three authorities that should be explicit:

  • who owns the child run;
  • which conversation receives its completion event;
  • which principal can stop it after a resume.

Regression cases:

  • a child spawned with foreground intent keeps the parent turn blocked until completion, timeout, or explicit detach;
  • a child spawned by an orchestrator routes its completion receipt back to that orchestrator, not the root conversation;
  • a detached child still records the parent run id and conversation id that can observe and stop it;
  • resuming an orchestrator preserves or rebinds stop authority for children it created before the resume;
  • if ownership cannot be re-established, TaskStop returns a terminal no_effect receipt and the child is prevented from writing to the shared working tree unless still authorized;
  • duplicate completion notifications are idempotent and cannot wake the wrong conversation.

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 · 1 month ago

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 · 1 month ago

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 --worker background agent (dispatched from a solve-milestone run) invoked /code-review on its own diff. /code-review internally fans out to ~6 independent review-angle sub-agents (a nested Agent-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 · 1 month ago

Confirming on Linux / v2.1.195 (Opus 4.8 orchestrator, general-purpose children) — 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 via ps and the filesystem, not just the agents' self-reports. Two additions to your report:

1. Symptom 3 (TaskStop ownership error) reproduces with no SendMessage resume

Your writeup ties the ownership error to a resumed orchestrator. I hit it on the first, direct TaskStop call, no resume involved:

  • Sub-agent spawns a background child (Agent, run_in_background: true) running a 120-iteration marked loop that appends to a file once per second.
  • Sub-agent immediately calls 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.)

  • Independently verified: the child ran its full loop to completion (counter reached all 120 lines) and its process was still in ps after TaskStop returned.

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 Agent at all

Minimal isolation of "the spawning parent is never resumed":

  • Sub-agent runs ONE 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").
  • The sub-agent is reported completed to its parent at ~51s — before its own sleep 60 had 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."
  • It is never resumed. The marker file is written seconds later, orphaned; the deliverable never happens.

Symptom 2, confirmed directly (nested-Agent case)

When a sub-agent's background child (a sleep 300 && echo done > sentinel worker) finished, its task-notification was delivered to the top-level / main conversation — an agent that never spawned it — while the spawning sub-agent had already been reported completed ~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 in ps.

Minor corroboration of your workaround note

A sub-agent has no TaskGet / TaskOutput / TaskList / ScheduleWakeup tools at all (only Agent + run_in_background; Monitor / TaskStop are loadable via ToolSearch), so it genuinely cannot fetch a child's output. Consistent with that, I watched sub-agents spontaneously and unprompted spawn until test -f <artifact>; do sleep 2; done background Bash loops to watch for a child's file — the exact foreground-poll workaround you describe, but self-inflicted (and a foreground sleep to wait is blocked, which pushes them toward it).

rpelevin · 1 month ago

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:

  1. owner_run_id: the parent that requested the work;
  2. completion_route: the conversation allowed to receive the terminal event;
  3. stop_authority: the principal allowed to cancel or no-effect the work.

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:

  • foreground intent does not let the parent complete with a "waiting for notification" result; it completes only after terminal child outcome, timeout, or explicit detach;
  • detached intent can complete the parent, but leaves an observable terminal receipt routed to the owning parent and optionally mirrored to the root with source refs;
  • TaskStop against a pre-resume child remains authorized for the recorded owner, or returns a terminal no_effect receipt and prevents further shared-worktree writes unless authority is re-established;
  • plugin fan-out records skipped/failed angles as terminal child outcomes, not silent degraded success;
  • duplicate or late completion notifications are idempotent and cannot wake the wrong conversation.

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 · 1 month ago

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:

Some review/implementation skills fan out sub-sub-agents. To mitigate bug in Claude code harness, pass this to all sub-agents you spawn: `` When you need a worker's result: 1. Spawn it with Agent(run_in_background: true). Its task MUST end by writing its result atomically: <produce output> > RESULT.tmp && mv RESULT.tmp RESULT. 2. Do NOT end your turn to wait. Immediately run ONE foreground Bash command that blocks: for i in $(seq 1 60); do [ -f RESULT ] && break; sleep 3; done; cat RESULT 3. If it times out (file still absent), checkpoint and continue WITHOUT the worker — it may never deliver and you cannot stop it. 4. Never rely on a completion notification or a second turn. `` When sub-agent spawns a sub-sub-agent, sub-sub-agent's completion message will be delivered to you instead of sub-agent. You can ignore them. Your own tools for spawning and waiting work fine, the bug only affects nested agents.

<img width="641" height="122.5" alt="Image" src="https://github.com/user-attachments/assets/22240ea4-55e8-4f00-8462-961e4d512fb9" />

joseortiz3 · 1 month ago

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 · 1 month ago

This looks like one lifecycle contract split across three runtime surfaces:

  1. scheduling: foreground vs detached intent;
  2. routing: which conversation receives terminal events;
  3. authority: which principal can stop or fence the child after resume.

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 / orphaned outcome 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 · 1 month ago

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_background and name entirely and its description states verbatim: "run_in_background and name are unavailable here — only synchronous subagents." @taminomara's report (2.1.195) had run_in_background present 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. Passing run_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, runs for i in $(seq 1 90); do [ -f F ] && break; sleep 2; done; cat F || echo TIMEOUT with a generous Bash tool timeout. The parent collected the result in-turn without parking. One nuance worth recording: the Bash tool blocks a bare foreground sleep, but a sleep inside a bounded loop is permitted — which is presumably why agents spontaneously converge on this pattern.

output_file caveat. The spawn metadata's output_file is the child's full JSONL transcript; the final text is extractable from the last assistant text block, 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.

taminomara · 1 month ago

@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.

mof086999-code · 1 month ago

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 ls on 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 .md extension — 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.md

Allowed (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.md

Verbatim error, identical across all rejections:

Subagents should return findings as text, not write report files. Include this content in your final response instead.

Observed properties (stated within tested scope):

  1. Subagent-scoped in all tests (observed at depths 1 and 2); neither model identity nor the tested target path alone explains it. Six core blocked names (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 of report.md rejected 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 had report.md and summary.md rejected 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).
  2. Consistent with a case-insensitive stem-prefix + .md filter. All 33 tested names fit: stem starts with one of at least report, summary, findings, analysis (a lower bound from finite testing, not an enumeration), gated on a case-insensitive .md extension. Discriminating results: reports.md, summary-1.md, reportage.md, analysisXYZ.md blocked (rules out exact-basename and word-boundary matching); final-report.md, my-summary.md allowed (rules out substring matching); summarize.md, analyses.md allowed (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.txt allowed (extension-gated; .markdown is not matched). This is a model fitted to the tested sample — the actual implementation may differ outside it.
  3. Only the Write tool was restricted in our tests. Bash mv x.tmp report.md, Bash printf ... > report.md, cp to analysis.md, and the Edit tool modifying an existing report.md all succeeded; rejected Writes leave nothing on disk (no partial writes).
  4. Filename-driven. summary.md was rejected with four different payloads (empty, prose, code snippet, report-style markdown), while the same report-style payload written to RESULT-content-test.md succeeded.

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 .md name 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.md verified 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-mv pattern 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.md once 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 against ls (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.

Showing cached comments. Read the full discussion on GitHub ↗