Yoda
参考Agent 设计指南Session

上下文压缩 Compaction

长会话的生存机制——CC 用三档微压缩 + LLM 全量摘要 + 指针手术落盘,Codex 用服务端压缩 + 全量快照落盘

上下文压缩 Compaction

结论

Compaction 是「上下文逼近窗口上限时,用摘要替换旧历史」的生存机制,两个 runtime 在生成落盘两端都选了相反的设计:

  • 生成端:Claude Code 在动用 LLM 全量摘要之前有一整套零/低成本「微压缩」分层(缓存编辑删旧 tool_result / 时间触发清空 / 复用 session memory),Codex 没有这层,但 Codex 有 CC 没有的服务端压缩(OpenAI/Azure 走 /compact 端点,不耗本地采样)和 turn 中途压缩MidTurn 相位)。
  • 落盘端:CC 做「指针手术」——transcript 里插一条 parentUuid:null 的 boundary 截断消息链,摘要以普通 user 消息明文落盘,O(1) 增量;Codex 做「全量快照」——rollout 里 append 一条 CompactedItem,内嵌压缩后的完整 replacement_history,O(history) 冗余换 resume 简单性。
  • 对外通道:CC 的 PostCompact hook 把摘要全文直接推给外部(compact_summary 字段);Codex 没有任何推送摘要全文的通道,外部只能挖 rollout 文件。

上层产品(harness)两件事必须做对:拿摘要走 CC 的 hook / Codex 的 rollout 解析(注意远程压缩 message 为空的坑);判压缩后会话视图不要自己按 parentUuid 重建链(CC 保留段消息在磁盘上的 parentUuid 是「错」的)。

研究问题

  • 什么时候触发压缩?阈值如何计算、能否关闭?
  • 摘要怎么生成(本地 LLM / 服务端 / prompt 形态)?
  • 压缩在磁盘上如何编码?外部进程如何感知并拿到摘要全文?
  • 重启 / resume 后压缩态如何重建?

各 Agent 设计与实现

Claude Code

触发 [一手源码]:阈值 = 有效上下文窗口 − 13K buffer(services/compact/autoCompact.ts:62 AUTOCOMPACT_BUFFER_TOKENS = 13_000);每个 query 入口检查。带熔断:连续失败 3 次停(autoCompact.ts:67,注释记录了真实生产事故——盲目重压曾全球每天浪费 ~25 万次 API 调用)。可禁用:DISABLE_COMPACT(全禁)/ DISABLE_AUTO_COMPACT(仅自动)。手动 /compact [自定义指令]

微压缩三档(动 LLM 之前的零/低成本层,文档零提及) [一手源码] services/compact/microCompact.ts:253 按优先级分流:

  1. 缓存编辑:305):用 API 原生 cache_edits 在传输层删旧 tool_result(白名单:Read/Bash/Grep/Glob/WebSearch/WebFetch/Edit/Write),本地消息不动、缓存前缀不失效,零 LLM 调用;
  2. 时间触发:422):距上条 assistant 消息超 60 分钟(对齐缓存 TTL),旧 tool_result 替换为 [Old tool result content cleared] 占位,保留最近 5 个;
  3. Session Memory:复用独立进程预抽取的 .claude/session_memory.md,不临时召唤摘要模型。

全量摘要生成 [一手源码]:本地 LLM 调用,9 段式 handoff prompt(Primary Request / Key Concepts / Files / Errors / All user messages / Pending Tasks / Current Work / Next Step,services/compact/prompt.ts:19)。

落盘编码(指针手术) [一手源码]

// utils/sessionStorage.ts:1040
parentUuid: isCompactBoundary ? null : effectiveParentUuid,   // 物理截断消息链
logicalParentUuid: isCompactBoundary ? parentUuid : undefined, // 留一根"逻辑脐带"

boundary 携带 compactMetadata: { trigger, preTokens, preservedSegment?: {headUuid, anchorUuid, tailUuid} }utils/messages.ts:4530-4555)。摘要本身是一条普通 user 消息(isCompactSummary: true, isVisibleInTranscriptOnly: truecompact.ts:613-624),全文明文在 JSONL 里。旧历史不删,文件 append-only。

部分压缩的保留段:被保留的尾部消息在磁盘上保持压缩前的旧 parentUuid 不动,重链只发生在加载时内存里的 applyPreservedSegmentRelinkssessionStorage.ts:1839-1956,加载入口 :3704)。多个 boundary 只认绝对最后一个的 preservedSegment(:1846-1870)。

对外同步 [一手源码] [一手文档]

  • PreCompact hook 可阻断压缩、可注入自定义指令(docs hooks.md);
  • PostCompact hook 收到 { trigger, compact_summary: <摘要全文> }utils/hooks.ts:4044-4049)——外部拿摘要最省事的通道;
  • SDK / stream-json 收到 SDKCompactBoundaryMessage(只有 compact_metadata,无摘要文本,QueryEngine.ts:935-941;公开文档 agent-sdk__agent-loop.md);
  • ~/.claude/sessions/<pid>.json 状态文件不带压缩信号。

Resume [一手源码]:parse 全量 → 内存重链 → buildConversationChain 沿 parentUuid 回溯,到 boundary 的 parentUuid:null 自然停(sessionStorage.ts:2069-2094)。>5MB 大文件直接在 fd 级跳过压缩前字节(SKIP_PRECOMPACT_THRESHOLD = 5MBsessionStoragePortable.ts:480;可用 CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP 关闭)。

Codex CLI

触发 [一手源码]model_auto_compact_token_limit,且计 token 口径可配(core/src/session/turn.rs:707-745):total(全上下文)或 body_after_prefix(只算上次 carry-forward 之后新增——避免反复压缩已携带上下文,CC 没有的精细度)。三种相位:PreTurn / MidTurn(turn 进行中、模型还要 follow-up 时压缩——CC 只在 query 边界压) / StandaloneTurn(手动);另有模型降档触发(CompactionReason::ModelDownshift)。关闭 = 把阈值设极大。

摘要生成:压缩 prompt 是「上下文检查点」语气(prompts/templates/compact/prompt.md),比 CC 的 9 段式短得多。摘要以 SUMMARY_PREFIX 包装后伪装成 user 消息注回历史(core/src/compact.rs:292)。服务端路径(Codex 独有):OpenAI/Azure provider 调 /compact 端点(compact_remote.rs / compact_remote_v2.rs,V2 流式、64K 保留预算),门槛 supports_remote_compaction()model-provider-info/src/lib.rs:399);其他 provider 退回本地。本地压缩保留最近 20K token 的 user 消息。

落盘编码(全量快照) [一手源码] protocol/src/protocol.rs:2826-2840

pub struct CompactedItem {
    pub message: String,                                // 摘要文本(本地压缩才有!)
    pub replacement_history: Option<Vec<ResponseItem>>, // 压缩后的完整新历史
}

持久化白名单显式放行(rollout/src/policy.rs:10-13)。远程 v2 压缩 message 是空串compact_remote_v2.rs:301-304),摘要藏在 replacement_history 的 ResponseItem 里——解析器必须兜底。旧历史同样不删,rollout append-only。

对外同步 [一手源码]:压缩作为 turn item 走 ItemStarted/ItemCompleted 事件(TurnItem::ContextCompactioncompact.rs:201/317;v1 的 ContextCompactedEvent 已弃用)——事件不带摘要文本。压缩完成排队一个 SessionStart(source="compact") hook(session/mod.rs:2678 + hooks/src/events/session_start.rs:27),payload 无摘要字段,下一 turn 才触发。没有 PostCompact 等价物。 另发 Warning 事件提醒多次压缩降准确率,trace 开启时写 rollout-trace checkpoint(压缩前后双份历史)。

Resume [一手源码] core/src/session/rollout_reconstruction.rs:96-128:倒扫 rollout,最新带 replacement_historyCompacted 即基底,其后条目向前重放;多次压缩天然 newest-wins,与 ThreadRolledBack 联动(回滚范围内的快照不作基底)。

差异矩阵

维度Claude CodeCodex CLI
自动触发阈值窗口 − 13K 固定 buffermodel_auto_compact_token_limit,口径可配(total / body_after_prefix)
触发时机query 边界PreTurn / MidTurn(turn 中途) / 手动 / 模型降档
LLM 前的微压缩层✅ 三档(缓存编辑 / 时间触发 / session memory)❌ 无独立层
服务端压缩✅ OpenAI/Azure /compact 端点(V2 流式,64K 保留)
磁盘编码指针手术:boundary + 摘要 user 消息,O(1) 增量全量快照:CompactedItem 内嵌新历史,O(history)
摘要全文在磁盘✅ JSONL user 消息明文⚠️ 本地在 message;远程 v2 message:"" 须挖 replacement_history
HookPreCompact(可阻断)+ PostCompact含摘要全文SessionStart(source=compact)(无摘要,延迟到下一 turn)
协议/SDK 事件SDKCompactBoundaryMessage(metadata only)ItemStarted(ContextCompaction)(metadata only)
Resume 重建内存指针重链 + parentUuid 回溯;>5MB fd 级跳读倒扫取最新快照 + 重放后缀
熔断/降级连续失败 3 次熔断无熔断;非 OpenAI provider 退回本地压缩
可禁用DISABLE_COMPACT / DISABLE_AUTO_COMPACT阈值设极大近似关闭

最小复现

# CC:手动压缩 + hook 拿摘要全文
# .claude/settings.json 挂 PostCompact hook: jq .compact_summary 即摘要
claude> /compact 重点保留 API 设计决策
# transcript 验证:boundary + 摘要两行
grep -c '"subtype":"compact_boundary"' ~/.claude/projects/<slug>/<session>.jsonl

# Codex:rollout 里找压缩快照
grep '"type":"compacted"' ~/.codex/sessions/$(date +%Y/%m/%d)/rollout-*.jsonl | head -1 | \
  python3 -c "import json,sys; d=json.loads(sys.stdin.read()); p=d['payload']; print(p['message'][:200] or '[REMOTE COMPACT: summary in replacement_history]')"

Harness 接入建议(Yoda 实践)

  1. 拿摘要全文:CC 首选 PostCompact hook(全文直推 stdin,零解析);被动方案 watch transcript,匹配 compact_boundary 行 → 读下一条 isCompactSummary user 消息。Codex 只能 tail rollout,匹配 "type":"compacted"payload.message 为空时进 replacement_historySUMMARY_PREFIX 开头的 user 消息。
  2. 压缩后会话视图:不要自己按 parentUuid 重建 CC 的链——保留段消息磁盘上的 parentUuid 指向已截断的旧链,必须复刻 applyPreservedSegmentRelinks 的指针手术,否则把保留段判成孤儿。Codex 侧照搬「倒扫取最新快照」即可,逻辑简单得多。
  3. 压缩感知信号:Yoda 的 session 状态管线可把 CC 的 compact_boundary(SDK 事件或 JSONL 行)与 Codex 的 ItemStarted(ContextCompaction) 统一映射成「会话被压缩」badge,提示用户上下文已瘦身、模型可能丢细节。
  4. 自研 runtime 取舍:摘要同步学 CC(hook 直推全文);持久化学 Codex(自包含快照,外部重建零指针手术)。两者不冲突,最优组合 = 快照落盘 + 事件/hook 推全文。
  5. 隐私提醒:两边「压缩掉」的旧历史都还在磁盘明文里(CC 默认 30 天清理兜底,Codex 无自动清理)——做日志展示/导出功能时按完整文件处理。

失效条件

  • CC 源码快照为 2026-03-31,microcompact 的 GrowthBook 默认值(如 time-based enabled:false)可能已在线上变更;
  • CC 若把摘要从「user 消息明文」改为独立条目类型,外部解析器需跟进;
  • Codex compact_prompt 自定义仍带 experimental_ 前缀,迭代中;远程 V2 受 Feature::RemoteCompactionV2 门控;
  • Codex 若给 SessionStart(compact) hook 或协议事件补上摘要字段,本章「无全文推送通道」结论过期。

参考资料

  • 深度报告:agent-research/output/手工川-会话实时摘要机制-2026-06-09-v0.1.md(生成侧全量证据)
  • 深度报告:agent-research/output/手工川-compact-summary-sync-cc-vs-codex-2026-06-11-v0.1.md(落盘/同步侧全量证据)
  • CC docs: hooks.md(PreCompact/PostCompact)、agent-sdk__agent-loop.md(compact_boundary SDK 事件)、costs.md
  • Codex source: core/src/compact.rscompact_remote_v2.rsrollout/src/policy.rscore/src/session/rollout_reconstruction.rs

On this page