Skip to content

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

Open

Description

@mof086999-code

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

Activity

  1. mof086999-code commented on Jul 7, 2026

    @mof086999-code
    Author

    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.

  2. mof086999-code commented on Jul 7, 2026

    @mof086999-code
    Author

    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.

  3. rpelevin commented on Jul 7, 2026

    @rpelevin

    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.

  4. kcarriedo commented on Jul 7, 2026

    @kcarriedo

    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.

  5. kenmulford commented on Jul 7, 2026

    @kenmulford

    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

  6. taminomara commented on Jul 7, 2026

    @taminomara

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

  7. rpelevin commented on Jul 7, 2026

    @rpelevin

    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.

  8. taminomara commented on Jul 8, 2026

    @taminomara

    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.

    Image
  9. joseortiz3 commented on Jul 13, 2026

    @joseortiz3

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

  10. 0xbrainkid commented on Jul 13, 2026

    @0xbrainkid

    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.

  11. vstratful commented on Jul 19, 2026

    @vstratful

    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.

  12. 1 remaining item

  13. mof086999-code commented on Jul 19, 2026

    @mof086999-code
    Author

    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.

  14. added a commit that references this issue on Jul 31, 2026
  15. bcherny commented on Aug 15, 2026

    @bcherny
    Collaborator

    Reproduced on v2.1.233 (macOS) — partially. Of the three symptoms, symptom 3 (TaskStop ownership error) reproduces and is a real bug; symptom 1 (nested child runs in the background without run_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

    1. claude (v2.1.233), fresh project directory.
    2. Ask the main conversation to spawn an orchestrator subagent with run_in_background: true, whose prompt is: spawn one child via the Agent tool without setting run_in_background, then write the Agent tool's raw result verbatim to a file.
    3. Second run: same, but the orchestrator spawns a background child (sleep 120) and immediately calls TaskStop on the returned agent id, writing the result to a file.

    Observed

    • Run 1: the orchestrator's Agent call returned immediately with Async agent launched successfully. ... You will be notified automatically when it completes. — the child's answer was not in the tool result (symptom 1).
    • Run 1, ~26 s later: the child's <task-notification> was delivered to the orchestrator, which was resumed and processed it. Symptom 2 did not reproduce in this simple case.
    • Run 2: TaskStop from the orchestrator on the child it had just spawned (no resume involved) failed with Task 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

    • A subagent that spawns a child should be able to stop it, or the tool should say up front that it can't.

    Assessment:

    • Symptom 1 is working as designed: with fork mode on (the default in interactive sessions) every subagent Claude spawns runs in the background, nested or not, and its result arrives as a completion notification rather than as the Agent tool's return value — see https://code.claude.com/docs/en/sub-agents#run-subagents-in-foreground-or-background. The tool result's "you will be notified when it completes" is the intended model.
    • Symptom 2 looks fixed by v2.1.233 in the case we tested (the notification went to the spawning subagent, which was resumed). If you still see child notifications landing in the main conversation on v2.1.233 or later, a fresh transcript would help — the earlier reports here are from v2.1.195–v2.1.215.
    • Symptom 3 is a genuine bug: the ownership check that TaskStop applies 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 by TaskStop. 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

  16. added
    reproducedBug reproduced by maintainers on a released build
    on Aug 15, 2026
  17. bcherny commented on Aug 17, 2026

    @bcherny
    Collaborator

    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:

    1. Foreground nesting (your failure 1): appears fixed. A child spawned by an orchestrator subagent with run_in_background: false now 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_DEPTH controls it.
    2. Notification routing (your failure 2): confirmed, still reproduces on 2.1.233. A background child's completion notification is delivered to the top-level conversation, not the orchestrator that spawned it, and the orchestrator is not auto-resumed — it has to be manually re-messaged. This overlaps with open issue [Bug] Subagent results route to root teammate instead of spawning teammate #69212.
    3. TaskStop ownership after resume (your failure 3): could not reproduce in headless testing (by the time the resumed orchestrator acted, the completed child had been reaped). 2.1.203 also fixed TaskStop/TaskOutput failing to find agents spawned by another agent, which may cover part of what you saw. If you still hit the ownership error on a current release, a fresh transcript would help.

    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

  18. vstratful commented on Aug 18, 2026

    @vstratful

    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: false and 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_background isn'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 declares additionalProperties: 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: false and true. 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:

    Task ad06a6f3c28043124 is owned by ad06a6f3c28043124; agent a33e3b00329d5e81c cannot stop it.
    

    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_ms 81006, 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_SUBAGENT unset, interactive not headless.

  19. taminomara commented on Aug 18, 2026

    @taminomara

    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:

    I'm trying to reproduce a bug in claude code harness related to sub-agents.
    
    Spawn a sub-agent in background with the following prompt:
    
    ```
    I'm trying to reproduce a bug in claude code harness related to sub-agents. You are the first-level sub-agent, your task is to check whether you can spawn a second-level sub-agent and wait for it normally, without having to invent some work arounds.
    
    1. Spawn an agent using Agent tool, instruct it to run the following Bash command: `for i in $(seq 1 60); do sleep 3; done; echo "hello, world" > probe.txt`. After command finishes, the agent should stop with message "Done".
    
    2. Yield by responding with a message that doesn't contain any tool calls. This should, in theory, put you in waiting mode and resume your execution when spawned agent finishes.
    
    3. When resumed, reply with a message describing your interactions with tools, and how they worked.
    ```
    
    While this sub-agent runs, report if you see any messages that were sent from sub-sub-agent to sub-agent. After your sub-agent returns, relay its output verbatim.
    

    Item 3:

    I'm trying to reproduce a bug in claude code harness related to sub-agents.
    
    Spawn a sub-agent in background with the following prompt:
    
    ```
    I'm trying to reproduce a bug in claude code harness related to sub-agents. You are the first-level sub-agent, your task is to check whether you can spawn a second-level sub-agent and then stop it.
    
    1. Spawn an agent using Agent tool, instruct it to run the following Bash command: `for i in $(seq 1 60); do sleep 3; done; echo "hello, world" > probe.txt`. After command finishes, the agent should stop with message "Done".
    
    2. Try using TaskStop to stop it before its bash operation completes.
    
    3. If TaskStop works, report so. If it doesn't, wait for it normally by sending a message without tool calls, then report failure.
    ```
    
    While this sub-agent runs, report if you see any messages that were sent from sub-sub-agent to sub-agent. After your sub-agent returns, relay its output verbatim.
    
  20. Zorast commented on Aug 26, 2026

    @Zorast

    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.

  21. mof086999-code commented on Aug 31, 2026

    @mof086999-code
    Author

    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-operation entries 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-json under a claude remote-control parent, CLAUDE_CODE_ENTRYPOINT=sdk-cli, CLAUDE_CODE_CHILD_SESSION=1, CLAUDE_CODE_ENVIRONMENT_KIND=bridge, CLAUDE_CODE_FORK_SUBAGENT/CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS unset; and (ii) two dedicated headless claude -p sessions (one per item-2 variant, nothing else running in them). The bridge session's main conversation and its general-purpose sub-agents ran on Fable 5 (inherited); the two claude -p sessions 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 two claude -p sessions 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

    • The first-level sub-agent's Agent tool schema contains run_in_background again. 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 (name still 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: false was honored in all 3 trials: each Agent call was accepted (no validation error), blocked for the child's full duration (child duration_ms 56387 / 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.
    • Omitting the key still detaches with "Async agent launched successfully. […] You will be notified automatically when it completes." — consistent with the documented background default (symptom 1 as WAD, per the docs link above).

    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_background key (input keys ["description", "prompt", "subagent_type"] in the transcript) and one with true; then one claude -p session per variant. Result identical in all 4:

    • The child's completion notification was enqueued and later consumed by the root conversation. Bridge session JSONL: enqueue L119 @ 23:48:14.171Z (child acc8bb3294655e72d), L124 @ 23:48:18.741Z (child a08eaa2de809e307d), each with the full result payload. Same pattern in both claude -p sessions (e.g. grandchild enqueue @ 01:42:21.570Z, removed at the root's next tool boundary @ 01:44:27.706Z).
    • No parent was ever resumed. All 4 parent transcripts contain zero task-notification entries; the last entry in each is its own yield message, unchanged minutes after the children completed (bridge session parents checked 3+ minutes after; claude -p sessions ran to session end).
    • About 1.6s after each parent yielded (child still live), the root received a completed task-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):

    • Active parent: a parent spawned a ~158s child and stayed in-turn via one long foreground Bash. The child's notification was enqueued @ 00:11:55.294Z and consumed at the parent's tool-result boundary @ 00:12:59.024Z — the parent (not the root) received it mid-turn and quoted the full payload (matching tool-use-id and duration_ms). One run.
    • SendMessage-resumed parent: a parent yielded, was resumed via SendMessage, then held an active turn while its child completed. Same result: enqueue @ 00:17:58.646Z, consumed by the parent @ 00:19:22.490Z at its tool boundary. 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-operation entries (operation: enqueue|dequeue|remove with 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):

    Task a08eaa2de809e307d is owned by a08eaa2de809e307d; agent aa8fef1e733232e6b cannot stop it.
    Task a3c109a484db45c3c is owned by a3c109a484db45c3c; agent a9eacd0c91039623f cannot stop it.
    Task a665b654d4198e783 is owned by a665b654d4198e783; agent aa00bb5f8803bdb99 cannot stop it.
    

    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_ms 196760 / 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 (status killed) 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.

  22. mof086999-code commented on Aug 31, 2026

    @mof086999-code
    Author

    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 (claude in 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 headless claude -p sessions 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 omitted run_in_background here (background by default) where the headless runs set it explicitly to true. Result: the yielded parent WAS auto-resumed by its child's completion (1/1).

    • The parent recorded the spawn metadata and yielded at 03:58:54.811Z; the child finished at 04:01:01.756Z.
    • The parent's transcript then contains the child's task-notification injected as a user-type entry at 04:01:03.142Z, followed by its post-resume report ("Resumed by task-notification. …").
    • Queue journal: the child's notification was enqueued @ 04:01:03.126Z and removed @ 04:01:03.142Z — millisecond-identical to the parent's resume entry, so the consumer is machine-verifiable.

    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 completed notification was enqueued almost immediately after its yield (~1.6s in the bridge session, ~23–33ms in the headless claude -p sessions).

    On one machine and one build, the observed outcomes aligned with session kind in these runs:

    Session kind Yielded parent, at yield time Child completion
    Interactive TUI (1/1 run) stayed waiting (no completion event enqueued) auto-resumed the parent
    Non-interactive — remote-control bridge worker, headless claude -p (4/4 runs) finalized as completed within ~2s did not resume the parent; consumed by root

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

  23. mof086999-code commented on Aug 31, 2026

    @mof086999-code
    Author

    Addendum: more controls, including a fork-gate 2×2 with raw schema capture. Result: the CLAUDE_CODE_FORK_SUBAGENT gate 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_background key 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 boolean true used 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 no run_in_background parameter 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_SUBAGENT is 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 omitted run_in_background and 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 actual tools array from the constructed API request bodies via OTEL_LOG_RAW_API_BODIES=file:… (raw bodies kept private; only the Agent schema entry was inspected):

    CLAUDE_CODE_FORK_SUBAGENT=0 =1 / default
    headless claude -p schema declares run_in_background (raw capture); yielded parent not resumed schema omits it (raw capture); yielded parent not resumed
    interactive TUI schema declares it (raw capture); yielded parent auto-resumed schema omits it (introspection, run 4); yielded parents auto-resumed (runs 1–3)

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

  24. mof086999-code commented on Sep 2, 2026

    @mof086999-code
    Author

    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 with reason: absorbed_mid_turn by the root in headless runs vs delivered_to_agent interactively, 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 with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1, whereas ours ran with it unset — so the flag does not appear to matter either way.

  25. cwj2001 commented on Sep 16, 2026

    @cwj2001

    Related non-interactive streamed-input postmortem.

    Environment

    • Linux
    • claude-agent-sdk==0.2.140
    • bundled Claude Code CLI 2.1.235
    • persistent Python SDK streamed-input host; spawned CLI uses --output-format stream-json
    • custom Anthropic-compatible gateway/model, not a clean first-party-provider reproduction

    Observed ordered lifecycle

    A dynamically supplied local_agent task starts inside one parent run. Its durable ordered journal then records:

    1. normal early tool activity;
    2. system/task_progress / TaskProgressMessage approximately every 6–12 seconds for more than 45 minutes;
    3. progress usage increasing from 45 to more than 370 tool uses;
    4. TaskProgress metadata reporting last_tool_name=TaskStop repeatedly during the later loop;
    5. no parent ResultMessage and 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

    • The only remaining claude OS process is the parent query CLI for this SDK session; it is alive and waits in epoll_wait between progress notifications.
    • No separate Claude subagent OS process remains.
    • The sandbox exec daemon is alive and idle; earlier Bash/Read work from this run completed. This is not an execd command hang.
    • The SDK host receives and persists every TaskProgressMessage in its run journal. It does not receive a final ResultMessage.

    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_progress with last_tool_name=TaskStop and no parent ResultMessage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:agentsbugSomething isn't workinghas reproHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOSreproducedBug reproduced by maintainers on a released build

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions