Skip to content

[BUG] Resume/continue cache invalidation #43657

Description

@simpolism

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions