Theme / v13.28.0

Claude-Mem

Claude Code 持久化記憶系統

基礎觀念

v13.24.2 → v13.24.23:二十個 Patch 與「殺迴圈」的成因

理解 claude-mem 為何在一天內連發近二十個 patch、bundle 版本戳記錯誤如何造成 worker 殺迴圈,以及長壽背景服務的「所有權」為何必須可驗證。

版本重點

claude-mem 從 v13.24.1 到 v13.24.23 是一段密集的修補列車——絕大多數版本在 2026-09-11 當天連續發布。

先說明一件事實:GitHub Release 涵蓋 v13.24.5 ~ v13.24.23,v13.24.2~v13.24.4 沒有建立對應的 Release(僅 npm 發佈)。

這一段沒有任何新的核心 memory API。它是一個穩定性與正確性區間,而且幾乎所有修正都繞著同一個主題打轉:

長壽背景服務的「身分」與「所有權」必須可驗證。

前一課談過 v13.24.1 修掉的 bundle 重建問題;這一課要說明那個問題為什麼會變成殺迴圈,以及後續二十個版本如何把同一類缺口補完。

核心案例:bundle 戳記錯誤造成的殺迴圈

這是這整段最重要的一個 bug。

發生什麼

v13.24.5 發佈時,committed plugin bundle 被錯誤地蓋成 13.24.1 的戳記。

於是執行時出現這個狀態:

manifest / package.json 說:13.24.5
實際 bundle 內容戳記說:  13.24.1

worker 偵測到 version mismatch → 認為自己過舊 → SIGKILL → respawn → 新的 worker 還是同一個錯戳記的 bundle → 再次 mismatch。

這就是殺迴圈 (kill loop):不斷重複回收風暴,而且每次重啟都得到同樣結果。

修法

v13.24.6 做了兩件事:

  1. worker-service 驗證自己的 bundle 版本,在 mismatch 時乾淨結束(而不是被殺掉後無限重生)。
  2. 以正確的 13.24.6 戳記重建所有 bundle。

第一點才是關鍵。「乾淨結束」與「被 SIGKILL」在管理員眼中是兩件完全不同的事——前者是可觀察的失敗,後者會被誤判為當機而觸發重啟。

為什麼這值得學

這是一個自我指涉驗證的經典案例。系統需要能回答:

我(這個元件)是否真的是我被宣稱的那個版本?

如果元件無法檢查自身一致性,那麼唯一的檢測者是外部協調者,而外部協調者能做的只有「殺掉再試一次」——也就是這個迴圈。

同一個家族的問題在後續版本反覆出現:

版本 無法驗證的東西 後果
v13.24.5/6 bundle 版本戳記 殺迴圈
v13.24.7 Chroma writer owner(#3919)、無人擁有的 chroma-mcp 樹(#3905) 鎖無法回收 / 殭屍行程樹
v13.24.8 過期 origin_device_id(#3973) sync outbox 永久阻塞
v13.24.17 memory_session_id 重複註冊(#4027) sync outbox 放大
v13.24.15 daemon 繼承的舊 cwd 寫入錯誤位置

主軸一:Chroma / 向量儲存的擁有權

Chroma 是 claude-mem 的向量儲存層,而它的行程與鎖生命週期是這段的第二大戰場。

修正 內容
writer owner 重用 同一行程內重用 Chroma writer owner(#3919)
無法讀取的 lock 寬限期後回收(#3916)
backfill 失敗 連續批次失敗後停止,不無限重試
chroma-mcp 樹 開機時回收沒有 worker 擁有的行程樹(#3905)
dead transport handshake 失敗歸類為 ChromaUnavailableError(#3631)
JSON 解析崩潰 不再中止整個 sync(#4040)
搜尋命中順序 依相關度而非日期 hydrate(#3881)

「開機時回收沒有 worker 擁有的行程樹」與「寬限期後回收無法讀取的 lock」這兩個修正,本質上都在處理同一件事:孤兒資源。

孤兒資源的判定需要「所有權」這個概念。沒有它,你就只能選擇「永遠不回收」(洩漏)或「一律回收」(可能殺掉活著的東西)。兩者都錯。

主軸二:Worker 與 Supervisor 的併發

  • parked slot-waiter 跟隨實際生效的併發上限(#3909)。注意「實際生效」這四個字——設定檔寫的不等於執行時跑的。
  • provider 切換時重啟 parked generator(#3909)。
  • health probe 遵循呼叫端剩餘的 deadline(#4026 / #3575)。給每個探測器一個固定逾時,在總預算有限時會累積超支。
  • grammar 只建置一次,而非每次查詢重新編譯(#3926)。
  • field optimizer 遵循 deadline(#3939)。

daemon 的 cwd 問題(#3727 / #3706)

daemon 是長壽行程,保留著它啟動時的 cwd。修正讓 daemon 不再繼承使用者的專案 cwd,worker cwd 固定為 DATA_DIR。

這修的是 Windows 上的資料夾鎖定問題。它與前一課 mempalace 的 mine --daemon 錯誤專案問題是完全相同的形狀:

相對路徑的意義取決於誰在解讀它。長壽行程的 cwd 是一個陷阱。

主軸三:Sync / Outbox 的邊界

雲端同步的 outbox 是一種「必須有界」的結構,否則單一壞項目就能卡住整條管線。

修正 內容
過期 device id 被伺服器以 400 invalid_ops 拒絕的 ops 隔離,不再阻塞整個 outbox(#3973)
未設定雲端同步 限制 sync_outbox 成長(#4031)
memory_session_id 只註冊一次並保留第一個 id(#4027)
NULL 寫入 generator 啟動時不再寫 NULL memory_session_id(#3629)
投影修復 排程的 projection repair 工作改為有界(#4046)

**「隔離壞項目」與「重試」是不同的策略。**對於伺服器明確判定為無效(400)的項目,重試永遠不會成功,只會佔用管線。隔離才是正確處理。

主軸四:Hooks 與 Session 正確性

  • claude --resume 也會觸發 SessionStart hook(#3969)——恢復的 session 一樣能拿到記憶脈絡。
  • 空字串 cwd 視為缺值(#3977),而非解析到錯誤專案。這與 mempalace 的 known_entities 是同一個形狀:空值不該被當成有效值使用。
  • 每 session 去重複 file-context 注入(#3486)。
  • 不在 Claude Code hooks 上重建 login-shell PATH(#3453)。
  • manifest 格式錯誤的 hook stdin 改為 fail open(#4006)——hook 壞掉不該讓整個 session 卡住。
  • 略過 plugin cache session(#4042);project 名稱錨定到 Claude project 目錄(#4055)。

主軸五:Observer 的行為邊界

Observer 是「旁路記錄」元件,它的正確性標準與一般功能不同——它不該有副作用。

修正 內容
prose-only observation 保留而非丟棄(#3363)
無文字的 assistant frame 批次確認前略過(#3501)
init 回覆 不儲存從中解析出的 observation(#3877)
transport 失敗 重新排入佇列(#3998)
反應式 quota 錯誤 暫停 session(#3999)
圖片 payload 從 observation prompt 移除(#3996)
session block 配合 hook 輸出上限(#3995)

「同步失敗要重新排隊」與「配額錯誤要暫停」是兩個必須區分的失敗類型:前者是暫時性、後者需要停止消耗。把它們當成同一種處理,就會一直對已知耗盡的配額發送註定失敗的請求。

主軸六:安全與憑證

  • shell-quote 升級至 1.9.0,修補 CVE-2026-13311(#4018)。
  • never send an empty messages array to the OpenRouter API(#3493)。
  • macOS keychain account 正規化以符合 Claude Code(#4045 / #4037)——keychain 項目名稱不一致會讓憑證「存在但找不到」。
  • 支援 CLAUDE_CONFIG_DIR 於 keychain 項目與 SDK 子行程環境(#3908)。
  • Windows CredRead shim 可編譯(#3726)——OAuth token 查詢在 Windows 上才真正可用。
  • 改為發佈 runtime-only 的 package.json,不再把開發用的 tree-sitter grammars 出貨到 marketplace(#3640)。

主軸七:模型與 Provider

  • OpenRouter 改以精確主機名辨識,而非 URL 子字串比對(#3979)。子字串比對會讓相似網域被誤導向——這是一類很容易忽略的錯誤,因為它在正常情況下完全看不出來。
  • 模型清單對應到原生 models[] fallback 陣列(#3971)。
  • GPT-5 的 400 錯誤重試 max_completion_tokens(#4003)。
  • 接受空的 OpenRouter reasoning 回應(#4017)。
  • 可設定的 LLM 每次嘗試 deadline(#4000)。

升級建議

直接升到 v13.24.23。

特別提醒:若你停留在 v13.24.5,會遇到 bundle 戳記錯誤造成的殺迴圈,務必升級。

按使用情境對應的收益:

你的情境 相關修正
使用雲端同步 outbox 隔離與成長上限(#3973、#4031)
使用 Codex 或 Windsurf hook trust、BOM 容忍(#4010、#3773)
使用 claude --resume SessionStart hook 觸發(#3969)
Windows daemon cwd、CredRead、Volta shim(#3727、#3726、#4008)
macOS keychain 正規化(#4045)
使用 OpenRouter 精確主機名辨識、模型 fallback(#3979、#3971)

學習重點

長壽背景服務的「身分」與「所有權」必須可驗證。

把這段 20 個版本的修正攤開,會看到它們處理的幾乎都是同一種缺口:

某個東西宣稱自己是 X
  → 系統無法驗證
  → 只能選擇相信或殺掉重試
  → 兩者都會出問題

具體的清單:

宣稱 驗證方式 沒有它會怎樣
「我是 13.24.5」 bundle 戳記自檢 殺迴圈
「這把鎖是我的」 writer owner 身分 鎖無法回收
「這個行程樹是我的」 worker 擁有權 殭屍行程堆積
「這筆 op 仍有效」 伺服器回應(400 → 隔離) outbox 永久阻塞
「這是我啟動時的 cwd」 絕對路徑 寫入錯誤位置

第二個可帶走的原則:「乾淨結束」與「被殺掉」是兩件不同的事。前者是可觀察、可診斷的失敗;後者會被誤判為當機並觸發重啟。讓元件能檢查自身一致性並在不符時主動結束,比讓外部協調者不斷重試要可靠得多。

來源:v13.24.5、v13.24.6、v13.24.7、v13.24.8、v13.24.23