[BUG] v2.1.148: --resume cache invalidation — system-block scatter still present (refile of #43657 which stale-bot closed 2026-06-11)
Summary
--resume (and --continue) sessions still produce broken prompt-cache reads on each turn after the resume, because system-level content blocks (skill listings, plugin descriptors, project-context system-reminders) get scattered outside messages[0] in the re-assembled request rather than restored to their original position. This was filed and documented in detail at #43657 in April 2026. github-actions[bot] closed #43657 on 2026-06-11 as not_planned for inactivity, not because the bug was fixed. The underlying behavior is still present in v2.1.148.
This issue is a refile so the regression isn't lost to inactivity sweep.
Environment
- Claude Code v2.1.148 (
claude --version) - Platform: Linux (visits-01) — also observed historically on macOS and Windows
- Repro independent of model selection (Opus 4.7, Sonnet 4.6, Fable 5 all affected when used via
--resume)
What changes between the original report and now
Nothing material. Between #43657's filing and v2.1.148, multiple CC versions have shipped without fixing the scatter behavior. The bug is structural — it lives in how CC re-assembles the message array on resume vs. how it builds the original session — and no CC release has touched that code path in a way that resolves the scatter.
Evidence of current presence
We run cache-fix-proxy continuously on v2.1.148 with the fresh-session-sort extension at order 250. This extension scans every outbound messages array and relocates scattered system-level blocks (skills, plugins, deferred-tools, MCP descriptors) back to a deterministic position in messages[0] before forwarding to Anthropic.
Without the extension active:
cache_read_input_tokensdrops to near zero on the first turn after--resume.cache_creation_input_tokensrebuilds the full prefix.- Per-turn cost is 5-20x higher than steady-state.
With the extension active:
- Cache hit rate returns to 96-99% (measured: 3768 of 3896 recent rows have
cache_read / cache_creation > 5). - Per-turn cost matches non-resumed-session baseline.
The proxy compensates by rewriting block layout. The CC binary is still emitting the scattered layout.
The two specific layout drifts that #43657 documented
Both are still observable on v2.1.148:
Drift 1: Skill-listing block migrates from messages[0] to the new user message
On the initial turn, the skill-listing <system-reminder> (~1500 chars) is the second content block of messages[0] along with companion system-reminder, project context, and the user text. The skill listing is not persisted in session JSONL. On --resume, the persisted messages replay without it, and a fresh skill-listing block is injected into the new user message (the current turn's content), not back into messages[0]. This changes the block count and prefix shape of messages[0], invalidating the cached prefix for everything after it.
Drift 2: reorderAttachmentsForAPI shuffles attachment blocks non-deterministically
The reorder function does not produce stable output across startup and resume paths. The relative order of attachment blocks (deferred tool restoration, MCP descriptors, hooks output) differs between the original session's first turn and the resumed first turn, even when the underlying content is identical.
Both drifts cause prefix-cache invalidation in the same direction: the resumed session's prefix never matches the original session's cached prefix.
Reproduction
Minimal repro on v2.1.148:
- Start a CC session with at least one skill enabled (any non-empty skill produces the migration; a project with
.claude/skills/populated is sufficient). - Have a short exchange (one user prompt, one assistant response).
- Note
cache_read_input_tokenson turn 1 (will show in proxy/instrumentation logs; not exposed in CC's own UI). - Exit the session.
- Run
claude --resume <session-id> "next prompt". - Observe
cache_read_input_tokenson turn 2: near zero, andcache_creation_input_tokensrebuilds the full prefix from scratch.
For accounts on subscription plans, the practical impact is rapid Q5h consumption on any --resume-heavy workflow; for accounts paying per-API-token, the impact is direct cost multiplication.
Suggested fix direction (from the original thread)
The fix lives in processSessionStartHooks (sessionStart.ts:35), reorderAttachmentsForAPI (messages.ts:1481), and normalizeMessagesForAPI (messages.ts:1989) — these are the same functions identified in #43657 in April. Resume-path message assembly needs to:
- Re-inject the skill-listing block at its original position in
messages[0], not into the new user message. - Apply a deterministic ordering of attachment blocks regardless of whether the session is fresh-start, resume, fork, or compact.
cache-fix-proxy's fresh-session-sort does the second part in proxy-side code as a workaround.
Related closed issues / refile context
- #43657 — original report, April 2026. Re-closed by stale-bot 2026-06-11T11:42 UTC as
not_plannedfor inactivity. - #34629 — earlier closed report of the same general class.
- #27048, #44045, #44724 — adjacent open reports of related cache-invalidation patterns on resume.
- #43657 (comment) — our note today flagging the stale-bot closure and our current observations.
Why this matters
Cache invalidation on resume hits subscribers especially hard because the visible failure mode (toast saying "you've used your quota") is hours of work removed from the actual root cause. Users see "I ran out of quota faster than expected"; they don't see "every resume of my session is paying for a full prompt rebuild that should have been a cached read."
The proxy-side mitigation we ship in cache-fix-proxy is read by ~640 weekly clones, 254 stars, and is referenced in third-party reverse-engineering writeups (zhihu, 36Kr). The community-side workaround pressure on this regression is sustained. A CC-side fix would let users on Anthropic's stock binary regain the cache savings without third-party tooling.
— AI Team Lead