Book 第二十四章:Context Collapse 为什么值得单列
第五部分:会话续航与治理

第二十四章: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 里至少横跨四层:

  1. query 前的上下文投影
  2. overflow 恢复
  3. transcript 级持久化与 resume
  4. UI 级状态展示

这已经不是工具函数,而是正式子系统了。

3. 在 query 主循环里,它扮演的三个角色

源码锚点:

  • src/query.ts:18
  • src/query.ts:440
  • src/query.ts:618
  • src/query.ts:802
  • src/query.ts:1094

3.1 角色一:正常请求前的上下文重写器

在主循环里,Claude Code 会在:

  • snip
  • microcompact

之后调用:

  • 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 的恢复顺序是:

  1. recoverFromOverflow(...)
  2. 再考虑 reactive compact

所以 collapse 的恢复优先级比 reactive compact 更靠前。

这代表它承担的是:

“把已经 staged 的压缩收益立刻兑现” 这件事。

4. projectView() 说明它不是“修改原消息数组”,而是“投影模型视图”

源码锚点:

  • src/commands/context/context.tsx
  • src/commands/context/context-noninteractive.ts

这两个 /context 入口都做了同一件事:

  1. getMessagesAfterCompactBoundary(messages)
  2. 如果启用了 CONTEXT_COLLAPSE
  3. 再调用 projectView(view)
  4. 然后才跑 microcompactMessages(...)

这说明 context collapse 的一个核心契约是:

REPL 内部保留全量历史,模型真正看到的是一份 projection。

这和 compactConversation 的思路不一样。

compactConversation 更像:

  • 把消息数组真的重写了

projectView() 更像:

  • 原始历史还在
  • 但 API-bound 视图被改写了

这两种思路的差异很大。

如果你做 C# 版,这里最好明确拆成两个概念:

  • ConversationLog
  • ApiProjectedConversation

不要混成一个 list。

5. transcript 里真正持久化的不是“归档正文”,而是“折叠指令”

源码锚点:

  • src/types/logs.ts:240
  • src/utils/sessionStorage.ts:1541
  • src/utils/sessionStorage.ts:1559
  • src/utils/sessionStorage.ts:3695

这是这层设计里最值钱的点之一。

5.1 commit entry 里存什么

ContextCollapseCommitEntry 会持久化这些字段:

  • collapseId
  • summaryUuid
  • summaryContent
  • summary
  • firstArchivedUuid
  • lastArchivedUuid

它不会把 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:124
  • src/utils/sessionRestore.ts:497
  • src/screens/ResumeConversation.tsx:263

这几个地方都明确调用了:

  • restoreFromEntries(commits, snapshot)

而且注释写得很死:

  • 必须在第一次 query() 之前恢复
  • 因为 projectView() 需要基于这份 commit log 重建 collapsed view

这说明 context collapse 不是可有可无的 UI 状态,而是:

resume 后想得到正确 API 视图,必须先恢复的核心运行时状态。

也就是说,如果你未来做 C# 版恢复逻辑,顺序不能错:

  1. 先恢复 transcript messages
  2. 再恢复 content replacements
  3. 再恢复 collapse commits + snapshot
  4. 然后才能生成第一轮 projected view

7. compact boundary 会主动清空旧 collapse state

源码锚点:

  • src/utils/sessionStorage.ts:3660
  • src/services/compact/postCompactCleanup.ts:46

这里有两层清理动作。

7.1 transcript 载入时,看到 compact boundary 就丢弃旧 collapse log

loadTranscriptFile() 里一旦遇到 compact_boundary,就会:

  • contextCollapseCommits.length = 0
  • contextCollapseSnapshot = 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 全部失效。

这和 contentReplacementStatemicrocompact state 的 reset 逻辑其实是一脉相承的。

9. UI 并不直接显示 collapse placeholder,而是显示统计态

源码锚点:

  • src/components/TokenWarning.tsx
  • src/components/ContextVisualization.tsx
  • src/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.js
  • src/services/contextCollapse/operations.js
  • src/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. 这一层最值得抄的设计习惯

最后收一下,我觉得最值钱的是这五个点:

  1. 把 context collapse 设计成独立子系统,而不是 compact 的子函数。
  2. 原始历史和模型投影视图分离。
  3. 持久化存的是 splice instruction,不是归档正文副本。
  4. resume 必须先恢复 collapse store,再恢复 query。
  5. rewind 和 full compact 要把依赖旧 uuid 拓扑的 collapse state 一起清掉。

14. 一句话总结

就这份快照能确认的部分来看,context collapse 的本质不是“又一种压缩算法”,而是:

Claude Code 为长会话设计的一套可投影、可恢复、可持久化的上下文重写基础设施。