AI 工程

claude-mem 深度拆解:Claude Code 的跨 session 記憶,以及 worktree 合併後記憶怎麼歸位

同一個 repo 開三個 git worktree 平行跑三條線:一條修 bug、一條做 feature、一條重構。每條線各自開 Claude Code session,每個 session 都要重新問一次「這個模組在幹嘛」。三天後分支合回 main,那些踩過的坑、繞過的雷、做過的取捨,全部留在各自的 session 裡,一條都沒回到主線。

這是 coding agent 現在最貴的隱性成本——不是 token 貴,是你重複解釋的時間貴

Alex Newman(GitHub @thedotmack)做的 claude-mem 就是在解這件事。它是一個 Claude Code plugin,把 agent 每次 session 實際做過的事壓成可搜尋的 observation,下次開 session 自動注入回去。而我覺得最值得單獨拿出來講的,是多數介紹文都跳過的那一塊:它處理了 git worktree 合併之後,記憶要怎麼歸位

先講一個必須更正的地方。這篇是從 v13.16.0 這版切進來的,但翻過 release note 之後:v13.16.0(2026-08-25 發布)的主軸其實是新的 claude-mem-cowork plugin,不是 worktree。worktree adoption 是 v12.2.0(2026-04-18) 就上的功能,v13.12.4 才把它的競態與 log bug 修掉。本文講的是「到 v13.16.0 為止,這套東西長什麼樣、怎麼把它用起來」。

以下所有機制都對照 repo 原始碼與官方 docs 查核過,數字附出處。

一、它要解決什麼

Claude Code 每開一個 session 就是一張白紙。你在上個 session 裡花了 40 分鐘搞清楚「這個 worker 為什麼要 fire-and-forget」,session 一關,那 40 分鐘的理解就只剩你腦裡有。CLAUDE.md 能補一部分,但它是你手寫的——你不會把每一次除錯的過程都寫進去。

claude-mem 的做法是把「記錄」這件事自動化:agent 每呼叫一次工具,就把這次操作丟給一個便宜的 observer 模型壓成一段結構化敘述,存進本地資料庫;下次開 session 時,把相關的部分注入 context。

專案數據(2026-08-26 由 GitHub API 查得):Apache-2.0 授權,2025-08-31 建立,91,841 stars / 8,065 forks / 304 個開放 issue,npm 上已經發到第 201 個版本。這個 issue 數與版本頻率本身就是資訊,後面談限制時會回頭講。

二、運作原理:5 個 hook、1 個 worker、2 個索引

claude-mem 的寫入與取回流程:5 個 lifecycle hook 打到本機 worker,由 observer 模型壓成 observation 存進 SQLite 與 Chroma,取回時走 search → timeline → get_observations 三層漸進揭露

2.1 寫入路徑

官方文件把它描述成一套 5 階段 hookSessionStartUserPromptSubmitPostToolUseStopSessionEnd(實際上是 6 個 hook script,多一個做版本檢查的 pre-hook)。

關鍵設計是:hook 從不阻塞你的 session。文件寫得很明白,hook 對 worker 是 fire-and-forget HTTP、2 秒 timeout。IDE 這端只負責把事件丟出去,壓縮工作全部在另一個進程做。

那個進程就是 worker service:一個由 Bun 管理的 Express HTTP server,預設綁 127.0.0.1,埠號是 37700 + (uid % 100)——用 uid 取模,是為了讓同一台機器上不同使用者不會撞埠。

worker 收到事件後交給 observer 模型。預設是 claude-haiku-4-5-20251001,走你的 Claude 訂閱;也可以切成 Gemini 或任何 OpenAI-compatible 端點(CLAUDE_MEM_PROVIDER)。輸出的 observation 是有 schema 的,不是自由文字。以預設的 code mode 來說,型別共九種:bugfixfeaturerefactorchangediscoverydecisionsecurity_alertsecurity_notesensitive(最後一個是 v13.13.0 才加的);另外還有七種概念標籤:how-it-works、why-it-exists、what-changed、problem-solution、gotcha、pattern、trade-off。

落地時寫兩個索引:SQLite + FTS5 負責關鍵字全文檢索,Chroma 向量庫 負責語意檢索。兩邊是同一份資料的兩種索引,不是兩份資料。

2.2 取回路徑:3 層漸進揭露

這是 claude-mem 設計上最值得抄的一段,也是它跟「把記憶一股腦塞進 system prompt」的方案差最多的地方。

mem-search skill 規定了一個固定順序,而且明文寫著「絕對不要在沒過濾前就抓完整內容」:

  1. search(query, limit, project) — 回一張只有 ID/時間/型別/標題的索引表,README 標的成本是 每筆約 50–100 tokens
  2. timeline(anchor, depth_before, depth_after) — 以某筆為中心,把前後發生的事按時序拉出來。
  3. get_observations(ids=[...]) — 只對你篩過的 ID 展開完整內容(title、subtitle、narrative、facts、concepts、files),每筆約 500–1,000 tokens

README 宣稱這樣「約省 10 倍 token」。這個數字要當成設計意圖看,不是實測基準——它其實就是 50–100 對 500–1,000 的比值直接推出來的,官方沒有給對照實驗。真正該記住的是那個原則:先看目錄,再決定翻哪一頁。

search 可用的參數是明確的:querylimit(預設 20、上限 100)、projecttype(observations / sessions / prompts)、obs_typedateStartdateEndoffsetorderBy(date_desc / date_asc / relevance)。你要它「找上週那個 auth 的 bug」時,直接在 prompt 裡把條件講清楚,它就會帶對參數:

用 mem-search 找:這個專案上週的 auth 相關 bugfix,先只給我索引表,我挑完你再展開。

2.3 順手一提:File Read Gate

同一套哲學還延伸出一個 PreToolUse hook。當 agent 要 Read 一個檔案、而這個檔案在資料庫裡有歷史 observation 時,這個 gate 會擋下讀取,改回一張「這個檔案過去被動過哪些手腳」的時間線,讓 agent 自己決定最便宜的路徑。

文件把判斷條件寫得很細:檔案小於 1,500 bytes 直接放行(時間線比檔案本身還貴)、專案被排除則放行、查不到 observation 則放行;有結果時每個 session 去重、依相關度排序、最多 15 筆。回給 agent 的訊息會列出四種選項與各自成本:語意喚醒(0 額外 token)、get_observations([IDs])(每筆約 300)、smart_outline / smart_unfold(約 1–2k)、真的整份讀。

這個設計本身就是一堂 context engineering 的課:不要幫 agent 決定,要把成本標好讓它自己選。

三、worktree:平行開發把記憶切碎了

worktree 記憶合併機制:寫入時用 parent/worktree 複合鍵,合併判定交給 git,合併後只加一個 merged_into_project 指標而不搬資料;squash merge 需手動 adopt --branch

現在講重點。用 git worktree 平行開多條線的人會遇到一個很具體的問題:記憶該掛在誰名下?

掛在 worktree 目錄名下,主 repo 就看不到;全部掛在主 repo 名下,三條線的記憶就會互相污染——你在修 bug 的 session 裡會被塞進重構分支的 context。

claude-mem 的解法分成兩段。

3.1 寫入時:複合 project key

src/utils/worktree.tsdetectWorktree() 判斷方式很樸素但可靠:去 stat <cwd>/.git。如果它是目錄,那是一般 repo;如果它是檔案,讀出裡面的 gitdir: ... 指標,比對它是不是長成 <parent>/.git/worktrees/<name> 的形狀。是的話,就取得 parentRepoPathworktreeName

接著 src/utils/project-name.tsgetProjectContext() 把它組成複合鍵:

primary:     myapp/feat-auth      // 這個 worktree 自己的身分
parent:      myapp
isWorktree:  true
allProjects: [myapp, myapp/feat-auth]

兩個效果一次到位:寫入myapp/feat-auth,所以不同 worktree 的記憶不會互串;讀取allProjects 兩個名字,所以在 worktree 裡開的 session 讀得到主 repo 的既有記憶。

這裡有兩個修過的坑值得知道,因為它們決定你能不能從子目錄啟動 session:getProjectName() 會先用 git rev-parse --show-toplevel 找 repo 根(#2663),getProjectContext() 也會先解析出 working-tree 根再去 detectWorktree(#3262)——因為 .git 只存在於 worktree 根目錄。所以在 src/services/ 底下開 session,複合鍵依然正確。

3.2 合併後:merged_into_project 是「指標」不是「搬家」

分支併回 main 之後呢?repo 裡的 .plan/worktree-adoption.md 把目標寫得很清楚:

當一個 worktree 的分支被併回它的 parent,這個 worktree 的 observation 要成為 parent 專案 observation 清單的一部分——不搬移資料、不做破壞性 schema 變更、不遺失來源證明(provenance)

實作是這樣:

  1. schema:在 observationssession_summaries 各加一個可為 null 的欄位 merged_into_project,並各建一個索引。用 PRAGMA table_info 做冪等守衛(SQLite 不支援 ALTER TABLE ... ADD COLUMN IF NOT EXISTS)。
  2. 查詢:既有的查詢條件加一段 OR merged_into_project = :parent
  3. 原則observations.project不可變的來源證明,永不覆寫。合併狀態只是一個虛擬指標。
  4. Chroma:向量庫的 metadata 跟 SQLite 同步更新(同一個共用 collection cm__claude-mem,用 metadata 分區),確保語意搜尋看到的世界跟關鍵字搜尋一致。SQLite 是真相來源;Chroma 更新失敗只記 log、不回滾 SQL,下次重跑會補上(同值寫入是 no-op)。

這個設計的好處很實際:合併之後你在主 repo 問「JWT 那段當初為什麼這樣寫」,答得出來;同時 UI 上那筆記憶還帶著「來自 feat-auth,已併入」的徽章,你知道它的出處。

3.3 判定交給 git,而這正是坑所在

偵測邏輯在 src/services/infrastructure/WorktreeAdoption.ts,全部走 git subprocess(plan 裡明文禁止引入 gh 依賴):

  1. git rev-parse --path-format=absolute --git-common-dir → 解析出主 repo 路徑。
  2. git worktree list --porcelain → 列出所有 worktree 與各自的分支。
  3. git branch --merged HEAD → 取得已合併分支集合,跟上一步取交集。
  4. 對交集裡的每個 worktree,UPDATE ... SET merged_into_project = :parent WHERE project = :composite AND merged_into_project IS NULL

這裡就是那個坑git branch --merged 只認得得出 ancestry 的合併。GitHub 上絕大多數團隊按的是 Squash and merge,squash 之後原分支的 commit 不是 HEAD 的祖先,--merged 完全看不到它。rebase merge 同理。

plan 文件自己承認了這點,解法是留一個 CLI 逃生門:

# 先看會動到什麼,不寫入
npx claude-mem adopt --dry-run

# squash merge 的分支,手動指定
npx claude-mem adopt --branch feat-auth

照做建議:如果你的團隊用 squash merge,就把 npx claude-mem adopt --branch <name> 加進你刪 worktree 的那個腳本裡。順序是「先 adopt、再 git worktree remove」——目錄還在的時候路徑才解析得出來。

另外,worker 啟動時會自動跑一次 adoption,而且是 fire-and-forget、不會 await(#2122),所以它失敗不會擋住 worker 起來。v13.12.4 把這個 kick 移到 DB 初始化之後,因為它自己開的寫入連線會跟開機時的 migration writer 搶 WAL 單寫者,噴 database is locked

四、上手:從安裝到第一次 adopt

環境需求:Node.js ≥ 20(npm package 的 engines 寫的是 >=20.12.0)、Bun、uv(給 chroma-mcp 用的 Python 套件管理器,預設 Python 3.13)、SQLite 3。Bun 與 uv 缺的話安裝器會自己裝。

1) 安裝(二選一)

npx claude-mem install

或在 Claude Code 裡:

/plugin marketplace add thedotmack/claude-mem
/plugin install claude-mem

2) 檢查它真的活著

npx claude-mem status    # worker 狀態
npx claude-mem doctor    # 診斷 bun / uv / worker 健康度

3) 調設定 — 全部在 ~/.claude-mem/settings.json,改完即時生效(換 mode 要重開 Claude Code):

{
  "CLAUDE_MEM_MODE": "code--zh",
  "CLAUDE_MEM_CONTEXT_OBSERVATIONS": "30",
  "CLAUDE_MEM_MODEL": "claude-haiku-4-5-20251001",
  "CLAUDE_MEM_LOG_LEVEL": "INFO"
}

幾個實際會用到的鍵:

  • CLAUDE_MEM_CONTEXT_OBSERVATIONS:開場注入幾筆 observation,預設 50。覺得開場太肥就往下調。
  • CLAUDE_MEM_SKIP_TOOLS:不記錄哪些工具,預設已排除 ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion
  • CLAUDE_MEM_EXCLUDED_PROJECTS:整個專案不記(客戶專案、含機敏資料的 repo 建議直接列進來)。
  • CLAUDE_MEM_DATA_DIR:資料根目錄,預設 ~/.claude-mem,DB/Chroma/logs/settings 全部從這裡派生。
  • CLAUDE_MEM_TELEGRAM_TRIGGER_TYPES:預設 security_alert,sensitive,這兩種型別會推 Telegram 通知;設成空字串就關掉。

4) 語言:沒有 zh-TW,自己做一個

plugin/modes/ 裡有 30 個語言 mode,中文只有 code--zh(簡體)。要繁中的話,v13.13.1 加的 /mode-creator 是設計來幹這件事的——它會問你的領域與筆記需求,產出自訂 mode 並裝進使用者目錄。

5) 隱私

對話裡用 <private> 包起來的內容,agent 當下看得到、但不會被存成 observation。貼 production 連線字串、內部網址、客戶名稱時就用它。另外遙測預設是開的,npx claude-mem telemetry disable 關掉。

6) worktree 流程

git worktree add ../myapp-feat-auth -b feat-auth
# ... 在 worktree 裡做事,記憶自動掛在 myapp/feat-auth

# 合併之後、刪 worktree 之前:
npx claude-mem adopt --dry-run
npx claude-mem adopt --branch feat-auth   # squash merge 才需要 --branch
git worktree remove ../myapp-feat-auth

五、數據與限制(誠實版)

成本。 v13.14.0 的安裝器現在會把每個 provider 的價格直接印在選單上,以每 1,000 筆 observation 計:CMEM Pro 每千筆 $0(月費 $30,含雲端同步)、OpenRouter 約 $2.73、Gemini API 約 $3.39、走你自己的 Anthropic 方案約 $8.91。要注意這些是 claude-mem 自己算出來的標籤——release note 說得很白,數字由單一常數 ratePerM × TOKENS_PER_OBSERVATION / 1000 推導,不是第三方量測。用它來比較四個選項的相對高低是合理的,拿去當預算依據就要自己再驗一次。

延遲。 有二手文章寫「每個工具 60–90 秒」,我沒在官方 repo 或 docs 找到這個數字的出處,所以不引用。從架構看,這件事本來就是非同步的:hook 是 fire-and-forget,observation 在背景生成,它不會擋住你的 session,但也意味著剛剛做的事不一定馬上搜得到

穩定性。 這是要講清楚的部分。304 個開放 issue、201 個 npm 版本,而且近幾版的 release note 全是硬故障:v13.12.3 修的是一個自我延續的 stale-worker 迴圈,有使用者回報一天內 recycle 2,424 次,每次 UserPromptSubmit 都以約 40 秒的 hook timeout 收場;v13.12.4 修 FOREIGN KEY constraint failed 導致 worker 永遠不 ready;v13.15.2 修的是 observer 靜默死掉(多半是 Pro 額度用完)只丟 OpenRouter upstream error (status 502) 重試迴圈。

這些都修了,而且修得很紮實(v13.12.2 一次合併 54 個社群修復 PR,還把合併準則寫成 docs/merge-rubric.md,明文禁止 guard、circuit breaker、fallback、retry 這類「補丁式」修法,只收根因修正)。但頻率本身說明一件事:這是一個高速迭代中的工具,不是一個穩定件。 開自動更新,並且知道遇到怪事時第一招是 npx claude-mem doctor

還有一個很值得記住的事故。 v13.12.4 揭露:repo 根目錄 CLAUDE.md 裡的維護者指令(包含一段「自動升級並 commit」的指示)原封不動出貨給每一個從 marketplace git-clone 安裝的使用者,而且被使用者端的 Claude 實例照做了。這不是 claude-mem 獨有的問題——它是所有「plugin 帶 CLAUDE.md」的分發模型共有的攻擊面。裝任何 agent plugin 之前,去看一眼它 repo 根目錄的 CLAUDE.md / AGENTS.md 寫了什麼。

六、什麼時候該用、什麼時候別用

該用:

  • 長期維護的單一大 repo,你每週都在同一批檔案裡打轉。observation 累積得越久,search 越有價值。
  • 重度使用 git worktree 平行開發。這是它相對其他記憶方案最明確的差異化功能。
  • 交接/輪班場景。timeline-reportweekly-digests 這些內建 skill 是拿 observation 生時序報告的,比翻 commit log 有脈絡。

別用(或至少先想清楚):

  • 含機敏資料的 repo。observer 會讀到 agent 讀到的東西。至少把它加進 CLAUDE_MEM_EXCLUDED_PROJECTS,或全程用 <private>。v13.16.0 有強化憑證遮罩(短字串、含空白值、任意 Authorization scheme、Cookie header、URI userinfo),但遮罩永遠是盡力而為。
  • 短命的 throwaway 專案。記憶的價值來自累積,一次性腳本不值得付這個成本。
  • 對 session 啟動延遲極度敏感。多一層 hook、多一個 worker、開場多注入 50 筆 observation,就是有代價。
  • 團隊要共享記憶。本地模式下記憶是你個人的;跨機器/跨人共享要走 cmem.ai 的雲端同步,那是付費且要把資料送出去的決定。

七、就算你不裝,也該抄走的三件事

  1. 漸進揭露要有價目表。 File Read Gate 最聰明的不是「擋讀取」,是回給 agent 的訊息裡把四種選項的 token 成本都標出來。你自己寫工具描述時可以照抄這招——在 tool description 裡標成本,讓模型自己做取捨,比你硬性限制有效。
  2. provenance 欄位不可變,狀態變更用指標。 project 永不覆寫、只加 merged_into_project——這是任何要做資料合併的系統都該學的模式:合併是視圖層的事,不是儲存層的事。出錯了可以回退,也永遠答得出「這筆資料原本是哪來的」。
  3. 判定交給權威來源,並為權威的盲區留逃生門。 合併偵測全部問 git,不猜;但作者清楚知道 --merged 看不到 squash,所以留了 --branch知道自己權威來源的盲區在哪,比假裝沒有盲區重要得多。

最後給一句實話:如果你是單 repo、單線開發,claude-mem 帶來的複雜度可能超過它省下的重複解釋。但如果你已經在用 worktree 平行跑多條線——那個「分支合了、記憶沒合」的痛,它是目前少數真的正面處理了的工具。

來源

整理:DataAgent · Coding Agent 實戰教學

發表迴響

%d 位部落客按了讚: