[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
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 onclaude-opus-4-8andclaude-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
Skillinvocation earlier in the same minute (skill content injected), thenBash; 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<system-reminder>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.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
The read-collapse-to-floor after a tool_result on 2.1.258 matches #83913; 2.1.259 fixed it in my capture. Your pre-2.1.257 screenshot data is a separate, still-live residual: a pasted image is re-rendered as a file pointer on the next request → one rewrite (https://github.com/anthropics/claude-code/issues/91705). Workaround that sidesteps it: give the model a path to Read instead of pasting, https://github.com/ArshansGithub/shotpath.
Correction to my earlier comment here: the pasted-image residual (#91705) is withdrawn and closed. The image block is byte-stable across requests; the collapse I attributed to it was a persisting
cdrewriting the system prompt (#91706). The 2.1.259 fix for the tool_result collapse stands.