Agent tool description flips to its subagent variant in the main conversation while in-process teammates are alive — tools_changed cache miss on every fleet phase boundary

Status Open
Reported on v2.1.225
Maintainer reply None cached
Activity 0 comments · opened Aug 11, 2026

Version: 2.1.225 (also observed 2.1.219-2.1.226 era; unpatched npm bundle used for
source reading; deliberate reproduction attempts ran on 2.1.225.184)

What happens

When a session spawns in-process agents (Agent tool / agent teams), the main
conversation's own requests
begin rendering the subagent variant of the Agent
tool description — the one that says:

run_in_background and name are unavailable here — only synchronous subagents. The agent's final message is returned to you as the tool result; it is not shown to the user — relay what matters.

instead of the main-loop variant ("Subagents run in the background by default… Pass
run_in_background: false…", "The agent's final report is not shown to the user…").

The main conversation keeps rendering the subagent variant for the entire duration of
the fleet phase, then reverts when the teammates are torn down. Because tools[] is
part of the cached prefix, each flip invalidates the whole prompt cache for that
conversation
— the API's own diagnostics.cache_miss_reason reports
tools_changed on exactly these turns.

The text is also simply wrong while it is displayed: named background agents work fine
from the main loop throughout the affected window.

Evidence

Single main conversation, one model (claude-fable-5) throughout, requests walked in
transcript order via each assistant record's requestId and matched to the logged
request bodies (OTEL_LOG_RAW_API_BODIES), same metadata.user_id throughout:

| run | count | window (UTC) |
|---|---|---|
| main-loop variant | 14 | 17:27:29 – 18:17:10 |
| subagent variant | 64 | 18:21:42 – 20:15:16 |
| main-loop variant | 14 | 20:18:02 – |

The subagent-variant run brackets exactly the interval in which in-process teammates
existed (first Agent spawn → team teardown).

Comparing one main request from each window, from the same conversation: system is
byte-identical, and of the 20 tools present in both, only Agent's description
differs
.

Cost, measured across a 2-day window of one workstation's sessions: breaks whose
server-reported reason is tools_changed accounted for ~13.3M rebuilt input tokens in
a single bucket of the audit; every probed instance carried this exact signature
(tools[] diff = Agent description only, no tools added or removed).

Reproduction status

The evidence above is from natural sessions. Deliberate reproduction was attempted twice
and failed both times, under both models involved in the historical windows (once with the
main loop on claude-opus-5, once wire-verified claude-fable-5 on every request):

  • teammate merely alive (named background agent; the harness classifies it

task_type: in_process_teammate, the exact type whose turn runner enters the store)

  • a teammate-to-main SendMessage delivery waking an idle main loop
  • a tools[] recomposition (ToolSearch schema load) executed inside the delivery-woken

turn, with the teammate still alive

All main-conversation requests stayed on the main-loop variant throughout (28/28 in the
second attempt, confirmed from logged request bodies).

One further observation from the second attempt narrows the mechanism: the teammate's own
requests also rendered the main-loop variant
on every round. If in-process teammate turns
composed their requests inside the store's run() scope, teammate requests would carry the
subagent variant unconditionally. They do not, which matches the natural-session data
(main and teammates flip together, per process), and implies description composition
normally executes with the store unset in both directions. The observed multi-hour flipped
windows therefore look like a narrow race at composition time, not a steady-state
inheritance, which is consistent with it resisting on-demand reproduction.

The Agent description builder selects the variant with:

l = !KE() && !Xne()
  • KE() → env CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
  • Xne()nhs.getStore() !== undefined, where nhs is an async_hooks

AsyncLocalStorage whose only run() entry wraps the in-process teammate turn
runner
(task.type === "in_process_teammate")

So the variant is chosen from ambient async context at description-composition time
rather than from the identity of the conversation the request belongs to. When the
main loop's request composition executes on a continuation that originates inside a
teammate's nhs.run(...) scope — or is served a description composed there — the main
conversation gets the subagent text. (Per the reproduction-status section, composition
appears to run with the store unset in the common case for main loop and teammates alike;
the flipped windows are consistent with an occasional composition landing on a
teammate-rooted chain and its result then persisting for the whole process until
recomposed.)

Why it is worth fixing

  • Every fleet-phase boundary costs a full prefix rebuild of the main conversation.

Agent-team and multi-agent workflows pay this twice per phase (in and out).

  • The description asserts a capability restriction that does not apply to the

conversation reading it.

  • The same AsyncLocalStorage backs agentId, agentName, teamName, color,

parentSessionId, plan-mode, task-list id, message attribution and the durable-cron
guard. If the leak is whole-turn rather than composition-scoped, those read the
teammate's identity from the main loop too. (We could not settle whole-turn vs
composition-scoped from request bodies alone; a durable-cron attempt from the main
loop while a teammate is alive would discriminate, since that guard consults the same
store.)

Suggested direction

Resolve the description from the conversation/session role captured when the request is
built (or pass the role explicitly into the description builder), rather than from
ambient AsyncLocalStorage state at composition time. If the leak is whole-turn,
re-rooting the main loop's turn execution in its own store context would fix this class
and the identity readers at once.

View original on GitHub ↗