[BUG] Advisor is omitted from internal fork requests, so /compact re-bills the whole conversation as a cache write
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
/compact is served by an internal forked request: Claude Code re-sends the conversation to
the model with a summarisation instruction appended, using query_source: "compact". That
fork is supposed to share the main loop's prompt-cache prefix.
When the advisor tool is enabled, it does not. The fork is sent with a different tools
array and a different system prompt than the main loop, so it cannot reuse the conversation
cache the main loop wrote seconds earlier. Every compaction re-processes the whole
conversation body as a cache write.
Setting CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1 makes the two request bodies identical apart
from cache_control, and the summarisation request then reads the entire conversation from
cache with zero cache creation.
This is deterministic. It fires on every compaction, for every user who has the advisor
enabled, and the cost scales with context size.
A/B result
Same input file, same model, same procedure. The only difference isCLAUDE_CODE_DISABLE_ADVISOR_TOOL=1. Measured with OpenTelemetryclaude_code.api_request events.
| | advisor enabled | advisor disabled |
|---|---|---|
| context before /compact | 124,770 | 123,698 |
| last main-loop request | read 119,636 / create 5,134 | read 118,464 / create 5,234 |
| compact cache_read_tokens | 64,813 | 118,570 |
| compact cache_creation_tokens | 54,008 | 0 |
| compact input_tokens | 6,462 | 6,561 |
| compact cost_usd_micros | 86,493 | 24,688 |
3.5x cost for one compaction on Haiku 4.5. The 54,008 tokens re-written in the enabled arm
are the conversation body, which the main-loop request had cached seconds earlier.
The 64,813 that still reads in the enabled arm is a static head left in the cache by an
earlier request of the same fork shape, from a previous test session. It is not a partial hit
against the main loop's entry. With no such entry present the summarisation request reads
nothing at all:
| model and condition | context | compact read | compact create |
|---|---|---|---|
| haiku-4.5, real session via --fork-session | 78,411 | 0 | 75,249 |
| sonnet-5, MAX_THINKING_TOKENS=0 | 91,327 | 0 | 89,121 |
| sonnet-5, ENABLE_PROMPT_CACHING_1H=1 | 91,818 | 0 | 89,612 |
| sonnet-5, defaults | 91,634 | 31,175 | 58,259 |
Request body diff
Last main-loop request against the summarisation request, same session, seconds apart,
captured off the wire with a local recording proxy in front of api.anthropic.com.
Reproduced on three separate sessions.
| | main loop | compact |
|---|---|---|
| tools | 67 entries | 66 entries |
| missing entry | | advisor, which is index 66, the last element |
| system[2] length | 10,918 chars | 8,899 chars |
| relation | main equals compact plus a trailing 2,019-char # Advisor Tool section | |
| cache_control on system[1], system[2] | {"type":"ephemeral","ttl":"1h"} | {"type":"ephemeral"} |
| messages | 23 | 23 |
| model, thinking, effort, max_tokens | identical | identical |
The summarisation instruction is appended as a text block to the final user message, so the
message count does not change.
With CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1 the same comparison gives 66 tools on both sides
and byte-identical system blocks, and the summarisation request achieves a full cache read.
I am reporting the controlled A/B rather than asserting an implementation detail. The
one-tool and 2,019-char system difference coincides exactly with the miss, and removing the
difference removes the miss.
What I ruled out
- Cache TTL. The two requests do ask for different TTLs, but that is not the cause. Forcing
ENABLE_PROMPT_CACHING_1H=1 so both ask for 1h did not restore the hit (row 3 above: read
0, create 89,612, confirmed as a 1h write by reconciling cost_usd_micros against the 2x
multiplier). Conversely the advisor-disabled arm still has the TTL difference and still
achieves a full read. A 5-minute breakpoint reads a prefix written with ttl: "1h" fine.
- Effort. The
api_requestevents reporteffort: highfor both requests. - Thinking configuration. With
MAX_THINKING_TOKENS=0both requests send
{"type":"disabled"}, verified in the captured bodies, and the miss persists.
- Message content. Same message count, and the only addition is a text block on the final
user message.
Scope beyond /compact
The same mismatch appears in the other internal forked requests I captured, includingprompt_suggestion and generate_session_title. In one session prompt_suggestion
re-processed 90,264 tokens as a cache write on a single turn. The advisor appears to be
attached only for a small set of query sources, with internal forks excluded, but this report
stays focused on /compact because that is where the cost is largest and most predictable.
Not a duplicate of #81967
#81967 concerns the tools array changing over time within one conversation, primarily as
LSP tools are added or removed, and deferred tool schema loads growing tools[].
This report is different. The main-loop request shape is stable and the fork request shape is
stable. They are permanently different from each other whenever the advisor is enabled. No
mid-session mutation is required, and every warm-cache compaction reproduces the miss.
Related but distinct:
- #77306 is a session-level fork losing the parent cache, triggered by session-specific
system prompt content. This report concerns internal forked requests such as compaction.
- #81620, #84738 and #81029 concern advisor usage accounting inflating the auto-compact
counter. Different mechanism, different place where the loss occurs. Fixing those would not
fix this.
What Should Happen?
An internal forked request should carry the same tools array and system prompt as the main
loop, so that a warm-cache /compact reads the conversation from cache instead of re-writing
it.
If the advisor must not be callable from inside a fork, the fix should keep the tool declared
and reject the invocation, for example by marking it disabled in the fork and returning an
error if it is called. Removing the declaration from the request is what breaks the cache.
This is also what the documentation currently promises.
docs/en/prompt-caching, section "Compacting the conversation":
To produce the summary, Claude Code sends a separate request with the same system prompt, tools, and history as your conversation, plus a summarization instruction appended as a final user message. While the cache is warm, that request reads your prefix from the cache, so a mid-session /compact costs a fraction of what the context size suggests.
With the advisor enabled the system prompt and tools are not the same, and the request does
not read the prefix from the cache.
docs/en/advisor, section "Impact on prompt caching":
Enabling or disabling the advisor mid-session does not invalidate your main model's prompt cache.
That claim is about the main loop and may well hold there. It does not extend to internal
forks, and nothing in the docs warns that enabling the advisor turns every compaction into a
full cache write.
Error Messages/Logs
# advisor enabled: claude_code.api_request events, Haiku 4.5, MAX_THINKING_TOKENS=0
107 repl_main_thread in 5 read 114091 create 5545 out 109 cost 23049
114 repl_main_thread in 5 read 119636 create 5134 out 13 cost 22302
119 compact in 6462 read 64813 create 54008 out 1208 cost 86493
# same rig plus CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1
101 repl_main_thread in 5 read 113016 create 5448 out 110 cost 22753
108 repl_main_thread in 5 read 118464 create 5234 out 4 cost 22339
113 compact in 6561 read 118570 create 0 out 1254 cost 24688
# request body comparison, advisor enabled
main tools 67 comp tools 66 only in main: ['advisor'] (index 66 of 67)
main sys [70, 57, 10918] comp sys [70, 57, 8899]
main sys[2] == comp sys[2] + 2019 trailing chars ("# Advisor Tool" section)
# request body comparison, CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1
main tools 66 comp tools 66 tools identical: True
main sys [70, 57, 8899] comp sys [70, 57, 8899] system identical: True
Steps to Reproduce
Before starting, turn prompt suggestions off. prompt_suggestion is served by the same
internal fork and writes the same prefix shape, so with it enabled the compact request can
read an entry the suggestion request wrote moments earlier, and the measurement understates
the miss. I have seen exactly that: a suggestion request created 40,298 tokens and the
compact request three requests later read 40,298 with zero creation. The A/B above was taken
with suggestions disabled.
- Run a recording reverse proxy on
127.0.0.1:8788that forwards toapi.anthropic.com
and writes each /v1/messages request body to a file.
- Run an OTLP receiver on
127.0.0.1:4318. - Make sure an advisor is configured (
/advisor), then start a session through both:
``bash``
env ANTHROPIC_BASE_URL=http://127.0.0.1:8788 \
MAX_THINKING_TOKENS=0 \
CLAUDE_CODE_ENABLE_TELEMETRY=1 \
OTEL_METRICS_EXPORTER=none \
OTEL_LOGS_EXPORTER=otlp \
OTEL_EXPORTER_OTLP_PROTOCOL=http/json \
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 \
claude --model haiku
MAX_THINKING_TOKENS=0 is only there to remove thinking configuration as a variable; the
bug reproduces without it.
- Build roughly 120k of context. I used a generated 120 KB text file and asked the model to
read it in full with repeated Read calls.
- Run
/compact. - Look at the
claude_code.api_requestevent withquery_source: "compact". It reports
cache_creation_tokens covering the whole conversation and little or no
cache_read_tokens.
- Compare the last
repl_main_threadrequest body with thecompactrequest body. The
tools array differs by one entry and the last system block differs by the trailing
advisor section.
- Repeat the whole run with
CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1added. The two bodies now
match and the compact request reads the conversation from cache with zero creation.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
Not known. Only 2.1.239 was measured.
Claude Code Version
2.1.239 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
- Account is Claude Max, first-party API, no gateway or proxy in normal use. The recording
proxy was added only for this investigation.
- Reproduced on
claude-haiku-4-5-20251001and onclaude-sonnet-5. - Also reproduced on a real working session rather than a synthetic one, by duplicating it
with claude -r <id> --fork-session and compacting the copy: 78,411 tokens of context,
compact read 0, create 75,249.
- The summarisation request is not written to the transcript, so this cannot be diagnosed
from ~/.claude/projects/*/*.jsonl. The transcript records the main loop's cache write and
the compact_boundary entry, with nothing in between. OpenTelemetry or a proxy is needed
to see it.
- Practical impact for a heavy user: on a large Opus session, every compaction pays a full
cache write over the whole context instead of a cache read, at 1.25x base input rather than
0.1x. That is roughly a 12x difference on the dominant term, and it repeats for every
compaction in a long session.
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
The
platform:wsllabel is a misfire — this is not WSL-specific.The cause is request construction: the internal forked request omits one tool entry and a
trailing system prompt section relative to the main loop. That is independent of operating
system, terminal and shell. WSL is simply where I happened to measure it.
I would expect it to reproduce on macOS and native Linux for any user who has an advisor
configured.
area:costandarea:coreare right;platform:wslwill send triage in thewrong direction.
Additional cost data, since this is not limited to
/compact.prompt_suggestionis served by the same internal fork, fires at the end of essentiallyevery turn once the conversation has two assistant messages, and writes with
ttl: "1h", soits writes are billed at 2x base input — the same multiplier as the main loop.
With the advisor enabled it cannot read the main loop's entry either, so it builds and then
maintains a second cache lineage alongside the main one. The increments it writes over a
session sum to roughly the same total as the main loop's own writes. Not a parallel request,
a parallel lineage.
Four identical short turns on Haiku 4.5, only
CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1differing:| | advisor enabled | advisor disabled |
|---|---|---|
| main loop
cache_creation| 30,879 | 29,909 ||
prompt_suggestioncache_creation| 29,973 | 12 || main loop
cost_usd_micros| 71,365 | 68,846 ||
prompt_suggestioncost_usd_micros| 67,182 | 10,213 |Per suggestion call:
With the advisor disabled the suggestion request reads the main loop's entry and writes four
tokens. With it enabled it duplicates the lineage. Total session spend here was 1.75x, on a
trivial 30k conversation.
The 2x multiplier is confirmed from cost rather than assumed:
29,819 x $2/M + 343 x $1/M + 22 x $5/M = 60,091micro-USD, matching the reportedcost_usd_microsexactly. A 5m write at 1.25x would give 37,727.There is also a guard that looks like it was meant to prevent this spending. Suggestion
generation is skipped when
input_tokens + cache_creation_input_tokens + output_tokensexceeds 10,000 on the last assistant message. But it inspects the main loop's message, not
the fork's. With the advisor enabled the main loop stays warm and passes the guard, while the
fork behind it pays a full write.
Recommendation for anyone reading this
If you have an advisor configured, turn prompt suggestions off until this is fixed. With both
enabled you pay a second full set of cache writes, at the same 2x multiplier, for the whole
session.
This is easy to misread. I noticed my usage climb after enabling the advisor and put it down
to the advisor calls themselves being expensive. They are, but a large part of what I was
seeing was this: a duplicated cache lineage maintained turn after turn by the suggestion
request. The advisor's own calls are visible in the transcript; this is not, which is
probably part of why it has gone unreported.
Disabling suggestions keeps the advisor and removes most of the extra spend. What remains is
the compaction request itself, which is what this issue is about.
Your A/B with cost_usd_micros, and pulling prompt_suggestion out as its own
cache line, is the only writeup of this I've seen with numbers in it rather
than impressions.
The piece missing is the price. A cache write costs 1.25 times a normal input
token, so re-billing a conversation as a write is 25 percent more expensive
than sending those same tokens as plain input, and there's no read afterwards
to earn it back. Every compaction pays that on the whole prefix.
On ttl "1h" it's worse. Writes cost 2x there, and break-even moves from 0.28
reads per write to 1.11.
You filed the same shape against OpenCode earlier, where an injected
prunable-tools list broke the prefix. Same pattern, two harnesses.
How are you accounting for this per session rather than per request?
This makes sense mechanically — compact rebuilds the conversation prefix, so the entire compacted context
becomes a fresh cache_creation event. The question is how big that payload actually is.
From analyzing 82 of my own sessions: the average prompt at compaction time was around 292K tokens, and 93.9%
of that was tool output (grep results, file reads, build logs). So a single /compact can easily trigger a 250K+
cache_write in one shot.
I wrote a read-only CLI that plots the per-turn prompt size over a session — you can visually see the sawtooth
pattern where context grows linearly, /compact drops it, then the cache_write spike hits:
https://github.com/yingxiangge/agent-cost
Might help quantify exactly how much each /compact is costing in your case.
Still not fixed as of 2.1.251. Reproduced under the same conditions as the original report (Haiku 4.5,
MAX_THINKING_TOKENS=0): the compact request still omitsadvisorfrom the tools list (76 vs. 77) and drops its 2,019-char section fromsystem[2]— the same numbers as on 2.1.239. The A/B is also unchanged: one compaction costs 3.0x with the advisor enabled (cache read 0 / create 40,634) vs. disabled withCLAUDE_CODE_DISABLE_ADVISOR_TOOL=1(read 40,631 / create 0).