并行 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 独占一个可变 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.0.0 会在建 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 先 land,把 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 land 消费的是一个已经就绪的选定 Patchset。如果在 Land 之前 目标头又变了,这个操作会直接失败,而不是把中间那次结果盖过去。
为什么 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.0.0 实际生效的内存默认值#
这些默认值故意写死;1.0.0 不会按主机内存的百分比去推。
| 控制项 | 内置值 | 效果 |
|---|---|---|
| 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.0.0 没有任何字节或百分比阈值,会自动把新 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.0.0 不会 自动按 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.0.0 不会拿这个 值当作 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 让这份成本变得可见、 可控;它没有免掉容量规划这件事。