Worktree 隔离
把任务所有权绑定到受管 Git Worktree,解决共享目录下的写入冲突。
先看它怎样跑起来
这一章不是几个孤立知识点,而是一条会产生结果的因果链。
Worktree 隔离
亮起的是当前动作,留下的是已经满足的前置条件。点击任意一步,可以从那里继续。
- Task claim 只解决“归谁”,Worktree 解决“改哪片物理空间”。
- 工作目录不能由模型随意传入。
- 集成引用和清理策略要显式。
顺着原文把边界看清
01导读:问题背景与本章目标前一章已经让 Alice 和 Bob 从同一张 SQLite 任务板自主找活,并用 claim token 证明“这项任务现在归我”。⌄
前一章已经让 Alice 和 Bob 从同一张 SQLite 任务板自主找活,并用 claim token 证明“这项任务现在归我”。
但任务所有权不等于文件系统隔离。
Alice 认领了“重构认证模块”,Bob 认领了“重构登录页”。两人都在主工作目录执行 write_file("config.ts", ...) 时,后写入的一方仍会覆盖前一方。Task DAG、lease 和消息协议都没有出错,冲突发生在另一层:两个执行者共享同一片物理文件空间。
第 18 章在第 17 章全部能力之上只增加一项 worktree capability:为项目任务建立受管 Git Worktree,并在每次工具执行前把可信工作目录解析到当前 claim 对应的 Worktree。
02先固定验收场景1. 在 Git 仓库根目录创建 pending 任务 A、B;⌄
先不讨论实现,一个完整的最小场景应该能观察到这些结果:
- 在 Git 仓库根目录创建 pending 任务 A、B;
- Lead 分别为 A、B 创建
alice、bob两个受管 Worktree,显式指定refs/heads/main为集成引用; - 两个 binding 变成
active,但 A、B 仍是 pending; - Alice、Bob 手动认领或由 idle polling 自动认领各自任务;
- 两人写入同名文件,内容只出现在各自 Worktree,主目录不出现该文件;
- 完成 A 并把
wt/alice集成进明确引用后,clean Worktree 可以安全删除; - B 有未提交改动、未集成提交或 Git 检查失败时,删除请求变成
needs_review,目录和提交不被强制丢弃; - 重建 SQLite store 后,binding 和按顺序追加的审计事件完整恢复。
这组验收同时覆盖 Task、claim、执行目录、Git 生命周期和事务审计。只断言“创建出一个目录”远远不够。
03为什么文件锁不是答案文件锁只能保证“同一时刻一个人写”,不能给两项并行工作提供独立版本。⌄
文件锁只能保证“同一时刻一个人写”,不能给两项并行工作提供独立版本。
~~~text Alice 获得 config.ts 锁并写入 -> Alice 释放锁 -> Bob 获得锁并写入 -> Alice 的结果仍被 Bob 覆盖 ~~~
锁把并行写变成有序覆盖,没有解决“两个候选改动如何分别保存、审查和集成”。Git Worktree 提供的是物理目录隔离,同时复用同一个 Git 对象库:
~~~text repository-root/ ├── .git/ ├── .agent_tutorial/ │ ├── tasks.sqlite3 │ └── worktrees/ │ ├── alice/ branch: wt/alice │ └── bob/ branch: wt/bob └── source files integration ref 所在主工作目录 ~~~
Alice 和 Bob 可以同时修改 config.ts,但修改位于不同目录和不同分支。之后由明确的 review/merge 流程决定哪一份进入集成引用。
04不能把 worktree 塞进 Task最省代码的写法,似乎是在 Task 上增加一个可空 worktree 字段。这样做会混淆三条独立状态线:⌄
最省代码的写法,似乎是在 Task 上增加一个可空 worktree 字段。这样做会混淆三条独立状态线:
| 状态线 | 回答的问题 | 典型状态 |
|---|---|---|
| Project Task | 工作是否尚未开始、正在执行或已完成 | pending -> in_progress -> completed |
| Task Claim | 当前谁在租期内拥有执行权 | active、expired、cleared |
| Worktree Binding | 任务对应的隔离目录处于什么生命周期 | reserved、active、kept、needs_review、removed |
P18 没有修改 Task schema。SQLite 使用独立的 worktree_bindings 表,通过 task_id 外键关联任务;binding 的创建也不会把 Task 从 pending 推进到 in progress。
这是必要边界。Lead 可以先建立 DAG、预留所有隔离目录,再启动队友。真正的 Task 状态迁移仍只由 claim 和 complete 完成。
05Binding 是独立状态机P18 的 binding 不是一个可空路径,而是严格的五态生命周期:⌄
P18 的 binding 不是一个可空路径,而是严格的五态生命周期:
~~~text reserved -> active -> kept ---------> removed | ^ +-> needs_review --+ | +-----------------> removed ~~~
reserved:Task、名称、分支、路径、基线提交和集成引用已占位,Git 目录尚未证明创建成功;active:Git Worktree 已创建,并验证当前分支与预留分支一致;kept:Task 已完成,调用方明确保留目录等待 review 或后续处理;needs_review:运行时无法证明自动清理安全,或某一步 Git 操作失败;removed:安全清理完成后的终态。
核心模型保留足够多的不可变证据:
~~~ts export const WorktreeStatus = Object.freeze({ RESERVED: "reserved", ACTIVE: "active", KEPT: "kept", NEEDS_REVIEW: "needs_review", REMOVED: "removed", } as const);
export class WorktreeBinding { readonly taskId: string; readonly name: string; readonly branch: string; readonly relativePath: string; readonly integrationRef: string; readonly baselineCommit: string; readonly branchTip: string | null; readonly status: WorktreeStatus; readonly reviewReason: string | null; readonly createdAtUtc: Date; readonly updatedAtUtc: Date; } ~~~
分支和路径不是模型自由生成的:
~~~text branch = wt/{name} relative_path = .agent_tutorial/worktrees/{name} ~~~
name 复用安全的小写 Agent slug 校验,并拒绝 Windows 保留路径组件。integration_ref 必须是显式、安全的 refs/... 引用;空白、..、@{、Git 特殊字符和可能被解释为选项的输入都会在执行 Git 前失败。
06Lead 恰好增加三项工具P18 只向 Lead 注册三项 Worktree 生命周期工具:⌄
P18 只向 Lead 注册三项 Worktree 生命周期工具:
| 工具 | 输入 | 前置条件 | 成功结果 |
|---|---|---|---|
create_worktree | task_id、name、integration_ref | Task 为 pending,名称/分支/路径未占用 | reserved -> active,Task 仍 pending |
keep_worktree | task_id | Task 已 completed,binding 为 active | active -> kept |
remove_worktree | task_id | Task 已 completed,binding 可清理且安全证明成立 | active/kept/needs_review -> removed |
公开创建输入是严格 Zod schema,不接受额外字段:
~~~ts const createWorktreeSchema = z.strictObject({ task_id: z.string().uuid().toLowerCase(), name: z.string().trim().min(1), integration_ref: z.string().trim().min(1), }); ~~~
Teammate 和 Subagent 不获得这三项管理工具。它们可以在已绑定任务中工作,却不能自行创建、保留或删除隔离目录。生命周期决策仍归 Lead。
相关实现集中在:
- [Worktree 领域模型与 runtime](code/chapters/ch18/src/features/worktrees.ts)
- [参数化 Git adapter](code/chapters/ch18/src/adapters/git.ts)
- [SQLite Task 与 binding store](code/chapters/ch18/src/adapters/task-sqlite.ts)
- [共享认领服务](code/chapters/ch18/src/features/work-stealing.ts)
- [统一 Loop](code/chapters/ch18/src/core/loop.ts)
- [不可变 ToolContext 与工具注册表](code/chapters/ch18/src/core/tools.ts)
- [章节组合根](code/chapters/ch18/src/bootstrap.ts)
- [CLI 组合入口](code/chapters/ch18/src/cli.ts)
- [固定章节入口](code/chapters/ch18/src/chapters/ch18.ts)
07创建为什么先 reserve,再调用 Git创建 Worktree 跨越 SQLite 和 Git,两者无法放进同一个 ACID 事务。P18 不伪装成“跨系统原子操作”,而是先持久化意图,再记录已证明的结果:⌄
创建 Worktree 跨越 SQLite 和 Git,两者无法放进同一个 ACID 事务。P18 不伪装成“跨系统原子操作”,而是先持久化意图,再记录已证明的结果:
~~~text 验证 Git 仓库根目录 -> 将 integration_ref 解析为不可变 baseline commit -> SQLite 事务写入 reserved binding + reserve event -> git worktree add -b wt/{name} {path} {baseline_commit} -> 在新目录验证 HEAD 与当前分支 -> SQLite 事务迁移 active + create event ~~~
简化后的关键顺序如下:
~~~ts const baseline = await this.#resolveCommit(integrationRef, this.#workspaceRoot); const reserved = await this.#store.reserveWorktree(binding);
const result = await this.#runGit( ["worktree", "add", "-b", reserved.branch, path, reserved.baselineCommit], this.#workspaceRoot, ); if (result.returncode !== 0) { throw new WorktreeGitError("Git could not create the reserved worktree"); }
const branchTip = await this.#resolveCommit("HEAD", path); return await this.#store.activateWorktree(reserved.taskId, { branchTip, occurredAtUtc: this.#now(), }); ~~~
如果 git worktree add 失败,Task 仍是 pending,binding 保持 reserved,审计中只有 reserve 事件。系统不会删除证据,也不会谎称创建成功。后续恢复或人工处理可以明确看见“预留已发生、Git 创建未完成”。
Git 子进程边界由 SubprocessGitRunner 统一处理。execFile("git", ["--no-pager", ...]) 把参数数组直接作为 argv,不经过 shell,所以参数含空格或特殊字符也不会被解释。每次运行前先 realpath(cwd) 并确认是目录;超时、启动失败和非零退出码分别转成结构化结果或 GitExecutionError。原始 stderr 不进入 binding 的 review reason,避免把本机路径或敏感输出泄露给模型。
08手动认领和自动窃取必须走同一条路如果 claimtask 会检查 binding,而 idle polling 仍直接调用 SQLite 的普通 claimnext,自动认领就能绕过 Worktree 约束。P18 把认领抽成唯一的 TaskClaimService:⌄
如果 claim_task 会检查 binding,而 idle polling 仍直接调用 SQLite 的普通 claim_next,自动认领就能绕过 Worktree 约束。P18 把认领抽成唯一的 TaskClaimService:
~~~ts export interface TaskClaimService { readonly store: LeasedTaskStore; claimTask(taskId: string, context: ToolContext): Promise<TaskClaim>; claimNext(owner: string): Promise<TaskClaim | undefined>; completeTask( taskId: string, claimToken: string, context: ToolContext, ): Promise<TaskCompletion>; } ~~~
P17 默认使用直接 SQLite service,保持旧章节行为;P18 注入 WorktreeRuntime 作为 service,并强制它与 WorkStealingRuntime 共享同一个 SQLite store。
因此:
- 手动
claim_task只接受 active binding; - 自动
claim_next调用claim_next_bound,跳过未绑定任务; complete_task同时核对 identity、task ID 和 claim token;- Lead、Teammate、Subagent 看到的是同一份 claim 真相。
没有“手动路径安全、自动路径旁路”的第二套实现。
09cwd 不是线程字典,而是每次执行的可信上下文另一个常见补丁是在 Teammate 线程里保存 wtctx["path"],认领成功后更新它。这个方案有四个问题:⌄
另一个常见补丁是在 Teammate 线程里保存 wt_ctx["path"],认领成功后更新它。这个方案有四个问题:
- 它依赖解析工具返回文本,例如判断是否包含
Claimed; - 自动认领不经过 Teammate 的
claim_taskhandler,字典不会更新; - Subagent 和 Lead 还有各自的 Runner,状态无法自然传播;
- claim 过期后如果字典被清空,工具可能悄悄回到主目录继续写。
P18 扩展不可变 ToolContext,把当前任务与 Worktree 证据显式放在运行时上下文中:
~~~ts export interface ToolContext { readonly workspace: string; readonly identity: string; readonly idempotencyKey?: string; readonly taskId?: string; readonly claimToken?: string; readonly worktreeName?: string; readonly executionScope?: object; } ~~~
AgentRunner 对每一个已经通过 schema 校验的工具调用重新执行 ToolContextProvider.resolve(),然后才进入 PreToolUse、权限判断和 handler:
~~~text parse + validate arguments -> resolve trusted ToolContext -> PreToolUse Hook -> hard permission / approval -> handler or background dispatcher -> PostToolUse Hook -> paired tool result ~~~
这让同一个 assistant 回复中的以下调用也能正确工作:
~~~text tool call 1: claim_task(A) tool call 2: write_file("result.txt", "isolated") ~~~
第一个调用把 claim token 绑定到当前 execution scope;第二个调用重新解析时已经能得到 A 的 Worktree 路径,不需要等待下一轮模型请求。
executionScope 本身不保存路径,只保存一个弱引用对象。claim_task 成功时把 claim token 写入该 scope 的映射;后续工具调用先由 scope 找回 claim token,再由 ToolContextProvider 查回 active claim 与 binding,最终得到 Worktree 路径。显式 claimToken 的优先级高于 scope 映射,适合自动认领、Subagent 和事件恢复等需要跨回复携带 claim 的场景。
自动认领则把 claim token 同时作为 Runner 的 idempotencyKey 和显式 claimToken 传入。前者保留事件幂等语义;后者让 provider 区分 claim 与普通 runtime event,再查回 active claim 和 binding。Subagent 同样把当前解析到的 claim token 作为子 Runner 的 idempotencyKey 与 claimToken 传播。子 Runner 有自己的 execution scope,但仍能由同一个 provider 找回相同 claim,所以文件工具也落在相同 Worktree。
10失效 claim 不能回落主目录动态解析必须 fail closed。P18 在返回 cwd 前验证:⌄
动态解析必须 fail closed。P18 在返回 cwd 前验证:
- claim token 当前仍对应 in-progress Task;
- claim owner 与
ToolContext.identity一致; - 显式 task ID 与 claim 中的 Task 一致;
- binding 仍为 active;
- 显式 Worktree 名与 binding 一致;
- 持久化路径严格等于受管相对路径,解析后仍在仓库根目录内;
- 目录真实存在。
错误 identity、错误 task、过期或已完成 token、被替换的路径都会产生配对的 tool_context_error,handler 保持零调用。尤其是已经失效的自动认领 token,不允许回到主 workspace 当作“无绑定任务”继续执行。
这里也不能使用进程级 Set-Location 或 process.chdir()。Lead、多个 Teammate 和 Subagent 可以并发运行,进程 cwd 是共享全局状态;一次切换会影响所有执行者。P18 只把解析后的 workspace 路径传给文件和子进程边界。
11Keep 是显式保留,不是完成 Taskkeepworktree(taskid) 要求 Task 已 completed 且 binding 为 active。它解析并记录当前 branch tip,然后把 binding 迁移为 kept;目录和分支都保留。⌄
keep_worktree(task_id) 要求 Task 已 completed 且 binding 为 active。它解析并记录当前 branch tip,然后把 binding 迁移为 kept;目录和分支都保留。
Keep 不会自动 merge、cherry-pick、push 或修改 Task。Task 生命周期和 Worktree 生命周期仍然独立:
~~~text complete_task 只完成 Project Task keep_worktree 只保留 Managed Worktree remove_worktree 只尝试安全清理 Managed Worktree ~~~
如果连 branch tip 都无法可靠解析,keep 不伪造成功,而是把 active binding 转成 needs_review。
12Remove 必须先证明“可以删”removeworktree 不接受 discardchanges,也没有 force 开关。允许自动删除必须依次证明:⌄
remove_worktree 不接受 discard_changes,也没有 force 开关。允许自动删除必须依次证明:
- Task 已 completed;
- binding 为 active、kept 或 needs_review;
- 受管路径正是 Git 登记的 Worktree 根;
- 它与主仓库共享预期的 Git common directory;
- 当前分支仍是 binding 中的
wt/{name}; git status --porcelain=v1 --untracked-files=all成功且输出为空;- Worktree
HEAD与显式integration_ref都能解析为不可变 commit ID; git merge-base --is-ancestor <branch-tip> <integration-tip>成功。
第 8 步验证的是“集成引用当前提交包含 Worktree 分支顶端”,而不是“提交推送过”。@{push} 依赖本机 push 配置,也不能证明改动已经进入指定集成分支,因此 P18 禁止用它作为删除依据。
所有证明成立后,清理顺序固定为:
~~~text git switch --detach <branch-tip-sha> -> git branch -d wt/{name} -> git worktree remove <managed-path> -> SQLite binding -> removed + remove event ~~~
先 detach,才能用非强制 branch -d 删除原本正被 Worktree checkout 的分支。整个路径不使用 git branch -D、git worktree remove --force 或其他强制删除。
任意检查或 Git 命令失败时,runtime 停止后续自动破坏动作。active binding 转为 needs_review。原本已经是 kept 或 needs_review 的 binding 保持其保留态,不虚构另一条状态迁移。运行时不尝试用更强命令“补救”,现存目录和 Git commit 留给人工检查。
13审计必须和状态迁移同事务单独向 events.jsonl 追加一行有两个问题:多进程写入顺序难以和 binding 更新原子对应;状态提交成功但日志写失败时,恢复后无法判断真实迁移。⌄
单独向 events.jsonl 追加一行有两个问题:多进程写入顺序难以和 binding 更新原子对应;状态提交成功但日志写失败时,恢复后无法判断真实迁移。
P18 在同一个 tasks.sqlite3 中增加两张表:
~~~text worktree_bindings 当前 binding 快照 worktree_events 按 sequence 追加的生命周期事实 ~~~
每一次数据库迁移都在同一个 BEGIN IMMEDIATE 事务中完成:
~~~text 读取并验证 Task + 当前 binding -> 条件 UPDATE binding 的预期旧状态 -> INSERT 对应 action/status 的 event -> COMMIT ~~~
如果 event insert 失败,binding 更新一起回滚。worktree_events 还安装了拒绝 UPDATE 和 DELETE 的 SQLite trigger,直接修改历史会明确失败。事件因此是 append-only 的审计事实,而不是“尽量写一下”的调试日志。
创建流程有两个事件也正是这个原因:reserve 记录跨系统操作开始前已经持久化的事实,create 只记录 Git Worktree 验证成功后的 active 状态。
14非 Git workspace 要在状态创建前失败P18 的真实入口只接受 Git 仓库根目录,不接受普通目录,也不接受某个大仓库中的任意子目录。⌄
P18 的真实入口只接受 Git 仓库根目录,不接受普通目录,也不接受某个大仓库中的任意子目录。
启动顺序保持明确:
~~~text 验证四项 OpenAI 配置 -> 构造无持久副作用的 SQLite store handle -> WorktreeRuntime 验证 cwd 是 Git repository root -> 组合共享 store 的 WorkStealingRuntime -> 构造 Agent Runner ~~~
缺配置时在网络和运行状态前退出;配置有效但 cwd 不是 Git 根目录时,抛出 WorktreeRepositoryError,且不创建 .agent_tutorial。
当前教程目录虽然已有 Git 元数据,但混有文章与全部章节代码,不适合直接作为 P18 的目标仓库。运行 P18 时应进入一个单独的 Git 仓库根目录,并把 .agent_tutorial/ 加入该仓库的 .gitignore。
15两个运行入口$TutorialCode = 'F:\笔记\Agent实操\code'⌄
先在教程代码目录同步锁定依赖:
~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location $TutorialCode npm ci ~~~
固定章节入口:
~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location 'C:\workspace\my-agent-project' npm exec --prefix $TutorialCode -- tsx "$TutorialCode/chapters/ch18/src/chapters/ch18.ts" --prompt "创建两个任务并绑定独立 Worktree,再启动 alice 和 bob 分别认领" ~~~
通用入口:
~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location 'C:\workspace\my-agent-project' npm exec --prefix $TutorialCode -- tsx "$TutorialCode/chapters/ch20/src/cli.ts" run --chapter 18 --prompt "创建两个任务并绑定独立 Worktree,再启动 alice 和 bob 分别认领" ~~~
npm exec --prefix 只从教程代码目录解析 tsx;Agent 的运行 cwd 仍是当前 Git 仓库根目录。真实文件写入、Git 命令和 Worktree 生命周期工具继续经过既有 Hook、权限与审批边界。
固定入口 [chapters/ch18.ts](code/chapters/ch18/src/chapters/ch18.ts) 只是把 P18 profile 交给 runProfile,与通用 CLI 共用 [cli.ts](code/chapters/ch18/src/cli.ts) 中的 execute() 装配过程;两种入口都会验证 Git 根目录、构造同一个共享 SQLite store,并让 WorktreeRuntime 同时成为唯一 claim service 和 ToolContextProvider。
16如何验证P18 使用真实临时 Git 仓库验证文件隔离和非强制清理,并用可注入 store、clock、模型与 Git runner 覆盖失败分支:⌄
P18 使用真实临时 Git 仓库验证文件隔离和非强制清理,并用可注入 store、clock、模型与 Git runner 覆盖失败分支:
- [逐工具动态 cwd 测试](code/chapters/ch18/tests/ch18-loop-workspace.test.ts)
- [SQLite binding 与真实 Git 生命周期测试](code/chapters/ch18/tests/ch18-worktrees.test.ts)
- [手动/自动认领统一路由测试](code/chapters/ch18/tests/ch18-claim-routing.test.ts)
- [Lead、Teammate、Subagent 组合测试](code/chapters/ch18/tests/ch18-bootstrap.test.ts)
- [固定入口与 CLI 共享 runtime 测试](code/chapters/ch18/tests/ch18-entry.test.ts)
运行聚焦验证:
~~~powershell Set-Location 'F:\笔记\Agent实操\code' npm run typecheck npm run test:ch18 npm run lint npm run format:check npm run build ~~~
关键断言包括:
- 两个 Worktree 写入同名文件时内容隔离,主目录不出现文件;
- create 后 Task 保持 pending,重启后 binding 与 reserve/create event 精确恢复;
- Git worktree add 失败保留 reserved binding 和 pending Task;
- 手动 claim 后同一 assistant 回复里的下一项写工具立即进入 Worktree;
- 自动 claim 跳过未绑定任务,并用 claim token 路由 cwd;
- 错 identity、错 task 和失效 token 在 handler 前失败,不能回落主目录;
- Lead 与 Subagent、Teammate 都写入受管目录,但只有 Lead 拥有三项管理工具;
- dirty、未集成、登记根不符、Git 非零和进程异常都保留目录并进入 needs_review;
- 已明确集成且 clean 的 Worktree 只用 detach、
branch -d和非强制 remove 清理; - 审计插入失败回滚 binding 迁移,直接 UPDATE/DELETE 审计表被 trigger 拒绝;
- 缺配置或非 Git cwd 在创建运行状态前失败;
- CLI 只构造一个共享 SQLite store,并让
WorktreeRuntime成为唯一 claim service。
17从 ai-agent-book 学到什么:文件系统隔离、冲突边界与证明式清理这一章与 ai-agent-book 第 10 章的失败模式分析直接对应。第 10 章把共享文件系统冲突分成文件级写入冲突和跨文件语义冲突。乐观锁只能防止同文件覆盖;Coding Agent 并行改代码库时,主流做法是给每个 Agent 独立 Git 分支或 worktree,把冲突推迟到合并点。P18 正是后一种方案:不再把 SQLite 任务锁扩展成逐文件版本号,而是让 claim 与受管 Worktree 绑定,在“谁拥有任务”之外再建立“这次工具写在哪里”的可信边界。⌄
这一章与 ai-agent-book 第 10 章的失败模式分析直接对应。第 10 章把共享文件系统冲突分成文件级写入冲突和跨文件语义冲突。乐观锁只能防止同文件覆盖;Coding Agent 并行改代码库时,主流做法是给每个 Agent 独立 Git 分支或 worktree,把冲突推迟到合并点。P18 正是后一种方案:不再把 SQLite 任务锁扩展成逐文件版本号,而是让 claim 与受管 Worktree 绑定,在“谁拥有任务”之外再建立“这次工具写在哪里”的可信边界。
它的 [实验 10-4](ai-agent-book/chapter10/parallel-web-research/README.md) 还展示了同构 worker 的并发收束方式。每个 worker 有独立 Playwright Chromium context;第一个 target_found 在 asyncio.Lock 下只结算一次,只广播一次 terminate,losing worker 在安全点取消、ack 并关闭资源。验收 manifest 会审计上下文创建/关闭计数、ack 集合和实测串并行耗时。P18 把其中“独立资源、单次结算、安全点清理、证据审计”的原则翻译到文件系统。SQLite BEGIN IMMEDIATE 与 claim token 保证一次 claim 只成功一次;remove_worktree 只有完成安全证明后才清理。任何失败都保留现场并转入 needs_review,而不是用 force 删除来假装成功。
| ai-agent-book 的设计 | P18 的对应 |
|---|---|
| 共享文件系统冲突分为文件级写入冲突和跨文件语义冲突 | claim 不解决文件覆盖问题,active binding 把每个任务路由到独立 Worktree,语义冲突留给 merge/review |
| 乐观锁只保护同一文件的版本一致性 | SQLite 锁只保护 claim/状态迁移;文件系统隔离覆盖整个受管目录,不做逐文件版本号 |
| 每个 worker 使用独立浏览器 context | 每个 active claim 使用独立 Git Worktree 和受控 cwd |
| 第一个成功在锁下 settle once,后续结果不重复结算 | BEGIN IMMEDIATE + 条件 UPDATE + claim token 历史保证一次 claim/完成只成功一次 |
| terminate 广播后等待安全点 ack 与资源关闭 | remove 前依次证明 completed、registered、clean、integrated;清理失败转 needs_review,不强制删除 |
| 错误隔离,一个网站失败不影响其他 worker | 一个 Worktree 的 dirty/未集成状态不阻塞其他绑定任务,独立保留待审查 |
| 真实运行记录 manifest,审计 context 创建/关闭与实测耗时 | 真实临时 Git 仓库测试 + SQLite binding/event 恢复测试,验证隔离、失败保留和清理证据 |
| 并行搜索需要级联终止 | P18 不做 terminate 广播;删除只由 Lead 显式发起,防止后台清扫误删未集成改动 |
这一点比“给 write_file 加锁”更接近 ai-agent-book 的结论。多 Agent 并行协作中,文件冲突不是只有同一个文件被覆盖这一种失败模式。P18 把任务所有权和文件写入位置拆成两条独立状态线,用 Worktree 隔离降低文件级冲突,再用 SQLite 状态机与审计事件保留冲突、失败和未集成证据。最终结果由人工 review 而不是运行时强制合并决定。
18前后版本的变化| 任务所有权 | SQLite claim token + 半开 lease | 完整保留 |⌄
| 维度 | 第 17 章 | 第 18 章 |
|---|---|---|
| 任务所有权 | SQLite claim token + 半开 lease | 完整保留 |
| 文件目录 | 所有执行者共享主 workspace | active claim 路由到受管 Worktree |
| Task schema | SQLite Task DAG | 不增加 worktree 字段 |
| 新持久状态 | Task、dependency、token history | 增加独立 binding 与 append-only event |
| 自动认领 | 所有 ready Task | 只认领 active binding 的 ready Task |
| 工具上下文 | 固定 workspace | 每项工具前重新解析可信 cwd |
| Lead 新工具 | 无 Worktree 管理工具 | create_worktree、keep_worktree、remove_worktree |
| 清理证明 | 不适用 | completed + registered + clean + explicitly integrated |
任务 DAG、Mailbox、typed Protocol、计划门控、权限、Hook、背景任务和恢复逻辑都没有另起一套实现。P18 只是把“谁能执行任务”和“这次工具应在哪里执行”接到同一个可信运行时边界上。
19本章明确不做什么- Task 内嵌 worktree 字段或 JSON binding 兼容层;⌄
P18 没有引入:
- Task 内嵌
worktree字段或 JSON binding 兼容层; - 进程级 cwd 切换、线程级
wt_ctx或 Prompt 驱动的手动cd; - 未绑定任务自动回落主 workspace;
@{push}、git branch -D、git worktree remove --force;- 自动 merge、cherry-pick、push 或冲突解决;
- Git 与 SQLite 跨系统 exactly-once 承诺;
- 失败后静默删除 reservation、binding、目录或审计记录;
- MCP 动态工具发现。
这些限制不是功能残缺,而是本章的所有权边界:运行时负责隔离、路由、证明和保留;代码如何 review、由谁集成,以及冲突如何解决,仍是显式协作流程。
20与 Claude Code 的差异参照 learn-claude-code/s18worktreeisolation/README.md 与 Claude Code 官方 Worktrees 文档 中深入 CC 源码的分析,P18 的 Worktree 隔离在教学简化上与 Claude Code 的真实机制存在以下差异:⌄
参照 learn-claude-code/s18_worktree_isolation/README.md 与 Claude Code 官方 Worktrees 文档 中深入 CC 源码的分析,P18 的 Worktree 隔离在教学简化上与 Claude Code 的真实机制存在以下差异:
隔离模型。 CC 的 EnterWorktree 通过 process.chdir() 切换会话级进程目录;AgentTool 的 worktree isolation 用 cwdOverridePath 包住子 agent。P18 不切换进程 cwd,而是在每次工具调用前通过 ToolContextProvider.resolve() 解析可信 workspace。
任务与 Worktree 关系。 CC 的 worktree 与 task 是两个独立系统,通过 PersistedWorktreeSession 和 transcript 关联,session 不含 taskId。P18 用独立 worktree_bindings 表通过 task_id 关联。Task、Claim、Binding 仍是三条独立状态线,创建 binding 不会推进 Task 状态。
状态与恢复。 CC 的 session state 以 worktree-state 类型写入 transcript。P18 把 binding 快照存在 SQLite,并维护 worktree_events 只追加审计表。binding 迁移与事件写入在同一事务提交;数据库 trigger 拒绝直接 UPDATE/DELETE 审计记录。
命名与路径。 CC 使用 .claude/worktrees/ 和 worktree-{slug} 分支,slug 规则为 [a-zA-Z0-9._-]。P18 使用 .agent_tutorial/worktrees/{name} 和 wt/{name} 分支,name 采用小写安全 Agent slug,并拒绝 Windows 保留路径组件。
基线来源。 CC 的 git worktree add -B 优先基于 origin/<defaultBranch>。P18 要求显式 integration_ref 必须是安全 refs/...,先 rev-parse 成不可变 commit,再执行 git worktree add -b ... <baseline>。
清理安全。 CC 的 remove 会检查未提交改动,必要时允许 discard_changes=true。P18 没有 discard_changes 或 force 选项。只有 completed、registered、clean 且 branch 顶端已包含在指定 integration ref 中时,才使用 detach、branch -d 和非强制 worktree remove;失败进入 needs_review。
认领路由。 CC 教学版通过 wt_ctx["path"] 字典切换 cwd,真实 CC 没有 task-worktree 绑定的 claim 路由。P18 的手动 claim_task、自动 claim_next、complete_task 共用同一 TaskClaimService。自动认领只选有 active binding 的 ready task,未绑定任务被跳过。
自动认领边界。 P18 的 executionScope 用 WeakMap 映射到 claim token。同一 assistant 回复中 claim 后,后续工具调用立即落到对应 Worktree。显式 claimToken 优先;失效 claim 明确失败,不回落主 workspace。
运行期锁定与清扫。 CC 会在 agent 运行期间执行 git worktree lock,会话结束后按 cleanupPeriodDays 清扫无改动或未推送提交的临时 worktree。P18 没有后台清扫,删除只由 Lead 显式发起,并依赖 SQLite binding 状态、审计事件和 Git 证据共同把关。
21小结Worktree 隔离真正困难的部分,不是执行一次 git worktree add,而是把它接进已有协作系统时仍然保持单一事实源:⌄
Worktree 隔离真正困难的部分,不是执行一次 git worktree add,而是把它接进已有协作系统时仍然保持单一事实源:
- Task、claim 与 binding 是三条独立状态线。
- 创建先 reserve,再执行 Git,失败证据可恢复。
- 手动认领、自动窃取和完成共用同一个
TaskClaimService。 - 每项工具在 Hook、权限和 handler 前重新解析可信 Execution Context。
- Lead、Teammate、Subagent 共用 provider,但生命周期工具只属于 Lead。
- 失效 claim 明确失败,绝不回落主 workspace。
- remove 只接受 clean、登记一致且已进入明确 integration ref 的分支顶端。
- 所有清理失败都停止强制动作;active 转入 needs_review,既有保留态维持不变。
- binding 迁移与 append-only event 在同一 SQLite 事务提交。
至此,Alice 和 Bob 不仅不会认领同一项任务,也不会在同一片文件空间互相覆盖。下一章再解决另一种静态边界:工具仍然在启动时写死,如何让外部能力通过 MCP 动态进入和退出当前工具池。
换个场景,你还会判断吗?
每题只测一个边界。先做决定,再看解释。
准备开始