anthropics / anthropics/claude-code
[BUG] Warm prompt cache fully re-written (cache_read collapses to system+tools floor) seconds after ToolSearch / Skill / tool_result on v2.1.258 — 4 events with request IDs; plus pre-2.1.257 screenshot size-cap data
- Lingua principale
- Python
- Stelle
- 145k
- Fork
- 23.1k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
## Environment
- Claude Code **v2.1.258** (macOS 25.4, Apple Silicon), Claude **Max 20x** subscription (OAuth login, no API key / gateway)
- Model: `claude-fable-5-1` (events below); the same signature was observed earlier on `claude-opus-4-8` and `claude-sonnet-5`
- A second machine (Windows 11, same account, Claude Code 2.1.250–2.1.252) shows the same signature independently
## Summary
Mid-session, with a **warm, established prompt cache** (≥100k tokens read on the previous call, same model, ≤65 s earlier), a single call **re-writes the whole context**: `cache_read_input_tokens` collapses to the system+tools floor (**33,376–33,405** on Fable 5.1; 27,182 on Opus 4.8) and `cache_creation_input_tokens` ≈ the entire conversation. The very next call reads the re-written cache normally again. Nothing in the *conversation* changed except an appended `tool_result` — so the invalidation must come from the client re-rendering the request prefix (tools / system / first message).
This looks like the class already described in #63930 (deferred-tool materialisation collapsing cache_read to the floor) and #81967 (tools array mutation invalidates everything after it). It is **still present on 2.1.258**.
## Events on v2.1.258 (2026-09-02, Fable 5.1) — request IDs for cache diagnostics
Each row: the last GOOD call → the call that re-wrote the cache. "Trigger" = tool(s) whose result was appended between them.
| # | eviction (UTC) | request IDs (good → eviction) | gap | cache_read before | cache_read at eviction | cache_creation at eviction | trigger (tool result appended in between) | screenshots in history |
|---|---|---|---|---|---|---|---|---|
| B1 | 05:13:17Z | `req_011Cee3DV4pAp9ca9hVD9Msd` → `req_011Cee3GufVmAn48jVFADzEU` | 44 s | 329,438 | **33,376** | **337,258** | Bash, Bash | 1 |
| B2 | 05:43:01Z | `req_011Cee5a8NwAWLH7JUT4eBhX` → `req_011Cee5aUcr9mWFkjzd2TNFJ` | 5 s | 133,834 | **33,403** | **111,403** | ToolSearch | 1 |
| B3 | 06:29:21Z | `req_011Cee96dKQrc63h5MnPUdWG` → `req_011Cee97HeG6CEvsuUDW4TFb` | 7 s | 179,492 | **33,405** | **154,475** | ToolSearch | 0 |
| B4 | 07:15:09Z | `req_011CeeCWoCVjihFg4ieDFar1` → `req_011CeeCYvMfeab85CV99UcuQ` | 65 s | 752,216 | **33,376** | **726,403** | playwright:browser_run_code_unsafe, chrome:computer | 53 |
Notes per event:
- **B2 / B3:** the preceding call was `ToolSearch` (deferred tool schemas materialised). 5–7 s later the next request misses everything after the tools block. This is exactly the #63930 "Mode A".
- **B1:** the preceding call was a `Skill` invocation earlier in the same minute (skill content injected), then `Bash`; the request after it missed everything.
- **B4:** the biggest one (726k re-written, ≈$9 at list on Fable pricing). Between the good call and the eviction: a Playwright tool result (`Connection closed`) and a `` block injected into the following user turn. The cache floor is identical to B1 (same session), i.e. the miss starts right after the tools block.
All four: same model, cache warm (5–65 s), no `/compact`, no `/model` switch, no resume, request well under the size cap (0–53 screenshots).
## Detection method (so the numbers are not inflated)
Local transcripts (`~/.claude/projects/**/*.jsonl`) parsed **one API call per `message.id`** (the transcript writes one line per content block, all repeating the same `usage` — counting lines over-counts ~2.2×). A call is classed as an eviction only if: cache established (≥100k read on the previous call), same model, ≤300 s since previous call, and cache_read collapses below 50% of the established size while cache_creation ≥ 50% of it. Session-start priming, model switches and >5-min TTL re-caches are **excluded**. Per-event dossiers (all fields above + everything appended between the calls) are available on request.
## Related, pre-2.1.257 class (for confirmation that the fix covers it)
On **2.1.250–2.1.252** the same account had **110 evictions over 31 Aug–1 Sep on two machines**, all in long screenshot-heavy Claude-in-Chrome sessions. Measured on the raw transcripts: once the cumulative base64 image payload in history reached **~28–40 MB (~21–30 MB binary)** — machine A from screenshot #298, machine B from #276 — **every subsequent screenshot turn** became a full re-write (cache_read = 27,182 floor, cache_creation 600k–950k, ≈$11–17/call at Opus list). Smoking-gun pair, same conversation, 15 s apart, warm cache: `req_011CecStEr48YXCWRaUFXc6v` (cache_read 833,928 / cache_creation 2,136) → `req_011CecStv4jEeDLZwgtLr2vh` (cache_read 27,182 / cache_creation 696,858), with only one screenshot `tool_result` in between. This matches the 2.1.257 changelog entry *"Fixed prompt-cache misses on every turn in long screenshot-heavy sessions once images exceeded the per-request size cap."* — could you confirm that fix targets exactly this (image stripping mutating early history), and whether a per-image/count threshold is involved? Since 2.1.257 we have not reproduced this class; the 4 events above are the remaining (tool-set) class.
## Expected
Materialising deferred tools / injecting skill or system-reminder content mid-session should not invalidate the cached prefix for the whole conversation (e.g. append new tool definitions after the cached breakpoint, or keep the tools block byte-stable and place the cache breakpoint before mutable content).
## Impact
On a subscription plan this burns the session/weekly quota ~10× faster per affected call (each event = re-caching the entire context at cache-write rates). Over 5 days: 114 real evictions ≈ $1.6k at list-equivalent, ~2/3 of which on the pre-2.1.257 screenshot class.
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Direzione di ricerca
Start by examining the local transcripts under ~/.claude/projects/**/*.jsonl and the per-message.id usage records, using the issue's eviction criteria to reproduce the cache collapse. Compare the ToolSearch, Skill, tool_result, and system-reminder cases with #63930 and #81967. Done means identifying the invalidation path and preventing the warm cached prefix from being rewritten without breaking deferred-tool or injected-content behavior.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- python
- Ambito
- cli, performance, tooling
- Tipo di issue
- Bug
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Stato di attività
- Attiva
- Chiarezza
- Da chiarire
- Idoneità per principianti
- 32/100