fix(subagents): queue root tool file leases
- queue root tool calls by declared or default footprint instead of rejecting sibling calls - keep parent/child cross-boundary write conflicts rejected - add cancellation, timeout, queue pumping, and regression coverage
This commit is contained in:
1 parent
b2d37472f0
commit
18aaca5843
4 files changed
+250
-11
No files matched your search
@@ -0,0 +1,55 @@
|
||||
# 子智能体文件租约:并发工具调用修复(2026-10-18)
|
||||
|
||||
## 用户可见问题
|
||||
|
||||
- 同一回合里模型并发发出多条命令时,其中一条会返回「相关文件正由其他任务修改,请等待子任务或当前文件操作结束后再执行。」,但当时没有任何别的任务在改文件,命令本身也大多是只读的(`dir`/`findstr`)。
|
||||
- 实测证据:用户会话 `39965c03-076b-4267-93dd-3eb45ba1386d`,原生转录 `%APPDATA%\YCode\omp-sessions\session-39965c03-076b-4267-93dd-3eb45ba1386d.jsonl`。
|
||||
- `09:00:47.937` `findstr /s /n …` 启动,`09:00:47.938` `dir /b src\renderer` 启动(相隔 1 ms),后者被判冲突。
|
||||
- `09:00:55.663` `dir /b /s … | findstr …` 与 `09:00:55.664` `powershell …` 同样只有一条成功,而两次里"胜者"相反 → 竞态而非固定顺序。
|
||||
- 该回合没有任何 `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`(HEAD `c7d6c55`)把非只读工具的租约路径取成 `'.'`(工作区根),并把它当独占锁;只要有任意租约与之重叠就返回错误。于是两种情况都会误判:并发兄弟调用互撞;子智能体持有文件租约时主智能体的所有命令被拒(子智能体可存活至 10 分钟超时)。
|
||||
|
||||
## 改动(`src/main/subagents/scheduler.ts`,`scripts/subagent-regression.ts`)
|
||||
|
||||
1. `leases: Map<string, string>` 保持为**子智能体声明文件**的独占租约(语义与改动前一致);新增 `rootIntents: Map<string, number>`(带计数)记录主智能体正在写入的具体文件。
|
||||
2. 主智能体侧新增按足迹的等待队列(`rootLeases` / `rootWaiters` / `acquireRootLease`):显式写工具(`write_file`/`edit_file`)取自身文件路径;其余工具(命令、连接器等)足迹未知,取工作区根。
|
||||
3. 冲突语义:
|
||||
- root ↔ root:**先到先得排队**,不再报错。
|
||||
- root 显式写 ↔ 子智能体声明文件:仍**拒绝**,文案改为可执行的「该文件正由子智能体修改,请先 wait_agent 或改用其他文件:<path>」。
|
||||
- 子智能体声明文件 ↔ 主智能体正在写该文件(写工具请求时即登记 `rootIntents`,含审批等待期):`spawn_agent` 被拒绝「该文件正在被主智能体写入,请等待该工具结束后再分配:<path>」。父子两个方向都互斥。
|
||||
- 未知足迹工具 ↔ 子智能体租约:不再互斥(子智能体运行期间主智能体仍可执行命令)。这是有意取舍,见限制。
|
||||
- 子智能体 ↔ 子智能体:保持原样,`spawn_agent` 阶段即拒绝。
|
||||
4. 等待有上限(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 结论。
|
||||
- 未重跑全部历史专项(沙盒等模块当时有并行修改),本轮只跑与本次改动直接相关的三组。
|
||||
@@ -1,5 +1,7 @@
|
||||
# YCode 接手状态(2026-10-15)
|
||||
|
||||
> 2026-10-18 子智能体文件租约并发修复:[实现、证据与限制](子智能体文件租约并发修复-2026-10-18.md)。修复同一 assistant 回合内并发宿主工具调用被误判为「相关文件正由其他任务修改」的问题:主智能体侧新增按足迹的先到先得等待队列(显式写工具取文件路径,命令等未知足迹取工作区根),root↔root 排队而非报错,等待有 10 分钟上限并随取消/超时推进队列;root 显式写↔子智能体声明文件、子智能体声明文件↔主智能体正在写的文件双向仍拒绝并给出可执行文案,子智能体↔子智能体保持 spawn 拒绝。用户会话 `39965c03` 原生转录显示两条命令相隔 1 ms 同时启动、后者被判冲突,属竞态。独立子代理评审发现并修掉父→子方向互斥缺失(P1)与出队后不推进队列(P2);`close()` 不清理 root 队列(P3)已记录未修。`npm run test:subagents` 40 项、`npm run test:subagents:app` 28 项(真实 Electron/OMP,0 renderer 错误)、typecheck、完整 build 通过。未重启用户安装版;未知足迹工具与子智能体租约不再互斥为有意取舍;`close()` 对卡死 worker 的等待仍无上限。
|
||||
|
||||
> 2026-10-18 v0.01 Windows测试分发包:[构建与限制](分发包-v0.01-2026-10-18.md)。用户指定文件名版本v0.01,内部SemVer改0.0.1;NSIS按用户安装、可选目录、保留卸载数据,产物`release/YCode-v0.01-Setup-x64.exe`(140.3MiB),附说明和SHA256SUMS。完整dist:installer及实际包内容/正式exe隔离启动10项通过,0未捕获renderer异常,已查看截图;报告`.tmp/release-v001-M4MP5b/report.json`。未签名、未测干净设备安装卸载/GPU全路径;OMP仍外部探测,个人配置/专家不随包供应。保留其他工作区修改,未提交、未外部发布。
|
||||
|
||||
> 2026-10-18 沙盒 Git 一次性对齐:[完整实现与限制](沙盒Git完整接入-2026-10-18.md)、[功能验收矩阵](沙盒Git接入验收矩阵-2026-10-18.md)。组件升级至 0.7.0 / protocol 1:内置 82 文件 MinGit,接入宿主普通仓库/linked worktree 历史和索引审批导入、本地常用 Git、项目+`.git` 原生组事务写回/恢复、HTTPS/SSH、DPAPI scoped 凭证、clone/fetch/pull/push/reconcile、维护 CLI 与生产 IPC。覆盖拒绝、并发、真实主进程死亡恢复、unknown push、TLS/host-key 校验、取消/策略撤销;本地29/29、安全25/25、远程40/40、生产 Electron/OMP 52/52、最终包内 Git20/20、旧兼容专项314项重新回归;`npm run build` 通过,总计480项。构建验证摘要见 `.tmp/git-complete-validation.json`,代码/日志保留在本工作区,未提交、未改用户真实仓库/远程或生产沙盒状态、未重启用户窗口。明确限制仍有标准用户/安装发布/独立安全审查、Git 大仓库/部分高级特性和原版 Git 运行时 1:1 证据。
|
||||
|
||||
Reference in new issue
Block a user