anthropics / anthropics/claude-code

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)

Open
#81,438 2 comments 1 reaction 0 assignees View on GitHub
area:agents
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

## Environment

- Claude Code 2.1.220, macOS (darwin 25.5.0)
- Interactive session; no agent-teams flag; `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` unset
- 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

1. Main session spawns orchestrator **O** via the Agent tool (`general-purpose`, `run_in_background: true`, unnamed).
2. O spawns child **C** (`general-purpose`, `run_in_background: true`, unnamed) and ends its turn.
3. C completes → O is re-invoked with C's task-notification, result inline. ✅ correct routing.
4. O calls `SendMessage(to: )`. 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.
5. 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 `SendMessage` from 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 `ownerAgentId` from 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 by `Agent ${e} is already running or being resumed`) reads the old registry record for `spawnDepth`/`startTime`/`result` but **never reads its `ownerAgentId`**, and re-registers with `ownerAgentId: ()` — 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: false` on 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)"`) stamps `ownerAgentId: parentAgentId ?? ` — 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=1` makes 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's `Output:` 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 `SendMessage` to 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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at the bundled SendMessage resume handler identified by the "resumed it in the background with your message" and "Agent ... is already running or being resumed" anchors, then compare it with the initial spawn and disk-restore registrars. Reproduce the nested O/C sequence and verify that a resumed child's completion re-invokes O rather than the top-level session.

Written by the indexing model from the issue text.

Assessment

Domain
ai-infra-agents
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.