Direct 通道 agc_write_file 持续报「项目正在被其他写操作占用」:写入桥与 App 自身写操作争用同一把不可重入的 .agent/project.lock #318

Closed
opened 2026-09-10 11:57:30 +08:00 by suzmii · 0 comments
Member

1. 现象

AGC 新建项目后,在该项目的第一轮 Direct 对话里,唯一的项目写入通道 agc_write_file 每次都失败

项目正在被其他写操作占用:<项目根>\.agent\project.lock

同时:

  • 只读工具正常(agc_list_registered_assetsagc_list_project_filesclient_session_info 都能返回)。
  • 连续重试 9 次(含并行)全部同一结果,没有一次成功。
  • agc_list_registered_assets 返回 pendingOperations: []
  • 结果是整轮无法写入任何项目文件,只能以阻断收场。

说明:pendingOperations 只来自 list_pending_local_project_resource_edits_atdirect_tool_bridge.rs:1241-1262),只表示没有在跑的付费生成/派生操作,与项目写锁无关,不构成"锁没有持有者"的证据。

2. 实测证据(本机复现现场,非推测)

现场:项目 C:\Users\dongy\Documents\Genarrative GameAgent\gameagent-13951477路径含空格),App 由 .worktrees/agc-canvas-v3 的 debug 构建启动。

2.1 时间线(来自客户端自己的 turn 记录与文件落盘时间)

时刻 事实 来源
11:29:16-17 项目创建,脚手架落盘(game/package-lock.json 目录 mtime
11:29:17 turn c6340875… 开始(recordedAtMs=1789010957683 .agent/runtime/direct-codex/turns/
11:32:50-11:34:14 该轮 15 次 agc_write_file 全部 status=failed,每次 durationMs24-42ms 同上
11:34:28 turn 结束(turn_endcompleted:truefirstDesign=write:game/src/config.js 同上
11:43:26 用户「你再试试」触发 turn 094d02c1…;同一秒 .agent/runtime/locks/direct-codex-art.lock 被 pid 57068 创建 turn 记录 + 锁文件内容
11:43:33-11:43:48 该轮 4 次写入继续失败(25-27ms) turn 记录
11:43:57 最后一次探针 probe-4 落盘成功game/src/config.js = probe-4\n),同时 turn 结束、project-revision.json 更新 文件 mtime + project-revision.json
11:47 复查:.agent\project.lock 不存在.agent 目录可写,project.lock 可正常 CreateNew 创建并删除 本机实测

关键点:每次失败都在 24-42ms 内返回,而仓库其它写入口用的是有界等待(project_gates.rs:1818-1859,2000×5ms≈10s)。也就是说 Direct 写路径根本没有等,任何重叠都直接被判死。

2.2 这不是残留锁,也不是 ACL 问题

事后复查(11:47):

  • .agent\project.lock 不存在;
  • .agent 目录 ACL 只有 WIN11_SUZUMIYA\dongy: FullControl(非继承),读写正常;
  • CreateNew 实测能立刻创建 project.lock 并删除;
  • C:\Users\dongy\Documents 不是 reparse point。

所以「残留锁 / 权限被拒」都被排除;锁是在失败窗口内被真实持有的,并在 11:43:57 前后释放。

2.3 App 自身的进程拓扑(App 是多进程,锁是同一 App 内部在争)

57068 genarrative-ai-game-creator-shell.exe            (GUI,target\debug)
├── 52032 同二进制 --agent-runner --gui-owner-required  (runner,监听 127.0.0.1:57131)
├── 2408  codex.exe app-server --stdio
└── 49948 codex.exe app-server --stdio
    └── 27680 同二进制 --agc-direct-tools-mcp           (agc_tools MCP 进程)
  • agc_tools 的 MCP 进程(27680)只是把调用转发到 App 自己的工具桥 HTTP 端点(GENARRATIVE_AGC_TOOL_BRIDGE_URL,由拉起 codex 的进程写入,见 codex_app_server.rs:2151;该进程按进程树是 GUI 57068)。
  • 也就是说:agc_write_file 的落盘与取锁发生在 App 自己的进程里,不是外部程序。
  • 同一时刻 direct-codex-art.lock 的持有者 pid 57068,与桥所在进程一致 ——「被其他写操作占用」里的"其他写操作"就是这个 App 自己的另一个写通道/另一轮工作

2.4 附带发现的锁文件残留

.agent\.manifest.json.lock(0 字节,11:29:17 创建)在项目里一直存在;同类 0 字节 *.lockgameagent-32983816gameagent-b9c2b569 也有。说明"锁文件生命周期"在 App 内普遍缺乏收口,用户看到"有锁文件"是合理怀疑对象。

3. 代码核对结果(静态,已定位到行)

事实 位置
agc_write_file → App 工具桥 → bridge_write_file apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs:468.../direct_tool_bridge.rs:2310
取锁点(零等待) .../direct_tool_bridge.rs:1476 acquire_project_write_lock(root, "direct-codex.file.write")
锁实现:create_new 独占创建 .agent/project.lock .../project/filesystem.rs:238-245
争用错误文案 .../project/filesystem.rs:306-308
同进程豁免只在 autonomous 通道生效 .../project/filesystem.rs:290-305(需 autonomous_game_build_root_run_active_at + 锁属本进程)
同进程第二次取锁必失败(不可重入) .../tests/project_tools.rs:5614
有界等待入口(其它写入口在用) .../agent/runtime_actions/project_gates.rs:1818-1881
Windows 5/32/33 与 PermissionDenied 一律归类为"争用" .../project/filesystem.rs:155-168
回收条件(pid 存活则永不回收) .../project/filesystem.rs:133-153

由此得到三条推断(未逐一在运行时证死,但与本机证据一致):

  1. 主因(推断):Direct 通道的写入桥与 App 自身的其它写操作(美术包 lane、revision、conversation、预览、checkpoint 等)都在同一个 App 进程里,文件锁不可重入,同进程第二次取锁与"外部程序正在写"在实现上不可区分;而 filesystem.rs:290-305 的同进程豁免只覆盖 autonomous 通道,Direct 通道拿不到 → 必然报「项目正在被其他写操作占用」,且与用户是否真的开了别的程序无关。
  2. 放大器(已实测)bridge_write_file 没有走有界等待,24-42ms 就返回失败;同等争用在其它通道会被等待窗口吸收。
  3. 不可诊断(已实测):错误文案不携带 commandId / pid / createdAt / ownerIsSelf,且把 ACL 拒绝、delete-pending、真实跨进程争用都压成同一句话;用户只能看到"有锁",看不到"谁在持锁",于是被合理误判为残留锁。

4. 影响

  • Direct 通道在受影响项目上完全失去写入能力,且用户没有任何自救手段(重试无效、没有清锁入口、.agent 属客户端控制面 Agent 不应自行删改)。
  • 现象可稳定复现为"新建项目后的第一轮",属于主链路阻断级别。
  • 排障成本高:读路径正常、错误文案指向"别人在写",把维护者引向错误方向(本次现场就被误判为残留锁)。
  • 同类家族已有前科:锁不可重入误判(docs/project-memory/shared-memory/pitfalls.md 的 planning v2 条目)、project.lock 触发 Windows UAC(提交 4127686e1)、同一句文案承载多种原因。

5. 建议修复方向

本次诉求:这条链路本来就全在客户端进程内(MCP 工具 → App 工具桥 → 落盘),为什么还要用文件锁?直接用进程内 mutex/串行化就行。

  1. 主修:把项目级串行化从"文件锁"改成"进程内串行化 + 显式跨进程边界"。
    • 同一 App 进程内的所有项目写入,用按项目粒度的进程内互斥(Mutex/异步串行队列)串行化:可重入可判定、不会留下锁文件、不会把"自己人"报成"别人"。
    • 只有确实需要跨进程互斥的场景(比如 runner 与 GUI 分立、将来允许多实例同时开同一项目),才保留跨进程锁;并且这时必须能报出真实持有者。
  2. 次修:Direct 写路径纳入统一的有界等待,语义与其它写入口一致(当前它零等待,是本次失败的放大器);同一轮并行的多个文件写应排队/串行,而不是报占用。
  3. 可诊断性:文案与分类必须做真。
    • 至少带上 commandId / pid / createdAtownerIsSelf,让用户和维护者一眼看出是谁在持锁;
    • 把 ACL/权限拒绝、delete-pending、真实跨进程争用拆成不同文案与不同处置,不要共用「项目正在被其他写操作占用」;
    • 注意 docs/project-memory/shared-memory/decision-log.md 要求错误里不得回传项目绝对路径,现有 redact_agent_runtime_error 已覆盖(用例 tests/project_tools.rs:5807),改造时不要破坏脱敏。
  4. 顺带收口锁文件生命周期.agent/.manifest.json.lock 这类 0 字节残留锁不应跨会话长期留存。

6. 验收判据

  • 新建项目后第一轮 Direct 对话:连续多次 agc_write_file 成功,包含同一轮并行写多个文件。
  • App 自身有其它写通道正在该项目工作时,agc_write_file 要么正常等待后成功,要么报出明确可行动的错误(带持有者身份),不再出现"没有其它程序在写却报被其他写操作占用"。
  • 权限拒绝场景不再投影为"被其他写操作占用"。
  • 定向用例覆盖:同进程重叠写、同轮并行写、真实外部进程持锁、ACL 拒绝、残留锁回收。

7. 不做项 / 边界

  • 不放宽 .agent/project.lock 的项目级串行化语义,也不放宽 AGC 的私有 ACL 边界。
  • 不改 pendingOperations 语义(与本问题无关)。
  • 本次不改 AGC 多进程拓扑本身(GUI/runner/MCP 进程拆分是既成架构),只要求"串行化原语与拓扑匹配"。

8. 取证脚本(供维护者复核)

$root = 'C:\Users\dongy\Documents\Genarrative GameAgent\gameagent-13951477'
# 1) 锁文件与目录权限(失败瞬间更好)
Test-Path -LiteralPath "$root\.agent\project.lock"
Get-ChildItem -LiteralPath "$root\.agent" -Force | Select-Object Name,Length,LastWriteTime
(Get-Acl -LiteralPath "$root\.agent").Access | Select-Object IdentityReference,FileSystemRights,IsInherited
# 2) 写入能力探针(创建后立即删除)
$p = Join-Path "$root\.agent" 'project.lock'
$fs = [System.IO.File]::Open($p, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None); $fs.Dispose(); [System.IO.File]::Delete($p)
# 3) 客户端自己的证据:每轮 turn 记录里 agc_write_file 的 status 与 durationMs
Get-ChildItem -LiteralPath "$root\.agent\runtime\direct-codex\turns" -Force | ForEach-Object { Get-Content -LiteralPath $_.FullName }
# 4) 进程拓扑(确认取锁进程与 App 同源)
Get-CimInstance Win32_Process -Filter "Name like '%game-creator%' or Name like '%codex%'" |
  Select-Object ProcessId,ParentProcessId,Name,CommandLine

9. 待补充

  • 出问题的 App commit / 构建时间(现场为 .worktrees/agc-canvas-v3 debug 构建)
  • 失败瞬间 .agent/project.lock 的实际内容(commandId / pid / createdAt)——本次锁文件在复查前已被释放删除,未能取到
  • 该轮持锁方是否为美术包 lane(direct-codex-art)或其它通道(turn 记录未直接给出持锁命令)
  • 同类问题在 autonomous 通道是否也会出现(该通道有同进程豁免,预计不会)
## 1. 现象 AGC 新建项目后,在该项目的第一轮 Direct 对话里,唯一的项目写入通道 `agc_write_file` **每次都失败**: ``` 项目正在被其他写操作占用:<项目根>\.agent\project.lock ``` 同时: - 只读工具正常(`agc_list_registered_assets`、`agc_list_project_files`、`client_session_info` 都能返回)。 - 连续重试 9 次(含并行)**全部同一结果**,没有一次成功。 - `agc_list_registered_assets` 返回 `pendingOperations: []`。 - 结果是整轮无法写入任何项目文件,只能以阻断收场。 > 说明:`pendingOperations` 只来自 `list_pending_local_project_resource_edits_at`(`direct_tool_bridge.rs:1241-1262`),只表示没有在跑的付费生成/派生操作,**与项目写锁无关**,不构成"锁没有持有者"的证据。 ## 2. 实测证据(本机复现现场,非推测) 现场:项目 `C:\Users\dongy\Documents\Genarrative GameAgent\gameagent-13951477`(**路径含空格**),App 由 `.worktrees/agc-canvas-v3` 的 debug 构建启动。 ### 2.1 时间线(来自客户端自己的 turn 记录与文件落盘时间) | 时刻 | 事实 | 来源 | | --- | --- | --- | | 11:29:16-17 | 项目创建,脚手架落盘(`game/`、`package-lock.json`) | 目录 mtime | | 11:29:17 | turn `c6340875…` 开始(`recordedAtMs=1789010957683`) | `.agent/runtime/direct-codex/turns/` | | 11:32:50-11:34:14 | 该轮 **15 次** `agc_write_file` 全部 `status=failed`,每次 `durationMs` 仅 **24-42ms** | 同上 | | 11:34:28 | turn 结束(`turn_end`,`completed:true`,`firstDesign=write:game/src/config.js`) | 同上 | | 11:43:26 | 用户「你再试试」触发 turn `094d02c1…`;同一秒 `.agent/runtime/locks/direct-codex-art.lock` 被 pid **57068** 创建 | turn 记录 + 锁文件内容 | | 11:43:33-11:43:48 | 该轮 4 次写入继续失败(25-27ms) | turn 记录 | | 11:43:57 | 最后一次探针 `probe-4` **落盘成功**(`game/src/config.js` = `probe-4\n`),同时 turn 结束、`project-revision.json` 更新 | 文件 mtime + `project-revision.json` | | 11:47 | 复查:`.agent\project.lock` **不存在**;`.agent` 目录可写,`project.lock` 可正常 `CreateNew` 创建并删除 | 本机实测 | **关键点:每次失败都在 24-42ms 内返回**,而仓库其它写入口用的是有界等待(`project_gates.rs:1818-1859`,2000×5ms≈10s)。也就是说 Direct 写路径**根本没有等**,任何重叠都直接被判死。 ### 2.2 这不是残留锁,也不是 ACL 问题 事后复查(11:47): - `.agent\project.lock` 不存在; - `.agent` 目录 ACL 只有 `WIN11_SUZUMIYA\dongy: FullControl`(非继承),读写正常; - 用 `CreateNew` 实测能立刻创建 `project.lock` 并删除; - `C:\Users\dongy\Documents` 不是 reparse point。 所以「残留锁 / 权限被拒」都被排除;锁是在**失败窗口内被真实持有的**,并在 11:43:57 前后释放。 ### 2.3 App 自身的进程拓扑(App 是多进程,锁是同一 App 内部在争) ``` 57068 genarrative-ai-game-creator-shell.exe (GUI,target\debug) ├── 52032 同二进制 --agent-runner --gui-owner-required (runner,监听 127.0.0.1:57131) ├── 2408 codex.exe app-server --stdio └── 49948 codex.exe app-server --stdio └── 27680 同二进制 --agc-direct-tools-mcp (agc_tools MCP 进程) ``` - `agc_tools` 的 MCP 进程(27680)只是把调用转发到 App 自己的工具桥 HTTP 端点(`GENARRATIVE_AGC_TOOL_BRIDGE_URL`,由拉起 codex 的进程写入,见 `codex_app_server.rs:2151`;该进程按进程树是 GUI 57068)。 - 也就是说:**`agc_write_file` 的落盘与取锁发生在 App 自己的进程里**,不是外部程序。 - 同一时刻 `direct-codex-art.lock` 的持有者 pid **57068**,与桥所在进程一致 ——「被其他写操作占用」里的"其他写操作"就是**这个 App 自己的另一个写通道/另一轮工作**。 ### 2.4 附带发现的锁文件残留 `.agent\.manifest.json.lock`(0 字节,11:29:17 创建)在项目里一直存在;同类 0 字节 `*.lock` 在 `gameagent-32983816`、`gameagent-b9c2b569` 也有。说明"锁文件生命周期"在 App 内普遍缺乏收口,用户看到"有锁文件"是合理怀疑对象。 ## 3. 代码核对结果(静态,已定位到行) | 事实 | 位置 | | --- | --- | | `agc_write_file` → App 工具桥 → `bridge_write_file` | `apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs:468`、`.../direct_tool_bridge.rs:2310` | | **取锁点(零等待)** | `.../direct_tool_bridge.rs:1476` `acquire_project_write_lock(root, "direct-codex.file.write")` | | 锁实现:`create_new` 独占创建 `.agent/project.lock` | `.../project/filesystem.rs:238-245` | | 争用错误文案 | `.../project/filesystem.rs:306-308` | | 同进程豁免只在 autonomous 通道生效 | `.../project/filesystem.rs:290-305`(需 `autonomous_game_build_root_run_active_at` + 锁属本进程) | | 同进程第二次取锁必失败(不可重入) | `.../tests/project_tools.rs:5614` | | 有界等待入口(其它写入口在用) | `.../agent/runtime_actions/project_gates.rs:1818-1881` | | Windows 5/32/33 与 `PermissionDenied` 一律归类为"争用" | `.../project/filesystem.rs:155-168` | | 回收条件(pid 存活则永不回收) | `.../project/filesystem.rs:133-153` | 由此得到三条**推断**(未逐一在运行时证死,但与本机证据一致): 1. **主因(推断)**:Direct 通道的写入桥与 App 自身的其它写操作(美术包 lane、revision、conversation、预览、checkpoint 等)都在同一个 App 进程里,文件锁不可重入,同进程第二次取锁与"外部程序正在写"在实现上不可区分;而 `filesystem.rs:290-305` 的同进程豁免只覆盖 autonomous 通道,Direct 通道拿不到 → 必然报「项目正在被其他写操作占用」,且与用户是否真的开了别的程序无关。 2. **放大器(已实测)**:`bridge_write_file` 没有走有界等待,24-42ms 就返回失败;同等争用在其它通道会被等待窗口吸收。 3. **不可诊断(已实测)**:错误文案不携带 `commandId / pid / createdAt / ownerIsSelf`,且把 ACL 拒绝、delete-pending、真实跨进程争用都压成同一句话;用户只能看到"有锁",看不到"谁在持锁",于是被合理误判为残留锁。 ## 4. 影响 - Direct 通道在受影响项目上**完全失去写入能力**,且用户没有任何自救手段(重试无效、没有清锁入口、`.agent` 属客户端控制面 Agent 不应自行删改)。 - 现象可稳定复现为"新建项目后的第一轮",属于主链路阻断级别。 - 排障成本高:读路径正常、错误文案指向"别人在写",把维护者引向错误方向(本次现场就被误判为残留锁)。 - 同类家族已有前科:锁不可重入误判(`docs/project-memory/shared-memory/pitfalls.md` 的 planning v2 条目)、`project.lock` 触发 Windows UAC(提交 `4127686e1`)、同一句文案承载多种原因。 ## 5. 建议修复方向 本次诉求:**这条链路本来就全在客户端进程内(MCP 工具 → App 工具桥 → 落盘),为什么还要用文件锁?直接用进程内 mutex/串行化就行。** 1. **主修:把项目级串行化从"文件锁"改成"进程内串行化 + 显式跨进程边界"。** - 同一 App 进程内的所有项目写入,用按项目粒度的进程内互斥(`Mutex`/异步串行队列)串行化:可重入可判定、不会留下锁文件、不会把"自己人"报成"别人"。 - 只有确实需要跨进程互斥的场景(比如 runner 与 GUI 分立、将来允许多实例同时开同一项目),才保留跨进程锁;并且这时必须能报出真实持有者。 2. **次修:Direct 写路径纳入统一的有界等待**,语义与其它写入口一致(当前它零等待,是本次失败的放大器);同一轮并行的多个文件写应排队/串行,而不是报占用。 3. **可诊断性:文案与分类必须做真。** - 至少带上 `commandId / pid / createdAt` 与 `ownerIsSelf`,让用户和维护者一眼看出是谁在持锁; - 把 ACL/权限拒绝、delete-pending、真实跨进程争用拆成不同文案与不同处置,不要共用「项目正在被其他写操作占用」; - 注意 `docs/project-memory/shared-memory/decision-log.md` 要求错误里不得回传项目绝对路径,现有 `redact_agent_runtime_error` 已覆盖(用例 `tests/project_tools.rs:5807`),改造时不要破坏脱敏。 4. **顺带收口锁文件生命周期**:`.agent/.manifest.json.lock` 这类 0 字节残留锁不应跨会话长期留存。 ## 6. 验收判据 - 新建项目后第一轮 Direct 对话:连续多次 `agc_write_file` 成功,包含同一轮并行写多个文件。 - App 自身有其它写通道正在该项目工作时,`agc_write_file` 要么正常等待后成功,要么报出**明确可行动的**错误(带持有者身份),不再出现"没有其它程序在写却报被其他写操作占用"。 - 权限拒绝场景不再投影为"被其他写操作占用"。 - 定向用例覆盖:同进程重叠写、同轮并行写、真实外部进程持锁、ACL 拒绝、残留锁回收。 ## 7. 不做项 / 边界 - 不放宽 `.agent/project.lock` 的项目级串行化语义,也不放宽 AGC 的私有 ACL 边界。 - 不改 `pendingOperations` 语义(与本问题无关)。 - 本次不改 AGC 多进程拓扑本身(GUI/runner/MCP 进程拆分是既成架构),只要求"串行化原语与拓扑匹配"。 ## 8. 取证脚本(供维护者复核) ```powershell $root = 'C:\Users\dongy\Documents\Genarrative GameAgent\gameagent-13951477' # 1) 锁文件与目录权限(失败瞬间更好) Test-Path -LiteralPath "$root\.agent\project.lock" Get-ChildItem -LiteralPath "$root\.agent" -Force | Select-Object Name,Length,LastWriteTime (Get-Acl -LiteralPath "$root\.agent").Access | Select-Object IdentityReference,FileSystemRights,IsInherited # 2) 写入能力探针(创建后立即删除) $p = Join-Path "$root\.agent" 'project.lock' $fs = [System.IO.File]::Open($p, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None); $fs.Dispose(); [System.IO.File]::Delete($p) # 3) 客户端自己的证据:每轮 turn 记录里 agc_write_file 的 status 与 durationMs Get-ChildItem -LiteralPath "$root\.agent\runtime\direct-codex\turns" -Force | ForEach-Object { Get-Content -LiteralPath $_.FullName } # 4) 进程拓扑(确认取锁进程与 App 同源) Get-CimInstance Win32_Process -Filter "Name like '%game-creator%' or Name like '%codex%'" | Select-Object ProcessId,ParentProcessId,Name,CommandLine ``` ## 9. 待补充 - [ ] 出问题的 App commit / 构建时间(现场为 `.worktrees/agc-canvas-v3` debug 构建) - [ ] 失败瞬间 `.agent/project.lock` 的实际内容(`commandId / pid / createdAt`)——本次锁文件在复查前已被释放删除,未能取到 - [ ] 该轮持锁方是否为美术包 lane(`direct-codex-art`)或其它通道(turn 记录未直接给出持锁命令) - [ ] 同类问题在 autonomous 通道是否也会出现(该通道有同进程豁免,预计不会)
suzmii added the
Priority
High
2
Kind/Bug
labels 2026-09-10 11:57:30 +08:00
suzmii self-assigned this 2026-09-10 12:00:49 +08:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#318