[BUG] Historical user-message content drift invalidates prompt cache prefix (trailing \n on </system-reminder>)

Resolved 💬 7 comments Opened Apr 15, 2026 by deafsquad Closed May 24, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.109)

What's Wrong?

Between two adjacent turns of the same Claude Code session, CC re-serializes historical user-message content with byte-level instability — specifically, a trailing \n after </system-reminder> is sometimes present and sometimes absent. A single 2-byte delta on any historical message invalidates the entire prompt-cache prefix for that turn, causing cache_read_input_tokens to drop to ~19k (tools block only) while cache_creation_input_tokens spikes to the full conversation size (~500k+ in our session).

This is distinct from #40652 (cch= hash substitution), #43657 (resume block scatter), #44045 (skill_listing scatter), and #48644 (isMeta positional drift) — it's content-level mutation of historical text blocks, not positional drift or a hash substitution.

Evidence (body-dump diff across a known cache miss)

Two consecutive /v1/messages request bodies captured via a fetch interceptor, one from a healthy cache-hit turn and the next from a full-cache-miss turn. 867 of 869 historical messages match byte-for-byte. The only diffs:

msg[749].content[2] (text block):
  healthy turn:  "...[action-tracker.py] Allowed\n</system-reminder>\n"
  miss turn:     "...[action-tracker.py] Allowed\n</system-reminder>"   <-- trailing \n stripped

msg[768].content[2] (text block):
  healthy turn:  "...[action-tracker.py] Allowed\n</system-reminder>\n"
  miss turn:     "...[action-tracker.py] Allowed\n</system-reminder>"

Same historical messages, same logical content, 2 bytes different → full prefix invalidates.

Reproduced 3+ times in the same session with identical pattern on different historical indices. The drift is stochastic per-turn (no correlation with turn density, idle time, or specific tool use patterns). Looks like a non-deterministic code path in CC's transcript-to-messages serializer.

Session details

  • Model: claude-opus-4-6 (1M context)
  • CC version: 2.1.109
  • Platform: Windows 11 (npm-installed @anthropic-ai/claude-code launched via node cli.js)
  • Approximate session size when miss fires: ~770 messages, ~1.7MB JSON request body
  • Per-miss cost: ~500k cache_creation_input_tokens × Opus pricing ≈ $9/event

What Should Happen

Historical messages[i].content[j].text should be byte-stable across turns. Whatever serializer path strips the trailing \n from </system-reminder> blocks on some turns should produce deterministic output.

Workaround we deployed client-side (via fetch interceptor): rstrip trailing whitespace from every text/content field before send. Makes the bug invisible at the cost of normalizing whitespace. But the underlying non-determinism remains and will bite anyone not running an interceptor.

Additional context

Related to the broader cache-invalidation bug family around CC's message serialization:

  • #40652 cch= hash substitution
  • #43657 resume cache invalidation
  • #44045 skill_listing block scatter
  • #48644 hidden isMeta reminder position drift
  • #41930 widespread usage drain (umbrella)

Given Anthropic's recent quota/pricing changes, these cache invalidation bugs have gone from "mildly annoying" to "session-ending expensive" for heavy users. A few bytes of non-determinism in historical content now costs ~$9 per occurrence instead of being invisible.

View original on GitHub ↗

This issue has 7 comments on GitHub. Read the full discussion on GitHub ↗