Claude Code 2.1.235 的 /compact 到底做了什么¶
/compact 改变的是 Claude Code 下一次发给模型的对话,不是工作区里的代码,也不是已经执行过的命令。
下面直接看一次 Claude Code 会话。任务就是分析 /compact 自己,不引入其他业务例子。
压缩前:模型和客户端已经来回执行了很多轮¶
用户在 Claude Code 中输入:
模型不能直接回答。它先让客户端搜索发布 bundle:
assistant:
tool_use(id=grep-compact, Grep)
pattern: "CRITICAL: Respond with TEXT ONLY|compact_boundary|messagesToPreserve"
path: reverse/javascript/cli.readable.js
Claude Code 客户端执行 Grep,再把结果作为同 ID 的工具结果放回消息历史:
user:
tool_result(tool_use_id=grep-compact)
261902 CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
261933 t2 = t2.replace(/<analysis>...<\/analysis>/, "")
262271 messagesToPreserve: m.flat()
263175 async function wmi(...)
399359 subtype: "compact_boundary"
模型接着读取 Summary 提示词和解析器:
assistant:
tool_use(id=read-summary, Read)
file_path: reverse/javascript/cli.readable.js
offset: 261901
limit: 160
工具返回的源码片段中包含:
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
Your entire response must be plain text:
an <analysis> block followed by a <summary> block.
function IzS(text) {
text = text.replace(/<analysis>[\s\S]*?<\/analysis>/, "");
// 匹配到 <summary> 时改写成 Summary: ...
}
到这里,模型只能证明“客户端会要求生成 Summary,并尝试删除 analysis”。它还没有证明 Summary 之外保留了什么。
用户继续纠正:
模型又发起多次工具调用:
Read 232845-232885 -> manual/reactive group compactor 与 finalize
Read 262247-262294 -> 合法消息分组和 messagesToPreserve
Read 263175-263235 -> 最近文件、Plan、Skills、MCP 与 Hook 附件
Read 211993-212001 -> compact_boundary 的序列化字段
Read analysis/runtime-probes/agent-loop-tool-result-resume.json
-> /compact、fork 和 resume 的真实请求变化
Bash python3 skill/claude-code-version-diff/scripts/validate_snapshot.py .
-> snapshot validation: PASS
每次 Read 都会把源码正文变成 tool_result,每次验证都会把输出放回下一轮请求。加上用户纠正、模型判断和其他上下文,这条会话会越来越长。
此刻任务状态是:
- Summary 提示词和双标签解析已经确认;
- 手动
/compact的合法消息分组已经确认; - 最近文件恢复的
5 / 5,000 / 50,000 token限制已经确认; - Boundary 字段和同版本 Probe 已经找到;
- 还需要把这些证据整理成一篇准确说明。
用户这时输入:
压缩后:下一次模型实际拿到什么¶
/compact 完成后,客户端不会把上面所有搜索结果、源码片段和验证输出原样再次发给模型。
下面是基于 2.1.235 客户端规则构造的说明性结果。Summary 的具体措辞由模型生成,保留多少近期消息也取决于本次切点;它不是固定 wire dump。
改变输入,亲自观察这次重建¶
下面的沙盘不会播放一条固定动画,范围明确限定为普通手动 /compact 且 precomputed miss,因此会真实走一次 Summary 请求;precomputed hit 仍由后文单独解释。输入参数会改变 Summary 的强调项和附件候选,消息组数量与保留数量会改变切点,附件恢复状态会改变重建后的精确材料;如果 PreCompact 阻止,客户端会停在原历史,不写成功 Boundary。每一步都标明执行 owner、证据类型和当前高亮的客户端阶段。
较早内容变成一段新文本¶
本例可能生成这样的 Summary:
Summary:
1. Primary Request and Intent:
解释 Claude Code 2.1.235 的 /compact。
必须覆盖 Summary、近期消息、文件恢复和 Resume。
2. Key Technical Concepts:
manual compact miss、合法消息 group、preserved messages、
attachment、compact_boundary、逻辑消息视图。
3. Files and Code Sections:
cli.readable.js 261901-262207:Summary prompt 与标签解析。
cli.readable.js 262247-262294:group compactor。
cli.readable.js 263175-263235:附件恢复。
cli.readable.js 211993-212001:Boundary 字段。
4. Errors and Fixes:
最初只分析了 Summary prompt;用户指出还必须验证近期消息、附件和 Resume。
随后补读 group compactor、附件和 Boundary 路径。
5. Problem Solving:
已确认 compact 不是只留一段摘要,而是重建下一次请求使用的历史。
6. All User Messages:
用户先要求解释四部分,随后纠正“不要只分析提示词”。
7. Pending Tasks:
把证据写成读者可理解的文章,并保留精确阈值和失败边界。
8. Current Work:
源码与 Probe 已核验,文章尚未完成。
9. Optional Next Step:
按客户端真实顺序写完 /compact 专题并运行验证。
这段新文本在实现中叫 compact Summary。九个标题来自客户端提示词;具体句子不是固定输出。
Errors and Fixes、Problem Solving 会保留失败和用户纠正。因此真实机制并不是“识别失败版本,删掉失败,只留下成功”。它要把失败原因压缩成后续仍可使用的语义。
最近的完整消息可以继续保留¶
假设本次切点保留了最后两个消息组,下一次请求里还可能存在:
assistant:
tool_use(id=read-boundary, Read)
file_path: reverse/javascript/cli.readable.js
offset: 211993
limit: 20
user:
tool_result(tool_use_id=read-boundary)
compact_metadata: trigger, pre_tokens, post_tokens,
preserved_segment, preserved_messages ...
assistant:
已确认 Boundary 保存前后 token 和 preserved UUID;
下一步把它与 Resume 修链放进文章。
第一条 assistant 工具调用和紧随的 user 工具结果属于一个消息组;后面的 assistant 判断通常是下一个组。客户端不能只保留 tool_result 而丢掉同 ID 的 tool_use,否则工具因果关系不合法。
这些没有被 Summary 改写、仍以原内容进入新上下文的近期消息,叫 preserved messages。当前 manual /compact 至少从保留最后一个合法组开始,但不保证固定保留最后 N 条,也不保证上面两个组每次都会同时留下。
最近使用的文件可以被重新装入¶
客户端会从最近文件集合中重新装入有限的精确材料。如果 reverse/javascript/cli.readable.js 进入恢复集合,模型会再次获得受限长度的文件内容,而不必完全依赖 Summary 复述源码。
这种压缩后重新加入的精确材料属于 attachment。2.1.235 最多恢复 5 个最近文件;每个文件最多 5,000 token,恢复文件合计最多 50,000 token。文件未被选中、超过预算或读取失败时,不能假装它已经被精确补回。
Boundary 留在本地会话记录中¶
Compact 成功后,客户端还会向本地 JSONL transcript 追加 system/compact_boundary。简化后可以这样理解:
compact_boundary:
trigger: manual
pre_tokens: 压缩前 token
post_tokens: 压缩后 token
preserved_messages: 被保留消息的 UUID
Boundary 不是第二份 Summary,也不是普通业务内容。它告诉后续 Resume:较早历史已经由 Summary 替代,哪些近期消息仍要接回新链。
为什么压缩后还能继续分析 /compact¶
下一轮模型现在掌握四类信息:
- 从 Summary 得知用户两次要求、已确认事实和剩余写作任务;
- 从 preserved messages 得到近期 Read 的精确输入、结果和判断;
- 从 attachment 重新取得仍需引用的源码材料;
- 从 compact 后的新输入得知接下来要继续完成什么。
所以 /compact 不是“把全部聊天压成一段摘要”。在当前手动路径中,它总结较早内容,保留部分近期完整消息,再补回仍需使用的精确文件。
客户端怎样完成这次切换¶
上面的例子已经展示了输入和结果。现在再看内部顺序。
先找一个不会拆散工具调用的切点¶
客户端运行 PreCompact hook。本例假设 Hook 放行,并且当前没有可复用的预计算结果。
随后客户端按合法消息组切分历史:较早组交给模型总结,近期组留在新上下文中。当前 manual 路径至少从保留最后一个合法组开始;如果 Summary 请求仍然超窗,它会多保留一些尾部组,缩短待总结前缀,再次尝试。
Preserved assistant message 的内容、UUID 和工具因果会保留,但四项旧 usage 计数会清零。因此“保留”指消息语义与关系继续存在,不是 JavaScript 对象每个字段逐字节不变。
总结请求只能生成文本,不能继续干活¶
客户端在待总结前缀末尾加入一条虚拟用户消息,核心要求是:
CRITICAL:停止当前业务工作,只生成对话总结。
不要调用 Read、Bash、Grep、Glob、Edit、Write 或任何其他工具。
先输出 <analysis>,按时间检查要求、文件、决定、错误和反馈;
再输出 <summary>,形成可继续工作的工程交接内容。
这次请求只允许一轮模型决策,工具权限函数固定返回 deny。模型不可用且策略允许时,客户端沿 fallback chain 重试;这里不存在一套永远固定的“模型 A 写代码、模型 B 管上下文”。
提示词要求先写 <analysis>、再写 <summary>。解析器匹配到标签时会删除 analysis,把 summary 改写成后续上下文里的 Summary:。这不是严格的结构化输出 schema:若模型返回非空文本却缺少完整标签,当前路径仍可能接受整段文本。
Summary、近期消息和附件被重新组装¶
客户端把模型生成的 Summary、选中的近期消息组、最近文件与当前任务状态组装成新的有效上下文。Summary 保存较早语义,近期消息保存刚发生的精确因果,附件补回当前材料。
几部分出现重复并非错误。例如 Summary 会记录某个函数和行号,attachment 里也可能再次出现同一段源码。前者保证任务含义仍在,后者保证精确代码不完全依赖模型复述。
Boundary 让 Resume 采用新历史¶
下一次模型请求使用的是:
本地 transcript 则仍能记录 compact 前事件、Summary、Boundary 和之后的新事件。Resume 读取 JSONL 时不会把所有物理行原样塞回模型,而是按 Boundary 中的保留关系重建逻辑消息链。
如果 Summary 漏了更早的精确片段,客户端会在路径可用时把完整 transcript 路径告诉模型;模型需要主动 Read。这是沿明确路径补查,不是客户端发现“记忆缺失”后自动搜索一个外部语义记忆库。
预计算只是把 Summary 提前写好¶
上下文到达 precompute line 后,客户端可以在后台对当时的历史预写 Summary,但不立即替换主历史。当前进程中的 ready result 命中时,客户端确认预计算点仍在当前历史,再把之后的新消息作为 messagesSince 接入保留区。
因此 precomputed hit 在这一次 compact不发新的 Summary 请求;模型调用只是提前发生。若用户给 /compact 加了新 instructions、Hook 追加了新要求,或预计算点已不在当前历史,客户端放弃复用,回到前面讲过的普通路径。
磁盘 sidecar 在重新装入内存前还会检查 session、model、7 天期限、150,000 token 增长、缩减一半和 preserve UUID。Sidecar 记录 CLI version,但 2.1.235 不会仅因 CLI version mismatch 拒绝复用。
自动触发线怎样算出来¶
手动 /compact 可以随时执行。自动路径必须先给模型输出留位置,再给“提前生成、提醒用户、执行 compact、最终阻断”留出不同缓冲。
以 200k 窗口为例,先预留最多 20k 输出,本例真正可用于输入判断的 budget/ceiling 是 180k:
input budget = 200k - 20k = 180k
precompute = 180k - 20% = 144k
compact = 180k - 13k = 167k
warn = 167k - 20k = 147k
blocked = 180k model input ceiling - 3k = 177k
| 线 | Token | 客户端动作 |
|---|---|---|
| precompute | 144k | 后台准备 Summary,主历史暂不切换 |
| warn | 147k | UI 开始提示余量 |
| compact | 167k | 触发自动 compact |
| blocked | 177k | 阻止继续无界增加输入 |
这些数字只属于这个算例。模型窗口、输出预算、设置和远程 precompute 配置都会改变结果;源码没有固定 T1-T5,也没有 30%、60%、75%、90% 四档。这里的 20k 是 auto-window 输出预留上限,不是 Summary 的固定输出上限。
失败时为什么不会留下半份新历史¶
只有 Summary、保留区、附件和 Boundary 完成组装,新的有效上下文才正式接管。
| 失败 | 客户端怎样处理 | 结果 |
|---|---|---|
PreCompact 阻止 |
保留原历史,不绕过 Hook | 不写成功 Boundary |
| Summary 请求超窗 | 多保留尾部消息组,缩短待总结前缀 | 自适应重试,耗尽后失败 |
| 当前模型不可用 | 沿允许的 fallback chain 切换 | 用下一模型重新总结 |
| 当前 manual/reactive 附件恢复失败 | 记录错误并使用仍能取得的材料 | 可能继续,但精确材料减少 |
失败时最重要的合同是:成功的新上下文和 Boundary 尚未形成,原 active history 就仍然有效。
无论 compact 成功还是失败,它都不能撤销文件修改、git push、数据库写入、MCP 写操作或远端 API 请求。Compact 改变的是模型以后看到的逻辑消息,不是外部系统的事务状态。
回到这次 Claude Code 源码分析¶
Compact 之后,模型继续任务时仍然知道:用户要求解释 Summary、近期消息、文件恢复和 Resume;最初只看提示词是不够的;关键源码范围和 Probe 已经找到;下一步是把这些事实写清楚并验证。
这些信息并非来自一份万能摘要。较早的要求与纠正由 Summary 承接,最近的工具因果由 preserved messages 保留,当前源码由 attachment 补回,Boundary 则让下一次请求和 Resume 知道从哪份短历史继续。
这就是 2.1.235 的 /compact:模型负责把较早经历写成有损文本,客户端负责选择合法切点、保留近期因果、重装精确材料并记录历史切换。
源码与运行证据
| 机制 | reverse/javascript/cli.readable.js 定位或 Probe |
|---|---|
| Auto threshold 与 effective window | 216095-216245;context.autocompact-thresholds |
| Summary 模板、九段结构与双标签解析 | 261901-262207 |
| Group summarizer、token-gap retry 与 preserved suffix | 262209-262294 |
Precomputed 生成、校验与 messagesSince |
262320-262680 |
| Tool deny、模型 fallback 和附件恢复 | 263099-263235 |
| Manual slash command 与 finalize/Boundary | 331301-331367、232845-232876 |
| Boundary 字段与累计 dropped token | 211531-212001 |
| 同版本 manual compact、fork 与 resume Probe | analysis/runtime-probes/agent-loop-tool-result-resume.json;probe.manual-compaction-boundary |
普通 manual miss 的调用主线是 gEv -> yEv -> nFa -> vmi -> Smi。vmi() 从至少保留一个合法消息组开始;Smi() 负责恢复附件、运行 compact hook、生成 Boundary 并产出新的 active view。
实现边界:manual/reactive 的 Summary 超窗会扩大 preserved suffix;full/partial 使用另一套最多 3 次的截断重试。Auto compact 连续失败 3 次会跳过本 session 后续 auto 尝试;compact 后少于 3 turns 又连续第 3 次触线会触发 rapid-refill breaker。这些保护都不禁止用户手动 /compact。
Prompt cache、Tool Search、context hint 与 microcompaction 是相邻但不同的问题,见上下文治理与多层缓存;UUID、checkpoint、resume、fork 和 Memory 见会话、检查点与记忆。
图表源文件位于 analysis/visuals/compact-*.dot,通过 Graphviz 确定性生成对应 SVG。