6.8 KiB
6.8 KiB
子智能体文件租约:并发工具调用修复(2026-10-18)
已被 2026-10-20 移除取代: 用户已要求移除文件所有权租约。移除记录。当前调度器不再维护
leases、rootIntents、rootLeases或等待队列;父子、兄弟调用和多个子智能体可同时声明同一文件。本文保留为历史实现与问题背景,不代表当前行为。
用户可见问题
- 同一回合里模型并发发出多条命令时,其中一条会返回「相关文件正由其他任务修改,请等待子任务或当前文件操作结束后再执行。」,但当时没有任何别的任务在改文件,命令本身也大多是只读的(
dir/findstr)。 - 实测证据:用户会话
39965c03-076b-4267-93dd-3eb45ba1386d,原生转录%APPDATA%\YCode\omp-sessions\session-39965c03-076b-4267-93dd-3eb45ba1386d.jsonl。09:00:47.937findstr /s /n …启动,09:00:47.938dir /b src\renderer启动(相隔 1 ms),后者被判冲突。09:00:55.663dir /b /s … | findstr …与09:00:55.664powershell …同样只有一条成功,而两次里"胜者"相反 → 竞态而非固定顺序。- 该回合没有任何
spawn_agent,所以与子智能体无关,是主模型自己在一轮里并行发了多条命令。
原因
src/main/agent/omp-turn.ts:149对每个host_tool_call起 fire-and-forget 协程,同一 assistant 消息里的多个宿主工具调用会真正并发执行;备用 HTTP 循环src/main/agent/loop.ts:144-162是串行的。- 改动前的
src/main/subagents/scheduler.ts(HEADc7d6c55)把非只读工具的租约路径取成'.'(工作区根),并把它当独占锁;只要有任意租约与之重叠就返回错误。于是两种情况都会误判:并发兄弟调用互撞;子智能体持有文件租约时主智能体的所有命令被拒(子智能体可存活至 10 分钟超时)。
改动(src/main/subagents/scheduler.ts,scripts/subagent-regression.ts)
leases: Map<string, string>保持为子智能体声明文件的独占租约(语义与改动前一致);新增rootIntents: Map<string, number>(带计数)记录主智能体正在写入的具体文件。- 主智能体侧新增按足迹的等待队列(
rootLeases/rootWaiters/acquireRootLease):显式写工具(write_file/edit_file)取自身文件路径;其余工具(命令、连接器等)足迹未知,取工作区根。 - 冲突语义:
- root ↔ root:先到先得排队,不再报错。
- root 显式写 ↔ 子智能体声明文件:仍拒绝,文案改为可执行的「该文件正由子智能体修改,请先 wait_agent 或改用其他文件:」。
- 子智能体声明文件 ↔ 主智能体正在写该文件(写工具请求时即登记
rootIntents,含审批等待期):spawn_agent被拒绝「该文件正在被主智能体写入,请等待该工具结束后再分配:」。父子两个方向都互斥。 - 未知足迹工具 ↔ 子智能体租约:不再互斥(子智能体运行期间主智能体仍可执行命令)。这是有意取舍,见限制。
- 子智能体 ↔ 子智能体:保持原样,
spawn_agent阶段即拒绝。
- 等待有上限(10 分钟,超时按可重试错误返回并带路径),取消或超时出队后会立即推进队列,避免泄漏的占用把后续无冲突工具一起挂住。
独立评审与处置
改动完成后由独立子代理(只读)审阅,发现并已修掉两处:
- P1(已修):第一版把主智能体租约只放进
rootLeases,导致「父先写、子后到」方向不再互斥(HEAD 原本双向互斥),同文件可能出现两个写入者。现在显式写工具在请求时登记rootIntents,acquire一并检查。 - P2(已修):等待者被取消或超时出队后没有推进队列,队首之后的不冲突等待者会被无谓阻塞。现在
onAbort/超时后都补一次pumpRootWaiters()。 - P3(未修,已记录):
close()/cancelAll()不清理模块级rootWaiters/rootLeases。现调用顺序(先abort再close,见chat-turn-service.ts:545-547)不会残留;若将来出现先 close 后 abort 的调用,排队者会存活到 10 分钟超时或回合结束后被授予。修复需要给 root 租约加 owner/run 标识。
方向参照(只读调研,非强制对齐):opencode 对同一文件的 edit/patch 采用按路径排队(packages/core/src/file-mutation.ts:59-78),不做拒绝式报错;本次与其方向一致,但保留了 YCode 自己的原子写与写前重读校验。
验证
| 验证 | 结果 |
|---|---|
npm run test:subagents |
40 项通过(新增 9 项:未知足迹兄弟调用串行不报错、排队者自动接续、并发三条都成功且同一时刻只有一条在跑、父写期间子智能体不能声明同一文件、父写正常完成、队首冲突时无关等待者排队、取消队首后无关等待者立刻接续、占用期间到来的调用排队、排队调用随取消以 AbortError 结束) |
npm run typecheck |
通过 |
npm run test:subagents:app |
28 项通过(真实 Electron + 已安装 OMP + 回环模型,0 renderer 错误);报告 .tmp/subagent-app-UpT2kI/report.json |
| 旧场景直接复现 | .tmp/lease-repro/repro.ts:同一回合并发 3 条未知足迹工具 + 1 次读取,修复前 2 条 BLOCKED,修复后 4 条全部 OK,命令串行(1207 ms ≈ 3 × 400 ms) |
npm run build |
通过(含 sandbox 原生构建);out/main/index.mjs 已确认不再包含旧文案 |
限制与未验证
- 未重启用户窗口:用户运行中的是 16:56 启动的安装版;安装目录
resources/app.asar仍是更早的构建,需要重新分发并重启才生效。 - 未知足迹工具(shell、连接器)与「子智能体正在写的文件」之间不再互斥:主智能体在子智能体运行期间执行会改文件的命令(格式化、
git checkout等)仍可能覆盖子智能体结果,靠wait_agent纪律约束;本次未做命令足迹静态分析。 close()对卡死 worker 的等待仍无上限(不在本次改动范围):若某运行时不响应取消,子智能体租约仍可能在进程内残留;等待队列有超时兜底,但租约本身不会自动清除。- 快速路径(无需排队时)不再二次检查
signal.aborted:取消恰好发生在进入执行之间时,工具会执行一次、结果被丢弃,与既有execute()的行为一致。 - 未做原版对照:WorkBuddy 没有子智能体文件租约概念,本项不产生 1:1 结论。
- 未重跑全部历史专项(沙盒等模块当时有并行修改),本轮只跑与本次改动直接相关的三组。