系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。
我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。
本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。
Base Repo: paulshaclaw
Change Ref: PR #43-feat(stage2): complete memory importer MVP
Issue: Agent、Session 與 CLI 一換,前一輪累積的工程脈絡通常只剩在聊天記錄、暫存檔或人的腦中。即使 PR、Skill 與文件都存在,下一個 Agent 也未必知道該去哪裡找,更無法確認某段內容來自哪個 Tool、哪次 Session、哪個 Project 與哪份原始 Payload。
Root Cause: Claude、Codex、Copilot 各自產生不同形狀的 Session/Transcript,但當時沒有統一的 Capture 與 Importer Boundary。工程經驗沒有一條固定入口被持久化,也沒有穩定的 Session Identity、Archive 與 Provenance 可供後續整理。
Solution: 完成 Stage 2 Memory Importer MVP:由各 CLI Hook 捕捉 Session Event,Installer 負責接線,Importer 統一解析、Routing、Archive 並保存 Provenance;Codex 同一 Session 的多次 Capture 改用 Per-event Unique Filename,避免 Queue Payload 互相覆寫。Review 後也補上 Repo Root 外的 Dry-run Import 與 Override Path 空白拒絕。
Evidence: Memory Test Suite 與 stage2_integration_check.sh 通過;PR 同時保留 Full Repo Test 中兩個與本變更無關的既有 Monitor/Service Failure。這些證據支持 Session Capture、Archive 與 Provenance 通道已成立;它不代表 Transcript 已經變成可重用 Knowledge,更不代表下一隻 Agent 已經讀過或採用其中經驗。
上一篇最後,Cortex 的五個 Runtime Package 終於搬完。
舊 Manager 關了,Thin Shim 也只剩轉送責任。
然後另一隻 Agent 看著刪除清單問:
「舊 Package 為什麼不能先留著相容?」
我把手放回鍵盤,準備從兩套 State Owner、舊 Timer、Cutover Window 開始再講一次。
打到一半,忽然覺得這個場面不只眼熟。
前面我們也遇過:
Agent 說工作做完了
→ D17 才發現 Completion Lifecycle 還沒完成
新 Repo 已經能啟動
→ D22 才發現舊 Runtime Authority 還沒退役
表面上一個在講 Agent,一個在講 Repo。
骨子裡都是:
某個局部能跑,不等於整條工程責任已經收完。
再往前翻,還有另一種:
D18:Ready 與 Fanout 各自解析一份 Plan
D22:新舊 Manager 都可能修改同一份 State
名字不同,最後都在問:
同一個 Fact,到底誰才是 Authority?
最直接的則是 D20。
feature-delivery-pipeline 明明早就寫成 Skill,換一隻 Agent,Phase 還是可以被它自行縮掉。
老Go看我翻完前面的紀錄。
「所以你不是遇到二十二個不同問題。」
「是幾個老問題一直換衣服回來。」
……聽起來更省事了。
因為衣服每次都不一樣,我也每次都重新付一次學費。
這些經驗其實沒有完全消失。
它們散在:
聊天記錄
PR Body
Issue Comment
Skill
Design Doc
測試與 Evidence
問題是,下一隻 Agent 不會因為「某個地方存在一份文件」,就自動知道:
小re問:
「你現在要保存的是一句結論,還是結論從哪裡來?」
如果只留一句:
One authority per fact
它很像一張好看的標語。
但真的出事時,我還是得回頭找:
是哪次 Ready / Fanout 分歧?
哪一份 Plan 被 Pin?
哪個 Service 還沒 Disable?
最後靠什麼 Evidence 才確認?
所以這裡講的 Engineering Memory,不是讓 Agent 記得我喜歡哪種命名。
它是把能跨 Agent 重用的工程經驗,連同來源一起保留下來。
但在談搜尋、推薦、排序以前,先有一個更基本的問題:
Session 結束後,原始經驗到底有沒有被留下?
當時 Claude、Codex、Copilot 都有自己的 Session 形狀。
有的從 Transcript 讀,有的從 History 讀;事件欄位、Session ID、檔案位置也不一樣。
如果每個工具各寫一套後續流程,最後很容易變成:
Claude 記得一種格式
Codex 記得另一種
Copilot 再補第三種
等我要查「這段經驗從哪裡來」,三邊又各有答案。
PR #43 做的第一件事,因此不是讓 Memory 變聰明。
而是先建立 Capture:由 Hook 把 Session Event 交進固定入口,再由 Importer 持久化 Session、來源與識別資訊。
流程大概是:
Claude / Codex / Copilot
↓ Hook Capture
Queue Payload
↓ Importer
Session Archive + Provenance
這裡的 Queue Payload,就是 Hook 先放進待處理區的原始 Session Event。
而 Importer Provenance 則記錄它來自哪個 Tool、哪次 Session、哪個 Project,以及對應哪份 Archived Payload。
這份 Captured Session Archive:
小bu看完流程,覺得有點樸素。
「所以現在只是先把聊天收進來?」
對。
先不要急著替聊天加上「知識」兩個字。
至少這次它不會在 Session 一關掉後,只剩我記得大概講過什麼。
Capture 接起來後,很快出現一個很實際的問題。
Codex 同一個 Session 可能不只產生一次 Capture Event。
如果 Queue Filename 只使用 Session Identity,第二次寫入時就可能直接蓋掉第一次。
結果會變成:
Session 看起來有 Archive
→ 但中間某次 Event 已經消失
→ Provenance 還能指到一個檔案
→ 只是那個檔案不是原本那份
這種狀況最麻煩的地方,是它不像 File Not Found 那麼乾脆。
檔案還在。
名字也對。
只有歷史被悄悄換過。
所以 PR #43 把 Queue Item 改成 Per-event Unique Filename:同一 Session 的不同 Capture 各自留下,不再互相覆寫。
小re沒有問「最後有沒有檔案」。
它問的是:
「每一次 Capture,還能不能對回它自己的原始 Payload?」
這才是 Importer 該守的東西。
Memory 如果連自己的來源都會被後一筆覆蓋,後面不管蒸餾得多漂亮,都只是把錯的歷史整理得很整齊。
第一版完成後,Review 還補了兩件事。
一個是 Stage 2 Integration 的 Dry-run,必須能從 Repo Root 以外執行。
因為真正的 Hook 不會每次都剛好站在 paulshaclaw 根目錄等你。
如果測試只有在 Repo Root 才能 Import,驗到的比較像:
我站的位置很正確。
不是部署後的 Capture 真的可用。
另一個是 Installer 的 Memory/Config Override Path,遇到空白要直接拒絕。
這不是因為路徑有空白天生邪惡。
而是當 Installer、Shell 與 Hook 對路徑的拆法不一致時,最後很可能出現兩個不同 Root,然後每一邊都覺得自己有成功寫入。
前面才剛把雙 Authority 關掉,我沒有很想讓 Memory 用路徑解析再養一組。
老Go看著這兩個 Follow-up。
「你本來想存經驗。」
「結果第一天先學到,連存放經驗的地址都不能靠大家自行理解。」
非常合理。
也非常符合本系列一路走來的運氣。
PR #43 最後跑過 Memory Test Suite 與 Stage 2 Integration Check。
它能支持的主張是:
它不能支持:
下一隻 Agent 已經能在碰到相似問題時,立刻找回正確教訓。
因為現在留下來的,仍然可能是一整段很長的 Transcript。
裡面有真正的決策,也有走錯的路、Command Output、閒聊、重複嘗試,以及當時還沒被推翻的猜測。
老Go把剛才那批 Archive 看了一眼。
「收據沒有丟,很好。」
「但你總不能叫下一隻 Agent 先把二十二天的收據全部讀完。」
嗯。
Memory 的入口有了。
下一個問題是,怎麼把一整段 Session,拆成下一隻 Agent 真正有機會讀得下去的工程知識。
下一篇,對話全存下來了,為什麼還是不能直接當 Memory?
Have a nice day.