
這篇在講什麼 exchange 是我自己用的內部工具,放在一個 private repo,沒有公開下載或 Pages 站。它已經 wire 進我每個 Claude Code / Codex session 的 hook(開場與每次送 prompt 會自動心跳 + 讀訂閱房間)—— 不過老實說,它在「多條工作線並行交接」時才會真的熱起來,單線做事的日子就安靜待命。這篇是把它的設計跟用法分享出來 —— 如果你也同時開好幾個 AI coding session、好幾台機器在同一堆 repo 上跑,可能會有共鳴,或想自己照著做一個。
為什麼需要它
現在寫 code 的日常變成這樣:一台機器上同時開兩三個 Claude Code session,旁邊還有 Codex,另一台機器上又有一條在跑。它們各自在不同的 repo、不同的工作線上動手,但這些 repo 彼此會互相依賴、會碰到同一份共享檔、同一台共享服務。
問題就來了:A session 剛把某個東西改掉、或剛 ship 了一個別人會依賴的東西,B session 完全不知道。 等到 B 動手才發現踩到同一塊,或重複做了一遍,或假設了一個已經不成立的前提。
人類團隊靠 Slack / standup 同步狀態。一堆並行的 AI agent + 終端,沒有這個東西。exchange 就是想補上這一塊 —— 一個極簡的、給工作線之間互通消息的信箱。
設計上的幾個拍板
做這個工具時,有幾個決定是刻意的:
- 地址是「工作線」,不是 session。 session 是短命的,開了又關,拿它當地址沒意義。所以地址單位是專案 / 工作線(像
mori-desktop、mori-ear、agentos),外加一個廣播位址all。session 只寫進信封的from欄當 provenance,知道「這封是誰發的」就好。 - 讀信狀態用 since 游標,server 不記已讀。 每個 reader 自己記住「我上次看到 id 幾號」,下次
pull只拿 id 比它大的。server 端完全不維護「誰讀過什麼」的狀態 —— 這讓 server 蠢到不會壞。 - 信封最小化,只有 7 欄:
id / ts / from / to / kind / summary / ref。 ref是指標,不是內容。 summary 寫一句人看的摘要,真相放在ref(repo 路徑 / git sha / branch / URL)。這樣才不會又生出一個會過時的真相來源 —— 要知道細節,順著指標去看 code 本身。- 同一支程式,dev 跟 prod 不改 code,只改 env。 本機開發跑
127.0.0.1、無認證;上正式機改成綁 LAN、設一個 token 就開 bearer 檢查。程式一行不動。
兩個組件
整個東西就兩個檔,都只用 Python 標準函式庫,沒有任何第三方依賴:
server.py—— HTTP API,SQLite 存訊息。核心端點是POST /msg(投信)、GET /inbox?to=&since=(收信)、GET /health(活著沒)。exchange—— CLI。身份、server URL、游標都自己管。
最基本的用法
設定一條工作線的身份,設一次就好:
./exchange config --set-from agentos --set-url http://127.0.0.1:18080
然後三個動作就涵蓋日常:
# 收尾 / 里程碑:告訴別條線我做了什麼
./exchange push --to mori-meeting-recorder --kind handoff \
--summary "已移除 mori-desktop MeetingMode" --ref chore/shed-meeting-mode
# 開場:拉「給我這條線 + 廣播」的新信,游標自動前進
./exchange pull
# 看但不動游標
./exchange pull --peek
實際融進工作流是這樣:開場 pull 先知道別條線做了什麼;動手改共享地盤前 pull 看有沒有人剛碰同一塊;做完別人依賴的東西就 push 廣播出去。
檔案搶佔:跨機器的 advisory lease
光是傳訊息還不夠 —— 兩條線可能同時想動同一批檔。所以加了一層 advisory file lease,動共享檔之前先宣告意圖:
./exchange reserve "src/**" --exclusive --ttl 3600 --reason "BI-6 重構"
./exchange leases # 列本工作線未過期的 lease(* = 自己持有)
./exchange release "src/**" # 釋放
reserve 撞到別人的 exclusive 重疊鎖,會印出持有者並以非零 exit 結束 —— 所以它可以塞進 script 或 pre-commit 當 gate。non-exclusive 則是純訊號、不擋人。lease 有 TTL,session 掛掉會自己過期,不會留下永遠卡著的鎖。因為 lease 放在共享 server 上,這個協調是跨所有機器的,不是單機而已。
會議室:自動心跳 + 注入新訊息
光靠人記得 pull 是不可靠的。所以做了一套「會議室」機制,搭配 hook 安裝:
./exchange install-hooks
裝進 Claude Code(~/.claude/settings.json)跟 Codex(~/.codex/hooks.json)兩個 CLI,idempotent、會先備份 .bak、保留既有設定。裝完之後,每個 session 開場 / 每次送 prompt 都會自動心跳一下,並把你訂閱房間裡的新訊息做成 digest 注入 context —— 不用手動 pull,別條線的動態就會自己出現在對話裡。
Codex 有個手動步驟:它預設跳過未信任的 hook,裝完要在互動式 session 打一次
/hookstrust(hook 指令變更後要重 trust)。install-hooks會把這個提示印出來。
digest 注入有兩個機制讓它「只看該看的、且不漏」:
- FIFO 排空(不漏): 一輪注入有上限(預設 8 則 / 1000 字)。超過上限的訊息不會被丟掉,而是排到下一輪續推,oldest-first。游標只推進到「真的處理掉的」,被擋下的下輪會重新出現。
- 相關性過濾(只看該看的): 每條工作線可以有自己的
filter-<workstream>.json,把某些來源整個靜音,或把某些房設成「只浮@我的點名 + 指定 kind」。同一個房,不同工作線會因此看到不同子集。
要回頭撈歷史,exchange room --since <N> 可以從指定 id 讀而不推進游標(--since 0 就是全量)。
正式機怎麼跑
dev 跑通之後上正式機,程式不動,只是換個跑法:server 跑在一台內網機器的 Docker 容器裡(python:3.12-slim、restart: unless-stopped),用 bind-mount 把 host 的 server.py 唯讀掛進容器 —— 換 host 那支檔再重啟容器就生效,不用 rebuild image。DB 放在 docker volume,重啟不動資料。env 設好 EXCHANGE_TOKEN 就開 bearer 認證。
部署等於 git pull && docker restart。各機器一次性 exchange config 設好 URL 跟 token 就接上同一台。因為正式機上跑的是真正的 production 服務,這個端點一定要 token + 限 LAN,不對外。
一點心得
這個工具刻意做得很小 —— 兩個 Python 檔、零依賴、server 蠢到不記狀態。它解的是一個只要多條工作線並行就會碰到、但以前只能靠「我自己記得」硬撐的問題:多條並行工作線之間的狀態同步。讀的那半(心跳 + 拉房間 digest)wire 進 hook、每個 session 自動跑;寫的那半(push 一則 handoff)是刻意動作 —— 所以沒有並行交接的日子,它就靜靜待命,不吵。
multi-agent 不一定要是某個花俏的編排框架。有時候只需要一個信箱,讓每條線在開場時知道別人做了什麼、動手前知道別人正在碰什麼。剩下的,讓 since 游標跟一點 hook 自動化去扛。
想支持持續開發? 請我喝杯咖啡 → buymeacoffee.com/yazelin