滿窗之後,砍哪一段留哪一段
三個時機共用一個分發器,三種實現由開關選中。同一份摘要在中途必須停在歷史最後一項。
- 看 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三張替換表各留下什麼
上面那一段混雜歷史,分別走中途遠端、走空窗、走採樣前遠端。寫出三張替換表:各砍掉哪幾塊、保住哪幾塊、摘要在不在最後、初始上下文此時在不在表裏。
進階一問:遠端第一次失敗時,為什麼普通滿窗路徑不會改走本地,換模型預壓縮卻可以再打一次遠端。