Agent tool `name` parameter silently switches to teammate protocol, losing background agent results

Status Open
Maintainer reply None cached
Activity 14 comments · opened Jun 27, 2026

Bug Description

When the Agent tool is called with a name parameter in a session that has (or has had) a team configuration, the spawn silently takes the teammate path instead of the regular background agent path. This causes the agent's results to be lost — the calling session never receives a task-notification with the work output.

Reproduction Steps

  1. Start a background session (--agent claude)
  2. Use the Agent tool without name — observe it returns "Async agent launched successfully" and eventually delivers results via task-notification
  3. Use the Agent tool with name parameter (e.g., name: "my-agent") — observe it returns:

``
Spawned successfully.
agent_id: my-agent@session-<id>
name: my-agent
The agent is now running and will receive instructions via mailbox.
``

  1. The named agent completes its work but sends idle_notification through the team inbox instead of a task-notification
  2. The calling session never receives the agent's results

Expected Behavior

The name parameter should only provide addressability (for SendMessage({to: name})), without changing the spawn path or result delivery mechanism. A background agent spawned with name should still deliver results via task-notification.

Actual Behavior

  • name parameter triggers teammate spawn path ("status": "teammate_spawned")
  • Agent is registered in ~/.claude/teams/session-<id>/config.json as a team member
  • Agent uses inbox-based idle_notification protocol instead of task-notification
  • Job state tracker records the agent in fan array but never receives completion signal → zombie entries
  • shutdown_request sent to the teammate does not terminate it — it continues sending idle_notification

Evidence from Real Session

Session 0287d9cb (Claude Code v2.1.185) experienced this:

| Condition | Spawn Response | Result Delivery | Outcome |
|-----------|---------------|-----------------|---------|
| No name param | "Async agent launched successfully" | task-notification, 3.8s | ✅ Result received |
| With name param | "Spawned successfully...via mailbox" | idle_notification | ❌ Result lost |

The session's own thinking confirmed the diagnosis:

"The subagents sent idle notifications but didn't report their findings. They seem to be using a team protocol that doesn't match the Agent tool's behavior."

Impact

  • Agent work results are silently lost (no error surfaced to user)
  • ~20 minutes wasted per occurrence (waiting + redoing work)
  • idle_notification messages pollute context window (~200 tokens each, recurring)
  • Job state fan array accumulates zombie entries that never clear
  • shutdown_request doesn't terminate the zombie teammates

Environment

  • Claude Code v2.1.185
  • macOS (Darwin 23.2.0, arm64)
  • Background session mode (--agent claude)

Suggested Fix

Either:

  1. Don't let name change the spawn path — always use the background agent protocol regardless of name; name should only add SendMessage addressability
  2. Or clearly document and error — if name triggers team mode, surface this as an explicit choice rather than a silent protocol switch

View original on GitHub ↗

8 Comments

yurukusa · 2 months ago

Your root-cause analysis looks right — the name parameter flips the spawn onto the teammate path, so completion arrives as an idle_notification through the team inbox instead of a task-notification to the caller. One thing worth adding, because it changes the severity: the results aren't actually lost — they're persisted to disk, and they're also still retrievable live.
The work is on disk. Each spawned agent persists its full turn-by-turn output to:

~/.claude/projects/<encoded-cwd>/<session-id>/subagents/agent-<id>.jsonl

I verified this layout first-hand on my own install (157 subagents/ directories, 440 agent-*.jsonl files, each holding the complete agent transcript — message, agentId, timestamp per line). The agent_id: my-agent@session-<id> your spawn returned maps to one of these files. So even though the calling session never got a task-notification, the named agent's actual output is sitting in …/subagents/agent-<id>.jsonl and can be read back:

ls ~/.claude/projects/*/<session-id>/subagents/agent-*.jsonl
tail -n 5 <that file>   # or jq over it

That turns a silent data-loss bug into a recoverable misrouting bug — useful both for anyone hitting this now and for triage severity.
It's also retrievable live. Because name registered the agent as a teammate (teammate_spawned, in ~/.claude/teams/session-<id>/config.json), it's addressable. Since it completed and sent idle_notification, it's idle-waiting in the inbox rather than gone — SendMessage({to: "my-agent"}) asking it to report its result should pull the output back through the team inbox. (That part is reasoned from the protocol you described, not verified on my end — but it follows from the agent being alive and addressable.)
Workaround: omit name for background agents whose results you want delivered to the caller via task-notification; only pass name when you actually want the teammate/mailbox addressability. The bug is that those two concerns are coupled — as you said, name should add addressability without changing the spawn path or the delivery mechanism.
For the maintainers: the fan-array zombie entry never getting a completion signal is the same coupling — a named background agent should still resolve its job-state row via task-notification, and addressability (SendMessage({to})) should be orthogonal to delivery. The data is preserved on disk either way, so this is a routing/notification bug, not loss.
(Not affiliated with Anthropic — the subagents/agent-*.jsonl persistence path is verified on my own install; the live SendMessage retrieval is inferred from the protocol you documented.)

kcarriedo · 2 months ago

This is a sharp edge. The name parameter looks like a display/addressability hint but it's actually a spawn-path switch -- nothing in the Agent tool signature signals that distinction. The silent switch to teammate protocol means the bug only surfaces when you go looking for results that never arrived.

The practical consequence in multi-agent orchestration: if you add name to improve observability (so you can address agents by name with SendMessage), you lose result delivery via task-notification. You can't have both in the current model, so you end up choosing between addressability and reliable result receipt.

Workaround while this is unresolved: avoid the name parameter and instead track agent identity via the session ID returned from the background spawn. If you need SendMessage addressability, register the session ID in a shared file (e.g., .claude/agents/registry.json) at spawn time and look it up by task name when you need to address it directly. Clunky but avoids the spawn-path switch.

The fix is a clear separation between name (addressability label, no spawn-path side effect) and something like teammate: true (explicit opt-in to the teammate protocol). The current implicit detection based on parameter presence is too easy to trigger accidentally.

Is the silent path switch observable in the Agent View / Workflows right pane? Seeing which protocol was used at spawn would at least make the failure mode diagnosable without reading the output carefully.

Macrox · 2 months ago

Observability answer: No, the protocol switch is NOT visible in Agent View

Per the official docs, subagents and teammates spawned within a session don't appear as separate rows in Agent View. The Agent View row for the parent session shows a done/total count for background subagents, but doesn't distinguish spawn protocol.

The teammate "agent panel" (below the prompt input) shows teammate names and status, but doesn't indicate whether a given agent was spawned as a regular subagent vs. a teammate at the protocol level.

The only observable signal is the tool result text in the conversation transcript:

  • Regular: "Async agent launched successfully." + agentId: a64f71c83af7a33ad
  • Teammate: "Spawned successfully.\nagent_id: my-agent@session-<id>\nThe agent is now running and will receive instructions via mailbox."

These messages scroll by in the transcript and are easy to miss — especially in multi-agent orchestration where many agents are spawned in rapid succession.

Worse than expected: name triggers team auto-creation

In the original investigation I assumed the team protocol only activated when a team already existed. Further PoC testing reveals it's worse: using name creates a team config from scratch if none exists.

In a fresh background session (no prior team), calling Agent({name: "test-named-agent", run_in_background: true, ...}):

  1. Created ~/.claude/teams/session-<id>/config.json with a team-lead entry
  2. Returned "teammate_spawned" status
  3. The agent sent idle_notification instead of delivering results via task-notification

This means the trigger condition is simply name parameter present — not "session has existing team." Any attempt to use name for observability in any session will silently activate the teammate path.

Shutdown is also broken

After the teammate completes its work and enters idle state, SendMessage({to: "agent-name", message: {type: "shutdown_request", ...}}) does not terminate it. The teammate responds with another idle_notification and continues running. In the investigated session:

  • shutdown_request sent at 23:13
  • idle_notification still arriving at 23:31, 23:41, and beyond

The zombie teammates accumulate in the job state fan array (never cleared) and keep polluting the parent's context window with periodic idle_notification messages (~200 tokens each).

Workaround confirmation

Your suggested workaround (avoid name, use the returned agentId for tracking) is confirmed working. In our PoC:

| Approach | Spawn Response | Result Delivery | Works? |
|----------|---------------|-----------------|--------|
| No name | agentId a64f71c83af7a33ad | task-notification, 3.8s | ✅ |
| With name | test-named-agent@session-<id> | idle_notification only | ❌ |

The agentId returned from the nameless spawn is usable with SendMessage({to: agentId}) to continue the agent — so addressability isn't fully lost, just less ergonomic (opaque ID vs. human-readable name).

+1 on the fix direction

Strongly agree with separating name (display label / addressability) from an explicit teammate: true opt-in for the team protocol. The current implicit detection via parameter presence violates the principle of least surprise — a display hint shouldn't change execution semantics.

fdjkgh580 · 1 month ago

Still present in v2.1.206, and I can add three data points the original report didn't cover.

1. It is not limited to background sessions (--agent claude)

I hit this in a plain interactive CLI session. The repro doesn't need a background session or a pre-existing team config — the first named spawn creates ~/.claude/teams/session-<id>/config.json and registers the agent as a member.

$ ls ~/.claude/teams/session-489e8e97/
config.json      # members=2, includes my agent "image-audit"

2. Controlled A/B in a single session — name is the only variable

Same session, same subagent_type: "general-purpose", same run_in_background: false. Only difference is the presence of name:

| name | Spawn response | Delivery | Wall clock |
|---|---|---|---|
| omitted | (normal) | ✅ result returned inline | 5.5s |
| "image-audit" | Spawned successfully… via mailbox | ❌ idle_notification, body empty | never |

Five named agents across this session (reverse-trace, omission-audit, semantic-audit, file-audit, image-audit) — all five lost their results. One unnamed control agent — returned fine.

3. The named agents do run. Only the return path is broken.

This matters for triage: it isn't a scheduling or startup failure. image-audit left 6 Python scripts and 4 unpacked directories in its scratchpad before going idle. The work completed; the result never propagated.

Corollary: the results are unrecoverable through the tool. I sent SendMessage to two of the idle named agents asking them to restate their findings — both responded with another empty idle_notification. The only workaround I found was to instruct the agent to write its report to a file and read it myself, bypassing the return path entirely.

4. Workflow is unaffected — same session, same models

The Workflow tool spawns its agents without a name, and in the same session it returned structured results from 12 agents with no losses. That's a useful control: the subagent execution environment is fine, the Agent-tool-with-name delivery path is not.

Suggestion

Given #74614 (run_in_background: false also ignored when name is present), both symptoms look like the same branch: presence of name diverts the spawn to the teammate path and everything downstream of it (delivery mechanism, sync/async honoring) follows the teammate contract instead of the Agent-tool contract.

If decoupling is non-trivial, an interim mitigation that would have saved me ~40 minutes: warn at spawn time when name is passed, e.g. note: named agents deliver via team inbox, not via task-notification. Silent divergence is the expensive part.

Environment

  • Claude Code v2.1.206
  • macOS 15.7.3, Darwin 24.6.0 arm64
  • Interactive session (no --agent flag)
  • Model: Opus 4.8 (1M context)
fdjkgh580 · 1 month ago

Correction / refinement to my previous comment — I ran one more experiment and the broken surface is narrower than I implied.

SendMessage from the named agent to main works. I instructed the idle named agent to call SendMessage({to: "main", ...}) explicitly, and the message arrived in the parent session intact:

<agent-message from="image-audit">
CHANNEL-TEST-OK / 我是 image-audit / 目前進度:稽核已全部完成 …
</agent-message>

So the agent→parent path is not dead in general. What is dead is specifically the final message / return value.

This also explains a failure mode I misread earlier. When I sent a named agent a SendMessage asking it to restate its findings, it answered — but it answered by ending its turn, which routes the answer into the final-message path, which is the black hole. From the parent's side that is indistinguishable from "it ignored me and went idle again." The agent believed it had delivered. In its own words, after the channel test:

完整報告已在上一則最終訊息中以文字交付 ("the full report was already delivered as text in my previous final message")

It had not. Nothing arrived.

Updated picture:

| Path | Named agent | Unnamed agent |
|---|---|---|
| Final message / return value | ❌ lost | ✅ delivered |
| SendMessage({to: "main"}) | ✅ delivered | n/a |
| idle_notification | ✅ delivered (empty body) | n/a |

Practical consequences for anyone hitting this:

  1. Work done by a named agent is recoverable — but only if you instruct it, before or after the fact, to either write results to a file or call SendMessage({to: "main"}) explicitly. Asking it to "report back" is not enough; it will use the final-message path and silently lose the payload.
  2. The agent has no way to know its delivery failed. It reports success to itself and goes idle. Any retry logic based on the agent's self-assessment will loop forever.

If the teammate path must stay, the minimal fix might be to route a completing teammate's final message through the same inbox mechanism that SendMessage already uses successfully — the transport clearly works.

fdjkgh580 · 1 month ago

One more finding, and I think it's the sharpest edge of this bug.

The harness actively steers the named agent into the black hole.

I told the stuck named agent to write its report to a file so I could read it directly, bypassing the dead return path. Its Write call was rejected by the harness with:

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

So the harness instructs the agent to deliver via its final response — which, for a named agent, is exactly the path that discards the payload. The two behaviours compose into a guaranteed loss:

  1. name diverts the spawn to the teammate path → final message is dropped
  2. Harness policy forbids the file-based workaround → "use your final response instead"
  3. Agent complies, reports success to itself, goes idle
  4. Parent sees an empty idle_notification

The agent's own words after I confirmed nothing had arrived:

完整報告已在上一則最終訊息中以文字交付 ("the full report was already delivered as text in my previous final message")

It had complied with the harness, and the harness threw the result away.

(Credit where due: the agent refused to route around the Write denial with a Bash heredoc, which is the correct behaviour. It then found the one legal channel that works — SendMessage({to: "main"}) — and delivered a 6000-word report through it. That was the only reason ~20 minutes of real work survived.)

Suggested triage priority: the Write denial message is safe and sensible for unnamed subagents, where the final response does reach the caller. For named/teammate agents it is actively harmful advice. Either exempt them, or fix the delivery path so the advice becomes true again.

0xbrainkid · 1 month ago

This is the kind of bug where the API surface needs to expose the protocol boundary as data, not prose.

Right now name looks like metadata, but it changes at least four contracts at once:

  1. spawn path: background agent vs teammate;
  2. completion path: task-notification vs final-message / inbox semantics;
  3. addressability: opaque agent id vs human label;
  4. authority/lifecycle: which parent can observe, cancel, or recover the work.

The expensive part is that the agent can believe it delivered successfully while the parent receives only an idle state. That means neither side has a reliable shared receipt for the handoff.

A small invariant might make this mechanically testable: every Agent spawn returns an explicit delivery contract before work starts, for example:

{
  "agent_id": "...",
  "display_name": "image-audit",
  "protocol": "background|teammate",
  "result_route": "task-notification|send-message|file-artifact",
  "final_message_delivery": "parent|team-inbox|not-supported",
  "cancellation_route": "..."
}

Then name can be tested as addressability only. If it changes protocol or result_route, that should be an explicit opt-in, or at minimum a warning with a different return shape. The harness advice also becomes route-aware: "return findings as text" is safe only when the declared final-message route reaches the parent.

This is the same boundary AgentFolio/SATP cares about at the reputation layer: not the private transcript, but whether a delegated agent produced verifiable receipts for who spawned it, where the result was supposed to go, and whether the terminal outcome actually reached that route. Without that receipt, the parent cannot distinguish "agent failed" from "runtime dropped the result."

kaiomp · 1 month ago

Still reproduces on 2.1.220 (macOS, darwin 25.5.0), and this issue's diagnosis matches what I observed.

Data point from one session: ~10 Agent dispatches, all with a name parameter, in a controller session that dispatched implementer and reviewer subagents sequentially. 4 of them completed their work but never delivered a result — the controller received only idle_notification with idleReason: "available". In every case the subagent had genuinely done the work (files written, commits made); only the final report was lost.

Sending the agent a SendMessage asking it to relay its result recovered the output every time, which matches the workaround described in #70641.

Two things that may help narrow it:

  1. run_in_background: false does not make a named dispatch synchronous. The spawn returns The agent is now running and will receive instructions via mailbox regardless, which is consistent with this issue's claim that name switches the spawn to the teammate path — the background/foreground flag appears to be ignored once that path is taken.
  1. It is not model-dependent. I initially suspected a model correlation because the first several silent agents happened to share a model. That was a sampling artifact — with more dispatches, agents on different models all exhibited it. Anyone else triaging this should not chase the model dimension.

On the workaround's cost: dropping name restores result delivery, but name is what makes an agent addressable via SendMessage({to: name}). In a review/fix loop that matters, because resuming the original implementer preserves its context, whereas a fresh unnamed agent has to rebuild it. So the choice is currently between reliable result delivery and resumable agents, and orchestration patterns that need both have no good option.

Happy to provide more detail if useful.

Showing cached comments. Read the full discussion on GitHub ↗