并行 worktree · 让子任务在隔离副本里跑
多个 subagent 并行改代码,怎么不互相踩?grok 用 xai-fast-worktree 给每个隔离出一个 git worktree。这一章拆它「快」的几层手段——CoW 反射链接、并行分片拷贝、--no-checkout、以及那个让 git status 瞬间清白的索引 stat-cache 补丁。
并行 worktree · 让子任务在隔离副本里跑
subagent 并行干活时有个硬问题:它们都要改代码,怎么不互相踩?grok 的答案是给每个隔离出一个 git worktree——一份独立的工作副本。难点在快:每开一个 subagent 都拷一遍仓库,慢得没法用。xai-fast-worktree 就是把这件事做快。
「快」的几层手段
它不是靠一招,而是层层叠加:
- CoW 反射链接(
reflink-copy):在 APFS/Btrfs/XFS 上,拷文件只共享数据块、不真复制——这是 macOS 的快路径。 - 并行分片拷贝:
ignore并行 walker 遍历,每个文件按rapidhash(父目录) % worker 数分片——同目录进同分片,于是建目录不用抢锁。worker 线程跑clone_file。 git worktree add --no-checkout:让 git 零 checkout、只建元数据,工作树由上面的 CoW 拷贝来填。- 索引 stat-cache 补丁(最关键的技巧):见下。
- O(1) 快路径:Linux 依次试 overlayfs 快照 → btrfs 快照 → grove-fuse,macOS 先试 grove-nfs(grove 两条要本机有 Grove 守护进程),都不行才回落到逐文件拷贝。
关键技巧:让 git status 瞬间清白
第 4 点值得单独讲。你把文件 CoW 拷进新 worktree 后,新文件的 inode、ctime 和源索引里记的对不上。git status 一看 stat 缓存全不匹配,就认为每个文件都脏了,于是逐个重新哈希——几万个文件,慢死。
grok 的解法:拷完索引后,update_index_stats 打开拷贝的索引,把每条目的 stat 缓存(mtime/ctime/size/dev/ino……)改写成新拷贝文件的实际元数据。于是 git status 一看全匹配,秒返回「干净」。这一个补丁,把「开个 worktree 后第一次 git status 卡几秒」变成瞬时。 只有干净文件会被打补丁(改过的排除)。
生命周期:建 → 用 → 存
- 建:
WorktreeBuilder::new(source, dest).creation_mode(...).create()(阻塞,调用方spawn_blocking)。中途用PartialWorktreeGuard(RAII)——任何?/取消/出错,Drop就把半成品 worktree 拆掉、并从.git/worktrees/注销,不留垃圾。 - 用:subagent 在里面自由改,和主会话、和别的 subagent 互不干扰。
- 存:subagent 结束销毁 worktree 前,
snapshot_worktree_to_ref把它的全部改动(含未跟踪文件)快照进一个 git ref(refs/grok/subagents/<id>)——work 不随 worktree 删除而消失,还能rehydrate回来。 - 收:新增的节流式自动 GC(
auto_gc.rs)按配置里各类 worktree 的最长存活期回收(手动建的默认永不过期);真要删之前还得过一道「删了会不会丢东西」的闸门(git/safety.rs)——任何失败、超时或读不了的路径,一律判为保留。
谁在用它
- subagent:每个拿一个
WorktreeKind::Subagent的 worktree,并行隔离执行。 - 会话 fork:
resume_local_session_in_worktree把会话复制进新 worktree。 - worktree 池(已删除):7 月版还有个预建 worktree 的池子,用文件系统
.ready→.claimed原子重命名做无竞争领取。新代码把它删了(配置注释说它从未接进生产),只留下清理残留池目录的cleanup_stale_pool_worktrees,[worktree_pool]配置段也标成了废弃。
和 hunk-tracker 的接缝
还记得 hunk 追踪按绝对路径存改动 review 状态吗?fork 到新 worktree(新 cwd)后,那些绝对路径全指向旧目录了。HunkTrackerSnapshot::rewrite_paths(old_cwd, canonical_old_cwd, new_cwd) 就是那道接缝——把每个 file_states 键和 hunk.path 从旧前缀重写到新前缀(还处理 macOS /var→/private/var 符号链接差异),让 fork 出来的会话的 hunk 状态对齐到新副本。两个子系统之间这种「路径重写」的接缝,是并行/隔离设计里必须处理的琐碎但要命的细节。
和《从 LLM 到 Coding Agent》对照
对照 subagents:最小实现里 subagent 就是「另开一个上下文窗口」,不涉及文件隔离。grok 的增量是真让它们在隔离的文件副本里跑——于是要解决「怎么开得快」(CoW + 分片 + stat-cache 补丁)、「怎么不留垃圾」(RAII 守卫)、「work 怎么不丢」(快照进 ref)、「状态怎么跟着搬」(路径重写)。这是把「并行」从「并行推理」落到「并行改文件」的全部工程代价。
Part 3 工作区与 Git 到此为止。下一部分扩展机制,看 grok 怎么被 AGENTS.md、Skills、MCP、Hooks、Plugins 扩展。
上一篇:图谱的真实用途