diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs b/apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs index f615b2ddb..9ed36a8a9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs @@ -860,14 +860,11 @@ pub(crate) fn acquire_project_write_lock_failure( } } } - if crate::agent::autonomous_game_build_root_run_active_at(root) - && project_write_lock_is_owned_by_current_process(&path) - { - // The autonomous game-build lane intentionally permits - // parallel specialist actions. If the durable lock belongs - // to this very process, contention is an in-process overlap, - // not another application editing the project. Return an - // advisory guard and leave the real lock untouched. + if project_write_lock_is_owned_by_current_process(&path) { + // A project lock is the client-use lock. Nested calls in + // the same client process must reuse that ownership instead + // of waiting on their own durable marker. Cross-process + // contenders still take the normal retryable path. return Ok(ProjectWriteLock { path, content: String::new(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs index bd77f59e6..b109e7995 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project_tools.rs @@ -5611,12 +5611,14 @@ fn local_project_checkpoint_diff_restore_and_index_are_recorded() { } #[test] -fn project_write_lock_rejects_parallel_writer_and_releases_on_drop() { +fn project_write_lock_reuses_same_process_owner_and_releases_on_drop() { let root = unique_project_path(); let first = acquire_project_write_lock(&root, "file.write").expect("first lock"); - let error = acquire_project_write_lock(&root, "file.delete").expect_err("second lock fails"); - assert!(error.contains("项目正在被其他写操作占用")); + let nested = acquire_project_write_lock(&root, "file.delete") + .expect("same process must reuse the client project lock"); + drop(nested); + assert!(root.join(PROJECT_WRITE_LOCK_PATH).exists()); drop(first); acquire_project_write_lock(&root, "file.delete").expect("lock released"); diff --git a/docs/project-memory/plans/【实施计划】项目客户端占用锁收敛-2026-09-14.md b/docs/project-memory/plans/【实施计划】项目客户端占用锁收敛-2026-09-14.md new file mode 100644 index 000000000..f1ffb1b07 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】项目客户端占用锁收敛-2026-09-14.md @@ -0,0 +1,34 @@ +# 【实施计划】项目客户端占用锁收敛-2026-09-14 + +Version: 1 +Status: in-progress +Date: 2026-09-14 +Milestone: `【里程碑】项目客户端占用锁收敛-2026-09-14.md` + +## 代码边界 + +- `apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs` +- `apps/ai-game-creator-shell/src-tauri/src/runner/project_owner.rs` +- `apps/ai-game-creator-shell/src-tauri/src/runner/protocol.rs` +- 锁恢复、Runtime 写入和 Runner owner 定向测试 + +## 修改顺序 + +1. 统一同进程嵌套调用的项目锁语义,禁止自等待。 +2. 盘点并迁移 Runner 的项目级 owner 文件到统一锁,保留诊断投影与跨 boot 恢复。 +3. 删除重复项目级锁路径及其专属调用,保留底层原子写和 Git 锁。 +4. 补齐同进程重入、跨进程占用、崩溃恢复和锁释放测试。 + +## 验证命令 + +- `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` +- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_write_lock_reuses_same_process_owner_and_releases_on_drop --no-default-features` +- Runner owner 与 response stream 相关定向测试 +- `npm run check:encoding` +- `git diff --check` + +## 风险与回滚点 + +- Runner 与 GUI 可能是不同进程;统一锁前必须验证同一客户端不会互相阻塞。 +- 旧 `.agent/runtime/execution-owner.lock` 残留需要按 PID/启动身份安全回收,不能直接删除。 +- 若跨 boot 恢复或 GUI/Runner 联动回归,回滚统一路径迁移,保留已验证的同进程重入修复。 diff --git a/docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md b/docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md new file mode 100644 index 000000000..ed1c643a2 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md @@ -0,0 +1,28 @@ +# 【里程碑】项目客户端占用锁收敛-2026-09-14 + +Version: 1 +Status: in-progress +Date: 2026-09-14 +Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` + +## 目标 + +项目只保留一个面向客户端占用的项目级跨进程锁,防止多个客户端同时打开同一项目;同一客户端进程内的嵌套调用复用既有项目锁,不因自身持锁进入等待。 + +## 边界 + +- 项目客户端占用锁与项目写入调用的职责统一,跨进程竞争仍返回占用语义。 +- Agent DB、session lane、manifest 原子写和 Git 自身的底层一致性机制不在本里程碑删除范围内。 +- 不改变项目 revision、权限、幂等、恢复和数据格式合同。 + +## 验收标准 + +- 同一进程内嵌套取得项目锁立即返回 advisory guard,不等待、不删除真实持有者锁。 +- 不同进程持有项目锁时仍保持占用失败与残留回收判据。 +- 客户端项目占用入口与 Runtime 写入入口不会各自维护第二个项目级锁文件。 +- 锁释放后下一客户端可重新取得锁。 +- 定向 Rust 锁测试、`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。 + +## 未决事项 + +- Runner 的 `execution-owner.lock` 如何迁移到统一客户端占用锁,需要补充跨进程启动、恢复和诊断测试后再落地。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index fd13ced6e..48f5be252 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1363,3 +1363,8 @@ DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过 - **成功后行为**:重读 manifest,**不切换版本**(没有新版本可切),**不自动重载 / 重启运行中的预览**(PRD §3.2 末条),不做运行时资源重映射。可见变化只有资源卡「当前使用」高亮移到替换素材、`@` 面板「当前版本素材」更新。 - **已知代价(用户已确认接受)**:**替换历史不可回溯**——替换前身份只剩那条审计与 manifest 的 `.previous` 副本;需要"某版本历史上换过什么"时要另立切片(PRD §3.2 / §5.3 保留为未来合同正是为此)。 - **验证**:绑定改写通道定向 7 条(`project/manifest/version_binding_rewrite_tests.rs`)+ 替换定向 8 条 + `shared-contracts` 20 条。**变异验证**:① 去掉「未放行版本整条相等」→ 两周转红;② 放行集合改成整个版本数组 → 「未放行版本」转红;③ 去掉长度检查 → 「不增不删」转红;④ 准入删掉 category → 硬门禁与候选两条转红;另有 ⑤ 改回"原地改既有版本但绕过放行口"→ 被「项目版本记录写入后不可修改、删除或重排」拦下。前端 13 条(模型 9 + 真链路 4)。AGC 全量 1231 passed / 4 skipped / 0 failed;共享美术画布组件 1385 passed;`npm run ai-game-creator-shell:typecheck`(含 check-config)、`cargo check --locked --all-targets`、`npm run check:encoding`、`git diff --check` 全绿。`/api/external/v1`、SpacetimeDB schema、`packages/shared` 与 `shared-contracts` 的 wire DTO、manifest 结构、布局 sidecar schema 均未改动。 + +## 2026-09-14 项目客户端占用锁收敛 + +项目锁职责收敛为“客户端占用项目”这一事实:同一客户端进程内的嵌套项目写入调用复用已有项目锁并返回 advisory guard,不再等待自身持有的 `.agent/project.lock`;跨进程竞争继续沿用现有占用、残留回收和权限分类。Runner 的 `.agent/runtime/execution-owner.lock` 迁移到统一项目占用锁仍属于进行中的里程碑,完成前不改变其恢复诊断合同。 +