A subagent resumed with `SendMessage` reports its completion to the main conversation instead of the subagent that resumed it
Summary
When a subagent uses SendMessage to resume a completed subagent it previously spawned, the resumed agent's completion notification is delivered to the main conversation, not to the subagent that resumed it. The resumer is never notified and never learns the result.
This contradicts two things at once:
- The
SendMessagetool response, which explicitly promises the sender:
> Agent "<id>" was stopped (completed); resumed it in the background with your message. **You'll be notified when it finishes.** Output: <path>
- The documented guarantee for nested subagents (sub-agents → Let subagents spawn their own subagents):
> Nested subagents suit a delegated task that itself splits into parallel subtasks, such as a reviewer subagent that dispatches a verifier per finding, so the intermediate output never reaches your main conversation. Only the top-level subagent's summary returns to you.
and (agent-teams, comparing subagents to teams):
> Context: Own context window; results return to the caller
The docs' own worked example — a reviewer subagent that dispatches verifiers — is exactly the shape that breaks. The first round (spawn → completion) works correctly and wakes the reviewer, which establishes the pattern; the second round (resume → completion) silently routes two levels up instead.
Environment
- Claude Code 2.1.220
- macOS 26.3.1 (darwin, arm64), zsh
- Main session: Opus; intermediary subagent: Sonnet; leaf subagent: Haiku
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMSnot set (SendMessageis available without it, as documented)- Default subagent spawn depth (3 layers)
Expected
Agent A — which spawned B, was woken by B's first completion, and then resumed B with SendMessage — is notified when B finishes, as the tool response promised. B's output does not reach the main conversation.
Actual
- A is never activated again. Confirmed from A's own context in a follow-up probe:
notified-of-child-completion: NO,child-final-result-known-to-me: I never received it, and "nothing else entered my context between my stop #2 report and now". - The main conversation receives B's completion as a
task-notificationnaming the leaf agent, including its<usage>telemetry — two levels below where the work was dispatched. - A's own notification is not even gated on that child: A stopped ~3s after sending, and main was notified while B still had ~27s of work left.
Minimal reproduction
Two agents, deterministic, cheapest models.
- From the main session, spawn one background agent A (Sonnet) with this prompt:
SEGMENT 1
1. Spawn exactly ONE background subagent with the Agent tool:
description: "leaf", subagent_type: "claude", model: "haiku", run_in_background: true
prompt: "Call the Bash tool exactly ONCE, in the FOREGROUND, with command
`python3 -c 'import time; time.sleep(25)' && echo SEG1` and timeout 60000.
Do NOT set run_in_background. Do NOT use Monitor. Do NOT spawn subagents.
Your final text output must be exactly: SEG1"
Record the agentId from the tool response.
2. The instant that returns, end your turn with exactly: "stop #1"
SEGMENT 2 - runs only when you are next activated
3. Call SendMessage with to: <the agentId you recorded>, summary: "round 2", message:
"Call the Bash tool exactly ONCE, in the FOREGROUND, with command
`python3 -c 'import time; time.sleep(20)' && echo SEG2` and timeout 60000.
Do NOT set run_in_background. Do NOT use Monitor. Do NOT spawn subagents.
Your final text output must be exactly: SEG2"
4. The instant that returns, end your turn, reporting the verbatim SendMessage response.
SEGMENT 3 - runs ONLY if you are activated yet again
5. Report "stop #3" and the verbatim text that activated you.
- Watch the main session's notifications.
Observed: segment 3 never runs. The main session receives a task-notification for the leaf with result SEG2.
Reproduced 2/2 as designed. A third variant (a depth-2 agent resuming an unrelated completed agent in a different branch by raw id) showed the same routing, so this is not specific to the parent/child relationship — it applies to any transcript-resume.
Evidence — timestamps from one run
From the subagent transcripts (<session>/subagents/agent-<id>.jsonl):
13:38:06.796 A starts
13:39:59.246 B starts (spawned by A, background)
13:40:15.321 B stop #1 -> A is correctly woken by B's task-notification
13:40:31.470 A calls SendMessage(to=B)
response: "...was stopped (completed); resumed it in the background with your
message. You'll be notified when it finishes."
13:40:31.482 B sees: "Another Claude session sent a message while you were working:"
13:41:01.670 B stop #2, result SEG2 -> notification delivered to MAIN, not to A
13:41:02.292 A stop #2 -> A never activated again
Second run, showing the gating asymmetry:
13:44:26.879 A calls SendMessage(to=B)
13:44:29.823 A stops -> main is notified immediately, while B is still running
13:44:56.842 B finishes -> notification delivered to MAIN
The mechanism, as far as black-box testing can show
I ran a controlled matrix (16 cells, ~26 agent instances) isolating this. The distinguishing variable is the target's state at SendMessage time, and the two paths are already visibly different in their responses:
| Path | Response message | Parent link |
|---|---|---|
| Target running | Message queued for delivery to <id> at its next tool round. | preserved — child's completion wakes the spawner |
| Target completed | Agent "<id>" was stopped (completed); resumed it in the background with your message. You'll be notified when it finishes. | lost — completion goes to main |
Note the inversion: the path that promises nothing correctly wakes the sender; the path that promises notification does not.
The behavior is otherwise internally consistent — a transcript-resumed agent isn't treated as the resumer's child for gating either (hence the second timestamp block above). So this reads like resume registering a fresh session-level task rather than re-attaching to the resumer, which lines up with the documented note that "Resuming a subagent that already finished takes a fresh slot without checking the limit" and "Resuming starts a new run of the agent under the same ID".
For contrast, plain spawning is correct at every depth I tested: a 3-level chain (main → A → B → C) delivered each completion to exactly its spawner, and the main conversation received exactly one notification.
Impact on multi-level agent systems
- Supervisors sleep forever on any work dispatched by resume. The natural review loop — spawn a worker, read its output,
SendMessagecorrections — deadlocks on round two. It doesn't error or hang; the supervisor simply completes, so the failure is silent and easy to misread as the worker having done nothing. - Deep agents' reports and cost telemetry surface at the session root, mixed into top-level work, defeating the documented context-isolation benefit that is the main reason to nest subagents.
- The intermediate supervisor loses the result permanently. There's no inbox and no polling affordance; its context simply never receives anything.
- No workaround is discoverable from inside a session. A child cannot notify its spawner: spawn
descriptions are not routable names, an agent cannot learn its own id, and the only upward route (to: "main") skips every intermediate level and lands at the session root — which is the bug's symptom, not a fix.
Suggested resolution
Either would close it:
- Preferred: make transcript-resume preserve the resumer as parent, so the resumed agent's completion wakes the resumer — matching both the promise and the already-correct queued-message path.
- Minimum: make the response text truthful when the sender is not the session root, e.g. "resumed it in the background with your message; its completion will be reported to the main conversation, not to you." This still leaves multi-level supervision impossible, but it stops the silent deadlock.
Secondary: task-notification wording
Every task-notification carries:
A task-notification fires each time this agent stops with no live background children of its own.
Two cases contradict this, both reproduced:
- Background
Bashtasks don't gate it (n=2). An agent started a 30s background shell and stopped; its notification fired after 13.7s, while the shell was still running. - Resumed agents don't gate it (see the second timestamp block).
Suggest narrowing the wording to child agents.
What I ruled out
To save triage time, these were tested and are not defects:
- Plain background spawn at depths 1–3 — correct, one notification to main, nothing leaks.
- Nested
run_in_background: false— honored, including with a 240-second child (duration_ms: 249725, result returned inline, no fallback note). I could not reproduce an async promotion at 4 minutes. - Delivery to a parent that is mid-turn — works; the notification is injected at the parent's next tool round.
"Another Claude session sent a message"framing — documented behavior.- Unreachable
SendMessagenames failing withsuccess: false— no session-top fallback exists, and none is documented.
Caveats
- Every agent in my session was nameless; the
Agenttool exposed nonameparameter, so I exercised the agent-id path only. The sibling roster (which supplies routable names) never appeared, as documented, because no agent had a name. - Not tested with
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1, where naming and the roster exist. I'd expect the same routing, but I haven't shown it. - Single machine, single session, interactive mode. No claim about
--print, headless/cron runs, or the Agent SDK.
Related but distinct: #50572 (background shell death + sync→async promotion; closed as not planned) independently matches a side-observation of mine that a subagent stopping with a live background shell is never re-woken and that shell's output is lost. It does not cover notification routing.
4 Comments
Additional data point that broadens the repro: this does not require a background spawn at all, so it shouldn't be characterized as a background-tasks edge case.
The spawn mode of round one is irrelevant
I re-ran the repro with the first round dispatched synchronously (
run_in_background: false), so the child's result came back to A inline rather than as a notification. Everything else identical.Result: B's second completion was delivered to the main conversation —
task-id aef98ae5eda29f32f | summary: Agent "CELL-2R-B leaf" finished | result: CELL-2R-B-SEG2-OK— and A was never activated again. A's own notification also fired to main while B was still running, i.e. no gating, matching the original report.So after round one B is a completed agent regardless of how it was spawned, and resuming a completed agent is the sole trigger.
run_in_background: falseremoves notification routing from round one only; it provides no protection for round two. Worth noting because "just dispatch synchronously" is an intuitive workaround that does not work.The defect is localized to who resumes
Summarizing across the cells, the distinguishing variable is whether the resumer is the session root:
| Resumer | Target | Who is notified | Correct? |
|---|---|---|---|
| Main conversation | its own completed subagent | main | ✅ works |
| A subagent | a completed subagent (spawned in background) | main, never the resumer | ❌ |
| A subagent | a completed subagent (spawned synchronously) | main, never the resumer | ❌ |
| A subagent | a still-running subagent (queued path) | the resumer is correctly woken on completion | ✅ works |
The main-conversation case working is what makes this hard to notice: the pattern is verified at the top level, then silently fails the moment the same idiom is used one level down — which is exactly the nested-supervisor arrangement the subagent docs recommend.
The queued-path row is also worth calling out as a hazard: messaging a running child preserves the parent link correctly, so whether a supervisor's follow-up works depends on whether the child happened to finish first. That makes the failure timing-dependent and intermittent in real workloads, rather than deterministic.
The timestamp data and controlled matrix here make the root cause hypothesis very concrete. The "fresh session-level task" framing lines up with what you're seeing: on resume, the agent gets re-registered as a root-level task rather than re-parented to the resumer, which is why completion routes to main instead of to the agent that called
SendMessage.The practical consequence you identified in point 4 is the most severe: there is no in-session workaround discoverable from inside the agent, because a child cannot learn its own id and the only upward route (
to: "main") is the bug's symptom. Any multi-level supervisor pattern that needs to dispatch corrections after reading a child's output hits a silent deadlock on the second round.Your suggested resolution order makes sense. The minimum bar -- accurate response text when the resumer is not the session root -- stops the silent failure mode that makes this hard to diagnose. The preferred fix (preserve the resumer as parent on resume) is what makes multi-level supervision architecturally reliable.
One clarifying question: in the case where the target is already running when
SendMessageis called (the "Message queued" path), does a subsequent resume of that same agent by the original spawner also route correctly? The table suggests yes (parent link preserved on the queued path), but confirming that the routing split is specifically on the completed-then-resumed path vs. running-when-messaged would help scope the fix.This is a significant gap for any team running review-loop or supervisor-worker patterns at more than one level of nesting. The controlled repro steps here should make it straightforward to add a regression test once the fix lands.
Confirming on 2.1.226 with a different parent type and configuration than the matrix covers, plus a larger observational sample.
Also — #81438 is the same bug, and neither thread references the other. That one has the bundle-level mechanism (the live-resume registrar never reads
ownerAgentId); this one has the controlled matrix. Anyone landing on either is seeing half the case.Environment
CLAUDE_CODE_FORK_SUBAGENT=1,CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1forksubagent (depth-1) dispatching depth-2 childrenn=74 completions across 8 parent agents. Production work, not synthetic.
| Path | → dispatching parent | → session root |
|---|---|---|
| First completion (fresh
Agentdispatch) | 52 | 0 || Completion after
SendMessageresume | 0 | 22 |Zero exceptions in either direction.
Cells this adds to the matrix in the OP (which disclaims teams):
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1— same routingsubagent_type: "fork"as the parent — same routingNot timing or dormancy. A resume completion landed at root while the parent was awake and mid-turn; a fresh completion landed correctly at a parent that was dormant. Timestamps on both sides.
Consistent with the OP's discriminator: all 22 of our resumes targeted completed agents. We have no counter-sample of messaging a still-running one, so nothing here contradicts "target state at
SendMessagetime is the trigger."Methodology note for anyone reproducing: our transcript JSONL under-records deliveries to the intermediate parent and does not under-record deliveries to root. Two parent-side deliveries were provable only by content — the parent quoted a hash appearing nowhere else in the session. A naive count off transcripts will over-report re-parenting.
Impact is silent. The intermediate parent logs the dispatch as fired, never sees a result, and waits on work that already finished. The documented guarantee is the opposite —
sub-agentsuses "a reviewer subagent that dispatches a verifier per finding, so the intermediate output never reaches your main conversation" as its worked example, and the agent-teams comparison table states results return to the caller.Workaround, same as #81438's: an intermediate parent never resumes a child — every follow-up is a fresh dispatch under a new label. Costs one cold read.
This is precisely what I've done as well. I built a pre-tool use hook that would block resume calls for subagents of subagents, explaining the bug and recommending a fresh start.
This does, however, cost more due to cold reads as there's no caching enabled in this case.