anthropics / anthropics/claude-code

[BUG] --resume never hits the prompt cache past the static prefix on Fable 5.1 (opus hits): session-start context message is replayed as a plain string instead of the content blocks it was sent with

Open
#93,490 4 comments 0 reactions 0 assignees View on GitHub
area:core area:cost bug has repro performance platform:macos
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

### Preflight Checklist

- [x] I have searched existing issues and this hasn't been reported yet (#91971 has the symptom without a cause, #83913 the same mechanism for a different message, #44045 is closed and covered messages[0])
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code

### What's Wrong?

every `--resume` (headless and interactive) re-writes the whole conversation into the cache even when the cache is warm. cache_read stays flat at the tools+system floor, cache_creation is the rest of the context, every single time.

i captured the requests with a local proxy and diffed a fresh session against its own resume. tools identical, system blocks identical, messages[0] identical (all 4 blocks, byte for byte). the first difference is messages[1], the `role: system` message that carries the SessionStart hook output plus the `# Environment` block:

- fresh start: `content` is an array with one text block, `cache_control: {type: ephemeral, ttl: 1h}`
- resume: `content` is a plain string. same text, same length (26285 chars), no cache_control

so the prefix stops matching right there and everything behind it gets written again. on a small probe thats ~18-25k tokens, on my real session it was a 385k re-write (a /background fork of a long session: read 27k, wrote 385k).

this is not TTL, not settings, not hooks. two fresh launches 30s apart hit each others cache completely (231238 read, 0 written), so the client can produce a stable prefix, it just doesnt on the resume path.

### What Should Happen?

a rebuilt history reproduces the request that was already sent, byte for byte, same carrier, same block shape, same cache_control. thats what #83913 already asks for PreToolUse/PostToolUse context, this is the same class for the session-start context message. #91971 has this exact symptom on chained `-p --resume` but no cause, i think this is the cause.

### Error Messages/Logs

no error, only the counters. structure of both captured requests (lengths + sha256 prefixes, no content):

```
fresh interactive start (first request):
tools: 30 defs, sha 3ad5cd726882
system[0]: 70 chars, sha 6b1c6e45dfc2, cache_control=None
system[1]: 57 chars, sha 2719b7a469d9, cache_control={'type': 'ephemeral', 'ttl': '1h'}
system[2]: 907 chars, sha a11f50f6b3db, cache_control=None
system[3]: 9983 chars, sha 75bb0c61bbc0, cache_control={'type': 'ephemeral', 'ttl': '1h'}
messages[0] role=user: content=ARRAY[4]: text 25023 sha ca6fa325c3d5 cc=None | text 1272 sha 75082207bd89 cc=None | text 347 sha 6c524f1b0cfd cc=None | text 30 sha d85f47a7c1a1 cc=None
messages[1] role=system: content=ARRAY[1]: text 26285 sha 33946e993933 cc={'type': 'ephemeral', 'ttl': '1h'}

interactive --resume 53s later (first request):
tools: 30 defs, sha 3ad5cd726882
system[0]: 70 chars, sha 6b1c6e45dfc2, cache_control=None
system[1]: 57 chars, sha 2719b7a469d9, cache_control={'type': 'ephemeral', 'ttl': '1h'}
system[2]: 907 chars, sha a11f50f6b3db, cache_control=None
system[3]: 9983 chars, sha 75bb0c61bbc0, cache_control={'type': 'ephemeral', 'ttl': '1h'}
messages[0] role=user: content=ARRAY[4]: text 25023 sha ca6fa325c3d5 cc=None | text 1272 sha 75082207bd89 cc=None | text 347 sha 6c524f1b0cfd cc=None | text 30 sha d85f47a7c1a1 cc=None
messages[1] role=system: content=STRING, 26285 chars, sha 33946e993933
messages[2] role=assistant: content=ARRAY[1]: text 2 sha 2689367b205c cc=None
messages[3] role=user: content=ARRAY[4]: text 246 sha 35c19d240c4d cc=None | text 129 sha c24432f46b9b cc=None | text 62 sha f8b4bb9c329c cc=None | text 36 sha 8daab17abfe0 cc=None
messages[4] role=system: content=ARRAY[1]: text 614 sha bcb751e6ee5d cc={'type': 'ephemeral', 'ttl': '1h'}
```

same hashes everywhere up to and including messages[1], only the shape of messages[1] differs.

### Steps to Reproduce

```bash
echo '{"mcpServers":{}}' > /tmp/empty-mcp.json
claude -p "Reply with the single word ok." --output-format json --strict-mcp-config --mcp-config /tmp/empty-mcp.json
# take session_id from the result, then within a few seconds:
claude -p "Reply with the single word ok again." --output-format json --strict-mcp-config --mcp-config /tmp/empty-mcp.json --resume
claude -p "Reply with the single word ok a third time." --output-format json --strict-mcp-config --mcp-config /tmp/empty-mcp.json --resume
```

what i get (`result.usage`), direct to the api, tool search on (default):

| call | cache_read | cache_creation |
|---|---|---|
| fresh | 12887 (tools+system were already cached from an earlier run) | 24383 |
| resume 1 | 12887 | 24644 |
| resume 2 | 12887 | 24907 |

with mcp servers on its the same shape, just bigger (12887 read / ~32k written on every resume, 4 resumes in a row).

note on the capture: the request diff below was taken through a local proxy (custom ANTHROPIC_BASE_URL), and in that mode tool search is off so all tools are inlined and there is no deferred-tools listing in messages[0]. in that capture messages[0] is byte-identical and the first divergence is messages[1]. direct to the api cache_read is even lower (12887 = tools+system only), so in normal mode messages[0] very likely differs as well (the deferred-tools listing block, see #49038 / #44045). i could not capture that one without disabling tool search.

interactive is the same:

fresh `claude` in a terminal, one prompt, `/exit`, `claude --resume ` 53s later, one prompt:

| | cache_read | cache_creation |
|---|---|---|
| fresh | 0 | 67824 |
| resume | 48825 | 19376 |

same diff: first divergence is messages[1], blocks with cache_control on fresh, plain string on resume.

the diff (content replaced by lengths):

fresh:
```json
{"role":"system","content":[{"type":"text","text":"<26285 chars: SessionStart hook additional context ... # Environment ...>","cache_control":{"type":"ephemeral","ttl":"1h"}}]}
```

resume:
```json
{"role":"system","content":""}
```

### Claude Model

fable 5.1 (claude-fable-5-1)

### Is this a regression?

dont know. first measured on 2.1.268, no earlier version measured for this exact path.

### Last Working Version

unknown

### Claude Code Version

2.1.268

### Platform

claude.ai subscription (Max), first party api, deferred tools on (default), promptCacheTtl and subagentPromptCacheTtl both 1h

### Operating System

macOS (Darwin 25.3.0, arm64)

### Terminal/Shell

iTerm2, zsh (headless repro is shell independent)

### Additional Information

two fresh launches 30s apart read each others cache in full (231238 read, 0 written), so the prefix is stable across processes and only the resume replay changes the shape of messages[1].

Contributor guide

No contributing guide indexed for this repository

Research direction

Reproduce the issue with the documented `claude -p ... --resume ` commands and compare the captured fresh and resumed requests. Start at the resume history-replay path, focusing on messages[1], and consider the work done when the resumed request preserves its content-block array and `cache_control` so stable context is read from the prompt cache instead of rewritten.

Written by the indexing model from the issue text.

Assessment

Domain
api, cli, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
54/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.