anthropics / anthropics/claude-code

[BUG] Turn-boundary prompt-cache misses on Fable 5.1 / Claude 5: 7% → 29% between 2.1.224 and 2.1.252, mechanism captured, fixed in 2.1.259 — with two residuals and one default worth changing

Đang mở
#91,707 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
area:core area:cost area:hooks area:plugins bug has repro platform:macos
Ngôn ngữ chính
Python
Star
145k
Fork
23.1k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

Connecting #83913, #87966, #87215, #87227 and #91514, which I believe are one regression. I captured full `/v1/messages` bodies on both sides of every turn boundary plus the server's `cache_miss_reason` (via the `cache-diagnosis-2026-04-07` beta) on 2.1.252 and again on 2.1.259.

Environment: macOS, OAuth (Max), `claude-fable-5-1[1m]` and `claude-fable-5`, one large agentic monorepo (~30 hooks, background subagents, teammate messages), Aug 20 – Sep 3.

## Symptom, from ~5,300 turn boundaries

| client | turn-boundary misses (<5 min idle) |
|---|---|
| 2.1.224 | 7.2% (n = 4,757) |
| 2.1.252 | **29.2%** (n = 538) |

Mid-turn tool loops: ~0.7% on every version. Misses tier by what survives (`cache_read` at the miss): **T1** read ≈ system+tools only, **T2** read ≈ system+tools+early history. Teammate/subagent-completion-opened turns missed at **10.6×** the typed-turn rate — that is #87215 and the "subagent wake" reports in #87966 / #91514. On Fable 5.1 across two days: 206M cache-write tokens, 96% from 484 such misses.

## Mechanism: hook `additionalContext` is dropped on the next history rebuild (= #83913)

3-arm control, haiku, `--strict-mcp-config`, identical 8-turn spawn-heavy workload, everything else byte-identical:

| arm | misses | cache_creation |
|---|---|---|
| PreToolUse hook emitting `additionalContext`, text changes each call | 7 (1×T1, 6×T2) | 210,497 |
| PreToolUse hook emitting `additionalContext`, text **constant** | 7 (1×T1, 6×T2) | 171,457 |
| no hook | **0** | 85,110 |

Byte diff, request N → N+1, at the user message carrying the spawn tool_results:

```
N : blocks = [tool_result ×5, text ×6]
…"type":"tool_result"},{"text":"\nPreToolUse:Agent hook additional context: …\n","type":"text"}, …
N+1 : blocks = [tool_result ×5]
…\n\n\nThe task tools haven't been used recently… ← re-inlined INTO the last tool_result
```

Every `` block carrying hook context is removed from that message and the between-turn reminder is folded into the last tool_result's text. `system`, `tools`, params, headers: identical. Divergence is always at the previous turn boundary; T1 vs T2 is only *where* in history that message sits. Teammate-opened turns are worst because the completion arrives as a between-turn notification and the rebuild runs against the message most likely to carry hook context from the spawn.

Refuted along the way so nobody re-chases them: string↔block content-shape flip (canonicalized server-side; controlled A/B = full hit), `tools` churn from `ListAgents`/`SendMessage` (byte-identical across a teammate lifecycle), output-style block, per-turn effort statement, MCP tool-list deltas.

## 2.1.259 fixes it

Same project, Fable 5.1, real hooks (one PreToolUse hook still emits `additionalContext`), background subagents + SendMessage + teammate-completion turns:

| session | requests | boundary misses |
|---|---|---|
| A | 20 | 0 — `cache_read` monotonic 98k → 123k |
| B | 37 | 1 (first typed turn); all 6 teammate-opened turns full hits |
| C (real work, 720 req) | 720 | 7: one pasted image (#91705), five `cd`-triggered system-prompt rewrites + one memory-index edit (#91706) |

On 2.1.259 the hook context is rendered as a `role:"system"` turn and survives every rebuild byte-for-byte. Suggest closing #87227 / #87215 / #91514 as duplicates of #83913 fixed in 2.1.259. Workaround for anyone pinned below: don't emit `additionalContext` from PreToolUse hooks; write it to a file the model reads.

## Residual 1 — a pasted image is re-rendered as a file pointer on the next request

Sent as base64 on the turn it's pasted; on the next request the same message becomes `[Image: source: ~/.claude/image-cache/…/1.png]`. Prefix breaks there: on a 232k-token session, read fell to 84k and 159k was rewritten for a 136 KB PNG — and the model no longer has the image (the pointer is text; it doesn't re-read the file). Keeping the block byte-stable would cost ~1.5k *cached* tokens/turn. Filed separately with the receipt: #91705. Workaround: deliver images as a path the model `Read`s (tool results are replayed byte-for-byte and the image stays in context); I built a menu bar app that makes the native screenshot shortcut do that, https://github.com/ArshansGithub/shotpath.

## Residual 2 — a persisting `cd` in Bash rewrites the system prompt and drops the entire cache

The most expensive thing left, and it took a recorder in front of the session to see. Claude Code tracks the shell cwd across Bash calls; on the next system-prompt rebuild it re-resolves project context from the new cwd, and in a monorepo with nested `.claude/` trees the agent-memory root flips and the first system block changes. Captured five times in one session at ~580k tokens each; two `cd` round-trips cost ~$24. Byte diff, mechanism, and the client-side fixes that would close it are in #91706. Worked around with a hook that denies a persisting `cd` and by consolidating the nested trees: zero collapses since.

(Earlier drafts of this issue blamed async notifications and the mid-conversation `role:"system"` fallback; both were timing coincidences and are withdrawn.)

## A second default worth revisiting: `security-guidance` ships enabled and runs an Opus 4.7 call at the end of every turn

Unrelated to caching, found while attributing account usage. The official `security-guidance` plugin is enabled by default with the marketplace. Its Stop hook sends every turn's diff to `claude-opus-4-7` for an LLM review, and its commit hook runs an SDK agent with Read/Grep/Glob on every `git commit`, all on the user's own OAuth token. On an agentic setup (orchestrator + subagents, so many turn-ends per minute) over the last 7 days:

| | |
|---|---|
| Stop-hook runs | 3,943 |
| Opus 4.7 review calls | 3,070 (avg 88k input tokens each, 5m cache tier) |
| findings returned to the model | 0 |
| findings that blocked anything | 0 |
| list-price equivalent | ~$1,390 (~$200/day) |

That was about the same magnitude as the entire Fable 5.1 cache waste above, on the account that then hit its weekly window. The regex layer is cheap and fine; the per-turn LLM review deserves to be opt-in, or at least to default to a small model and skip subagent turns.

## Result

2.1.259 + `promptCacheTtl: "1h"` + hooks not emitting `additionalContext`, measured on a 540-request real session: write share of context 8.7% → 1.7%, input cost per context token 2.2× lower, every thinking block replayed. Three quarters of the remaining writes were the four events above.

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Hướng nghiên cứu

Start by comparing captured /v1/messages bodies and cache_miss_reason on 2.1.252 versus 2.1.259, using a PreToolUse hook that emits additionalContext and the described spawn-heavy workload. Check the separate reports #91705 and #91706 for the two residuals. Done means the reported boundary miss mechanism is reproducible or confirmed fixed, with remaining image and cwd rewrites clearly separated.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
bash, git
Lĩnh vực
cli, developer-experience, performance
Loại issue
Lỗi
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Cần làm rõ
Mức phù hợp với người mới
15/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.