OpenAI Codex · 上下文

滿窗之後,砍哪一段留哪一段

三個時機共用一個分發器,三種實現由開關選中。同一份摘要在中途必須停在歷史最後一項。

課程目標讀完能説清三件事。第一,上下文滿了,Codex 先問時機再問實現,九種組合共用兩套入口。第二,服務端回來的 transcript 預設不可信,進門先丟掉指令包裝和工具回放。第三,同一份摘要在採樣前和中途要擺到不同位置,空窗開關打開則回憶不帶走。
先玩一遍 · 同一段歷史,三條管道各砍各的
窗口到頂:切換時機和實現,看各自砍掉哪一段、保住哪一段
時機
採樣前和手動壓完,下一輪才整包重注。中途壓完還要繼續採樣,摘要必須停在隊尾。
實現
預設 OpenAI 會話走遠端。提供方不支持就走本地再採樣。空窗跳過摘要,回憶不帶走。
中途 × 遠端 0砍掉 0保住 3版本
保住 砍掉 新注入的環境
濾網 摘要在最後 下一輪重注 空窗
等待開始。點播放,或先換一格再走一遍。
九宮格 · 點任一格,看這一格獨有的砍法
採樣前 中途 手動 遠端 本地 空窗
邏輯軌跡 · 動畫每一步對應源碼裏的哪一段
  1. 看 token_limit_reached,或用戶提交 Compactcontext_window.rs L77
  2. 採樣前走 run_pre_sampling_compactturn.rs L1012
  3. 中途走 should_roll_over,還要有 follow-upturn.rs L458
  4. 手動拉起 CompactTask,打斷當前 turntasks/compact.rs L19
  5. TokenBudget 開則換空窗,遠端和本地都被跳過turn.rs L1189
  6. 否則按遠端 v2 或本地分發turn.rs L1201
  7. 遠端結果過 should_keep 濾網compact_remote.rs L370
  8. 按 InitialContextInjection 決定插不插compact.rs L68
  9. replace_annotated 才把 history_version 加一history.rs L298
點播放,看同一段混雜歷史在當前這一格裏怎麼被重排。
遠端服務端回來的包裝不可信。developer、工具回放先丟掉,再按時機決定環境插不插。中途必須把摘要留在最後。
本地先追加合成提示和助手輸出,版本號還不變。成功之後整表換成截斷後的用戶原話加摘要前綴。
空窗跳過模型和服務端。新窗口裏只剩當前環境,沒有摘要,回憶不帶走。
教學示意:色塊數量與角色為課程化設定,用來展示三條管道各自砍什麼、留什麼。邏輯軌跡右側行號對應 openai/codex 倉庫 commit 4f39251a01。
思路一 · 時機和實現拆開
它解決什麼問題

你讓 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 行

用量到頂或 /compact 選相位 同一套分發器 空窗 TokenBudget 第一句 if,跳過摘要 遠端 v2 或舊接口 回來要過濾網 本地再採樣 提供方 Unsupported replace_compacted_history 三條管道的安裝點相同,差異在安裝之前的產物
教學化結構圖:先選相位,再進同一套分發器,最後都在同一個安裝點換窗。
為什麼長期成立

什麼時候該換窗,和窗裏裝什麼,是兩件獨立的事。換語言重寫,仍要先選相位再選管道。提供方只有一個時,也可以先把判定和安裝拆開。

思路二 · 遠端先過濾,摘要位置看相位
它解決什麼問題

服務端帶回的 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 行

採樣前 / 手動 · DoNotInject 用戶原話 摘要 環境此刻不進替換表 下一輪普通 turn 整包重注 中途 · BeforeLastUserMessage 當前環境 最後一條用戶 摘要必須在最後 同一輪接着採樣,訓練約束把摘要釘在隊尾
教學化對照:同一份摘要,兩種相位,兩種安置。

三種實現最後都進 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 回答同一問題,遠端要麼整窗安裝,要麼完全不裝。

兩側均已核對源碼 · 2026-08-22 · DSH · Compaction

Claude Code:遞歸靠標籤攔住,失敗三次就停手

預設自動路徑仍是再叫一次模型寫摘要。自動判定先擋遞歸:session_memorycompact 這兩種 querySource 直接返回 false,註釋寫明它們是 fork 出來的 agent,再觸發會死鎖。連續失敗 3 次之後電路熔斷,註釋記錄過單會話連續失敗上千次的事故。

Codex 的 Compact 任務不進 turn 循環,這種標籤可以不存在。換來的是中途路徑沒有對等的連續失敗計數器。作者把賭注寫在註釋裏:壓縮只要把用量壓到限額以下,就不必擔心死循環,於是計數器被省掉了。

兩側均已核對源碼 · 2026-08-22
課堂練習
01

三張替換表各留下什麼

上面那一段混雜歷史,分別走中途遠端、走空窗、走採樣前遠端。寫出三張替換表:各砍掉哪幾塊、保住哪幾塊、摘要在不在最後、初始上下文此時在不在表裏。

進階一問:遠端第一次失敗時,為什麼普通滿窗路徑不會改走本地,換模型預壓縮卻可以再打一次遠端。

Takeaway:時機和實現拆開,九種組合共用兩套入口。遠端先進濾網,中途摘要必須停在隊尾。空窗是壓縮的一種產物,回憶不帶走。