上下文快满时,Codex 到底压缩了什么
沿自动触发、local 与 remote compaction、replacement history 和 rollout reconstruction,解释 Codex 换掉的是模型工作集,不是 thread 日志或累计用量。
一次长 thread 接近上下文上限后,界面会出现 context compaction。把它概括成“总结旧消息,然后从零开始”会漏掉触发条件、实现路径、状态替换和恢复基线。
固定版本的核心动作是 replacement:compaction 生成一份新的模型工作 history,换掉 live ContextManager,再尝试向 rollout 追加一个 CompactedItem checkpoint。旧 JSONL 没有删除,session 累计 token 也没有归零。
触发条件先看 scope
ModelInfo::auto_compact_token_limit() 先解析 context_window,没有时回退 max_context_window。只要能得到 resolved window,就计算 90% 上限,并与显式 model limit 取更小值;没有 resolved window 时,显式 limit 原样返回。
Total scope 直接拿 active-context usage 对这个 helper。BodyAfterPrefix 从当前 window 的 prefill baseline 扣起,只计算 body 增长;它优先使用 config 的显式 body limit,同时仍用 effective context window 做 hard cap。因此“永远在 90% 触发”并不成立。
本节源码依据(2 处)
Remote v2:摘要本身是 opaque item
remote v2 从 clone 的 history 开始,先尝试把过大的 tool output 重写成截断占位,再调用 for_prompt。请求 input 末尾追加协议级 CompactionTrigger;它不是 durable response item。
response stream 必须到达 response.completed,并且所有 output item 中恰好有一个 ResponseItem::Compaction。零个或多个都是 fatal contract error;其他 output item 可以存在但不会进入 compaction output。server token usage 存在时,core 会据此写 rollout budget,并补齐 compaction analytics 里的 input、output 与 cached-input 细节;usage 缺失时 analytics 仍会记录,只是沿用 compaction 开始前的 active-context 观察值。
构造 replacement 时,v2 先把候选限制为 user/developer/system message,再删除 developer、system 和 contextual-only user item。能解析成真实 user message 或 persisted hook prompt 的输入才保留,并按 64,000 token 从新到旧裁剪,最后追加带 encrypted_content 的 opaque compaction item。客户端没有把这段内容还原成明文摘要的合同。
本节源码依据(4 处)
Replacement 是 checkpoint,不是删除旧日志
不论 replacement 来自哪条算法,最后都会进入 Session::replace_compacted_history。它先在 state lock 内整体替换 ContextManager,再追加完整 replacement_history 和 window ids 的 RolloutItem::Compacted;需要时,world-state baseline 和 turn context 紧随其后追加。
resume reconstruction 反向扫描 rollout,找到最新仍存活的 replacement checkpoint,把 rollout_suffix 指到 checkpoint 之后。正向 materialize 时先安装 replacement,再只重放 suffix 中的 response、inter-agent communication 和 rollback 等记录。
flowchart TB
accTitle: Compaction replacement 与 rollout reconstruction
accDescr: compaction creates replacement history, installs it into live ContextManager, appends a Compacted checkpoint, and later resume selects that checkpoint before replaying its suffix
TRIGGER["manual / pre-turn / mid-turn"] --> REPLACEMENT["replacement history"]
REPLACEMENT --> LIVE["ContextManager.replace"]
LIVE --> CHECKPOINT["append Compacted checkpoint"]
CHECKPOINT --> JSONL["older JSONL remains"]
JSONL --> REVERSE["reverse reconstruction scan"]
REVERSE --> BASE["newest checkpoint"]
BASE --> SUFFIX["replay surviving suffix"]
SUFFIX --> RESUMED["resumed live history"]
本节源码依据(3 处)
失败边界:停止、替换和落盘不是同一个结果
compaction 的 hook 生命周期把“没有开始”“已经替换但本轮被中止”和“算法本身失败”分开。这里先固定 compact 调用点和状态结果;matcher、parser 与共享 dispatcher 的差异留到第 35 章。pre-compaction hook 返回 stop 时,入口直接返回 TurnAborted,只记录 interrupted analytics,尚未调用 local/remote 的实现,也就没有 replacement history。算法返回错误时,入口按结果记录 Failed 或 Interrupted,remote 路径还会发 error event;同样不会安装新的 checkpoint。
post-compaction hook 的位置更容易让调用方误判:只有 run_*_compact_task_inner_impl 成功后才执行它。此时 replace_compacted_history 已经把新 history 放进 live state,并尝试追加 Compacted、world-state 和 turn-context rollout items;post hook 若要求 stop,入口仍返回 TurnAborted,但 replacement 不能回滚。也就是说,调用方看到“compaction turn aborted”时,不能推出旧 history 仍然在 live manager 中。
落盘失败又是另一层边界。Session::persist_rollout_items 只记录 LiveThread::append_items 的错误,不向 compaction caller 传播;因此 live replacement 成功、rollout checkpoint 缺失是一个可发生的中间状态。恢复时若没有 checkpoint,只能按现存 rollout 重建,不能把本次内存替换当成 durable 事实。
本节源码依据(3 处)
验证:绿色的 network test 可能什么也没跑
计划中的三条集成测试都带有网络可用性 guard。在当前 Codex sandbox 里,按指定命令执行得到的结果是测试进程以 1 passed 结束,但断言前打印了 early-return 提示:
: "${ARCHIVE_CODEX_RS:?先执行第五部导读的 archive 准备脚本}"
cd "$ARCHIVE_CODEX_RS"
just test --locked -p codex-core --test all auto_compact_runs_after_token_limit_hit
just test --locked -p codex-core --test all compact_resume_and_fork_preserve_model_history_view
just test --locked -p codex-app-server --test all thread_compact_start_triggers_compaction_and_returns_empty_response
对应输出分别包含:
Skipping test because it cannot execute when network is disabled in a Codex sandbox.
Skipping test because network is disabled in this sandbox
Skipping test because it cannot execute when network is disabled in a Codex sandbox.
所以这里的 1 passed 只证明测试函数在禁网环境下正常返回,不能证明自动触发、resume/fork history view 或 app-server compact response 的行为。取消 guard 后,wiremock 需要绑定 loopback port;当前 sandbox 返回 PermissionDenied,因此也没有把“能启动 mock server”写成源码验证。
能在无网络环境真实执行的 unit tests 则覆盖了替换算法的局部合同。以下命令各自都是 1 passed, 0 failed,没有 skip:
: "${ARCHIVE_CODEX_RS:?先执行第五部导读的 archive 准备脚本}"
cd "$ARCHIVE_CODEX_RS"
just test --locked -p codex-core --lib collect_user_messages_filters_legacy_warnings
just test --locked -p codex-core --lib build_token_limited_compacted_history_truncates_overlong_user_messages
just test --locked -p codex-core --lib build_token_limited_compacted_history_appends_summary_message
just test --locked -p codex-core --lib build_v2_compacted_history_filters_to_installed_retention_shape
just test --locked -p codex-core --lib build_v2_compacted_history_discards_messages_before_truncating
just test --locked -p codex-core --lib collect_compaction_output_accepts_additional_output_items
just test --locked -p codex-core --lib retained_history_truncation_keeps_newest_messages_first
just test --locked -p codex-core --lib reconstruct_history_uses_replacement_history_verbatim
just test --locked -p codex-core --lib reconstruct_history_matches_live_compactions
just test --locked -p codex-core --lib recompute_token_usage_uses_session_base_instructions
这些测试把真实 user-message filtering、20,000/64,000 retention、opaque output 收集、newest-first truncation、checkpoint reconstruction 和 active context token 重估钉在纯函数或本地状态上;累计 total_token_usage 不会被重算。它们仍不能替代需要 HTTP stream、mock server 或完整 turn loop 的三条集成测试。
本节源码依据(2 处)
compaction 后的 recompute_token_usage 用 replacement history 与 base instructions 重估当前 active context,把结果写进 last_token_usage.total_tokens,并更新 model context window 与 auto-compact prefill。它没有重算累计消耗;原有 total_token_usage 保留不变。
本节源码依据(1 处)
同一个入口,四条实现路径
手动 CompactTask 与自动 compaction 使用相同的分流原则:
| 路径 | 启用条件 | 请求或动作 | replacement 形状 |
|---|---|---|---|
| TokenBudget | Feature::TokenBudget | 新开 token-budget context window | 由实验性 window runtime 管理 |
| remote v2 | provider 支持 remote 且 v2 feature 开启 | prompt-ready history + CompactionTrigger | retained input + opaque Compaction |
| remote v1 | provider 支持 remote、v2 关闭 | compact conversation endpoint | 服务端返回 history,再经本地 filter |
| local | 其他 provider | summary prompt 走普通 Responses sampling | 最近真实 user message + user-role summary |
TokenBudget 在固定 tag 是 UnderDevelopment、默认关闭;RemoteCompactionV2 是 Stable、默认开启。但 feature 开启不等于所有 provider 都走 remote:provider gate 只承认 OpenAI 和识别出的 Azure Responses provider。
本节源码依据(4 处)
Local:保留真实 user message,再追加明文 handoff
local compaction 先在 history clone 末尾临时加入 summary prompt,然后走一次模型 sampling。若 compact 请求本身超出 context window,它从最旧 item 开始删除后重试;只剩一个 input 仍超窗时才失败。成功后,系统从最后一条 assistant message 取出 summary_suffix,再新建一条带 SUMMARY_PREFIX 的 user-role summary;原来的 assistant message 不会被改写。
collect_user_messages 只收集能解析成真实 UserMessage 的内容,排除已有 compaction summary 和 legacy warning。build_compacted_history 从最近消息往前选择,预算固定为 20,000 个近似 token;超长边界消息会被截断,最后追加一条 user-role summary。mid-turn 还会把 fresh initial context 放到最后一条真实 user message 或 summary 之前。
本节源码依据(3 处)
pre-turn 检查发生在新 context diff、skill/plugin injection 和本轮 user input 写入之前。源码保留了 TODO:当前没有估算 pending items 会不会把本轮推过线。mid-turn 则在 sampling 后检查,只有模型或 pending input 仍要求 follow-up,并且 token limit 已到或 runtime 请求新 window,才在下一次 sampling 前 compact。
本节源码依据(3 处)
交给下一章:checkpoint 之后,谁的身份还在
第 25 章已经把 replacement 的 owner/write-order 边界交给这里。第 27 章:Resume 与 Fork继续问另一个经常被合并的问题:同一个 thread 的 cold resume、带 history 的 resume 和 fork,分别保留哪一个 id、哪一段 checkpoint/suffix,又哪些 live resource 根本不会被复制。Memory 只消费 durable rollout;它不应从仍在 ContextManager 里的 replacement 猜 lineage。