基于 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 对应一个连续的消息区间(
firstArchivedUuid→lastArchivedUuid) 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 频率
关键区别
| 维度 | Commit | Snapshot |
|---|---|---|
| 写入时机 | 每次提交折叠时 | 每次 spawn 解析完成后 |
| 恢复策略 | 全部回放(append-only) | 最后写入胜出(last-wins) |
| 作用 | 重建已折叠的视图 | 恢复 staged 队列 + spawn 间隔状态 |
3. 完整生命周期
阶段 1:初始化
setup.ts:298 → initContextCollapse()
在会话启动时调用,初始化 collapse 模块的内部状态。
阶段 2:后台预计算(Spawn)
spawn agent 代号:marble_origami(也称 ctx-agent)
触发时机:在 applyCollapsesIfNeeded() 中决策(每次 API 调用前)。
触发条件(推断,基于 lastSpawnTokens 和阈值系统):
- 当前 token 使用量超过
lastSpawnTokens一定幅度 - spawn 处于
armed状态 - 启动后台 spawn agent(通过
runForkedAgent())
spawn agent 行为:
- 作为独立子进程运行(
querySource = 'marble_origami') - 分析最近的对话区间
- 用 LLM(推测为 Haiku)为高风险区间生成摘要
- 将结果写入 staged 队列(含
summary和risk分数) - 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 之前。
执行逻辑(推断):
- 检查当前 token 使用量
- 根据 token 水位决定是否提交(90% 阈值)
- 从 staged 队列中按
risk分数从高到低选择条目 - 将选中的条目转换为
ContextCollapseCommitEntry - 从消息列表中移除原始消息区间,注入
<collapsed>摘要占位符 - 持久化 commit entry(
recordContextCollapseCommit)
阶段 4:读取时投影(projectView)
collapsed view 是一种读取时投影——commit log 只记录哪些区间被折叠了,不修改原始消息数组。每次重建视图时:
projectView()回放所有 commit entries- 定位
firstArchivedUuid→lastArchivedUuid区间 - 将区间替换为
<collapsed>摘要占位符 - 返回给调用方作为
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-503 | CLI --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/PTL | recoverFromOverflow | 强制提交所有 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 Collapse | Autocompact |
|---|---|---|
| 触发时机 | 每次 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:802 | PTL 抑制 |
recoverFromOverflow(messages, source) | query.ts:1094 | 413 恢复 |
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.ts | 255-295 | CommitEntry / SnapshotEntry 类型定义 |
src/query.ts | 440-447 | applyCollapsesIfNeeded 调用点 |
src/query.ts | 615-635 | collapseOwnsIt blocking limit 绕过 |
src/query.ts | 800-809 | PTL 抑制 |
src/query.ts | 1085-1117 | recoverFromOverflow 413 恢复 |
src/services/compact/autoCompact.ts | 174-183 | spawn agent autocompact 禁用 |
src/services/compact/autoCompact.ts | 201-222 | collapse 模式 autocompact 抑制 |
src/services/compact/postCompactCleanup.ts | 42-49 | resetContextCollapse() |
src/utils/sessionStorage.ts | 1208-1215 | commit/snapshot 持久化分发 |
src/utils/sessionStorage.ts | 1541-1581 | recordContextCollapseCommit/Snapshot |
src/utils/sessionStorage.ts | 3654-3656 | compact 边界清除 collapse |
src/utils/sessionStorage.ts | 3694-3697 | JSONL parse 时收集 collapse entries |
src/utils/sessionRestore.ts | 127-136 | 交互式恢复 collapse 状态 |
src/utils/sessionRestore.ts | 494-503 | CLI 恢复 collapse 状态 |
src/commands/context/context-noninteractive.ts | 110-147 | /context 展示 collapse 统计 |
src/setup.ts | 295-301 | initContextCollapse() |
src/tools.ts | 110-112 | CtxInspectTool 注册 |