anthropics / anthropics/claude-code

[BUG] Advisor is omitted from internal fork requests, so /compact re-bills the whole conversation as a cache write

オープン
#88,755 コメント 6 件 リアクション 0 件 担当者 0 名 GitHub で見る
area:core area:cost bug has repro
主要言語
Python
スター
145k
フォーク
23.1k
PR マージ指標
PR 指標を取得中

説明

## 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 is
`CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1`. Measured with OpenTelemetry
`claude_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_request` events report `effort: high` for both requests.
- Thinking configuration. With `MAX_THINKING_TOKENS=0` both 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, including
`prompt_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

```shell
# 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.

1. Run a recording reverse proxy on `127.0.0.1:8788` that forwards to `api.anthropic.com`
and writes each `/v1/messages` request body to a file.
2. Run an OTLP receiver on `127.0.0.1:4318`.
3. 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.
4. 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.
5. Run `/compact`.
6. Look at the `claude_code.api_request` event with `query_source: "compact"`. It reports
`cache_creation_tokens` covering the whole conversation and little or no
`cache_read_tokens`.
7. Compare the last `repl_main_thread` request body with the `compact` request body. The
`tools` array differs by one entry and the last system block differs by the trailing
advisor section.
8. Repeat the whole run with `CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1` added. 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-20251001` and on `claude-sonnet-5`.
- Also reproduced on a real working session rather than a synthetic one, by duplicating it
with `claude -r --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.

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

調査の方向性

Start with the /compact internal request path and the query_source "compact" request, then compare it with the main-loop request as described in the report. Read docs/en/prompt-caching and docs/en/advisor for the intended behavior. Done means the fork preserves the main request's tools and system prompt and a warm-cache compaction reports no conversation-wide cache write.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
api, performance, tooling
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
52/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。