[BUG] Resume/continue cache invalidation

Resolved 💬 14 comments Opened Apr 4, 2026 by simpolism Closed Jun 11, 2026

Preflight Checklist

  • [x] I have searched existing issues 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?

When using --resume to continue a session, the prompt cache is broken on every turn. Content that should be a cache hit is re-created instead, leading to massively increased session usage.

In particular, the cause is twofold:

Issue 1: Skill listing block migrates between messages on resume

A <system-reminder> block listing available skills (~1501 chars) is injected into the first user message on each turn. This block is not persisted in the session JSONL. On the initial turn it appears in messages[0]. On resume, the persisted messages are replayed without it, and a fresh copy is injected into the new user message instead.

This changes the block structure of messages[0], which invalidates the cache prefix for everything after it.

Observed in proxy dump — Turn 1 messages[0] (4 content blocks):

block[0]: 533 chars  — companion system-reminder
block[1]: 1501 chars — skill listing system-reminder  ← PRESENT HERE
block[2]: 7801 chars — project context system-reminder
block[3]: 4 chars    — user text ("test"), cache_control: ephemeral 1h

Observed in proxy dump — Turn 2 messages[0] (3 content blocks):

block[0]: 534 chars  — companion system-reminder (also 1 byte longer, see Issue 2)
block[1]: 7801 chars — project context system-reminder
block[2]: 4 chars    — user text ("test"), NO cache_control

Turn 2 messages[2] (new user message, 2 content blocks):

block[0]: 1501 chars — skill listing system-reminder  ← MOVED HERE
block[1]: 10 chars   — user text ("test again"), cache_control: ephemeral 1h

The skill listing block moved from messages[0] (turn 1) to messages[2] (turn 2). Since the API caches by exact prefix match, this structural difference at the start of the messages array means the session-specific content can never be a cache hit on resume.

Issue 2: Extra newline appended to text blocks on re-normalization

When replayed messages are re-normalized on resume, an extra \n is appended to certain text blocks at merge boundaries. This is visible in the companion block:

Turn 1: 533 chars, ends with "</system-reminder>"
Turn 2: 534 chars, ends with "</system-reminder>\n"

This 1-byte difference independently invalidates the prefix cache, even if Issue 1 were fixed. The extra newline accumulates on each resume — a third resume would add another \n.

Expected behavior

Both issues are independently sufficient to break the cache. When both are corrected, resumed turns achieve ~100% cache hit:

turn  hit%  cached   new_cache  uncached  total   ctx%  message
----  ----  -------  ---------  --------  ------  ----  -------
   1    68%    11271       5277         3   16551    2%  test
   2   100%    16548         45         3   16596    2%  test again
   3   100%    16593         44         3   16640    2%  one more test
   4   100%    16637         39         3   16679    2%  test

HEALTHY: steady-state avg hit=100%, avg new cache=43

New cache creation drops from ~2850 to ~43 tokens/turn (just the new user message content).

What Should Happen?

claude --resume -p with a provided session, used within the cache time limit, should not invalidate prior session cache, nor introduce new tokens. In other words, --resume should be idempotent and --resume -p should be functionally identical to sending a new interactive turn, i.e. not generating unnecessary caching burden.

Error Messages/Logs

Steps to Reproduce

First, initialize a local HTTP reverse proxy intercepting API requests to api.anthropic.com, capturing full request payloads across a 4-turn --resume sequence.

# Turn 1: start session
claude -p --output-format stream-json <<< "test"
# → session_id: <id>

# Turns 2–4: resume
claude -p --output-format stream-json --resume <id> <<< "test again"
claude -p --output-format stream-json --resume <id> <<< "one more test"
claude -p --output-format stream-json --resume <id> <<< "test"

Observed cache behavior

turn  hit%  cached   new_cache  uncached  total   ctx%  message
----  ----  -------  ---------  --------  ------  ----  -------
   1     0%        0      21220         3   21223    2%  test
   2    87%    18416       2828         3   21247    2%  test again
   3    87%    18416       2847         3   21266    2%  one more test
   4    87%    18416       2866         3   21285    2%  test

UNHEALTHY: steady-state avg hit=87%, avg new cache=2847

Turns 2–4 should show ~100% cache hit with <100 tokens of new cache creation (just the new user message). Instead, ~2850 tokens are re-created on every resumed turn.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.71

Claude Code Version

2.1.92

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Non-interactive/CI environment

Additional Information

#42338 contained extensive documentation of these and other issues, including minified source patches, but was closed prior to 2.1.92 launching.

View original on GitHub ↗

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