Auto-compaction: two compounding cost bugs — stale precompute keeps ~200k tokens verbatim, and that prefix is repeatedly cache-created instead of cache-read
Summary
Two separate but compounding cost problems in the auto-compaction path. The first bloats the conversation; the second then pays to re-ingest that bloat repeatedly.
- Stale precompute → shallow compaction.
/compactconsumes a background precompute summary that was baked ~47 minutes earlier. Consuming it only summarizes up to the old anchor and preserves every message since (~200k tokens) verbatim, so compaction barely reduces context. There's no staleness/tail-size guard that would rebuild the precompute or fall back to a fresh summary. - **Cache-sharing miss → the bloated prefix is cache-created instead of cache-read. Because the main-thread request anchors its body
cache_controlbreakpoint on the last (volatile) message, the background precompute (and the following turn) cannot reuse the cached body — they re-create the full ~196k at the cache write rate ($10/MTok 1h) instead of the read** rate ($0.50/MTok), ~20× more, and it recurs on every precompute.
Environment
- Claude Code version: 2.1.186 (
claude-cli/2.1.186, native install) - Model:
claude-opus-4-8 - Platform: macOS (darwin arm64), Node v24.3.0
- Relevant settings (
settings.json→env, possibly influencing compaction trigger frequency):
"CLAUDE_CODE_DISABLE_1M_CONTEXT": "1",
"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "80"
(The first caps the context window at 200k instead of 1M; the second sets the auto-compact threshold to 80% of the window — together they make auto-compaction/precompute fire earlier and more often.)
Issue 1 — Stale precompute kept ~200k tokens verbatim
Precompute lifecycle from the debug log:
20:13:24 precomputed compact: started (main, 211 msgs, ~147k tok, attempt 2, trigger api_response)
20:15:07 precomputed compact: ready (103s)
21:02:46 precomputed compact: consumed (main, ready) <- consumed 47 min after it was built
- The
/compactran at 21:02:45; the summary it consumed is timestamped 20:15:07 — a 47-minute-old summary. - ~250 messages (180 assistant / 70 user) were created in that 47-minute window. The consume path preserves all messages-since-anchor verbatim (
messagesToPreserve = [...precompute.messagesToPreserve, ...messagesSinceAnchor]), so none of them are summarized. - Result: the post-compact conversation stays huge — observed at the next precompute as 452 messages / ~238k tokens. A fresh summarization at 21:02 would have collapsed those ~250 messages into a short summary instead.
Why it's a bug: a precompute is consumed regardless of how stale it is or how large the verbatim tail has grown. When the tail is ~200k tokens, the "compaction" frees almost nothing.
Issue 2 — The bloated prefix is cache-created, not read
Two back-to-back requests, 9s apart, on that ~238k-token conversation (162 messages):
REQ1 — main-thread user turn (source=repl_main_thread):
"usage":{"input_tokens":1494,"cache_creation_input_tokens":196093,"cache_read_input_tokens":39766,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":196093},"output_tokens":312,"service_tier":"standard","inference_geo":"not_available"}
REQ2 — background precompute (source=compact, forked reactive-compact):
"usage":{"input_tokens":3737,"cache_creation_input_tokens":195784,"cache_read_input_tokens":39766,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":195784},"output_tokens":6549,"service_tier":"standard","inference_geo":"not_available"}
Debug log for REQ2:
21:16:55 precomputed compact: started (main, 452 msgs, ~237,765 tok, attempt 3, trigger api_response)
21:16:56 [API REQUEST] /v1/messages source=compact
21:18:11 reactive-compact finished: cacheRead=39766 cacheCreate=195784
21:18:11 precomputed compact: ready (main, 75333ms)
cache_read_input_tokens is pinned at 39,766 (system prompt + tool defs only) in both; the entire ~196k message body is cache-created both times. Structural diff of the two request bodies:
- Messages
[0..159]are content-identical. - They diverge only at
messages[160](the final user turn): the precompute appends aCRITICAL: Respond with TEXT ONLY...+<analysis>/<summary>instruction. cache_controlbody breakpoint: REQ1 onmessages[160](the volatile last message); REQ2 onmessages[159].- The main thread only cached up to
messages[160](which the precompute then modifies), so the precompute has no usable checkpoint atmessages[159]and re-creates from the system head.
Why it's a bug: the precompute is intended to cache-read the conversation (tengu_compact_cache_prefix / tengu_compact_cache_sharing). The breakpoint-on-the-volatile-message placement defeats that, so the share misses and the body is re-created. The tengu_compact_cache_sharing_fallback event is presumably firing; its reason would quantify fleet-wide frequency.
Impact
- Issue 1: every post-compaction turn carries ~200k unnecessary tokens (≈ +$0.10/turn in cache reads at $0.50/MTok, plus larger periodic re-creates), and the next precompute must summarize a needlessly large ~238k context.
- Issue 2: each background precompute re-creates the full body at write rates — ~196k × ($10 − $0.50)/MTok ≈ ~$1.9 of avoidable spend per precompute. This session ran the precompute 3 times (18:22, 20:13, 21:16) → ~$5+ avoidable, and it scales with conversation size (so the longest sessions pay most). The single pair above cost ≈ $4.16, ~$3.9 of it avoidable cache-creation.
Suggested fixes
- Issue 1 — add a staleness / tail-size guard. When the messages-since-precompute exceed a token threshold (or the precompute is older than N minutes / M messages), rebuild the precompute or fall back to a fresh live summarization, so
/compactactually reduces context instead of preserving ~200k verbatim. - Issue 2 — align cache breakpoints. Either have the main-loop request place a breakpoint at the last committed message boundary (before the in-flight turn) so forks/precomputes can read up to it, or have the precompute reuse the main thread's existing breakpoint and append its instruction after that boundary — so only the small instruction delta is cache-created.
Repro / detection
- Long Opus session with auto-compact enabled, until
precomputed compact: startedfires. - Issue 1: compare the consumed summary's timestamp to the
/compacttime, and check how many messages were preserved verbatim. - Issue 2: on the
source=compactrequest,cache_creation_input_tokens≈ full context whilecache_read_input_tokens≈ only the system prompt indicates the miss. Checktengu_compact_cache_sharing_fallbackfor frequency/reason.
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗