10個代理同時跑了同一個型別檢查。解決辦法是一個目錄。
同一個檢出目錄裡17個編碼代理,同時跑著10個tsc,load average 37,空閒記憶體只剩87 MB。一次九十秒的型別檢查花了7分36。這裡是我們的測量資料,機器為什麼根本沒在計算,以及那個解決了問題的小小共享鎖。可以直接抄進任何儲存庫。
9月7日,我們的開發機器不再正常響應。不是崩潰,也不是卡死。只是所有事情都慢了十倍,連跟程式碼毫無關係的事情也一樣。
那些顯而易見的嫌疑物件全都錯了。筆記本沒有過熱:沒有記錄到任何降頻,電池30.6 C。沒有失控的程序在吃CPU。什麼都沒有部署。唯一不尋常的是同一個儲存庫裡活著17個代理CLI,而這在我們這兒是普通的一個工作日。
下面是真正發生的事情,測出來的,不是猜的。
測量
16 GB、8核的機器,已經開了五個半小時,17個代理在幹活:
| 我們測了什麼 | 數值 |
|---|---|
| 活著的代理CLI | 17 |
併發的 tsc --noEmit 程序,兩分鐘內觀察到 | 3,然後10 |
| Load average | 37到41 |
| 空閒記憶體 / 記憶體壓縮器 | 87 MB / 7.2 GB |
| 桌面應用的一次型別檢查,飽和的機器 | 26 秒CPU對應7分36的牆上時間 |
| 同一個型別檢查,空閒的機器 | 33 秒 |
關鍵的是倒數第二行。26秒的CPU攤在七分半里,也就是百分之十的利用率。型別檢查沒有在計算。它在等記憶體。
而且其中一個程序被作業系統在半路殺掉了。被殺掉的 tsc 會以非零碼和空輸出退出,這跟一個真正的型別錯誤沒法區分。所以這台機器不只是慢,它還在產出沒人敢信的判決。
沒有人做錯任何事
這一段值得停下來想想,因為正是它讓這種故障如此難以預見。
那些代理每一個都在遵守規則。每一個都改過TypeScript。每一個都被要求在交還之前驗證型別。每一個都跑了 tsc --noEmit 。沒有一個能看見別的代理。 不存在一塊公共黑板,讓某個代理在上面寫下「我正在做那件很貴的事,等一下」。
接著它開始自我餵養。機器飽和了,所以型別檢查變慢。盯著它的那個代理判斷它卡住了。於是殺掉,再起一個。這個反射單獨看是對的,放進一群裡就是災難,而這跟我們一個月前記錄過的故障是同一族:當時代理們留下了卡住的搜尋程序。Process Guard 是找出已經啟動的東西的安全網,而這裡做的是阻止它啟動。
我們沒有采納的三個答案
少開幾個代理。 這把症狀減半,把缺陷留下。在一台負載很重的機器上,兩個同時跑的型別檢查依然比一個慢,而縮減隊伍等於用讓工作變快的那樣東西去償付問題。
最後只做一次型別檢查。 很誘人,但它錯在一個跟效能毫無關係的地方。10個工單之後才發現的型別錯誤是個孤兒:寫下它的代理已經關閉,上下文已經丟失,得由一個人把整個話題重新開啟去改一行。我們不想推遲這次驗證。
增量編譯。 試過,放棄了。在 --noEmit 模式下收益存疑,而併發的程序會破壞共享的 .tsbuildinfo 。它解決了一半問題,代價是讓另一半更糟。
我們改成怎麼做,一次檢查,大家共享
規則不是「少檢查幾次」。而是每個專案、面向所有人、同一時刻只有一個型別檢查。一個包裝腳本把N次檢查換成一次,並回答三種情況:
- 距離上一次執行什麼都沒變,那就把它的結果交回去。
- 已經有一次執行在進行中,那就等它,拿它的結果。
- 否則,拿下鎖,成為機器上唯一的
tsc。
從代理的角度看什麼都沒變:它敲 yarn typecheck ,它拿到自己的型別錯誤。它也不會比以前等得更久,因為它等的那次執行是比它自己更早啟動的執行。機器付一次的錢,而不是十次。
整個想法就是這樣。有意思的是,它需要的兩套機制都比你以為的小得多。
鎖是一個目錄
不是檔案,不是資料庫,不是守護程序。是一個目錄。
try {
mkdirSync(lockDir); // 成功: 鎖歸我們
} catch (err) {
if (err.code === 'EEXIST') { /* 別人拿著,我們等 */ }
}
mkdir 要麼建立目錄,要麼以 EEXIST 失敗,而且它在macOS、Windows和Linux上都以原子方式完成,不需要依賴,也不需要原生呼叫。先寫一個檔案再檢查它是否存在,那就是兩步操作,而兩步操作恰恰是第二個代理擠進中間的地方。
在這個目錄裡,我們放一個帶著pid、主機名和啟動時間的 owner.json 。這個檔案用於診斷,也用於檢測一把死鎖。做互斥的從來不是它。
死鎖會在兩種情況下被自動收回:屬主程序已經消失,這一點只在主機名相符時才檢查,因為pid跨機器毫無意義;或者這把鎖已經超過十五分鐘。
一個讓我們付出了一個缺陷代價的陷阱。 在 mkdir 和寫入 owner.json 之間,存在一個屬主讀不到的視窗。在這個視窗裡判定鎖已死,就是從剛剛拿到它的那個程序手裡把它偷走,而這正是這個檔案存在要防止的競爭。所以當讀不到任何屬主時,我們按目錄的年齡來判斷,而不是按缺失的檔案。
指紋是一個日期加一個計數
第一種情況需要知道距離上一次執行有沒有東西變過。顯而易見的答案是對原始檔做雜湊。我們不這麼做。
指紋是 <最新的mtime>:<檔案數量> ,覆蓋從tsconfig的 include 推匯出的那些根目錄,再加上tsconfig本身。
在2300個檔案上,讀完每一個位元組比它省下的那次檢查還貴。只看日期會漏掉一次刪除。只看計數會漏掉一次編輯。兩個合在一起就把兩邊都蓋住了。我們認下的假陰性,是同一毫秒內的兩次修改讓計數保持不變,而那種情況下最壞的結果也不過是一個過期幾秒的快取結果,絕不會是一個悄無聲息的型別錯誤,因為真正攔人的驗證仍然在建置那一步。
一條寫下來的規則不夠,我們加了一個鉤子
這條指示從第一天起就寫在 AGENTS.md 裡:絕不直接跑 tsc ,永遠走共享腳本。它不夠用,而說清楚原因是值得的。
改完程式碼就去驗證型別,這是一個刻得很深的反射。在壓力之下,代理會不重讀指示就敲下 npx tsc --noEmit 。而只要有一個代理繞開規則,就足以重現這個鎖存在要阻止的一窩蜂。指示是可以商量的。鉤子不行。
於是我們在 Bash 工具上掛了一個 PreToolUse 鉤子,它拒絕直接的 tsc ,並在拒絕資訊裡點名正確的命令:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
]
}
]
}
}
鉤子從stdin以JSON讀取工具呼叫,拒絕時以2退出並把理由寫到stderr,放行時以0退出。有兩個細節決定了一個鉤子是有用還是煩人。
它匹配處在命令位置的 tsc ,而不是字串裡的任何位置。 到處去找這三個字母,連 grep -rn tsc AGENTS.md 都會被拒絕。所以這個模式要求 tsc 出現在行首,或者出現在 ; 、 && 、 || 、 | 、 ( 之後,前面可以帶一個套件管理器執行器和一段路徑。它也放行 tsc --version :沒有理由拒絕一個只輸出資訊的選項。
它還拒絕第二樣我們沒料到的東西。 我們看著一個代理在鎖後面等,過了一會兒認定這把鎖一定死了,就把鎖目錄刪掉好讓自己繼續。那會在活著的程序旁邊再啟動一個重程序,是對這把鎖所保護的一切最完美的繞過。所以刪除鎖目錄或快取目錄同樣被拒絕,並附上解釋:一把死鎖會自己被收回。
第二條拒絕,我們事先絕對寫不出來。它來自觀察代理被卡住時真正會做什麼,而這比想象它們可能會做什麼更適合當護欄的來源。
它到哪裡為止
這個鉤子只屬於Claude Code。隊伍裡其他的代理CLI只看得到寫下來的規則。這是一個已知的窟窿,我們認下了:在等一個所有CLI都會讀的鉤子標準出現之前,一道覆蓋大多數隊伍的護欄勝過沒有護欄。
共享腳本本身對供應商是中立的,因為它就是一條普通命令。任何能跑 yarn typecheck 的CLI都能從這把鎖裡受益,不管有沒有東西強制它這麼做。
從這裡該帶走什麼
型別檢查是我們動靜最大的那個例子,而不是一個特例。這個模式適用於任何昂貴的、在一小段時間視窗內冪等的、並且每個代理都出於同一個正當理由去啟動的命令:安裝依賴、跑完整的測試套件、建置生產版本、在固定埠上啟動開發伺服器。
按這個順序問三個問題,整套設計就齊了:
- 我能不能複用一個最近的結果。
- 我能不能匯入已經在進行的那次執行。
- 否則,我是不是那個獨自啟動它的人。
如果有好幾個代理共用你的機器,值得測量的不是有多少個在跑。而是其中有多少個在同一分鐘裡啟動了同一條命令。你的機器真正感受到的就是這個數字,而在你去看它之前,你只會去怪機器發熱。
如果你想看我們怎麼在同一個儲存庫上跑多個代理而不讓它們互相踩腳的完整做法,那在 如何並行執行3到8個編碼代理而不失去跟蹤 裡;至於那些確實已經啟動的程序,安全網是 Process Guard 。共享腳本和鉤子都住在AgentsRoom的儲存庫裡,也就是那天下午那17個代理幹活的地方。
下載 AgentsRoom
在一個視窗中執行你所有專案的所有 AI 代理。
配套應用:隨時隨地監控你的 Agent
使用 Claude、Codex、Antigravity CLI 或其他 AI 提供商。
把 Bug 和需求直接傳送到您的公開待辦清單。
AgentsRoom 實際執行一瞥。
繼續閱讀
一次 Claude Code 會話會觸發 30 個 hook 事件。只有 3 個能回話。
Claude Code hook 事件的完整清單:每個事件何時觸發、其中哪 15 個能阻斷,以及那條悄悄吞掉大部分 hook 輸出的 stdout 規則。一份在數千次代理會話的正式環境中跑出來的實戰參考。
閱讀全文我的 AI 跑步教練是一個 Git 儲存庫加一個 Claude 代理
跑完一趟,手錶自動同步,三分鐘後分析已經寫進我的儲存庫,本週計劃被重新調整過,教練還在 Strava 活動下留了一條評論。沒有做 App,沒有寫伺服器,沒有按 token 計費的帳單:一個 Claude 訂閱、AgentsRoom,加上一堆 Markdown 檔案。下面是完整的搭建過程,可以照著復現。
閱讀全文Antigravity CLI每台機器只保留一個Google登入,以下才是真正管用的做法
為什麼您無法在Antigravity CLI上交替使用兩份Google AI Pro訂閱、它究竟把您的登入資訊存在哪裡、那些帳戶切換工具對您的系統鑰匙串到底做了什麼、為什麼家庭方案並不會讓額度翻倍,以及唯一能真正並行執行多個帳戶的做法。
閱讀全文