满窗之后,砍哪一段留哪一段
三个时机共用一个分发器,三种实现由开关选中。同一份摘要在中途必须停在历史最后一项。
- 看 token_limit_reached,或用户提交 Compactcontext_window.rs L77
- 采样前走 run_pre_sampling_compactturn.rs L1012
- 中途走 should_roll_over,还要有 follow-upturn.rs L458
- 手动拉起 CompactTask,打断当前 turntasks/compact.rs L19
- TokenBudget 开则换空窗,远端和本地都被跳过turn.rs L1189
- 否则按远端 v2 或本地分发turn.rs L1201
- 远端结果过 should_keep 滤网compact_remote.rs L370
- 按 InitialContextInjection 决定插不插compact.rs L68
- replace_annotated 才把 history_version 加一history.rs L298
你让 coding agent 在同一个线程里改了三十轮。前二十轮还记得工作区在哪、哪几个文件不能动。到第三十轮,它突然问你「当前目录是什么」。再过几轮,已经批准过的 npm test 又被问了一遍。
用量顶到窗口边上之后,系统必须压历史。如果把什么时候压、怎么压写进同一段判断,换提供方或加一种不叫模型的空窗,判定入口都要跟着改。中途压完还要继续采样,发消息前压完则下一轮才会重注,这两件事对摘要位置的要求也不一样。
Codex 把时机写成 CompactionPhase:发消息前的 PreTurn、工具跑完还要续跑的 MidTurn、用户手动的 StandaloneTurn。实现写成 CompactionImplementation:本地再采一次样、旧的 /responses/compact、新的 compaction_trigger。分析事件另有 Trigger 和 Reason 两套标签。自动路径共用 run_auto_compact,手动路径共用 CompactTask。九宫格是乘法表,源码里没有九套并列函数。
三个自动时机都先问同一个函数。token_limit_reached 在缓冲后的 compact 限额或满窗任一触发时为真。中途还要多一道闸:后面还要继续,并且模型刚请求了新窗口或 token 已经到顶。只满不续,这一轮自然结束,下一轮用户消息到来时走预采样。
出处:codex-rs/core/src/session/context_window.rs 第 74 至 91 行;codex-rs/core/src/session/turn.rs 第 458 至 483 行
分发器第一句看 TokenBudget。开了就换空窗,远端和本地都被跳过。关了再按提供方能力选:认 compaction_trigger 且特性开,走远端 v2;认 V2 但特性关,走旧远端接口;Unsupported 走本地。TokenBudget 默认关,RemoteCompactionV2 默认开。默认 OpenAI 会话走远端 v2。
出处:codex-rs/core/src/session/turn.rs 第 1178 至 1201 行;codex-rs/features/src/lib.rs 第 1428 至 1433 行、第 1542 至 1547 行
什么时候该换窗,和窗里装什么,是两件独立的事。换语言重写,仍要先选相位再选管道。提供方只有一个时,也可以先把判定和安装拆开。
服务端带回的 transcript 可能夹着过期 developer 指令。不滤就和本地按当前 world state 新渲染的环境叠在一起,效果等价于有人改了旧行。中途压完还要在同一轮继续采样,模型被训练成摘要是历史上最后一项。上下文如果插到摘要后面,训练约束就破了。
滤网是完整的穷尽 match。丢掉 developer、非用户内容的 user 包装、工具调用和压缩触发项。留下真实用户消息、持久化 hook prompt、assistant、压缩项。v2 复用同一函数。
出处:codex-rs/core/src/compact_remote.rs 第 370 至 397 行
然后按 InitialContextInjection 决定插不插。预采样和手动用 DoNotInject:替换历史里没有初始上下文,并清掉 reference_context_item,下一轮普通 turn 会走完整重注。中途用 BeforeLastUserMessage:当前环境和权限插到最后一条真实用户消息之上,摘要仍在队尾。插入函数还有兜底:没有真实用户就插在摘要前,再没有就插在最后一条 compaction 项前。
出处:codex-rs/core/src/compact.rs 第 59 至 74 行
三种实现最后都进 replace_compacted_history。live history 整表替换。history_version 只在这时加一,追加不碰版本号。Guardian 复用 transcript 时会核对 parent_history_version,版本变了就不能复用旧的审查前缀。
出处:codex-rs/core/src/context_manager/history.rs 第 298 至 302 行
远端结果只要不是本地函数吐出来的,进门先丢掉指令包装。插入点挡的是训练约束和重注时机,这两件事本来就分开。版本号只在重写时前进,因为追加每轮都发生。
有时用户只想换一扇干净的窗,不要摘要。空窗如果走另一套生命周期,hook 和 ContextCompaction 条目会看不见这件事。这个开关默认关,避免用户在没意识到时丢掉整段对话。
TokenBudget 跳过模型和服务端摘要,安装一扇新窗口,摘要字段是空字符串。模型在新窗口里看不到旧对话,只看到此刻的环境和权限。这和 new_context 工具那句合同一致:换窗,不摘要。它仍然走压缩生命周期,pre-compact hook 若停下,窗口还没换。
出处:codex-rs/core/src/compact_token_budget.rs 第 21 至 25 行
本地路径自己碰上满窗,不递归调用自动压缩。它删最老的一条再打一次。只剩一条还超,就标满窗并返回错误。远端失败也不会改走本地:普通满窗触发的预压缩和中途压缩都把 fallback 传成 None,第一次远端失败直接返回。超时不在可重试名单里。
出处:codex-rs/core/src/compact_model_fallback.rs 第 8 至 20 行
压缩是一次换窗的生命周期,产物可以是摘要,也可以是空房间。递归要自己挡住:手动任务不进 turn 循环,本地超窗靠修剪,中途路径靠压成功就会低于限额。假设不成立时,循环可以再次进入。
DSH:不叫模型的那一刀可以先落地
DSH 的压缩家族在 packages/compaction/。压力触发时先可选地 prune,再量一次。prune 之后如果已经回到阈值以下,摘要不跑。两条路径都在本地,没有 Codex 那种 compact 客户端。
溢出恢复把 replaceGeneration 当重试许可。prune 先落地、随后摘要抛错,只要 generation 前进了,就允许从新表层再试。generation 只在 replace 计划提交时加一,append 不加。Codex 用 history_version 回答同一问题,远端要么整窗安装,要么完全不装。
Claude Code:递归靠标签拦住,失败三次就停手
默认自动路径仍是再叫一次模型写摘要。自动判定先挡递归:session_memory 和 compact 这两种 querySource 直接返回 false,注释写明它们是 fork 出来的 agent,再触发会死锁。连续失败 3 次之后电路熔断,注释记录过单会话连续失败上千次的事故。
Codex 的 Compact 任务不进 turn 循环,这种标签可以不存在。换来的是中途路径没有对等的连续失败计数器。作者把赌注写在注释里:压缩只要把用量压到限额以下,就不必担心死循环,于是计数器被省掉了。
两侧均已核对源码 · 2026-08-22三张替换表各留下什么
上面那一段混杂历史,分别走中途远端、走空窗、走采样前远端。写出三张替换表:各砍掉哪几块、保住哪几块、摘要在不在最后、初始上下文此时在不在表里。
进阶一问:远端第一次失败时,为什么普通满窗路径不会改走本地,换模型预压缩却可以再打一次远端。