Context Collapse(消息折叠)机制分析

4 阅读8分钟

基于 claude-code 源码分析。 核心实现位于 src/services/contextCollapse/,是闭源的(内部构建专有),在开源 fork 中通过 feature('CONTEXT_COLLAPSE') 编译时标志树摇删除。


1. 概述

Context Collapse 是 Claude Code 的渐进式上下文管理系统。不同于 autocompact(一次性将大量消息压缩为单个摘要),collapse 通过后台预计算 + 按需提交的方式,在上下文增长过程中持续将早期的消息区间替换为 LLM 生成的摘要。

设计目标

  • 保持粒度:每次只折叠一部分早期消息,保留最近消息的完整性
  • 避免竞争:抢占 autocompact 的触发时机,防止 autocompact 一次性销毁大量上下文
  • 零阻塞:摘要预计算在后台 spawn agent 中完成,不阻塞主查询流程
  • 持久化:折叠结果写入 JSONL transcript,--resume 时恢复

2. 核心数据结构

ContextCollapseCommitEntry

每次提交折叠产生一个 commit entry,记录一次折叠操作:

// src/types/logs.ts:255-269
export type ContextCollapseCommitEntry = {
  type: 'marble-origami-commit'
  sessionId: UUID
  /** 16-digit collapse ID. Max across entries reseeds the ID counter. */
  collapseId: string
  /** The summary placeholder's uuid — registerSummary() needs it. */
  summaryUuid: string
  /** Full <collapsed id="...">text</collapsed> string for the placeholder. */
  summaryContent: string
  /** Plain summary text for ctx_inspect. */
  summary: string
  /** Span boundaries — projectView finds these in the resumed Message[]. */
  firstArchivedUuid: string
  lastArchivedUuid: string
}
  • 每个 commit 对应一个连续的消息区间firstArchivedUuidlastArchivedUuid
  • summaryContent 是存入消息列表的占位符(<collapsed> 标签格式)
  • summary 是纯文本摘要(用于 ctx_inspect 工具查看)

ContextCollapseSnapshotEntry

记录 staged 队列和 spawn trigger 状态的快照:

// src/types/logs.ts:282-295
export type ContextCollapseSnapshotEntry = {
  type: 'marble-origami-snapshot'
  sessionId: UUID
  staged: Array<{
    startUuid: string
    endUuid: string
    summary: string    // 预计算的摘要文本
    risk: number       // 风险分数,用于优先级排序
    stagedAt: number   // 进入 staged 的时间戳
  }>
  /** Spawn trigger state — so the +interval clock picks up where it left off. */
  armed: boolean
  lastSpawnTokens: number
}
  • staged[]:已由 spawn agent 预计算好摘要、等待提交的条目队列
  • armed:spawn 是否已"上膛"
  • lastSpawnTokens:上次 spawn 时的 token 数,用于控制 spawn 频率

关键区别

维度CommitSnapshot
写入时机每次提交折叠时每次 spawn 解析完成后
恢复策略全部回放(append-only)最后写入胜出(last-wins)
作用重建已折叠的视图恢复 staged 队列 + spawn 间隔状态

3. 完整生命周期

阶段 1:初始化

setup.ts:298initContextCollapse()

在会话启动时调用,初始化 collapse 模块的内部状态。

阶段 2:后台预计算(Spawn)

spawn agent 代号marble_origami(也称 ctx-agent

触发时机:在 applyCollapsesIfNeeded() 中决策(每次 API 调用前)。

触发条件(推断,基于 lastSpawnTokens 和阈值系统):

  1. 当前 token 使用量超过 lastSpawnTokens 一定幅度
  2. spawn 处于 armed 状态
  3. 启动后台 spawn agent(通过 runForkedAgent()

spawn agent 行为

  1. 作为独立子进程运行(querySource = 'marble_origami'
  2. 分析最近的对话区间
  3. 用 LLM(推测为 Haiku)为高风险区间生成摘要
  4. 将结果写入 staged 队列(含 summaryrisk 分数)
  5. spawn 完成后持久化 snapshot(recordContextCollapseSnapshot

spawn agent 的特殊保护

  • spawn agent 自身的 autocompact 被禁止(autoCompact.ts:179-183),因为如果它的上下文爆炸触发了 autocompact,resetContextCollapse()销毁主线程的 committed log(模块级状态在所有 fork 间共享)

阶段 3:提交折叠(Commit)

入口query.ts:440-447

if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
  const collapseResult = await contextCollapse.applyCollapsesIfNeeded(
    messagesForQuery, toolUseContext, querySource,
  )
  messagesForQuery = collapseResult.messages
}

执行时机:每次 API 调用前,microcompact 之后、autocompact 之前

执行逻辑(推断):

  1. 检查当前 token 使用量
  2. 根据 token 水位决定是否提交(90% 阈值)
  3. 从 staged 队列中按 risk 分数从高到低选择条目
  4. 将选中的条目转换为 ContextCollapseCommitEntry
  5. 从消息列表中移除原始消息区间,注入 <collapsed> 摘要占位符
  6. 持久化 commit entry(recordContextCollapseCommit

阶段 4:读取时投影(projectView)

collapsed view 是一种读取时投影——commit log 只记录哪些区间被折叠了,不修改原始消息数组。每次重建视图时:

  1. projectView() 回放所有 commit entries
  2. 定位 firstArchivedUuidlastArchivedUuid 区间
  3. 将区间替换为 <collapsed> 摘要占位符
  4. 返回给调用方作为 messagesForQuery

这样做的好处:

  • 原始消息在 JSONL 中完整保留
  • collapse 可以跨 turn 持久化
  • 同一 turn 内第二次调用 projectView() 是 no-op(已折叠的消息已不在输入中)

阶段 5:持久化

两种 entry 都通过 sessionStorage.ts 写入 JSONL transcript:

// commit: 顺序追加,恢复时全量回放(顺序重要)
} else if (entry.type === 'marble-origami-commit') {
  void this.enqueueWrite(sessionFile, entry)

// snapshot: 顺序追加,恢复时只取最后一个(later entries supersede)
} else if (entry.type === 'marble-origami-snapshot') {
  void this.enqueueWrite(sessionFile, entry)

阶段 6:会话恢复

两个恢复入口:

入口场景
sessionRestore.ts:127-135交互式 /resume(经由 REPL.tsx)
sessionRestore.ts:494-503CLI --continue / --resume

两者都调用:

require('../services/contextCollapse/persist.js').restoreFromEntries(
  result.contextCollapseCommits ?? [],
  result.contextCollapseSnapshot,
)

必须在第一次 query() 之前调用,这样 projectView() 才能从恢复的消息列表中正确重建折叠视图。

阶段 7:压缩时重置

当 autocompact 或 reactive compact 触发时:

// postCompactCleanup.ts:42-49
if (feature('CONTEXT_COLLAPSE')) {
  if (isMainThreadCompact) {
    require('../contextCollapse/index.js').resetContextCollapse()
  }
}

resetContextCollapse() 清空模块级的 commit log 和 staged 队列。同时 JSONL parser 在遇到 compact boundary message 时也会清空:

// sessionStorage.ts:3654-3656
if (isCompactBoundaryMessage(entry)) {
  contextCollapseCommits.length = 0
  contextCollapseSnapshot = undefined
}

4. 阈值系统

Context Collapse 和 autocompact 之间有精密的阈值协调:

有效上下文窗口: |←·······················································→|
                                      90%      93%      95%
                                      ├────────┼────────┼────────────────┤
collapse 提交开始:                     ★←──────┤        │
collapse blocking-spawn:                      │        ★←──────┤
autocompact 常规触发:                          │   ★←─────┤
                                            (被抑制)
水位动作说明
~90%开始提交 staged 条目applyCollapsesIfNeeded 将高 risk 的 staged 条目转为 commit
~93%autocompact 抑制点autocompact 被禁用(autoCompact.ts:215-222),避免它与 collapse 竞争并"误杀"可折叠的上下文
~95%blocking-spawn 模式紧急模式,主线程可能需要等待 collapse 腾出空间
413/PTLrecoverFromOverflow强制提交所有 staged 条目

autocompact 抑制逻辑

// autoCompact.ts:201-222
// Collapse IS the context management system when it's on — the 90% commit /
// 95% blocking-spawn flow owns the headroom problem. Autocompact firing at
// effective-13k (~93% of effective) sits right between collapse's
// commit-start (90%) and blocking (95%), so it would race collapse and
// usually win, nuking granular context that collapse was about to save.

blockingLimit 绕过

当 collapse 处于激活状态时,blocking limit 检查也会被跳过(collapseOwnsIt 标志):

// query.ts:615-635
let collapseOwnsIt = false
if (feature('CONTEXT_COLLAPSE')) {
  collapseOwnsIt =
    (contextCollapse?.isContextCollapseEnabled() ?? false) &&
    isAutoCompactEnabled()
}
// 如果 collapseOwnsIt 为 true,跳过 blocking limit 检查

但这只抑制主动 autocompact——reactiveCompact(作为 413 的被动后备)仍然可用。


5. 413 恢复(recoverFromOverflow)

当 API 返回 prompt_too_long(413)时:

// query.ts:1085-1117
if (feature('CONTEXT_COLLAPSE') && contextCollapse &&
    state.transition?.reason !== 'collapse_drain_retry') {
  const drained = contextCollapse.recoverFromOverflow(
    messagesForQuery, querySource,
  )
  if (drained.committed > 0) {
    // 继续循环,transition.reason = 'collapse_drain_retry'
  }
}
  • 强制提交所有 staged 条目(不等待 spawn agent)
  • 如果仍然 413,才 fall through 到 reactive compact
  • collapse_drain_retry 防止无限重试

6. Stats 与监控

getStats() 导出的健康指标(在 /context 命令中展示):

const s = getStats()
// s.collapsedSpans   - 已折叠的区间数
// s.collapsedMessages - 已折叠的消息总数
// s.stagedSpans       - 等待提交的区间数
// s.health.totalSpawns        - spawn 总次数
// s.health.totalErrors        - spawn 失败次数
// s.health.lastError          - 最后一次错误信息
// s.health.totalEmptySpawns   - 连续空跑次数(spawn 了但没有内容要 stage)
// s.health.emptySpawnWarningEmitted - 空跑警告标志

/context 展示示例:

**Context strategy:** collapse (3 spans summarized (47 messages), 2 staged)
**Collapse errors:** 1/10 spawns failed (last: rate limit exceeded)

7. 与 autocompact 的对比

维度Context CollapseAutocompact
触发时机每次 API 调用前(渐进)每次 API 调用前(一次性)
摘要生成LLM(spawn agent 预计算)LLM(主线程)
粒度每次折叠一个消息区间一次性折叠大量消息
对上下文的影响保留最近消息的完整性可能丢失细粒度信息
持久化commit log + snapshot写入 summary message
恢复restoreFromEntries 重建JSONL 中已有 summary

8. 会话隔离

场景staged queue 行为
当前会话进行中累加当前会话的预计算摘要
exit 后 --resume 恢复从 JSONL 重建,继续同一个会话
/clear 新会话清空,重新开始
autocompact 触发resetContextCollapse() 清空

staged queue 和 commit log 只绑定当前会话,不会跨会话共享。每次新会话(非 resume)都是干净的 collapse 状态。


9. 导出接口总览

src/services/contextCollapse/index.js 导出的函数:

函数调用位置用途
initContextCollapse()setup.ts:298初始化
applyCollapsesIfNeeded(messages, ctx, source)query.ts:441提交折叠
isContextCollapseEnabled()query.ts:618, autoCompact.ts:217门控检查
isWithheldPromptTooLong(msg, fn, source)query.ts:802PTL 抑制
recoverFromOverflow(messages, source)query.ts:1094413 恢复
getStats()context-noninteractive.ts:115统计信息
resetContextCollapse()postCompactCleanup.ts:47压缩时重置

src/services/contextCollapse/persist.js

函数调用位置用途
restoreFromEntries(commits, snapshot)sessionRestore.ts:130,497会话恢复

src/services/contextCollapse/operations.js

函数调用位置用途
projectView(messages)context-noninteractive.ts:55读取时投影

10. 关键代码位置

文件行号内容
src/types/logs.ts255-295CommitEntry / SnapshotEntry 类型定义
src/query.ts440-447applyCollapsesIfNeeded 调用点
src/query.ts615-635collapseOwnsIt blocking limit 绕过
src/query.ts800-809PTL 抑制
src/query.ts1085-1117recoverFromOverflow 413 恢复
src/services/compact/autoCompact.ts174-183spawn agent autocompact 禁用
src/services/compact/autoCompact.ts201-222collapse 模式 autocompact 抑制
src/services/compact/postCompactCleanup.ts42-49resetContextCollapse()
src/utils/sessionStorage.ts1208-1215commit/snapshot 持久化分发
src/utils/sessionStorage.ts1541-1581recordContextCollapseCommit/Snapshot
src/utils/sessionStorage.ts3654-3656compact 边界清除 collapse
src/utils/sessionStorage.ts3694-3697JSONL parse 时收集 collapse entries
src/utils/sessionRestore.ts127-136交互式恢复 collapse 状态
src/utils/sessionRestore.ts494-503CLI 恢复 collapse 状态
src/commands/context/context-noninteractive.ts110-147/context 展示 collapse 统计
src/setup.ts295-301initContextCollapse()
src/tools.ts110-112CtxInspectTool 注册