anthropics / anthropics/claude-code
[BUG] Resuming a fork forfeits its prompt cache: deterministic miss after any tool loop, fork 5m vs parent 1h TTL, full-context rewrites while the prefix is live
- Vorherrschende Sprache
- Python
- Sterne
- 145k
- Forks
- 23.1k
- PR-Merge-Kennzahlen
- PR-Kennzahlen ausstehend
Beschreibung
### Preflight Checklist
- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
# What's Wrong?
Claude Code's own system prompt steers agents into resuming spawned agents (verbatim quotes under Additional Information).
Resuming a fork is silently cache-hostile, in three distinct ways sharing one fix surface (fork wake serialization/caching):
## Problem 1 (P1) — deterministic: one tool call in a fork's turn guarantees the next wake misses with `messages_changed`
- One trivial `Bash echo` in the fork's turn → the **very next wake misses**. Text-only turns never trigger it.
- Idle duration is irrelevant: misses at **63 ms**, 71 s, 166 s, 21.7 min.
Predicted in advance and confirmed at 3 scales / 2 models (controlled probes, synthetic sessions), zero failed predictions — each after consecutive clean wakes, each induced by one tool call:
| Fork context | Model | Tokens missed |
| :----------- | :--------- | ------------: |
| ~35k | Opus | **21,991** |
| ~81k | Opus | **67,249** |
| ~110k | Sonnet 4.6 | **80,440** |
At scale the same trigger escalates from a diagnostic to a full context re-buy — see Problem 3.
**What the client serializes differently is unidentified.** Byte-level `/v1/messages` captures (Sonnet 4.6 fork, reverse proxy) prove a byte-stable mode exists: system/tools/history byte-identical across spawn + tool loop + two wakes, breakpoint only advancing, `thinking` preserved verbatim. That fork never fired `messages_changed`. No capture straddles an expensive wake yet.
## Problem 2 (P2) — forks are second-class in the client's own cache policy: 5m TTL where the parent gets 1h, same shared prefix
- Every parent `cache_control` breakpoint carries `ttl: "1h"` — **136/136 requests across three sessions, zero exceptions** (37/37, 65/65, 34/34). Every post-spawn fork breakpoint carries **no `ttl`** (5m default). Not sampled inference: these are Claude Code's own request bytes.
- Only the fork's **spawn** request writes `ephemeral_1h`; every later fork request writes `ephemeral_5m`.
- So any wake >5m idle lands back on the **spawn boundary**: `cache_read` = spawn read + spawn 1h write, verified to the token (32,381 = 27,667 + 4,714; 108,591 = 96,961 + 11,630), and the whole post-spawn tail is re-bought at write prices, **growing every cycle** (2,908 → 4,039 over five wakes).
- `cache_miss_reason` is `null` throughout. No expiry diagnostic exists: a 386,393-request scan shows all six observed reason values diagnose _changes_; nothing ever reports "aged out". The cost is structurally invisible.
## Problem 3 (P3) — catastrophic full-floor collapse: the wake loses even the prefix the fork matched at spawn
**70 occurrences** across 54 forks in 41 sessions — a census of all 1,449 fork transcripts among 5,397 subagent files, every occurrence firing `messages_changed`. The original report documented 5 of them as detailed case studies (three forks, two sessions, ~120–157k inherited context); the census shows the phenomenon is far more common and spans every model family tested: `claude-fable-5` (36), `claude-opus-5` (22), `claude-opus-4-8` (4), `claude-sonnet-5` (4), `claude-sonnet-4-5` (2). The cache-read drop ranges from ~600 to ~191,500 tokens below the fork's spawn read; two occurrences dropped to `cache_read` = 0.
The largest organic events — ordinary working sessions, no probing involved; both verified against their transcript records, both `messages_changed`:
| organic collapse (fork wake) | model | spawn read | wake read | created on wake | reported missed |
| :--------------------------- | :------ | ----------: | --------: | --------------: | --------------: |
| 2026-08-16, Fable 5 fork | fable-5 | **218,867** | 27,324 | 199,544 | 178,792 |
| 2026-08-20, Opus 5 fork | opus-5 | **204,759** | 14,474 | **230,770** | 212,937 |
On every collapse `cache_read` falls to the session's system+tools floor — a per-session constant (**exactly 14,479 on every event in one case-study session, 14,834 in the other**; the floor tracks the session, not the fork) — _below the fork's own spawn read_ (88,802 in the three-wake fork below). Problem 2 lands _on_ the spawn boundary; Problem 3 lands _beneath_ it. The whole context is re-bought at 5m write prices: one event cost **142,746 created / 14,479 read = 91.5¢**, cent-exact at Opus 5 list prices (a 5m write is 12.5× a hit).
One organic fork (2.1.237, Fable 5), woken three times after `end_turn` (every turn contained tool calls) — read pinned at the floor, the re-written span growing every wake:
| | create | read | `cache_miss_reason` |
| :----- | ------: | ---------: | :-------------------------------- |
| spawn | 1,973 | **88,802** | — (inherited) |
| wake 1 | 99,758 | **14,834** | `messages_changed`, 86,406 missed |
| wake 2 | 105,416 | **14,834** | `messages_changed`, 90,973 missed |
| wake 3 | 107,213 | **14,834** | `messages_changed`, 92,796 missed |
- The prefix was provably live: the parent hit it cleanly **4 s** before one collapse (parent read 134,035; the fork read 14,834), and another collapse fell inside the fork's own still-live `ephemeral_1h` spawn block. Refused match, not expiry.
- Parent activity is not the cause: one fork's parent issued **zero requests** between the fork's `end_turn` and its wake — the fork still collapsed (102,898 created / 14,479 read).
- Same trigger class as P1; not deterministically reproduced — controlled runs to ~110k yield only P1's diagnostic form with the read holding. Mechanism unidentified. Related silent class: one controlled rebuild (**24,276** created / 12,455 read) carried `cache_miss_reason: null` — the costliest controlled event had no diagnostic at all.
- Also: the sender cannot observe the turn boundary that separates a cheap mid-turn delivery from an expensive wake — a message landing **63 ms** after `end_turn` paid a full rewrite (102,898 created).
**Census methodology.** For each of the 1,449 fork transcripts (identified by `.meta.json`): deduplicate assistant records by `requestId`, then flag any post-spawn record whose `cache_read_input_tokens` falls below the spawn record's while `cache_miss_reason` contains `messages_changed`. Per-occurrence evidence (session UUIDs, line numbers, token figures) available on request.
### What Should Happen?
Resuming an idle fork must do one of two things: **return an error**, or **resume with a cache hit** on the inherited prefix. What it must never do is what it does now: accept the wake and silently re-buy the entire context at write-tier prices.
Concretely:
1. **A woken fork must read the cache at least as deep as its spawn read.** The prefix is live — the parent is actively warming it, and the fork matched it at spawn. A wake reading *below* the spawn read (Problem 3) is a refused match on a live prefix, not expiry, and should be impossible.
2. **Fork requests must carry `ttl: "1h"` like the parent's** (Problem 2). Claude Code already writes `ttl: "1h"` on every parent `cache_control` breakpoint and omits it on fork breakpoints — the fix surface is a serializer literal.
3. **Any cache rebuild must be diagnosable.** Today both the 5m-expiry re-buy and the costliest controlled rebuild carried `cache_miss_reason: null` — the cost is structurally invisible.
### Error Messages/Logs
N/A — no error is emitted anywhere; the failure is silent billing (see What Should Happen, point 3). Measured wake-by-wake figures are under Additional Information.
### Steps to Reproduce
**P1** (magnitude scales with inherited context):
1. Spawn a **named** fork (`subagent_type: "fork"` + `name`) from a parent with substantial history.
2. `SendMessage`: have it run one trivial tool call (`Bash echo`) and end its turn.
3. `SendMessage` again (any gap; 71 s sufficed).
4. Wake request shows `cache_miss_reason: {"type": "messages_changed"}`. Wakes without a preceding tool loop stay `null` (control).
**P2**:
1. Spawn a named fork, note spawn read + `ephemeral_1h` write (= boundary).
2. Idle >5 min after its turn ends.
3. Wake: `cache_read` = boundary exactly, tail re-written at `ephemeral_5m`, no diagnostic.
4. Parent writes are `ephemeral_1h` throughout.
### Claude Model
Other — `claude-opus-5`, `claude-fable-5`, `claude-sonnet-4-6`. P2 is model-independent (client request bodies). P1's diagnostic form reproduces on Opus and Sonnet. P3 (full-floor collapse) occurs across all model families: fable-5, opus-5, opus-4-8, sonnet-5, sonnet-4-5 (census of 1,449 fork transcripts); only `claude-sonnet-4-6` showed no full-floor events (P1 diagnostic form only).
### Claude Code Version
2.1.238 (also observed on 2.1.237)
### Platform
Anthropic API
### Operating System
Ubuntu/Debian Linux
### Additional Information
**Wake-by-wake figures.** Every event below is read straight from the session transcripts (assistant records deduplicated by `requestId`); the diagnostic column is the API's own `message.diagnostics.cache_miss_reason`. Note where it reads `null`: the recurring 5m-expiry re-buys carry no diagnostic at all — the silent billing that What Should Happen, point 3 is about.
```text
ORGANIC (ordinary working sessions, no probing):
P3 organic — largest observed (Fable 5, 2026-08-16; spawn read 218,867):
wake: create 199,544 (5m) | read 27,324 | messages_changed, 178,792 missed
P3 organic (Opus 5, 2026-08-20; spawn read 204,759):
wake: create 230,770 (5m) | read 14,474 | messages_changed, 212,937 missed
P3 organic (Opus, 21.7 min idle; spawn: read 142,687 / wrote 2,407 @1h):
wake: create 142,746 (5m) | read 14,479 | messages_changed, 131,501 missed (~91.5¢)
CONTROLLED / SYNTHETIC (deliberate probe sessions, reproduction detail):
P1 controlled (Opus, ~35k fork):
wake 6 (text-only baseline): create 4,039 (5m) | read 32,381 | null
wake 7 (one Bash echo): create 103+242 | read 36,420 | null
wake 8 (71 s later): create 126 | read 36,523 | messages_changed, 21,991 missed
P2 controlled (Opus, ~35k fork; spawn boundary 27,667+4,714 = 32,381):
5 wakes, 8.7–31.2 min idle: read pinned at 32,381 every time; tail re-written @5m,
growing 2,908 → 4,039; cache_miss_reason null every time
```
**System-prompt guidance steering agents into fork resumption** (Claude Code 2.1.238, Opus 5 system prompt, verbatim):
- **Agent tool**:
> `Use SendMessage with the agent's ID or name to continue a previously spawned agent with its context intact; a new Agent call starts fresh.`
- **SendMessage**:
> `Refer to agents by name — names keep working after an agent completes (a send resumes it from its transcript).`
- **Agent listing**:
> `**IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage.`
Related, but not duplicate:
- **#77306**
- *About:* session-level forks (`--fork-session`, `/branch`) lose the whole cache on their *first* request — the scratchpad section interpolates the new session id into the system prompt (`system_changed`; fix accepted upstream).
- *Not a duplicate because:* theirs is a one-shot system-prefix divergence at fork creation; ours starts after a successful spawn and recurs on every wake with `messages_changed`, in forks that share the parent's system prompt.
- **#63930**
- *About:* main-thread floor collapse, attributed at 128k-call scale to late mutation of an already-sent message — dominantly right after a typed user turn.
- *Not a duplicate because:* their mechanism needs a live thread mutating its own next request; our fork is byte-frozen between `end_turn` and wake, collapses with no writer present, and carries the fork-only TTL omission (P2), which has no counterpart there.
- **#87966**
- *About:* intermittent collapse in one very long session — `cache_read` pins to the system+tools prefix and a 600–750k tail rewrites, server-reported as `previous_message_not_found`.
- *Not a duplicate because:* its corpus is entirely non-sidechain with zero 5m-tier writes, so our fork-specific mechanisms cannot occur there; a lookup failure vs our `messages_changed` divergence.
- **#44724**
- *About:* the first `SendMessage` resume of a fresh subagent reads `cache_read` 0 — `system[1]` swaps identity strings between `Agent()` and `SendMessage()` — then self-heals.
- *Not a duplicate because:* their swap zeroes the entire read once; ours keeps the system+tools floor hitting while `messages` is reported changed, recurring and growing every wake.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Beitragsleitfaden
Für dieses Repository ist kein Beitragsleitfaden indexiert
Rechercherichtung
Start with the named-fork and SendMessage reproduction steps, comparing request bytes, fork transcripts, cache reads, writes, and cache_miss_reason across the P1 and P2 controls. The issue is done when resumed forks either return an error or retain at least the spawn cache prefix, use the parent's 1h TTL, and expose any rebuild diagnostically.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- python
- Bereich
- api, cli
- Issue-Typ
- Bug
- Schwierigkeit
- 5/5
- Geschätzter Aufwand
- Über eine Woche
- Aktivitätsstatus
- Aktiv
- Klarheit
- Muss geklärt werden
- Anfängerfreundlichkeit
- 25/100