Subagent model pin is lost on injected wake/resume turns — the resumed turn runs (and bills) at the waker's model, not the pin

Open 💬 1 comment Opened Jul 5, 2026 by postoso

Summary

A subagent spawned with a pinned model keeps that pin only until its first injected wake event. From that turn on, it generates — and by every proxy we can see, bills — at a different model determined by who woke it. There are three distinct triggers with different targets and version histories. This is the same class of bug as the depth leak fixed in v2.1.187 ("a background subagent's depth is fixed when first spawned, and resuming it later doesn't change that depth") — model has the identical resumer-context-bleed shape, unpatched.

The three triggers

| # | Trigger | Resumed turns run at | Scope | Version history |
|---|---|---|---|---|
| A | SendMessage / coordinator / user-message resume of a stopped agent | the sender's current model | generic (any sender tier) | long-standing — present ≥ v2.1.174 |
| B | Park-wake, child-AGENT channel — a parked agent woken by its own child Agent/Task completing | the session main's current model, at any nesting depth | generic (any main) | long-standing — promotes at ≥ v2.1.195 (earliest verified); no version observed holding |
| C | Park-wake, background-BASH channel — a parked agent woken by its own background Bash completing | the session main's current model | generic | regression: held ≤ v2.1.198, promotes ≥ v2.1.199 |

An agent that never rests keeps its pin: foreground child spawns (run_in_background=false) and foreground waits do not flip the model (verified clean).

Why this is a bug, not intended behavior

  • agent-teams.md: "A teammate's model and fast mode are fixed when it spawns."
  • sub-agents.md gives an invocation-time model-resolution order, and its "Resume subagents" section says resumed agents "pick up exactly where they stopped" — no mention of a model change.

Reproduction (must be interactive — see caveat)

  1. In an interactive session whose main model is expensive (Opus or Fable), spawn an unnamed background subagent with model: "sonnet" whose prompt is: spawn one child agent (a background Task subagent that does any brief task and ends — a few seconds' delay helps ensure the parent parks first), end your turn, and when that child agent's completion wakes you, say a marker phrase and end your turn.
  2. After the wake, read the subagent's transcript at ~/.claude/projects/<project>/<session>/subagents/agent-<id>.jsonl and grep the per-turn "model" field.
  3. Observed: the pre-park turns carry the pinned model (claude-sonnet-5); every turn from the injected isMeta "[SYSTEM NOTIFICATION … automated background-task event]" wake onward carries the session main's model (claude-opus-4-8 under an Opus main, claude-fable-5 under a Fable main). The flip persists until the next wake re-resolves it (each wake re-hosts at that waker's tier — a subsequent resume from a cheap sender demotes the agent back, verified); an agent that is never woken again stays flipped for its remaining life (observed up to 68 consecutive promoted turns on a long-running orchestrator).
  4. Trigger A repro: SendMessage-resume any stopped pinned subagent — the resumed turn carries the sender's model. Trigger C repro (≥2.1.199): same as above but the agent parks on a background Bash instead of a child agent.

Critical repro caveat: triggers B and C reproduce ONLY in the interactive harness. Headless claude -p never generates the isMeta background-task wake — the child completion is delivered without one, the parent resumes at its pin, and the run looks clean (isMeta count = 0 in headless manager transcripts). A headless version-sweep will falsely conclude the bug is absent or main-model-specific (we made that wrong turn ourselves before catching it). Trigger A does reproduce headless.

The mechanism appears to be: the injected wake turn re-resolves the model against the waker's context (sender for A, session main for B/C) instead of keeping the spawn pin.

Version notes (all interactively verified via transcript message.model)

  • A (SendMessage → sender): present at v2.1.174, 195, 198 — long-standing and sender-generic (an Opus sender promotes a Sonnet child; a Sonnet-pinned relay demotes a resume from a Fable session).
  • B (child-agent wake → session main): promotes at v2.1.195 (opus→fable, and 3/3 sonnet→opus drills), 2.1.198 (3/3 sonnet→fable drills + a depth-2 case promoting to session main, not the parent's tier), 2.1.199, 2.1.201. No version observed holding. (Earlier "clean ≤174" evidence turned out to contain zero park-wake events — it tested nothing.)
  • C (background-bash wake → session main): held at 2.1.195 (3/3) and 2.1.198 (7/7); promotes at 2.1.199 (verified on an agent that used zero Agent tools — pure bash park). Regression window 198→199.

message.model is the transcript-reported generation model — the best available billing proxy, not a billing-ledger fact.

Impact

Any orchestration pattern where a cheap-pinned manager parks on child completions (i.e., most multi-agent fleets) silently runs all post-wake turns at the session main's model, at any nesting depth, for the rest of the agent's life. Under an expensive main this multiplies cost invisibly; under quota-limited models (Fable) it drains scarce quota that the user deliberately pinned away from.

Workarounds found

  • A: route resume-bumps through a throwaway cheap pinned relay child whose only job is "SendMessage this to <agentId>" — the resumed agent runs at the relay's model (verified). The same move recovers an already-flipped agent: a relay bump re-hosts it back at the relay's cheap tier (verified on a Fable-flipped manager demoted to Sonnet).
  • B/C: no pin fixes B. Run parked fleets from a cheap session main; use foreground child waits (run_in_background=false, verified clean); or dispatch straight-through (no park-on-child, no mid-run bumps). Pinning the CLI ≤ v2.1.198 avoids C specifically.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗