SendMessage-resumed nested subagent's completion notification routes to the top-level session, not the depth-1 parent that resumed it (first completion routes correctly)
Environment
- Claude Code 2.1.220, macOS (darwin 25.5.0)
- Interactive session; no agent-teams flag;
CLAUDE_CODE_DISABLE_BACKGROUND_TASKSunset - Spawn depth 3 available (default since 2.1.219)
Summary
A background depth-1 subagent that spawns a background depth-2 child receives that child's first completion notification correctly. But after the depth-1 agent resumes the completed child via SendMessage, the resumed run's completion notification is delivered to the top-level session, not to the depth-1 agent that resumed it — despite the SendMessage tool result promising "You'll be notified when it finishes." The depth-1 agent is left idle forever with no wake event, and the top-level session receives a completion for a child it never spawned or resumed.
This looks like the remaining slice of #75043: on 2.1.220 the initial-dispatch notification routing that issue reported broken now works (first completion reached the depth-1 parent in our runs), while the resume path still misroutes. Filing separately because the trigger (subagent resuming its own child, not main resuming the orchestrator), the baseline (first completion routes correctly), and the mechanism below are all distinct from what #75043 describes.
Repro
- Main session spawns orchestrator O via the Agent tool (
general-purpose,run_in_background: true, unnamed). - O spawns child C (
general-purpose,run_in_background: true, unnamed) and ends its turn. - C completes → O is re-invoked with C's task-notification, result inline. ✅ correct routing.
- O calls
SendMessage(to: <C's raw agent id>). Tool result:Agent "…" was stopped (completed); resumed it in the background with your message. You'll be notified when it finishes.O ends its turn to wait. - C's resumed run completes (~30s) → the task-notification (same task-id as step 3) appears in the main session's transcript. ❌ O is never re-invoked; a 5-minute watch confirmed it stays stranded. Only a manual
SendMessagefrom main to O's raw agent id recovers it.
Mechanism (static analysis of the 2.1.220 bundle)
The live SendMessage-resume path re-registers the task with a hardcoded main-session owner, discarding the owner stamp the first run carried:
- The initial background spawn derives
ownerAgentIdfrom the spawner (falling back to the main session id only when the spawner doesn't qualify) — which is why the first completion routes correctly. - The resume engine (string anchor:
; resumed it in the background with your message. You'll be notified when it finishes. Output:→ the function anchored byAgent ${e} is already running or being resumed) reads the old registry record forspawnDepth/startTime/resultbut never reads itsownerAgentId, and re-registers withownerAgentId: <main-session id resolver>()— a process-global that always resolves to the top-level main agent id in an interactive session. The calling agent's id is in scope in the same handler (used elsewhere in it) but is never consulted for the owner stamp. - The re-registration also resets
notified: falseon the same task-id, which is why one task-id legally notifies twice (once to the parent, once — post-resume — to main). - For contrast, the disk-restore resume registrar (string anchor:
"(resumed agent)") stampsownerAgentId: parentAgentId ?? <main>— it at least tries the parent. The live-resume path doesn't.
Suggested fix shape: derive the owner the same way the initial spawn does, falling back to the previous record's owner before the main-session fallback — e.g. ownerAgentId: deriveOwner(callerAgentId) ?? previousRecord.ownerAgentId ?? mainSessionId().
Adjacent observation: the resume tool result includes resumedAgentId exactly when the owner is (or falls back to) the main session, which suggests partial awareness of this gap in the current code.
Impact / workarounds
- Any nested orchestrator that resumes its own workers strands permanently; recovery requires a human noticing stray completions in the top-level session and manually resuming the orchestrator.
- No configuration lever fixes the routing.
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1makes the resume run foreground-inline (result returned in the tool result, no misroutable notification), but it also disables all background tasks including the child dispatch that creates the scenario. - Practical workaround: subagents should never
SendMessage-resume their own children — dispatch a fresh child instead (fresh dispatches get a correct owner stamp), or poll the resume result'sOutput:file.
Related issues
- #75043 — nearest match (v2.1.201): reported completion notifications never reaching the subagent parent, plus TaskStop ownership errors after resume. On 2.1.220 we observe the first-completion routing working, so that symptom appears at least partially resolved; this issue covers the still-broken resume path. (We did not re-test its always-async or TaskStop symptoms.)
- #77950 — grandchild
SendMessageto parent falls back to main (child-initiated messaging; likely sibling of the same missing parent-identity propagation, but a different path than system task-notifications). - #76681 — notification enqueued-but-undelivered for a SendMessage-resumed agent's orphaned Bash task (same resume-boundary bookkeeping, different task kind and observable).
- #69212 — teams framing of nested-result misrouting.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗