並行 Task 隔離
透過隔離的 worktree、AIT_RAM 扇出、不可變證據和原子 Land,並行跑多個 coding agent 的 Task。
適用對象: 開發者、coding agent 與維運
運作模型#
AIT 允許並行編寫、序列接納。多個 coding agent 可以同時幹活,但 它們不共用一個可變的 checkout,也不會直接往 main 上寫。
每個受管的請求都會拿到自己的 Task、Change、功能 Line 和繫結的 worktree。agent 在這條沿革裡記錄不可變的 Snapshot。只有當某個候選 確切的 Patchset 攢齊了要求的證據,並且目標頭又被重新核對過之後,它 才會被接納進目標 Line。
這幾層分工很重要:
- Task 繫結的 worktree 加上命令鎖,避免工作區操作互相撞車。
- 不可變的 Snapshot 保證被評審過的那個修訂版,不會在證據底下被換掉;
- rebase 和目標頭核對,避免一個過期的候選覆蓋掉更新的工作;以及
- 原子的 Land,避免共享目標 Line 變成"誰最後寫誰贏"。
AIT 不會讓兩個 agent 編輯同一個 worktree 變得安全。高並行度來自開出 更多隔離的 Task worktree,而不是把一個目錄共享得更狠。
高併發命令扇出#
不同 Task 的獨立 agent 可以同時下達並執行 ait task start、檢視、 Snapshot、測試與 readiness 命令。每條命令都解析自己的 Task worktree 和 沿革,因此大型佇列不需要共享一個全域性可變 checkout。儲存庫命令鎖保護 AIT 權威更新,但不會把多個執行者對同一 worktree 普通檔案的無協調寫入變安全。
指向同一條 Line 的 finish 刻意不做吞吐競賽。每個候選都會重新校驗目前 目標頭,按需要 rebase 或停止,然後原子推進目標。這就是產品高併發 Task 支援的邊界:獨立編寫與驗證,對共享權威的接納則序列進行。
一個 Task 獨佔一個可變 checkout#
| 物件 | 在並行執行裡的職責 |
|---|---|
| Sprint 條目 | 給一個 Task 一個明確的目標結果和確切的 [ref]。 |
| Task | 擁有這個有界的工作單元和收尾狀態。 |
| Change | 擁有這個 Task 可變的評審沿革。 |
| 功能 Line | 讓 Task 的頭和目標 Line 分開。 |
| 繫結的 worktree | 給一個 agent 一份物理上分開、可寫的 checkout。 |
| Snapshot | 把那份 checkout 凍結成一個不可變的修訂版。 |
| Patchset | 選中由 CI、評審、attestation 和策略來評估的那個確切 Snapshot。 |
| Land | 重新校驗,並原子地推進目標 Line。 |
ait task start 會建立並繫結這些工作身份。它的輸出裡會打出確切的 cd 命令。agent 必須先進到那個路徑,再改程式碼。
ait task start \
--from docs/sprints/login.md#login/password-visibility \
--intent "Add accessible password visibility"第二個 agent 起的是另一個未完成條目,拿到的是另一套 Task、Change、 Line 和 worktree。
ait task start \
--from docs/sprints/login.md#login/session-regression \
--intent "Repair session renewal regression"別把同一個 sprint 條目派給兩個 agent。1.1.1 會在建 Task 之前校驗這個 條目是未完成且可開成 Task 的,但它並不聲稱某個 Plan 條目有一把完整 的、跨客戶端的、服務端持有的唯一性鎖。並行的 Task,應該從各不相同的 確切條目引用起步。
兩個 agent,一條目標 Line#
假設兩個 Task 都從 main 的 Snapshot S0 分出來:
main @ S0
|
+-- Task A -> Change A -> feature Line A -> worktree A
| `-> Snapshot A -> Patchset A
|
`-- Task B -> Change B -> feature Line B -> worktree B
`-> Snapshot B -> Patchset B兩個 agent 各自在列印出來的路徑裡並行實作和測試。
# Agent A, inside worktree A
ait snapshot create --message "Add password visibility control"
ait workflow ready <change-a> --apply
# Agent B, inside worktree B
ait snapshot create --message "Repair session renewal regression"
ait workflow ready <change-b> --apply假設 Task A 先 finish,把 main 從 S0 推到了 S1。Task B 不會拿一個 基於 S0 的候選去覆蓋 S1。
Task A: main S0 -> S1
Task B readiness:
base S0 != current main S1
|
+-- clean rebase -> refresh Snapshot/Patchset evidence -> eligible Land
|
`-- conflict -> stop with main unchanged -> explicit resolution required對一個乾淨的繫結 worktree,引導式的就緒流程可以在發布下一個候選之前, 先把它 rebase 到目前目標上。rebase 出來的候選是新內容,必須為那個 確切選定的 Patchset 帶上證據;之前那次 CI 透過,不會拿來給 rebase 之後的修訂版當證明。
rebase 衝突時,AIT 會記錄下暫停狀態,目標 Line 保持不動。檢視之後, 明確選擇繼續還是中止:
ait worktree status --json
ait worktree rebase --continue
# or
ait worktree rebase --abort衝突解完之後,按目前工作流程輸出的指引,把結果修訂版記錄下來並準備好。 ait task finish 消費的是一個已經就緒的選定 Patchset。如果在接納之前 目標頭又變了,這個操作會直接失敗,而不是把中間那次結果蓋過去。
為什麼 AIT_RAM 是這套策略的一部分#
檔案系統隔離是對的,但每個 Task 都要把整個儲存庫還原到磁碟上,代價可能 不小。AIT_RAM 策略把受管的 Task worktree 放在一個驗證過的記憶體檔案 系統上,並維護一份可複用的只讀 main-seed,讓扇出更快。
AIT_RAM 是執行層和快取層。它不是 AIT 權威,不是備份,也不是 ait-server 恢復的替代品。Task、Line、Snapshot 以及其他儲存庫權威, 仍然留在持久的儲存庫狀態或設定好的服務端裡。放進易失記憶體的,只有物化 出來的 checkout。
Persistent repository
|
+-- .ait/ Task, Line, Snapshot authority
+-- .ait-worktree-links/task-a --+
`-- .ait-worktree-links/task-b --+---- stable local aliases
|
/Volumes/AIT_RAM/ | memory-backed execution
`-- .ait-repos/<repository-key>/ |
| |
+-- main-seed | read-only main Snapshot
+-- task-a <------------------+ writable Agent A checkout
`-- task-b <------------------+ writable Agent B checkoutRepository key 是從權威儲存庫路徑推出來的。它把共享的記憶體根切分開, 這樣即使不同儲存庫的 Task worktree 名字很像,也不會撞到一起。預設的 穩定別名根是儲存庫裡的 .ait-worktree-links。
別猜路徑,直接查解析出來的結果:
ait config show --json
ait worktree list --json設定投影會報出 task_worktree.memory_root、推匯出來的 ephemeral_root、別名根,以及 main-seed 的 RAM 預算(如果有)。
1.1.1 實際生效的記憶體預設值#
這些預設值故意寫死;1.1.1 不會按主機記憶體的百分比去推。
| 控制項 | 內建值 | 效果 |
|---|---|---|
| macOS 受管 RAM 卷 | 8 GiB(8,589,934,592 位元組,即 16,777,216 個 512 位元組扇區) | 內建 /Volumes/AIT_RAM 映象的容量。 |
| Linux 或 Windows 的 RAM 容量 | AIT 不做預設分配 | AIT 用的是已經掛好的 tmpfs/ramfs 或 DRIVE_RAMDISK 的容量。 |
儲存庫的 main_seed_ram_max_bytes | 未設定(null) | 不對 RAM 資格做 Snapshot 大小的截斷。這不代表實體記憶體是無限的。 |
| Task 放置的空閒空間閾值 | 無 | 1.1.1 沒有任何位元組或百分比閾值,會自動把新 Task 挪到 SSD。 |
ait doctor memory-root | 只讀 | 這個診斷從不掛載、置備、修復或建立任何根。 |
用 ait config show --json 和 ait doctor memory-root --json 把存下來 的儲存庫策略跟主機目前的容量區分開。前者報的是帶型別的記憶體根選擇和儲存庫 本地的 seed 上限;後者報的是請求的容量、總位元組、可用位元組,以及內建的 零位元組校驗下限,每個值還附帶來源。
記憶體根的校驗與置備#
在真正依賴 RAM 放置之前,先證明設定的這個根確實是記憶體支撐的,而且 可寫:
ait doctor memory-root這個診斷永遠是隻讀的。受管的 Task 放置走的是另一條路:在解析新的 worktree 時,它在 macOS 上可以置備設定的或內建的 RAM 卷。如果這次嘗試 拿不出一個可用的根,放置就繼續走持久磁碟的回退路徑。一個只是名字叫 AIT_RAM 的路徑,不能當作"這塊儲存是記憶體支撐的"的證明。
| 平臺 | 需要的證明 | 置備行為 |
|---|---|---|
| macOS | 確切的 /Volumes/<name> 掛載點,由一個可寫的 hdiutil RAM 映象支撐,且 APFS 卷身份符合預期。 | 新 Task 的放置可以置備帶型別的或內建的卷;診斷從來不會。 |
| Linux | 一個已存在的掛載點,檔案系統型別是 tmpfs 或 ramfs。 | AIT 只校驗,不建立系統掛載。 |
| Windows | 一個已存在的根,被報告為 DRIVE_RAMDISK。 | AIT 只校驗,不建立 RAM 磁碟。 |
儲存庫設定用的是這幾個帶型別的欄位:
| 欄位 | 用途 |
|---|---|
task_worktree.memory_root.kind | 平臺證明:macos_ram_volume、linux_memory_root 或 windows_ramdisk。 |
task_worktree.memory_root.root | 確切的記憶體根絕對路徑。 |
task_worktree.memory_root.volume_name | macOS RAM 卷標籤;Linux 和 Windows 不用。 |
task_worktree.memory_root.sector_count | macOS ram:// 的正整數扇區數。省略時用 16,777,216 個扇區。 |
task_worktree.main_seed_ram_max_bytes | 可選的非負值,是預設 Line 的 Snapshot 大小上限,用來判定 RAM 資格。 |
已經檢測到受支援的根時,ait init 會把它記下來。在 macOS 上,即使 儲存庫沒存記憶體根,新 Task 的放置照樣可以去試內建的 /Volumes/AIT_RAM 規格。這份 Task worktree 契約沒有環境變數可以覆蓋。
main-seed 扇出#
main-seed 是預設 Line 頭上某一個確切 Snapshot 的只讀物化結果。它是 快取,不是一個可變的共享 checkout。
一個 Task 從目前預設 Line 分出來時,引導過程走這條路徑:
- 解析出驗證過的記憶體根,以及該 Repository 專屬的 worktree 根。
- 核對
main-seed與確切的預設 Line Snapshot 是否一致。 - seed 缺失、過期或無效時,從 Snapshot 權威重新整理或重建。
- 把 seed 拷進新的 Task 路徑。
- 只把拷出來的那棵 Task 樹設為可寫,並掛上它的 Task、Change 和功能 Line 後設資料。
- 把穩定的別名暴露出來,列印給 agent。
逐檔案複製的優先順序是:
- macOS 的
clonefile; - Linux 的
reflink; - 寫時複製用不了時,退回普通檔案複製。
寫時複製讓你在 Task 啟動時不必付出一份完整獨立的物理複製。只要 agent 改了某個檔案,各個 worktree 在邏輯上照樣是彼此獨立的。如果複製 seed 以安全的方式失敗了,引導過程會把拷了一半的目標刪掉,直接從功能 Line 的 Snapshot 還原。Task 該隔離還是隔離;變的只是那條加速路徑。
讓 seed 保持最新#
成功 land 到預設 Line 之後,AIT 會把 main-seed 對齊到 land 下來的 那個 Snapshot。重新整理路徑會:
- 用一把 Repository 和 Line 專屬的重新整理鎖;
- 拿候選 worktree 跟 land 下來的 Snapshot 做校驗;
- 可以把已完成的繫結 worktree 提上去,也可以從 Snapshot 權威重建;
- 準備一份暫存 seed,以及一份可回退的上一版 seed 備份;
- 把裝好的 seed 設為只讀;以及
- 在原子目錄切換之前和切換過程中,重新核對目標 Line 的世代號。
如果重新整理還在準備的時候 main 又往前走了,AIT 會拒絕裝那份過期的 seed。快取重新整理失敗,不會偽造出一個成功的 seed 狀態。它會被單獨報出來, 免得把 land 下來的程式碼權威和這份用完可棄的加速快取混為一談。
容量與磁碟迴退#
RAM 上的並行度必須明確設上限。預設情況下 task_worktree.main_seed_ram_max_bytes 是未設定的,所以 1.1.1 不會 自動按 Snapshot 大小截斷。維運可以設定允許走 main-seed 支撐的 RAM 放置的預設 Line Snapshot 大小:
ait config set \
--task-worktree-main-seed-ram-max-bytes <bytes>
ait config unset task-worktree-main-seed-ram-max-bytes每個新 Task 的放置判定是這樣的:
configured seed limit exists AND Snapshot bytes > limit
-> Repository-internal persistent disk
else if a supported RAM root can be selected and its Repository directory created
-> RAM
else
-> Repository-internal persistent disk比較用的是嚴格大於。正好等於設定上限的 Snapshot,仍然有 RAM 資格。 超出上限時,AIT 會記下 main_seed_ram_budget_exceeded,並選用持久 Repository 根(通常是 SSD)下的 .ait-worktree/<worktree-name>。用內建的未設定值時,這個按大小觸發 的切換是關掉的。
當所有符合條件的記憶體候選都不可用或者用不了時,AIT 同樣會選那個持久 根。這包括:Linux 上沒檢測到 tmpfs/ramfs、Windows 上沒有 DRIVE_RAMDISK、macOS 上 RAM 卷不可用或者嘗試失敗、設定的記憶體/臨時 路徑在它宣告的根之下不被接受,或者建立 Repository 專屬的 worktree 目錄失敗。
那個只讀的記憶體根診斷,會報出一個內建的最小值零位元組。1.1.1 不會拿這個 值當作 Task 放置的自動開關,也沒有隱藏的低記憶體百分比閾值。需要留一份 餘量的維運,得靠主機監控來落實,並在檔案系統被吃光之前停止接納新的 Task。
放置不會把一個已有的 Task 中途從 RAM 挪到磁碟。如果在選定 RAM 路徑 之後複製 main-seed 失敗,AIT 會先在同一個路徑上直接試 Snapshot 還原;那是物化的回退,不是切到 SSD。如果選中的檔案系統完不成這次還原, 或者之後被寫滿了,命令會失敗,維運得保住已記錄的 Snapshot、解決容量 問題,再在一個合格的根上重建或重新開始工作。
回退改變的是效能,不是 Task 身份、Snapshot 語義或 Land 的安全性。查 Task/worktree 輸出裡的 root_source、ephemeral_enabled、 target_path 和 fallback_reason。root_source 是 repo_internal_fallback 且 ephemeral_enabled: false,就證明放在了 持久根上;別因為看到一個穩定別名,就推斷它一定指向 RAM。
開始大規模扇出之前,先看看目前容量和活著的 worktree:
ait doctor memory-root --json
ait worktree doctor --refresh --json
ait queue summary永續性與恢復邊界#
| 狀態 | RAM 卷丟了還在嗎? | 恢復來源 |
|---|---|---|
| Task、Change 和 Line 權威 | 在 | 持久的本地權威,或者設定好的 ait-server。 |
| 已記錄的 Snapshot 內容 | 在 | 持久的 Snapshot 儲存;發布過的話,還有遠端內容權威。 |
main-seed | 不要求 | 從目前預設 Line 的 Snapshot 重建。 |
| 乾淨的 Task worktree | 不要求 | 從它記錄的功能 Line 頭重建。 |
| 還沒進 Snapshot 的髒改動 | 不在 | 沒有任何自動恢復保證。 |
維運規矩就一條:工作必須扛得住易失執行儲存丟失時,就在語義完整的檢查 點上建立 Snapshot。AIT_RAM 加速的是 worktree 的物化;它不會讓沒進 Snapshot 的檔案變得持久。
意外解除安裝或重啟之後,先診斷,別急著刪後設資料:
ait doctor memory-root
ait worktree doctor --refresh --json
ait worktree recreate <worktree-name> --dry-run
ait worktree recreate <worktree-name>如果登錄檔的恢復必須從一個已知的 Task 和 Change 繫結開始,用 ait worktree recover-task --help 展示的那套明確的恢復入口。恢復絕不 會僅僅因為某個老目錄還在,就把已完成或已取消的權威重新開啟。
清理與操作規矩#
Task 成功收尾時,會在確認被接納的 Snapshot 仍然是頭之後,刪掉繫結的 worktree 並歸檔它的 Task 功能 Line。這樣既釋放了易失容量,又不會刪掉 Snapshot 歷史。
對中斷之後留下的殘餘,先預演一遍帶歸屬判斷的清理:
ait worktree cleanup-candidates --json
ait worktree cleanup --dry-run
ait worktree prune-stale --dry-run同時開很多 agent 時,守住這幾條:
- 每個 agent 都給一個不同的 sprint 條目和 Task。
- 只進那個 Task 列印出來的
cd路徑。 - 永遠別把儲存庫的規範根目錄當成共享編輯目錄。
- 在依賴一個易失 worktree 之前,先記下不可變的檢查點。
- 讓就緒流程去重新整理過期的候選;絕不要繞過基線頭的衝突。
- 把 CI 當成某一個選定 Patchset 的證據,而不是某個 Task 名字的通行證。
- 透過 AIT 來 Land,這樣目標頭核對和原子切換才一直有效。
- 刪 worktree 之前,先診斷、先預演清理。
這套東西不保證什麼#
worktree 隔離擋住的是檔案系統層面的互相覆蓋。它證明不了兩處文字上不 衝突的改動,行為上也相容。兩個 agent 可以改不同的檔案,卻動了同一個 執行時契約。儲存庫自己的迴歸測試、評審和策略,照樣少不了。
AIT 也不提供無限的 RAM 併發。儲存庫大小、髒增量、工具快取、測試程序和 runner 容量,仍然決定了安全的扇出規模。AIT_RAM 讓這份成本變得可見、 可控;它沒有免掉容量規劃這件事。