[BUG] /compact misses the warm prompt cache on Opus 5.5 / Sonnet 5.5 and re-sends the whole conversation (fixed by CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS; Opus 5 unaffected)

Status Open
Reported on v2.1.289
Maintainer reply None cached
Activity 0 comments · opened Oct 5, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closest: #88755, same symptom, but its advisor workaround does not help here)
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.289)

What's Wrong?

On the 5.5 models (Opus 5.5, Sonnet 5.5), /compact misses the cached conversation and re-sends the whole history as a cache write. Only the system prompt and tools are read from cache. This happens even though the cache is warm: the compaction runs seconds after the previous turn, in the same live process.

The prompt caching docs say the opposite should happen: "To produce the summary, Claude Code sends a separate request with the same system prompt, tools, and history as your conversation [...] While the cache is warm, that request reads your prefix from the cache" (https://code.claude.com/docs/en/prompt-caching#compacting-the-conversation).

On Opus 5 the same procedure with the same version reads the full conversation from cache. On Opus 5.5 with CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1, it also reads the full conversation from cache. So some pre-release capability that is on by default makes the compaction request differ from the main loop's request on the 5.5 models.

It is not the advisor (#88755): CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1 alone does not change the result. It is not the TTL buckets either: FORCE_PROMPT_CACHING_5M=1 and CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL=1h both still miss.

The cost scales with context size. For a session of several hundred thousand tokens, every /compact (and presumably auto-compaction) re-bills the whole conversation at the cache write rate instead of the read rate.

What Should Happen?

While the cache is warm, the compaction request should read the conversation prefix from cache, as it does on Opus 5 and as documented.

Error Messages/Logs

The figures below are for the /compact step alone: deltas of modelUsage between the result before /compact and the result of /compact, in one live --input-format stream-json process. pre_tokens comes from compact_boundary.compact_metadata.

| Version | Model | Flags | pre_tokens | cache read | cache write |
|---|---|---|---|---|---|
| 2.1.284 | claude-opus-5-5[1m] | default | 17,276 | 2,943 | 14,230 |
| 2.1.284 | claude-opus-5-5[1m] | CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL=1h | 17,293 | 2,943 | 14,256 |
| 2.1.284 | claude-sonnet-5-5 | default | 17,219 | 2,850 | 14,249 |
| 2.1.289 | claude-opus-5-5[1m] | default | 36,516 | 12,500 | 23,897 |
| 2.1.289 | claude-opus-5-5[1m] | CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1 | 36,583 | 12,500 | 23,957 |
| 2.1.289 | claude-opus-5-5[1m] | CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 | 51,397 | 51,285 | 28 |
| 2.1.289 | claude-opus-5[1m] | default | 37,612 | 37,509 | 4 |
| 2.1.260 | claude-opus-5[1m] | default | 36,889 | 36,802 | 0 |
| 2.1.240 | claude-opus-5[1m] | default | 40,404 | 40,313 | 0 |

The 2.1.284 rows ran with --tools Read; the 2.1.289 / 2.1.260 / 2.1.240 rows used the default tools, which is why pre_tokens is larger there. A FORCE_PROMPT_CACHING_5M=1 run on 2.1.284 / Opus 5.5 showed the same miss pattern. On 2.1.260, claude-opus-5-5[1m] is rejected with "does not support this model", so that combination could not be tested.

In every miss, the turns before /compact hit the cache normally: each turn reads the previous prefix and writes only the new exchange.

Steps to Reproduce

  1. Save the script below as probe.py.
  2. Run MODEL='claude-opus-5-5[1m]' python3 probe.py. compact_read is about the system prompt size and compact_write is about the whole conversation.
  3. Run MODEL='claude-opus-5[1m]' python3 probe.py. compact_read is about equal to pre and compact_write is about 0.
  4. Run CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 MODEL='claude-opus-5-5[1m]' python3 probe.py. Same full hit as step 3.
import json, subprocess, random, os, sys
random.seed(int(os.environ.get('SEED', '51')))
words = ['orbit', 'cache', 'window', 'summary', 'thread', 'engine', 'token', 'prefix', 'compact', 'session']
blob = lambda n, s: ' '.join(random.choice(words) + str(s + i) for i in range(n))
args = [os.environ.get('CLAUDE_BIN', 'claude'), '-p', '--input-format', 'stream-json', '--output-format', 'stream-json',
        '--verbose', '--strict-mcp-config'] + (['--model', os.environ['MODEL']] if os.environ.get('MODEL') else [])
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1,
                     env={**os.environ, 'DISABLE_AUTOUPDATER': '1'})
tot = {}
def send(text):
    p.stdin.write(json.dumps({"type": "user", "message": {"role": "user", "content": text}}) + "\n"); p.stdin.flush()
    meta = None; model = None
    while True:
        line = p.stdout.readline()
        if not line: sys.exit('cli exited')
        o = json.loads(line)
        if o.get('type') == 'system' and o.get('subtype') == 'init': model = o.get('model')
        if o.get('type') == 'system' and o.get('subtype') == 'compact_boundary': meta = o['compact_metadata']
        if o.get('type') == 'result':
            now = {}
            for v in (o.get('modelUsage') or {}).values():
                for k in ('cacheReadInputTokens', 'cacheCreationInputTokens', 'inputTokens', 'outputTokens'):
                    now[k] = now.get(k, 0) + (v.get(k) or 0)
            d = {k: now.get(k, 0) - tot.get(k, 0) for k in now}; tot.update(now)
            return d, meta, model
for t in [f"Notes part 1, keep in mind: {blob(1500, 0)}. Reply only OK.",
          f"Notes part 2: {blob(1500, 2000)}. Reply only OK.",
          "Which word+number came first in part 1? Answer in five words."]:
    _, _, model = send(t)
d, meta, _ = send('/compact')
print(json.dumps({"model": model, "pre": meta and meta.get('pre_tokens'), "post": meta and meta.get('post_tokens'),
                  "compact_read": d.get('cacheReadInputTokens'), "compact_write": d.get('cacheCreationInputTokens'),
                  "compact_input": d.get('inputTokens'), "compact_out": d.get('outputTokens')}))
p.stdin.close(); p.wait()

Claude Model

Opus 5.5 and Sonnet 5.5 (affected); Opus 5 (not affected)

Is this a regression?

I don't know. It depends on the model rather than the CLI version: Opus 5 works on 2.1.240, 2.1.260 and 2.1.289.

Claude Code Version

2.1.289 (also 2.1.284)

Platform

Anthropic API (Claude subscription; main-conversation writes use the 1-hour TTL)

Operating System

macOS

Terminal/Shell

Non-interactive (-p --input-format stream-json)

Additional Information

CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 is not a practical workaround. It fixes compaction and keeps the 1-hour TTL, but a one-line -p prompt then wrote 81,033 tokens to cache instead of 21,588. Every request carries roughly 60k more tokens, likely because tool definitions load up front.

Related: #88755 (same symptom, advisor cause), #70459 (auto-compaction cache creation).

View original on GitHub ↗