第二十四章:Context Collapse 为什么值得单列
站在接口边界而不是内部算法上,看 context collapse 这套子系统到底承担什么角色。
1. 为什么这篇要强调“边界”
前面我们已经拆到 context collapse 在运行时里的位置了:
- 它在
query.ts里先于 autocompact - overflow 时还能参与恢复
/context和 UI 里都能看到它的状态
但这份仓库快照有个很明确的边界:
src/services/contextCollapse/* 的主体实现文件不在仓库里。
所以这篇不能像前几篇那样讲内部算法细节,只能讲:
- 外部能确认的接口
- 它和 query / transcript / UI 怎么接
- 它对会话恢复和上下文可视化提出了什么契约
换句话说,这篇不是“算法拆解”,而是:
runtime contract 拆解。
2. 先说结论
我对这层设计的判断是:
Claude Code 把 context collapse 做成了一个独立的上下文管理子系统,而不是 compact 里的一个小功能。
从现有快照能确认的结构,大概是这样:
flowchart TD
A["setup.ts"] --> B["initContextCollapse()"]
C["query.ts"] --> D["applyCollapsesIfNeeded(messages, context, querySource)"]
C --> E["isContextCollapseEnabled()"]
C --> F["isWithheldPromptTooLong(...)"]
C --> G["recoverFromOverflow(...)"]
H["commands/context/*"] --> I["projectView(messages)"]
J["analyzeContext.ts"] --> E
K["TokenWarning.tsx"] --> L["getStats() + subscribe()"]
M["ContextVisualization.tsx"] --> E
N["sessionStorage.ts"] --> O["recordContextCollapseCommit(...)"]
N --> P["recordContextCollapseSnapshot(...)"]
Q["sessionRestore.ts / ResumeConversation.tsx"] --> R["restoreFromEntries(commits, snapshot)"]
S["REPL rewind / compact cleanup"] --> T["resetContextCollapse()"]
这张图说明了一个很关键的事实:
context collapse 在 Claude Code 里至少横跨四层:
- query 前的上下文投影
- overflow 恢复
- transcript 级持久化与 resume
- UI 级状态展示
这已经不是工具函数,而是正式子系统了。
3. 在 query 主循环里,它扮演的三个角色
源码锚点:
src/query.ts:18src/query.ts:440src/query.ts:618src/query.ts:802src/query.ts:1094
3.1 角色一:正常请求前的上下文重写器
在主循环里,Claude Code 会在:
snipmicrocompact
之后调用:
contextCollapse.applyCollapsesIfNeeded(...)
这说明它不是被动恢复器,而是平时就会主动参与“模型究竟看到哪份上下文”的计算。
也就是说,context collapse 不是只在爆了之后兜底,而是平时就可能在后台慢慢把历史压成更细粒度的摘要单元。
3.2 角色二:blocking-limit 的 owner 之一
在 preflight blocking limit 判断那里,源码明确把 context collapse 当成 overflow owner 之一。
含义是:
- 如果它接管了上下文阈值管理
- 那 preempt 逻辑要给它让路
这个信号很强,因为它说明在作者心里:
collapse 不是辅助观察工具,而是真正会影响是否发请求、何时发请求的主流程治理器。
3.3 角色三:真实 413 之后的第一恢复手段
当 prompt-too-long 真的发生时,query.ts 的恢复顺序是:
- 先
recoverFromOverflow(...) - 再考虑
reactive compact
所以 collapse 的恢复优先级比 reactive compact 更靠前。
这代表它承担的是:
“把已经 staged 的压缩收益立刻兑现” 这件事。
4. projectView() 说明它不是“修改原消息数组”,而是“投影模型视图”
源码锚点:
src/commands/context/context.tsxsrc/commands/context/context-noninteractive.ts
这两个 /context 入口都做了同一件事:
- 先
getMessagesAfterCompactBoundary(messages) - 如果启用了
CONTEXT_COLLAPSE - 再调用
projectView(view) - 然后才跑
microcompactMessages(...)
这说明 context collapse 的一个核心契约是:
REPL 内部保留全量历史,模型真正看到的是一份 projection。
这和 compactConversation 的思路不一样。
compactConversation 更像:
- 把消息数组真的重写了
而 projectView() 更像:
- 原始历史还在
- 但 API-bound 视图被改写了
这两种思路的差异很大。
如果你做 C# 版,这里最好明确拆成两个概念:
ConversationLogApiProjectedConversation
不要混成一个 list。
5. transcript 里真正持久化的不是“归档正文”,而是“折叠指令”
源码锚点:
src/types/logs.ts:240src/utils/sessionStorage.ts:1541src/utils/sessionStorage.ts:1559src/utils/sessionStorage.ts:3695
这是这层设计里最值钱的点之一。
5.1 commit entry 里存什么
ContextCollapseCommitEntry 会持久化这些字段:
collapseIdsummaryUuidsummaryContentsummaryfirstArchivedUuidlastArchivedUuid
它不会把 archived messages 再存一份。
原因也写得很明确:
- 原消息本来就已经在 transcript 里
- commit 只需要告诉系统“哪一段被折叠了,以及该用什么 summary placeholder 来接”
这说明它存的不是副本,而是:
splice instruction。
5.2 snapshot entry 里存什么
ContextCollapseSnapshotEntry 是 last-wins 的快照,存的是:
- staged queue
- armed 状态
lastSpawnTokens
它不是 commit log 的补充正文,而是:
运行中状态快照。
5.3 为什么这种设计很高级
因为它把两件事分开了:
- 已经 committed 的 collapse 历史:append-only log
- 当前还没 fully committed 的运行时状态:last-wins snapshot
这是很典型的事件源 + 当前状态快照混合设计。
对长期会话特别合适。
6. resume 时,它要求先恢复 collapse store,再跑 query
源码锚点:
src/utils/sessionRestore.ts:124src/utils/sessionRestore.ts:497src/screens/ResumeConversation.tsx:263
这几个地方都明确调用了:
restoreFromEntries(commits, snapshot)
而且注释写得很死:
- 必须在第一次
query()之前恢复 - 因为
projectView()需要基于这份 commit log 重建 collapsed view
这说明 context collapse 不是可有可无的 UI 状态,而是:
resume 后想得到正确 API 视图,必须先恢复的核心运行时状态。
也就是说,如果你未来做 C# 版恢复逻辑,顺序不能错:
- 先恢复 transcript messages
- 再恢复 content replacements
- 再恢复 collapse commits + snapshot
- 然后才能生成第一轮 projected view
7. compact boundary 会主动清空旧 collapse state
源码锚点:
src/utils/sessionStorage.ts:3660src/services/compact/postCompactCleanup.ts:46
这里有两层清理动作。
7.1 transcript 载入时,看到 compact boundary 就丢弃旧 collapse log
loadTranscriptFile() 里一旦遇到 compact_boundary,就会:
contextCollapseCommits.length = 0contextCollapseSnapshot = undefined
原因也很清楚:
- pre-boundary 的 collapse commit 引用的是旧消息跨度
- compact 之后这些跨度已经不再是同一条可投影链
所以作者直接把它们视为失效状态。
7.2 runtime cleanup 时,post-compact 也会 resetContextCollapse()
runPostCompactCleanup() 在主线程 compact 后还会显式:
resetContextCollapse()
也就是说,从系统角度看:
full compact 是一次更高阶的上下文重写,会直接让之前的 collapse 运行时状态失效。
这点很重要,因为它说明:
- collapse 和 compact 不是同一层
- compact 会覆盖 collapse
8. rewind 也会强制 reset collapse
源码锚点:
src/screens/REPL.tsx:3674
REPL 的 rewind 逻辑里,作者也单独处理了 context collapse:
- rewind 会截断 REPL 消息数组
- 这时已存在的 staged queue、ID map、commit span 都可能引用失效 uuid
- 最安全的办法就是直接
resetContextCollapse()
也就是说,在 Claude Code 的设计里:
rewind 不是“只改 message list”这么简单,它还会让依赖旧 uuid 拓扑的 runtime store 全部失效。
这和 contentReplacementState、microcompact state 的 reset 逻辑其实是一脉相承的。
9. UI 并不直接显示 collapse placeholder,而是显示统计态
源码锚点:
src/components/TokenWarning.tsxsrc/components/ContextVisualization.tsxsrc/commands/context/context-noninteractive.ts
9.1 TokenWarning 显示 live progress
TokenWarning.tsx 会通过:
getStats()subscribe()
显示比如:
x / y summarized- staged 数量
- error 数量
- empty spawn 警告
这说明 collapse store 本身至少暴露了:
- 可订阅状态
- 聚合统计
- health 指标
9.2 /context 和可视化页把它当成正式 context strategy
ContextVisualization.tsx 和 /context 都会在 collapse 开启时展示:
- 当前 strategy 是 collapse
- 已 summarized 的 spans / messages
- staged 数量
- total spawns
- error / idle 情况
这点很关键,因为它说明 Claude Code 团队已经把它视为:
对用户可解释的正式上下文管理策略。
不是实验实现埋在后台就算了。
10. analyzeContext.ts 也会为 collapse 改写“剩余空间”的解释
源码锚点:
src/utils/analyzeContext.ts:1107
analyzeContext.ts 里有个挺重要的判断:
- 如果是 reactive-only mode
- 或者
context collapse开着 - 就不要再显示 autocompact buffer 这种保留区
原因很简单:
- 这时上下文管理已经由别的机制接管
- 再显示旧 buffer 只会误导用户
这说明 collapse 不只是“多了一层压缩”,还会反过来改写:
整个系统对 context pressure 的解释模型。
11. 这份快照里,关于 contextCollapse 能确认和不能确认的边界
11.1 能确认的
从当前快照,我们可以明确确认这些接口或行为存在:
initContextCollapse()isContextCollapseEnabled()applyCollapsesIfNeeded(...)projectView(...)recoverFromOverflow(...)isWithheldPromptTooLong(...)resetContextCollapse()getStats()subscribe()restoreFromEntries(...)
还能确认这些持久化入口存在:
recordContextCollapseCommit(...)recordContextCollapseSnapshot(...)
11.2 当前不能当成事实写死的
这份仓库里看不到:
src/services/contextCollapse/index.jssrc/services/contextCollapse/operations.jssrc/services/contextCollapse/persist.js
所以现在不能直接断言的包括:
- collapse 的触发阈值到底怎么算
- summary 由谁生成、何时生成
- staged span 怎么选
- spawn 策略和 agent 交互的具体实现
- projectView 的精确重写算法
所以这篇文档只讨论:
它对外暴露出来的运行时契约。
12. 如果转成 C#,我建议至少拆成这几块
12.1 collapse store
public interface IContextCollapseStore
{
CollapseStats GetStats();
IDisposable Subscribe(Action onChanged);
void Reset();
}
12.2 projection / recovery coordinator
public interface IContextCollapseCoordinator
{
bool IsEnabled();
Task<ProjectedConversation> ApplyCollapsesIfNeededAsync(...);
ProjectedConversation ProjectView(IReadOnlyList<Message> messages);
OverflowRecoveryResult RecoverFromOverflow(...);
}
12.3 persistence adapter
public interface IContextCollapsePersistence
{
Task RecordCommitAsync(ContextCollapseCommitEntry entry);
Task RecordSnapshotAsync(ContextCollapseSnapshotEntry entry);
void RestoreFromEntries(
IReadOnlyList<ContextCollapseCommitEntry> commits,
ContextCollapseSnapshotEntry? snapshot);
}
12.4 一个很重要的原则
不要把 collapsed span 的正文再存第二份。
Claude Code 现在的思路很对:
- transcript 里已经有原消息
- collapse log 只存“如何折叠”
这会让持久化模型清爽很多。
13. 这一层最值得抄的设计习惯
最后收一下,我觉得最值钱的是这五个点:
- 把 context collapse 设计成独立子系统,而不是 compact 的子函数。
- 原始历史和模型投影视图分离。
- 持久化存的是 splice instruction,不是归档正文副本。
- resume 必须先恢复 collapse store,再恢复 query。
- rewind 和 full compact 要把依赖旧 uuid 拓扑的 collapse state 一起清掉。
14. 一句话总结
就这份快照能确认的部分来看,context collapse 的本质不是“又一种压缩算法”,而是:
Claude Code 为长会话设计的一套可投影、可恢复、可持久化的上下文重写基础设施。