Compaction 雙路徑與 replaceGeneration
上下文快滿了就主動收拾,真撐爆了就先收拾再重試。重試前先對一遍世代號,收拾沒起效就不許重試。
replaceGeneration 這個只增不減的世代號,插件自己的返回值為什麼不算數。
把上下文窗口想成一隻行李箱:每條消息是一件衣物,虛綫是八分滿警戒綫。左下角的收拾次數就是 replaceGeneration(世代號),只增不減。三個情景對應壓縮的三種命運,每一步都有字幕解説。
packages/compaction/compaction-basic/src/index.ts 第 147 至 223 行。它解決什麼問題
假設只做主動閾值這一條路:每次請求前量一下,超過八成就收拾。聽起來夠了,實際不夠。token 數是估出來的,估算和 provider 的真實計數總有出入。一條超大的工具結果突然塞進來,測量還沒到閾值,請求已經超限,provider 直接拒絕。這時候沒有任何補救邏輯接手,turn 就地報錯終止,用戶看到的是一次莫名其妙的失敗。
一次算錯就直接撞牆,沒有第二道防綫。這就是單觸發器的問題。
思路是什麼
DSH 把這件事拆成兩個獨立的觸發器。快滿了主動收,撞牆了被動救,兩條路掛不同的事件、用不同的條件、有不同的失敗語義。
pressure 路徑掛在每個 Step(一次模型請求)開始之前。先量總 token,超過容量的 0.8 就動手收拾,收拾時給最近的對話留 16% 的原文尾巴。它的失敗語義很鬆:收拾中途出了錯,日誌裏記一句就繼續走,提前收拾失敗了天塌不下來。
context-overflow 路徑掛在請求報錯之後,只認適配器規範化過的錯誤碼 CONTEXT_WINDOW_EXCEEDED,其他錯誤一律放行。它不看閾值,保留預算直接清零,強制做一次真實的縮減。它的失敗語義很嚴:必須給出決定,要麼重試,要麼保留原始錯誤上報。重試上限預設 1 次,每收到一條成功的模型回覆就清零計數,正常幹活的會話不會被卡住。
出處:pressure 監聽器在 packages/compaction/compaction-basic/src/index.ts 第 147 至 165 行,overflow 監聽器在第 179 至 223 行;0.8 與 0.16 兩個預設值在同包 config.ts 第 20 與 23 行,重試上限預設 1 次在第 93 行,都可按 provider 加 model 的組合逐一覆蓋。
為什麼長期成立
把預防和兜底分開,是可靠性工程的通則。備份和恢復是兩套系統,限流和熔斷是兩道閘門,道理相同:預防路徑追求便宜、常跑、失敗無所謂;兜底路徑追求可靠、少跑、失敗必須有交代。這兩種訴求塞進同一段邏輯裏必然互相遷就。所以哪怕換個語言重寫整個 harness,只要模型有上下文上限、token 靠估算,這兩個觸發器就都得在。
它解決什麼問題
撞牆之後收拾了一次,接下來要重發請求。問題係:點樣確認收拾真係起效?compaction 是一個開放接縫,第三方可以接自定義後端。假設某個後端每次都報告成功,但從來沒真正改過模型可見的內容:如果只看返回值就重試,請求原樣超限,再報錯,再壓縮,再重試,每一圈都是白花的 API 錢,死循環燒到天亮。
思路是什麼
DSH 的答案是一個世代號。先説背景:surface 是會話日誌裏模型可見事件的即時投影,可以理解成模型眼裏的那份對話。replaceGeneration 是它身上的一個只讀計數器,記的是這份對話被替換過幾次。整個程式碼庫只有一處會讓它加一:一段舊消息真的被摘要替換、真的落盤的那一刻。沒有任何 API 能把它改小或重置。所以世代號前進了,在數學上等價於至少發生過一次真實的、已落盤的替換。
溢出恢復的用法就三步:動手收拾之前先拍快照記下當前值;收拾;收拾完拿新值和快照比。嚴格變大才允許重試,否則放行,原始錯誤原樣上報。插件説什麼不重要,帳本上的數字變沒變才重要。
還有一個反方向的細節。就算收拾中途拋了異常,只要前面的免費剪枝已經落盤、世代號已經前進,這份進展照樣夠格授權重試。憑證據放行,憑證據拒絕,兩邊用的是同一條標準。
出處:快照與比對在 packages/compaction/compaction-basic/src/index.ts 第 191 行與第 218 至 222 行,異常後憑已落盤進展重試在第 195 至 208 行;replaceGeneration 的定義在 packages/core/session/src/surface.ts 第 136 至 142 行,全庫唯一的加一處在第 361 至 371 行。項目的 Agent Note(.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md)明確否決過只看返回值的寫法,理由是自定義後端可能報告成功卻沒有改變模型可見狀態。
為什麼長期成立
用單調遞增的版本號證明狀態確實變了,這個套路數據庫的樂觀鎖用了幾十年,Git 的 commit 鏈、分佈式系統裏的 epoch 也都是它的變體。它的好處是把信任問題變成算術問題:執行者可以撒謊,帳本不會。只要系統裏存在不受信任的擴展點,重試之前核對一個改不了的計數器,永遠是最便宜的防綫。
主動閾值路徑和 DSH 的 pressure 同構:預設 85% 觸發,另有預設關閉的 two-pass 預摘要,細節見站內 Compaction:85% 閾值與可選 two-pass。請求報錯後的被動恢復加世代號對賬這條路,在已核對的 Grok Build 材料裏沒有見到等價機制。這一條基於已公開證據,保留未知項。
主動方向做得最厚,每次調 API 前要過裁剪、微壓縮、摺疊、全量摘要四道工序。防燒錢的答案是計數熔斷:自動壓縮連續失敗 3 次就停手。這個 3 來自真實事故,源碼註釋記載曾有 1279 個 session 連續失敗 50 次以上,全球每天浪費約 25 萬次 API 調用。出處:claude-code-sourcemap-main/study/chapters/03-context-management.md 第 78 至 81、121 至 124 行。
對比焦點就一個:壓縮失敗會不會循環燒錢。Claude Code 數失敗次數,數到 3 就熔斷,止損綫是拿事故數據校準出來的,屬於計數器止損:先允許問題發生幾次,再靠上限兜住。DSH 不數次數,要求每次重試都出示世代號前進的證據,一次無效重試都不放行,屬於結構性證明:讓無效重試從機制上發不出去。前者的 3 需要事故餵出來,後者的 0 是推導出來的。再加上雙路徑正交這一點,這兩處是 DSH 在三家對比裏獨有的設計。
手推一個謊報成功的後端
設重試上限為 1,裝一個自定義壓縮後端:每次都報告收拾成功,但從不真正替換模型可見的內容。現在第一次溢出報錯發生了。
問題一:DSH 會唔會發起第二次壓縮嘗試?提示:對賬失敗後原始錯誤直接上報,turn 就結束了,重試計數根本沒機會增加。
問題二:如果把判定標準改成只看後端的返回值,同一場景下每圈耗時 5 秒,第一分鐘會發出幾多次註定失敗嘅請求?Claude Code 嘅 3 次熔斷又會喺第幾次請求之後止損?
replaceGeneration 的單調前進,插件的返回值不算數。要證據,別信口頭彙報。