M9:為什麼 proxy 要自己解析 ccr_retrieve?

headroom 的設計是這樣的:對話內容太長時,proxy 把舊訊息壓縮並存入 CCR store, 然後注入一個 ccr_retrieve 工具讓模型能主動召回。 這樣模型在需要時可以自己要求取回,而不是等 proxy 猜測何時該展開。

問題是:模型回傳「呼叫 ccr_retrieve」這個 tool_use 之後,誰來執行? 最直覺的答案是 client,但那意味著 client 要知道 CCR store 的存在、 知道怎麼查、知道把結果送回去。設計上 client 應該全程無感。 所以 proxy 得自己做:攔下 tool_use、查 store、組 follow-up 訊息、重問上游, 把真答案送回 client,client 看到的只是一個完整的回應。

server-side resolve loop 怎麼收

M9 的實作分兩部分。第一部分是 POST /ccr/retrieve 側信道端點: 與 /v1/messages 的 forward 路由共用同一個 Mutex<CcrStore>, 直接查 key,命中回 200 附上內容,查無回 404。 用顯式路由優先,不會誤觸上游的 forward fallback。

第二部分是回程的 resolve loop:gate 條件是「POST /v1/messages 且回應是 JSON(非 SSE)」, 只有在這個條件成立時才掃 tool_use。 extract_ccr_calls 的邏輯更嚴格:只有全部的 tool_use 都是 ccr_retrieve, 才接管處理;只要其中有 client 自己的工具,就整包放行、不動任何一個。 這個設計確保不會吃掉 client 原本期待自己處理的 tool_use。

確認攔截後:把原始 assistant turn 附上去、加一筆 user tool_result 給出 CCR 內容、 強制 stream: false 重呼上游,迴圈直到回應裡沒有 ccr_retrieve。 上限 MAX_RESOLVE_HOPS=8 防無限迴圈。普通 JSON 回應不重序列化,逐字節穿透。

這塊在學習版是全新設計——精讀解答本才發現,解答本的 Rust proxy 也沒在 proxy 層做 retrieve, store 只有 put/get,「honoring retrieval 在上游 Python runtime layer」。 M9 的 server-side resolve 是學習版自創,不是照抄。commit 和 memory 都標注了這一點, 避免日後誤以為這是原版設計。

精讀解答本:「SSE 不攔截」不是 gap,是哲學

M9 做完,下一個問題是 SSE 串流。工業版解答本的 proxy 在串流模式下不做 retrieve, 原本以為是遺漏、是下一個需要填的 gap。

精讀 sse/framing.rs(SseFramer)和 sse/anthropic.rs(AnthropicStreamState)之後, 答案變了。工業版的串流哲學是:bytes 原樣送給 client(byte-passthrough 是神聖不可侵犯的), 每個 chunk 另外 try_send 進一個有界 mpsc(深度 256),丟給獨立 spawned task 跑狀態機, 純粹做 telemetry 收集(usage / stop_reason / cache 指標)。 「never block on parser readiness、滿了就丟掉、不影響 byte path」。

SSE 串流裡之所以不攔截 ccr_retrieve,理由很清楚: bytes 一旦送出去就收不回來了,要攔就必須 buffer 整個串流才能改寫, 那就不是串流了。工業版刻意不在串流路徑上做任何攔截,是架構抉擇,不是缺陷。 先讀才動手,省下了去做 buffer 攔截的那條死路。

M10:觀察式探針,不碰 byte path

精讀之後問攻法:要繞過 byte-passthrough 的限制硬做攔截,還是忠於解答本做旁觀? 選了觀察式——用 sse::SseCcrProbe 被動記錄串流裡出現的 ccr_retrieve, 一個 byte 都不動。

實作上,SseCcrProbe 復用 SseByteSplitter 切事件邊界, 按 index 追蹤 tool_use,input_json_delta 累積 partial JSON, 等到 content_block_stop 才解析出完整 key,認出 ccr_retrieve 就回報。 parse_event 把帶邊界的原始 chunk 剝成 (event_name, data), 多行 data 依 SSE 規範用 \n 串接——避免 Python 版本「跨 chunk 切到 emoji」的 1,946 次 parse fail 舊問題。

proxy 的 rechunk_sse 每個 chunk 同時餵進 probe 和往 client 送, 偵測到 ccr_retrieve 就記在 stderr 觀測線。 學習版是單 consumer 內聯(省掉 task/mpsc), 與工業版 spawned task + 有界 mpsc 的差異,誠實寫進 commit 和 README,不假裝等價。

這個 session 的方法論

精讀解答本能把「gap」重新定義成「抉擇」:原以為 SSE 不攔截是缺陷,讀完才知道工業版刻意如此。先讀才動手,省下做錯方向的成本。

無解答可抄時要明說:M9 的 server-side resolve 是學習版自創,解答本不在 proxy 層做這件事。commit 和 memory 標清楚,避免日後誤以為照抄。

誠實記差異:M10 單通道 tee vs 工業版 spawned task + 有界 mpsc,功能相近但架構不同。差異寫進 commit,不試圖掩蓋。

TDD 全程不例外:M9 新增 6 個測試(端點命中/404、resolve 閉環、foreign tool 放行、plain JSON byte-faithful、hop 上限);M10 新增 5 個測試(整餵/碎 chunk 跨邊界、ccr key 萃取、foreign tool、純文字、串流 byte-faithful)。全套 Rust 52 + Python 37,零 warning,parity 5 個 fixtures byte-for-byte 通過。

來源:個人開發日誌 2026-06-18 · headroom 學習分支 realign-rewrite-learn(fork、SSH)· M9 fd2e135+f0ada1f、M10 d5c7220+8088954 · Rust 52 / Python 37 / parity 5 fixtures