Claude Code CLI 2.1.235 上下文治理与多层缓存¶
从三轮 Read 请求看上下文怎样增长¶
用户在 Claude Code 里输入:
读取
reverse/javascript/cli.readable.js第 408719 行附近,解释消息缓存点怎样选择。
模型此时还没看到目标代码。第一轮 Messages 请求中,与这个问题直接相关的内容可以缩写成:
system:
Claude Code 的固定行为规则、工具使用规则……
tools:
Read { file_path: string, offset?: number, limit?: number }
messages:
user: 读取 cli.readable.js 第 408719 行附近,解释消息缓存点怎样选择。
模型先返回一个工具调用:
assistant: tool_use(id=toolu_1, name=Read,
input={file_path:"reverse/javascript/cli.readable.js", offset:408719, limit:30})
Claude Code 执行 Read 后才发第二轮请求。这一轮不是只发送刚读到的代码,而是在第一轮消息后追加工具调用和结果:
system: [与第一轮相同]
tools: [与第一轮相同的 Read schema]
messages:
user: 读取 cli.readable.js 第 408719 行附近……
assistant: tool_use(id=toolu_1, name=Read, ...)
user: tool_result(tool_use_id=toolu_1):
function $km({ messages: e, enablePromptCaching: t2, ... }) {
...
}
模型为了确认 $km() 的结果怎样写回请求,又读取第 410209 行附近。第三轮请求继续保留前两轮内容,并在末尾再追加一组消息:
assistant: tool_use(id=toolu_2, name=Read,
input={file_path:"reverse/javascript/cli.readable.js", offset:410209, limit:16})
user: tool_result(tool_use_id=toolu_2):
function CpT(e, t2, r2, ...) {
let { markerIndices: a } = $km(...)
...
}
这三个请求最前面的 system 规则和 Read 定义没有变化。它们就是适合复用的稳定前缀。Claude Code 仍然按请求语义携带这些内容,只是在合适的 block 上写入 cache_control;前缀相同且缓存仍有效时,API 可以复用已经处理过的部分。这才是 Prompt Cache:它降低重复输入的处理费用和首 token 延迟,不会从逻辑上下文中删除 system prompt,也不会让后面的 tool_result 消失。
这里只有一个 Read,工具定义很短。如果同一位置还放着几十到上百个 MCP 工具的 name、description 和 input schema,它们即使从未被调用,也会占用上下文。Tool Search 解决的是这部分常驻体积:候选工具先以 deferred 形式存在,Claude 选中某个工具后再取得其完整 schema。它改变的是 schema 何时进入模型上下文,不是缓存 Read 返回的文件正文。
再看 messages:第二轮加入 $km() 的源码,第三轮又加入 CpT() 的源码;继续读文件、跑测试时,每个新的 tool_result 都会沿这条消息链累积。Prompt Cache 可以复用已经出现过的消息前缀,却不能阻止逻辑 token 总量继续增长。旧工具结果满足后文的候选范围和节省阈值时,Claude Code 才会用 microcompaction 把其中的大块正文换成持久化提示或 [Old tool result content cleared],同时留下工具调用关系和近期结果。
局部清理之后,用户需求、模型判断、工具调用和仍需保留的结果还会继续增长。如果用户没有手动输入 /compact,客户端会等这份活跃历史逼近有效上下文窗口,才自动用 compact 处理更大的范围:让模型总结较早历史,再用较短的消息表示继续后续请求。它改写的是历史表示,不是 Prompt Cache;各条 compact 路径怎样选切点、是否保留近期消息组以及 resume 怎样借 boundary 恢复,后文分别展开。第一次单独理解 /compact 时,可配合阅读图文机制专题。
上面的字段是为了阅读而缩写的请求形状,省略了 UUID、HTTP header、完整 system 文本和 schema,不是逐字抓包。2.1.235 只启用 Read 时发出的首个 Messages 请求已由 runtime-controls.json 捕获;该 Probe 使用的是 REQUEST_SHAPE_MARKER,文中的源码问题只负责把同一请求结构具体化。本文中的函数名是压缩 bundle 的定位符,不是 Anthropic 原始源码 API。主要代码证据来自 reverse/javascript/cli.readable.js,机器结构来自 analysis/source-inventory/。
下面从第一轮请求内部开始,逐项看这些内容由谁装入、怎样计量,以及何时被复用、延迟或改写。
一次请求到底装了什么¶
1. 原始会话历史¶
会话历史不只是 user/assistant 文本。内部消息还包括:
- tool use 与 tool result;
- attachment 和系统提醒;
- permission、hook、background task 等产生的元消息;
api_system中途系统消息;compact_boundary与 compact summary;- fork/resume 所需的 UUID、parent UUID 和 logical parent。
发 API 前,代码先规范化消息,再决定哪些消息末尾可以放 cache marker。assistant 最后一个 block 如果是 thinking、redacted thinking 或不可缓存 block,不会被当成合法 breakpoint;代码会向前退到最近可缓存消息。证据见可读版 408719-408747、410209-410219。
2. System prompt 固定层¶
l8() 负责构建默认 system prompt。它包含的不是一段固定字符串,而是一组有条件的 section,例如:
- communication、action caution、task continuity;
- session guidance、tool 参数规则、语言和输出风格;
- memory、环境信息、scratchpad;
- context management、brief、focus mode;
- Agent/autonomy/deferred-tool 规则;
- Harness、工具权限和 hook 行为。
不同模型、provider、permission mode、工具集合、feature gate 和运行模式会改变 section 集合。代码通过 q2(section-key, builder) 这类结构生成 section,因此 system prompt 本身也是版本对比的重要产品表面,而不是一个常量。证据见 408055-408085。
3. 机器动态层¶
下列内容天然容易变化:
- cwd、日期和环境信息;
- memory path 与加载到的
CLAUDE.md/rules; - Git status;
- MCP server 当前连接状态;
- 本轮动态 context reminder。
CLI 选项 --exclude-dynamic-system-prompt-sections 会把 cwd、env info、memory paths、Git status 从 system prompt 移到第一条 user message。它只对默认 system prompt 生效,显式 --system-prompt 时忽略。目的不是少发这些内容,而是避免每台机器、每个目录都改变 system prompt 的稳定前缀,从而提高跨用户 prompt-cache 复用。证据见 603768 附近的 CLI 描述和 597472-597477 的装配路径。
这也解释了 2.1.235 的 LSP 修复:LSP 连接状态属于动态机器状态,不应该让长期稳定的 prompt 前缀整体失效。
4. User context 与 system context¶
k7(session) 和 O2(session) 分别装入用户/项目上下文及系统动态上下文。Ekm() 还会计算实际上下文构成并记录 tengu_context_size:
| 字段 | 含义 |
|---|---|
git_status_size |
Git 状态文本长度,不是 token 数 |
claude_md_size |
已装入 CLAUDE.md/rules 文本长度 |
total_context_size |
上述两项字符长度之和 |
project_file_count_rounded |
项目文件数的隐私化/取整结果 |
mcp_tools_count |
MCP 工具数量 |
mcp_servers_count |
从 MCP 工具名解析出的 server 数量 |
mcp_tools_tokens |
MCP schema 的估算 token |
non_mcp_tools_count |
内置及非 MCP 工具数量 |
non_mcp_tools_tokens |
非 MCP schema 的估算 token |
这里故意同时记录“字符”和“token”两类量。Git/CLAUDE.md 是文本体积;工具是序列化 schema 的 token 成本。证据见 408431-408466。
5. 工具 schema¶
工具定义至少包含 name、description、input_schema,并可带 strict、eager_input_streaming、defer_loading、cache_control。$Hi() 会按模型、provider、工具类型、JSON schema 和若干运行状态构造一个稳定 key,并把生成结果存进进程内 K5o 缓存。
因此“工具成本”有两部分:
- 生成工具描述/schema 的本地 CPU 成本;
- 把完整 schema 放进模型上下文的 token 成本。
进程缓存只解决第一部分,tool search 才解决第二部分。
一个先于所有缓存细节的结论:稳定前缀塑造了 Harness¶
官方 2026-04-30 的 Prompt caching is everything 给出了一个很重要的设计解释:Claude Code 不是在功能完成后“顺手加了缓存”,而是反过来让 prefix matching 约束整个 Harness 的形状。官方将顺序概括为 stable system/tools -> CLAUDE.md -> session context -> conversation messages,并明确把动态更新放进下一条 user/tool-result 的 <system-reminder>,而不是回头修改稳定 system 前缀。
2.1.235 的本地实现能把这条公开设计落到具体机制:
| 产品设计 | 如果直接实现会怎样 | 本版实际选择 | Cache 之外的收益 |
|---|---|---|---|
| 模式切换 | Plan Mode 进入时删除写工具 | tools 保持,使用 Enter/ExitPlanMode、permission mode 与 reminder 表达状态 | 模型也能主动进入 Plan;工具身份不漂移 |
| MCP 工具过多 | 按当前需求增删完整 schema | 以 defer_loading + Tool Search 延迟完整 schema |
降低工具选择噪声和常驻 token |
| 日期、文件、IDE、任务变化 | 重写 top-level system prompt | 生成 attachment,追加到当前消息尾部 | 变化有类型、owner、去重和恢复语义 |
| Compact | 用另一套 “summarize” system/no-tools 请求全量历史 | 尽量复用 parent 的 system/context/tools/history,只在尾部加 summary request | fork 能继承同一消息合法性和 provider 配置 |
| LSP 断开/重连 | 从 tool list 删除再加入 | 2.1.235 起尽量保持已出现 LSP tool 的请求表面稳定 | 修复本版 release note 中的 whole-cache invalidation |
这条主线也解释了为什么 Prompt Assembly 必须单独讲:<system-reminder> 不是随手拼的一段提示词,而是把变化放到稳定前缀之后的更新协议;它还承担 origin、tool-pair、compact/resume 和文件新鲜度语义。完整调用顺序见 Prompt Assembly 专题。
公开文章解释的是团队的设计原则,不是 2.1.235 逐字段合同。实际 block scope、TTL、Tool Search gate、compact reserve 和运行效果仍以本 bundle 与 Probe 为准。
多层缓存逐层解释¶
“多层缓存”在这个版本里不能只写成一句营销词。下面每层缓存的对象、命中、失效和成本都不同。
| 层 | 缓存对象 | 命中条件 | 主要失效条件 | 用户影响 |
|---|---|---|---|---|
| 进程内工具 schema cache | 工具 name/description/input schema 的构造结果 | 构造 key 中的工具、模型、provider、schema 和模式一致 | 全局 invalidate、进程退出、任一 key 输入变化 | 减少重复本地构造,不减少 API token |
| 进程内 model config cache | models.retrieve() 返回的模型配置 promise/result |
规范化模型 ID 相同 | 进程退出或 provider cache 重建 | 减少配置网络等待,不等于模型响应 cache |
| API system-prefix cache | 稳定 system prompt block | 同模型/provider/beta 下,breakpoint 前渲染前缀一致且 TTL 未过期 | 稳定块内容/顺序变化、模型/beta/provider 变化、TTL 到期、显式关闭 | 直接降低重复输入费用和 TTFT |
| API message-prefix cache | 历史消息直到 marker 的前缀 | marker 前消息序列可复用 | 新 breakpoint 前的消息变化、fork 位置变化、skipCacheWrite、TTL 到期 |
长会话每轮不用全价重算全部历史 |
| 预计算 compact cache | 已生成但尚未交换进主历史的 summary/boundary | session、agent、model、边界 UUID、时效和历史增长校验通过 | 模型/session 不一致、边界缺失、超时、内容继续演化不兼容 | 把等待从“满了以后”前移到用户仍在工作时 |
| 认证/实验等依赖 cache | OAuth token、GrowthBook、本地依赖缓存 | 各依赖自己的 key/expiry | token expiry、配置刷新、登出等 | 属于基础设施,不应算作上下文 cache |
进程内工具 schema cache¶
$Hi() 的 cache key 包含 provider、模型特性、工具名、输入 JSON schema 以及是否启用特定行为。命中后直接复用已经构造的 schema 对象;K5o.invalidateAll() 会整体清空。
它不会让 API 少收 token。相同 schema 最终仍可能被序列化进每次请求,只是 Claude Code 不必重复生成描述和转换 schema。
进程内模型配置 cache¶
Hkm(model) 先查 providerCache.modelConfigs,未命中才发 models.retrieve(),并把进行中的 promise 放入 map。因此并发请求同一模型时也能合并等待。请求有 5 秒超时,失败会记录 timeout、not_found、http_<status> 或 connection,结果为 null 时走本地 baked catalog/既有逻辑。证据见 408665-408696。
System prompt 分段缓存¶
Jml() 把 system prompt 切成最多四类 block:
- billing header:
cacheScope=null,不写 cache control; - identity/特殊组织块:根据分支为 null 或 org;
- boundary 之前的稳定块:
cacheScope=global; - boundary 之后的动态块:
cacheScope=org。
YSe() 最终生成:
只有内部值为 global 时,serializer 才真的在 wire 上写出 scope:"global"。内部 cacheScope:"org" 在请求里省略 scope 字段;本版只能证明客户端把它当作非 global 分支,不能从省略值推断服务端最终采用哪种组织、账户或默认作用域。没有 boundary、关闭相关 beta、provider 不符合条件时,代码会退回这个非 global/无 marker 分支。证据见 408355-408412、409028-409043、410218-410219;省略字段后的服务端解释属于 Boundary。
工具列表也会影响 system prompt 的 global cache 策略。如果存在不能安全放进全局前缀的 MCP schema,代码可把 skipGlobalCacheForSystemPrompt 打开,避免把机器/组织特定工具描述纳入 global 前缀。
消息 breakpoint 与 fork pin¶
$km() 从消息尾部寻找可缓存位置:
- 正常情况标记最后一个合法消息;
skipCacheWrite=true时向前退一步,避免把本轮尾部写入 cache;- 尾部是合法
api_system时可把 marker 放到 system 尾部; - fork 有明确 UUID 时,可额外 pin fork point;
- fork 没有 UUID 且允许 pin 时,可再标记前一个稳定点;
- assistant 尾部如果是 thinking/不可缓存 block,会继续向前找。
CpT() 把 marker 写到 user、assistant 或 api_system 的最后一个可缓存 content block,并记录:
| 字段 | 含义 |
|---|---|
totalMessageCount |
参与本次 API 转换的内部消息数 |
cachingEnabled |
本模型/本请求是否启用 prompt cache |
skipCacheWrite |
本轮是否禁止在尾部新建 cache |
forkPointPinned |
是否额外保住 fork point breakpoint |
markerCount |
实际插入的 breakpoint 数 |
这套逻辑的用户价值是:主线继续增长时,常用尾部前缀可以滚动复用;fork 后仍能复用分叉前的共同历史,而不是从第一条消息重新付费。
5 分钟与 1 小时 TTL¶
默认 cache_control 不写 ttl,即 5 分钟。mHe(querySource) 决定是否使用 1 小时:
优先级如下:
FORCE_PROMPT_CACHING_5M=true:强制 5 分钟;ENABLE_PROMPT_CACHING_1H=true:强制 1 小时;- Bedrock 的专用 1h 开关:对 Bedrock 强制 1 小时;
- 非一方订阅路径或正在使用 overage:不自动启用 1 小时;
- 其余情况按 query source allowlist,默认包含
repl_main_thread*、sdk、auto_mode、memdir_relevance。
全局关闭和分模型关闭由 WCi() 处理:DISABLE_PROMPT_CACHING 以及 Haiku/Sonnet/Opus/Fable/Mythos 家族开关都能阻止 marker 写入。
缓存失效不是玄学:Desktop/Cowork 怎样定位一次 break¶
先写清适用边界:下面的自动归因器只在 Cowork 或 CLAUDE_CODE_ENTRYPOINT=claude-desktop 打开。Prompt Cache 本身适用于 CLI,但普通 terminal CLI 不能声称会自动产出这套 tengu_prompt_cache_break 归因。gate:readable L232410-L232416
它不是看到 system/tools 有变化就立即宣布 cache miss,而是“结构候选 + 响应 usage”两阶段判断:
flowchart LR
A[归因器观察的 request 子集] --> B[请求前结构 fingerprint]
B --> C[保存 system/tools/model/beta/message prefix 候选变化]
C --> D[成功 streaming response usage]
D --> E{cache_read 下降超过 5% 且少至少 2000?}
E -->|否| F[不认定显著 break]
E -->|是| G{有本地结构变化?}
G -->|是| H[列出 model/system/tools/TTL/beta/message 等候选]
G -->|否| I[按 >1h / >5m / <5m 推断 TTL 或 server-side]
H --> J[event + debug warning]
I --> J
第一步:归因器只 fingerprint 非 deferred 活跃子集¶
Tool Search、provider 与 capability gate 已经运行完,归因器拿到最终system blocks、主动过滤后的非deferred tool schemas、betas、TTL、fast/auto/overage/effort、extra body和规范化messages。它另外记录anyDeferLoading。tracker调用点:L409430-L409446
这不是完整request fingerprint:真实body仍由完整active tool数组序列化,可能包含带defer_loading:true的schema。某个deferred schema内容改变但“是否存在deferred schema”仍为true时,这套本地tracker可能既不列toolSchemasChanged,也不列presence flip,最后只能落到TTL/server-side/unknown候选。这个盲区必须保留,不能把归因器写成服务端cache key的复刻。
cJp() 为当前归因槽保存:L232613-L232654
- system 与 tools 的整体结构 hash;
- 每个 system block 的 hash、长度;
- 每个 tool schema 的 hash和工具名;
- 独立的
cacheControlHash,避免把内容变化和 marker scope/TTL 混写; - model、fast、global cache strategy、betas、auto mode、overage、effort、extra body;
- wire message 的轻量 shape fingerprint。
这里的 systemHash/toolsHash 是 Bun 32-bit 结构 hash,用于相邻调用比较,不是 SHA-256 完整性证明,也不是“日志中保存了完整 prompt”。system 内容比较会排除 billing header,cache_control 又被拆到独立 hash,减少假归因。
第二步:普通追加消息不算历史突变¶
下一请求会比较:
| 字段 | 人话含义 |
|---|---|
systemPromptChanged |
去掉 marker/计费头后的 system 内容变了 |
toolSchemasChanged |
非 deferred 有效 schema 数组变了;再拆 added/removed/changed tools |
cacheControlChanged |
内容未必变,但 breakpoint scope 或 TTL 变了 |
globalCacheStrategyChanged |
none/tool_based/system_prompt 策略切换 |
deferLoadingPresenceChanged |
是否存在 deferred schema 翻转;不等于逐个工具变化 |
messagesHistoryChanged |
旧可复用 message prefix 被改写或截短 |
firstChangedMessageIndex |
旧前缀从哪一项开始不同 |
message 比较用旧数组逐项找第一个不同,因此正常在尾部增加一轮不会被标成 history mutation;compact、rewind、fallback salvage 或错误修链改了旧前缀才会。候选变化只先放进 pendingChanges,不等于服务端已经 miss。比较:L232629-L232651
第三步:只有 cache-read 真下降才认定 break¶
成功streaming response的usage到手后,uJp()才比较cache_read_input_tokens;non-streaming fallback没有同一客户端调用点。第一个样本只建立baseline;Haiku、预期的cache deletion和没有可比状态的路径跳过。当前值必须同时满足:
否则不记显著 break。判定:L232656-L232681
达到门槛后,先列本地候选:model、system、tools、fast、global strategy、cache control、betas、auto、overage、cache diagnosis、effort、extra body、defer presence、message mutation。全部稳定时才根据距最后 assistant 的时间做排除式提示:
> 1h:possible 1h TTL expiry;> 5m:possible 5min TTL expiry;< 5m:likely server-side;- 无有效时间:unknown cause。
这些仍是客户端候选,不是服务端判决。服务端返回的 diagnostics.cache_miss_reason 走另一条 tengu_prompt_cache_diagnosis_received 事件,不能与本地推断合并。归因与事件:L232682-L232728
主线程/SDK按固定query source分槽;compact归一到repl_main_thread,Agent调用则优先用agent ID分槽。进程内最多保留10个槽;只有固定source key能写入不超过4MiB的cache-break-state-<session-hash>.json,agent-ID槽不会跨进程恢复,并在Agent结束时清理。baselineFromDisk会让旧model/removed list更保守,避免拿不完整的跨进程状态制造精确diff。状态与key:L232423-L232483
用户真正能从中判断什么¶
同一会话突然TTFT变慢、cache_creation_input_tokens上升,不一定是“上下文变长”:切模型、effort、beta、Auto Mode、非deferred MCP schema、TTL或旧消息mutation都能被这套owner直接点名;deferred schema变化则可能落入tracker盲区。反过来,本地结构完全稳定也不证明服务端一定命中;likely server-side只是排除已观测本地变化与明显TTL后的分类。
compact 成功会清空旧 cache-read baseline;context-hint microcompaction 会设置 cacheDeletionsPending,让下一次预期下降不被误报为异常。见 L232730-L232749。
成本算例:100k Sonnet 4.6 稳定前缀¶
bundle 的 baked catalog 给 Sonnet 4.6 的价格是每百万 token:普通输入 $3、5m cache write $3.75、1h cache write $6、cache read $0.30。
假设稳定前缀是 100k token:
| 情况 | 首次 | 后续每次 | 两次请求合计 | 三次请求合计 |
|---|---|---|---|---|
| 不缓存 | $0.300 | $0.300 | $0.600 | $0.900 |
| 5m cache | $0.375 | $0.030 | $0.405 | $0.435 |
| 1h cache | $0.600 | $0.030 | $0.630 | $0.660 |
因此 5m cache 只要发生一次命中就已经比两次全价输入便宜。1h cache 的首次写入更贵,需要至少两次后续命中才开始省钱,但它能覆盖更长的思考、工具执行和离开终端时间。
实际账单还包含动态 user 消息、tool result、输出 token、web search 和 fast/speed 等项目。验证命中应看响应 usage:
input_tokens:本次全价处理的未缓存输入;cache_creation_input_tokens:本次新写入 cache 的输入;cache_read_input_tokens:本次从 cache 读取的输入;cache_creation.ephemeral_5m_input_tokens/ephemeral_1h_input_tokens:写入 token 的 TTL 拆分。
当前总 prompt 量不是只看 input_tokens,而是三项输入之和。
Tool Search:延迟 schema,而不是“缓存工具”¶
为什么需要它¶
每个 MCP 工具完整驻留时都要发送 name、description 和 input schema。几十到上百个工具会在用户还没调用它们之前就占满大量上下文,并且工具列表变化还会破坏 prompt prefix。
Tool Search 的做法是:
- 请求中把候选工具标记为
defer_loading:true; - 模型初始只知道名字/延迟工具提示,不装入完整 schema;
- Claude 通过
ToolSearch选择需要的工具; - 被发现工具的完整 schema 追加到后续上下文;
- MCP 断开/重连通过
deferred_tools_delta通知可用性变化。
这不是把 schema 缓存在 API 外面,而是改变完整 schema 进入模型上下文的时机。未发现工具不常驻完整 description/input schema,但它们的名字仍会出现在 deferred-tool reminder 中,ToolSearch 自身也要携带 prompt 和 schema;因此收益是显著减少未使用工具的 schema token,而不是降到零 token。
启用与回退¶
ENABLE_TOOL_SEARCH 支持布尔、force、auto 和 auto:N 形态。auto 模式按模型窗口的一定比例换算字符阈值,并估算可延迟工具 schema 的体积,超过阈值才启用。
代码会在以下情况关闭:
- 模型不支持
tool_reference; - 旧于支持代际的 Vertex 模型会拒绝 tool-search beta;
- Foundry deployment capability 不支持;
ToolSearch本身被 disallowed;- 没有可延迟工具且没有 pending MCP server;
- 非一方自定义 base URL 未显式声明代理支持。
启用后,请求带 tool-search beta;Bedrock/Vertex/Mantle/Gateway 与一方使用的 beta 名可能不同。证据见 114268-114368、370198-370225、409385-409409。
Context hint 与 microcompaction¶
两者的关系¶
Context hint 是服务端协作协议;microcompaction 是本地实际清理动作。服务端可以提示客户端清理特定工具族的旧结果,但客户端必须能在 beta 不支持、服务繁忙或流式错误时自行回退。
controller 只在以下条件成立时创建:
- 含一方 beta;
- query source 以
repl_main_thread开头; tengu_hazel_ospreygate 激活。
构建请求前,客户端先估算“清理除最近 5 个之外的工具结果”能节省多少 token。只有潜在节省达到 20,000 token 才发送:
target_tokens_saved 默认目标为 75,000,但可由 feature 配置调整。
本地清理规则¶
qUa() 只从内置候选工具集合产生的 tool-use ID 中收集可清理 tool result,并默认保留最近 5 个相关结果;它不是“清空所有工具输出”的通用遍历器。以下内容不重复处理:
- 已经是
[Old tool result content cleared]; - 已经是
<persisted-output>...; - 没达到 20k token 总节省阈值。
Jof() 对旧结果逐个调用 persistence callback。保存成功后,tool result 被替换成包含文件路径的短提示;失败或不适合保存时替换为 [Old tool result content cleared]。工具调用本身、tool use ID 和最近结果仍保留,所以模型知道过去调用过什么,但不再携带大段旧输出。改写对象是后续请求使用的 active message view;物理 transcript 中较早的原事件不因此被证明已经删除。
清理结果包括:
messages:改写后的消息;tokensSaved:估算节省 token;clearedIds:被清理的 tool use ID;clearedContent:ID 到持久化提示/占位符的映射。
证据见 263484-263519、408922-408970。
错误回退状态机¶
| API 结果 | 客户端动作 | 历史是否本地清理 |
|---|---|---|
| hint 被接受 | 使用服务端协作结果继续 | 由协议结果决定 |
| HTTP 422/424 | 关闭 hint,执行本地 microcompaction 后重试/继续 | 是 |
| HTTP 400,beta/header 不支持 | 关闭 hint,保留原消息继续 | 否 |
| HTTP 409 | 关闭 hint,保留原消息继续 | 否 |
| HTTP 529 | 关闭 hint,保留原消息继续 | 否 |
| 流式 invalid request | 标记 stream fallback,再执行本地 microcompaction | 是 |
| 其他错误 | 不由 context-hint controller 吞掉 | 否 |
这个设计避免了一个危险失败模式:服务端 hint 不可用时,CLI 不会把“没有清理”误报为“清理成功”,也不会对所有 API 错误擅自改写历史。
Auto-compact:阈值不是一个数字¶
总开关¶
DISABLE_COMPACT 或 DISABLE_AUTO_COMPACT 任一为真,auto-compact 关闭。否则读取 autoCompactEnabled,默认值是 true。显式在 user settings 关闭时,UI 可以区分“用户关闭”与其他来源关闭。
有效窗口优先级¶
jG(model, settingsWindow) 依次选择:
CLAUDE_CODE_AUTO_COMPACT_WINDOW;- settings 中
autoCompactWindow; - client data 下发窗口;
- experiment 窗口;
- 特定模型默认窗口;
- baked model/default/unknown-model 规则;
- 最后回到模型最大 context window。
所有显式窗口必须在 100k 到 1M 之间,并最终被模型最大窗口封顶。/autocompact 接受 auto、500k、200000 或 200 这类写法;环境变量优先时,命令会明确拒绝覆盖并提示先 unset。
四条线¶
确定 window 后,代码还会预留输出空间:
input_budget = window - min(max_output_tokens, 20k)
compact_line = input_budget - 13k
precompute_line = min(input_budget - input_budget * precompute_fraction,
compact_line)
warn_line = compact_line - 20k
blocked_line = model_input_ceiling - 3k
默认 precompute fraction 是 20%,可按窗口和 REPL/SDK 使用远程表调整。CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 和 blocking override 属于测试/覆盖入口。
200k 模型算例¶
以 200k context、输出预留 20k 为例:
| 状态 | token 线 | 行为 |
|---|---|---|
| precompute | 144k | 后台提前生成摘要,用户继续工作 |
| warn | 147k | UI 开始提示上下文余量 |
| compact | 167k | 触发 reactive auto-compact |
| blocked | 177k | 不再允许继续堆输入,必须先腾空间 |
所以“200k 窗口”不表示会等到 200k 才总结。输出预算、API 安全余量、预计算等待和最终阻塞都要提前占位。
blocked_line 与前三条线的来源必须分开理解。假设同一个 200k 模型被 autoCompactWindow=140k 限制,输出仍预留 20k:compact/precompute/warn 都改用 120k 的 effective input budget 计算,但 blocked 仍由模型输入 ceiling 180k 再减 3k,保持 177k。配置更小的 auto-compact window 会让总结更早发生,不会把底层模型硬阻塞线一起降到 117k。
预计算 compact¶
预计算结果不是立即替换主历史。它先写进 session precompute 状态,并可持久化 precompact.json sidecar。sidecar 格式版本为 2,单文件上限 8,000,000 bytes;adapter 可用时写 Storage v5 sidecar key,否则写既有文件路径。复用时会校验:
- format version;
- session ID 和 main agent key;
- model;
- 创建时间和 ready duration;
precomputedAtUuid是否仍存在于当前历史;- pre-compact token 与 hook 结果是否可接受。
Sidecar 同时保存 cliVersion,rehydrate telemetry 也记录 cliVersionMatch;但 2.1.235 的拒绝分支不会仅因 CLI version mismatch 拒绝复用。不能把“字段被记录”写成“版本不一致必然失效”。
具体拒绝线包括:创建超过 604,800,000ms(7 天)、当前历史比预计算点增长超过 150,000 token、缩减超过当时 token 的一半、boundary UUID 或任一 preserve UUID 缺失。通过后,达到真正 compact line 时直接 swap 已准备摘要,并把预计算之后的新消息作为 messagesSince 追加保留。校验失败会删除 sidecar、记录具体原因并重新走普通总结,不把旧 summary 硬塞进新历史。连续 3 次可计数失败后不再继续 re-arm。证据见 262324-262680。
Main sidecar 与跨 Agent 借用¶
持久 payload 的 schema 明确要求 agentKey === "main",所以磁盘/Storage v5 sidecar 不是通用子 Agent 持久缓存。进程内 precompute registry 才按 main 或 agent ID 分槽。fork/subagent 的 toolUseContext.precomputeSourceKey 可以指定借用槽:
- 借用项仍是 pending 时,当前 turn 等待它 settled;等待期间 turn abort 会返回 aborted,但保留源 entry;
- 借用 ready 项成功 swap 后,源 entry 不被移除,原 owner 之后仍可消费;
- 没有 borrow key 时读取自己的槽,读取后无论 ready/failed 都 remove,并在属于当前 session 时排队删除 sidecar;
- 借用结果的 boundary UUID 不在当前消息图时只放弃本次应用,不错误消费源摘要。
因此 precomputed compact 的状态不是“谁先读谁拿走”的全局 Promise,而是 owner 槽 + 可借用引用。它减少 fork/subagent 重复总结的延迟和 token,但要求当前消息图仍包含同一个 precomputedAtUuid,否则不能跨分支硬套摘要。证据见 262425、262487-262518、262601-262654。
四条 compact 路径不能混写¶
| 路径 | 旧消息内容与因果关系怎样保留 | 证据锚点 |
|---|---|---|
current manual /compact |
至少保留最后 1 个合法 group;必要时增大保留量 | 331301-331367、232845-232876、262247-262294 |
| partial/message-selector | 保留选择器另一侧 m,写 preserved UUID |
263048-263087 |
| reactive prompt-too-long | 按合法 group 保留 suffix | 262247-262294 |
| precomputed swap | 保留预计算的 preserve UUID 与其后 messagesSince |
262425-262680 |
| cold/full auto 或特定 SDK full compact | 无,返回 messagesToKeep: [] |
263020-263036、263375-263414、290267 |
这里的“保留”不是对象逐字段不变。Smi() 会对保留区执行 messagesToPreserve.map(tNt):非 assistant 消息直接复用,assistant 消息保留内容、UUID 和工具因果关系,但把 input_tokens、output_tokens、cache_creation_input_tokens、cache_read_input_tokens 清零。可直接确认的结果是 rebuilt context 不再携带这些字段的旧计数;代码没有在这里给出更高层计费意图。证据见 232865、232883-232885。
因此“compact 后固定保留最后 N 条消息”仍是错误模型:手动/reactive 是按合法 group 和 token 缺口选择,partial 按选择器,precomputed 还合并 messagesSince,cold/full 才没有后缀。连续性来自 Summary、条件性 preserved messages、重新生成的 attachments/hooks 和后续输入共同组成的表示。
PreCompact hook 和失败¶
实际 compact 前先运行 PreCompact hook。hook 可以:
- 返回给用户显示的信息;
- 注入新的总结指令;
- 明确阻止 compact。
总结 API 失败时会记录 trigger、耗时、preTokens 和错误,不创建成功 boundary。UI 会收到 compact failed 状态;如果上下文继续增长到 blocked line,用户必须重试 compact、清理内容或开启自动压缩,不能把失败摘要当成有效历史。
API context management 的真实状态¶
bundle 中存在:
- 环境变量
USE_API_CONTEXT_MANAGEMENT; - beta
context-management-2025-06-27; - 请求字段
context_management; - 模型 catalog 的
context_managementcapability; - thinking 清理策略
clear_thinking_20251015。
但 2.1.235 的 beta 构造分支是 K.USE_API_CONTEXT_MANAGEMENT && false。这意味着单独设置该环境变量不能启用这条路径。当前可达启用条件来自模型/远程能力判断 AW_(model),并且 provider 还要支持对应 beta。请求只有在 beta 已加入、模型支持且存在策略时才写 context_management。
这是字段清单无法单独告诉读者的典型例子:字段“存在”不等于开关“可用”,必须检查调用分支是否可达。
Compact boundary、JSONL 和 resume¶
Boundary 保存什么¶
compact 成功后写入 system/compact_boundary。wire/JSONL 字段包括:
| 字段 | 含义 |
|---|---|
trigger |
auto、manual 等触发来源 |
pre_tokens |
总结前估算 token |
post_tokens |
总结后估算 token |
cumulative_dropped_tokens |
多次 compact 累计丢弃的原始 token |
duration_ms |
compact 耗时 |
user_context |
总结时需要保留的用户上下文 |
messages_summarized |
被摘要的消息数 |
precomputed |
是否来自预计算结果 |
pre_compact_discovered_tools |
compact 前已发现的延迟工具 |
preserved_segment |
保留链段的 head/anchor/tail UUID |
preserved_messages |
保留消息 anchor 与 UUID 列表 |
logical_parent_uuid |
压缩后逻辑父节点,用于恢复链 |
_k(messages) 在后续处理时只取最后一个 compact boundary 之后的有效表示,避免把已被 summary 取代的整段历史再次发送。
Resume 如何修链¶
resume 先逐行解析 JSONL,只接收有 UUID 的 user、assistant、progress、system、attachment。发现 compact boundary 后:
preserved_messages模式按 UUID 列表重接 parent chain;preserved_segment模式把 head 接到 anchor,并把原 anchor 的其他后继接到 tail;- 最后重新找叶节点和会话分支。
因此 compact 不是简单“删掉前 N 行”。它保留一张可恢复的逻辑消息图:当前 manual/reactive/partial/precomputed 把 preserved messages 接入 summary/boundary 之后;cold/full compact 只建立新的 summary/boundary 表示。证据见 323188-323443 和 211993-212001。
Transcript、memory 与 cache 的边界¶
| 机制 | 是否发送给模型 | 生命周期 | 目的 |
|---|---|---|---|
| Prompt cache | API 端复用前缀,不改变逻辑内容 | 5m/1h TTL | 降成本、降 prefill 延迟 |
| Process memoization | 通常不直接发送 | 当前 CLI 进程 | 降本地重复计算/请求 |
| Tool deferral | 只发送名字/提示,schema 按需追加 | 当前会话/连接 | 降常驻上下文 |
| Microcompaction | 替换候选工具族的旧 tool result 内容 | 当前 active message view;原 transcript 事件不被证明已删除 | 局部释放 token |
| Auto/manual compact | 用 summary + boundary 替代长历史 | transcript 可恢复 | 全局释放 token |
| JSONL transcript | resume 时重新装载 | 默认保留 30 天,可配置 | 会话持久化与审计 |
| Auto-memory | 相关内容会在未来装入 context | 跨会话文件 | 保存长期项目知识 |
cleanupPeriodDays 默认 30 天,只控制 transcript 自动清理;--no-session-persistence 才是完全不写 transcript。autoMemoryEnabled=false 会阻止读写 auto-memory;autoMemoryDirectory 的 project settings 值因安全原因会被忽略;autoDreamEnabled 控制后台 memory consolidation。
把这些都叫“缓存”会掩盖真实行为。例如删除 prompt cache 不会删除 transcript,清理 transcript 也不会让已经发送的 API cache 立即失效,关闭 auto-memory 更不会阻止当前会话历史存在。
用户如何判断问题在哪一层¶
每轮都很贵,但上下文不大¶
先看 usage:
cache_read_input_tokens长期为 0:前缀没有命中;cache_creation_input_tokens每轮很高:稳定前缀一直被改写或 breakpoint 在滚动重建;input_tokens高:动态消息、tool result 或 schema 本身太大。
再检查模型、provider、beta、system prompt、工具列表和动态机器段是否每轮变化。
会话没多长,context 却很满¶
看 /context 类统计的组成:
- MCP tools 高:启用/修复 tool search,或减少 always-load 工具;
- memory files 高:把全局 CLAUDE.md 的项目专用内容下沉到项目/子目录;
- skills 高:description 会受 1536 字符单项上限和 1% context 字符预算控制;
- messages 中 tool result 高:需要 microcompaction 或更短工具输出。
Resume 后突然产生大量 token¶
检查:
- 手工把
autoCompactWindow调得过高; - 上次离开前 cache TTL 已过期;
- resume 改了模型/provider/beta;
- transcript 没有近期 compact boundary;
- 预计算 summary 因版本、模型或边界变化不能 rehydrate。
/autocompact 本身会提示:覆盖 auto 可能让长会话 resume 产生高 token 消耗。
Auto-compact 没触发¶
按顺序检查:
DISABLE_COMPACT/DISABLE_AUTO_COMPACT;autoCompactEnabled;- worker 下发的
autocompact_state.enabled/enforced; - 当前 token 是否真的达到 effective threshold;
- PreCompact hook 是否 block;
- 当前 query source 是否属于排除类型;
- 上次 compact 是否失败。
thin client 会采用 worker 的真实状态:enabled、effective_window、threshold、enforced、source。客户端本地显示不能覆盖一个 enforced=true 的 worker 决策。
精确请求形状:缓存不是只存在于源码常量¶
runtime-controls.json 捕获了精确 2.1.235 二进制发出的首个 Messages 请求。受控输入只启用 Read,请求仍出现 3 个 cache_control 对象,携带 anthropic-version、beta header 和完整 Read schema。认证使用 Authorization: Bearer $TOKEN,没有 x-api-key。
这条 probe 证明了三件事:缓存 marker 真正进入 wire body;tool schema 与 system/message cache breakpoint 同时参与本轮请求成本;credential source 会改变 header 形状。它没有证明服务端命中率或账单结果,后者仍需响应 usage 中的 cache_read_input_tokens / cache_creation_input_tokens 验证。
probe.prompt-cache-disable 对同类请求设置 DISABLE_PROMPT_CACHING=1。默认请求的 cacheControlCount=3,禁用后的请求为 0,两次命令均成功。这把“开关被读取”升级为“请求体确实改变”;它仍不等于服务端返回 cache miss,因为禁用后客户端根本不再声明这些 breakpoint。
probe.manual-compaction-boundary 则验证另一条完全不同的路径:/compact 输出流出现 system:compact_boundary,trigger=manual、preTokens=104。随后 fork 请求保留生成的 compact summary 和当前 prompt,但不再包含 compact 前 prompt、旧 tool-use ID 和旧 assistant result。这里删除的是送模结构历史,不是删除磁盘上一切事件,也不是清空 memory。
当前官方 Troubleshooting 还解释了 autocompact thrashing:如果 compact 后大文件或 tool output 数次立即重新填满窗口,CLI 会停止重试以避免继续浪费 API 调用。这条 Public 主张登记为 public.autocompact-thrashing;2.1.235 的具体计数器和终止分支仍以 bundle 静态证据为准,不能从当前文档倒推所有旧版本阈值。
本版本可观测字段¶
和上下文治理直接相关的事件至少包括:
tengu_context_size:常驻上下文构成;tengu_api_cache_breakpoints:marker 决策;tengu_sysprompt_boundary_found:稳定/动态 system 分段;tengu_tool_search_mode_decision:tool search 开关原因;tengu_context_hint_reject:hint 回退与节省量;tengu_time_based_microcompact:清理数量和 token;tengu_reactive_compact_triggered/succeeded/failed:全局压缩生命周期;tengu_precomputed_compact_*:预计算、持久化和 rehydrate;tengu_remote_autocompact_state_adopted:worker 状态同步;- OTEL
compactionevent/span:管理员自建观测出口。
事件字段如何阅读见 inventory-field-guide.md,传输、默认开关和隐私控制见 telemetry.md。
证据边界¶
本文能证明 2.1.235 客户端 bundle 中的装配、门控、字段、请求构造、回退和本地持久化路径。它不能证明:
- Anthropic 服务端实际 cache key 的未发布内部实现;
- 远程 feature/client data 在某个账号此刻返回什么值;
- 服务端 context hint/compact 的内部算法;
- 构建前已删除的源码、注释和原始模块名。
因此文中把“客户端会发送什么、何时发送、收到错误后怎样处理”写成确定事实,把服务端未随产物发布的部分保留为边界。
本专题直接使用的运行主张包括 probe.request-cache-shape、probe.prompt-cache-disable 和 probe.manual-compaction-boundary;公开主张包括 public.context-auto-compaction、public.cache-invalidation、public.autocompact-thrashing。逐条命令、输入、literal output 与 exit status 见 精确二进制运行证据指南。