From 4e553bb15d095b7bd89c6b96c81e4bed835c8002 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 14 Sep 2026 15:41:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=AA=84=E9=A1=B9=E7=9B=AE=E5=86=99?= =?UTF-8?q?=E9=94=81=E5=90=8C=E8=BF=9B=E7=A8=8B=E5=A4=8D=E7=94=A8=E5=88=A4?= =?UTF-8?q?=E6=8D=AE=E4=B8=BA=E5=90=8C=E7=BA=BF=E7=A8=8B=E9=87=8D=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 write_lock.rs:advisory 复用判据从「本进程持锁」收窄为「同一线程重入或自主流水线」,新增按锁路径登记真实持锁线程的 PROJECT_WRITE_LOCK_THREAD_OWNERS,登记在 create_new 成功处、在 guard Drop 里按路径注销(guard 会被移到别的线程再 Drop) - 恢复本进程其它线程写通道的串行化:一致快照读、project.diff / action_history、command.output_read、steer 序号分配、项目 revision 侧车、pending sidecar 复核与恢复安装重新等待并保持终态占用 - 调整 tests/project_tools.rs 的 agent_runtime_file_write_lock_failure_redacts_project_path:改为在另一条线程持锁,保持「别的写者持锁时 file.write 失败关闭且脱敏」的断言语义 - 调整 ui_editor/persistence.rs 的 recovery_install_respects_the_project_write_lock:同样改为在另一条线程持锁,保持占用失败断言 - 同步技术方案「2026-09-14 项目客户端占用锁收敛」段落,以及里程碑和实施计划的目标、验收标准、修改顺序、验证命令与未决事项 - 在 decision-log 记录复用判据收窄为同线程重入,并在 pitfalls 记录「同进程复用判据不能只看 pid」的现场、原因、处理与易错点 - 回答 AGC 生命周期文档「同 PID advisory guard 是否放过并行写」的未决项:确认会放过并行写,已收窄 Co-authored-by: DotCraft <273930855+dotcraft-ai@users.noreply.github.com> --- .../src-tauri/src/project/write_lock.rs | 71 ++++++++++++++++--- .../src-tauri/src/tests/project_tools.rs | 26 ++++++- .../src-tauri/src/ui_editor/persistence.rs | 27 ++++++- ...施计划】项目客户端占用锁收敛-2026-09-14.md | 11 +-- ...里程碑】项目客户端占用锁收敛-2026-09-14.md | 10 +-- .../shared-memory/decision-log.md | 7 ++ docs/project-memory/shared-memory/pitfalls.md | 9 +++ ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- ...】AGC客户端稳定版生命周期大切换-2026-09-14.md | 2 +- 9 files changed, 138 insertions(+), 27 deletions(-) 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 9ed36a8a9..bdf5bd33d 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 @@ -14,16 +14,59 @@ const PROJECT_WRITE_LOCK_UNWRITTEN_GRACE_SECONDS: u64 = 30; const PROJECT_WRITE_LOCK_PID_REUSE_TOLERANCE_SECONDS: u64 = 5; const PROJECT_WRITE_LOCK_MAX_BYTES: u64 = 4 * 1024; +/// 本进程内真正落盘持有项目写锁的线程登记表。 +/// +/// `.agent/project.lock` 的 `pid` 只能证明“锁由本进程的某条写通道持有”,它分不清 +/// 两种完全不同的局面: +/// - **同一条调用链再次取锁**:持锁方就是自己,必须放行,否则每次嵌套项目写入都要 +/// 白等一个等待预算再报“项目正在被其他写操作占用”; +/// - **本进程另一条写通道正在写**:项目 revision 侧车、steer 序号、一致快照读、 +/// pending sidecar 复核和恢复安装都靠这把锁串行化,必须照旧等待。 +/// +/// 复用判据因此不能停在 `pid`:只有**当前线程**就是真实持锁线程时才返回 advisory +/// guard,本进程其余争用继续走有界等待与终态占用。登记按路径进行、按路径注销: +/// guard 可能被移到别的线程再 Drop(例如写入路径把锁交给阻塞线程池的持有者), +/// 按线程注销会漏项,让后续的重入判断失真。 +static PROJECT_WRITE_LOCK_THREAD_OWNERS: std::sync::Mutex> = + std::sync::Mutex::new(Vec::new()); + +fn project_write_lock_thread_owners( +) -> std::sync::MutexGuard<'static, Vec<(PathBuf, std::thread::ThreadId)>> { + // 登记表只是复用判据的加速器:中毒时继续用内部值,不能让一次取锁失败升级成 + // 整个进程再也写不了项目。 + PROJECT_WRITE_LOCK_THREAD_OWNERS + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +fn register_project_write_lock_thread_owner(path: &Path) { + let mut owners = project_write_lock_thread_owners(); + if owners.iter().any(|(owner, _)| owner == path) { + return; + } + owners.push((path.to_path_buf(), std::thread::current().id())); +} + +fn unregister_project_write_lock_thread_owner(path: &Path) { + project_write_lock_thread_owners().retain(|(owner, _)| owner != path); +} + +/// 当前线程是否就是这条锁路径上真实落盘的持有者(同线程重入)。 +fn project_write_lock_reentered_by_current_thread(path: &Path) -> bool { + let thread = std::thread::current().id(); + project_write_lock_thread_owners() + .iter() + .any(|(owner, owner_thread)| owner == path && *owner_thread == thread) +} + #[derive(Debug)] pub(crate) struct ProjectWriteLock { path: PathBuf, content: String, - /// In the free-form autonomous lane a single Runtime process may have - /// several specialist actions in flight at once. A file lock is still - /// useful across processes, but making same-process contenders fail turns - /// ordinary parallel work into a dead run (and can deadlock nested tool - /// calls). Such a contender receives an in-process/advisory guard instead - /// of deleting the real holder's lock on drop. + /// 两种“本进程持锁但不必自等”的争用会拿到 advisory guard:同一线程重入(同一条 + /// 调用链再次取锁)和自主游戏构建流水线(它有意让并行专家动作同时在飞)。这两种 + /// 情况下争用是进程内重叠而不是另一个客户端在改项目,返回的 guard 不拥有 + /// `.agent/project.lock`,Drop 时也不得删除真实持有者的锁。 bypassed_same_process: bool, } @@ -47,6 +90,7 @@ impl Drop for ProjectWriteLock { if self.bypassed_same_process { return; } + unregister_project_write_lock_thread_owner(&self.path); if fs::read_to_string(&self.path).is_ok_and(|content| content == self.content) { let _ = fs::remove_file(&self.path); } @@ -815,6 +859,7 @@ pub(crate) fn acquire_project_write_lock_failure( path.display() ))); } + register_project_write_lock_thread_owner(&path); return Ok(ProjectWriteLock { path, content: content.clone(), @@ -860,11 +905,15 @@ pub(crate) fn acquire_project_write_lock_failure( } } } - 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. + if project_write_lock_is_owned_by_current_process(&path) + && (crate::agent::autonomous_game_build_root_run_active_at(root) + || project_write_lock_reentered_by_current_thread(&path)) + { + // 持锁方就是本进程自己时必须区分重入与并发:同一条调用链(同一 + // 线程)再次取锁,以及自主流水线有意并行专家动作,返回 advisory + // guard、不自等、不动真实锁;本进程**其它线程**正在写则继续走 + // 有界等待,保住 revision 侧车、steer 序号、一致快照读与恢复安装 + // 的串行化。 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 7e39fccbe..8709bc5b0 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 @@ -5818,8 +5818,27 @@ async fn agent_runtime_file_write_lock_failure_redacts_project_path() { }, ) .expect("allow direct file write"); - let lock = acquire_project_write_lock(&root, "persistent-writer") - .expect("acquire persistent project writer"); + // 持锁方必须是**另一条线程**:本用例验证的是“别的写通道正在写时 file.write 必须 + // 走满等待预算并失败关闭”,同一条调用链自持锁属于重入复用,不会失败。 + let holder_root = root.clone(); + let (release_sender, release_receiver) = mpsc::channel::<()>(); + let holder = std::thread::spawn(move || { + let lock = acquire_project_write_lock(&holder_root, "persistent-writer") + .expect("acquire persistent project writer"); + let _ = release_receiver.recv(); + drop(lock); + }); + let lock_path = root.join(PROJECT_WRITE_LOCK_PATH); + for _ in 0..400 { + if lock_path.is_file() { + break; + } + std::thread::sleep(Duration::from_millis(5)); + } + assert!( + lock_path.is_file(), + "persistent writer must hold the project write lock" + ); let observation = execute_game_creator_agent_runtime_tool_action( &root, @@ -5837,7 +5856,8 @@ async fn agent_runtime_file_write_lock_failure_redacts_project_path() { ) .await; - drop(lock); + let _ = release_sender.send(()); + holder.join().expect("join persistent project writer"); assert_eq!(observation.status, "failed"); assert!(!observation .summary diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs index 04b72c0b3..bc7d08d88 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs @@ -1214,8 +1214,28 @@ mod tests { .expect("resolve primary"); fs::write(&primary, b"{broken").expect("corrupt primary"); - let project_lock = acquire_project_write_lock(directory.path(), "test.concurrent-save") - .expect("hold project write lock"); + // 持锁方必须是**另一条线程**:本用例验证的是“另一个写者持锁时恢复安装必须失败 + // 关闭”,同一条调用链自持锁属于重入复用,不再产生占用失败。 + let holder_root = directory.path().to_path_buf(); + let (release_sender, release_receiver) = std::sync::mpsc::channel::<()>(); + let holder = std::thread::spawn(move || { + let lock = acquire_project_write_lock(&holder_root, "test.concurrent-save") + .expect("hold project write lock"); + let _ = release_receiver.recv(); + drop(lock); + }); + let lock_path = resolve_local_project_path(directory.path(), PROJECT_WRITE_LOCK_PATH) + .expect("resolve project write lock path"); + for _ in 0..400 { + if lock_path.is_file() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + assert!( + lock_path.is_file(), + "concurrent writer must hold the project write lock" + ); let error = load_ui_design_state_at(LoadUiDesignStateInput { project_path: directory.path().to_string_lossy().into_owned(), expected_project_id: PROJECT_ID.to_string(), @@ -1224,7 +1244,8 @@ mod tests { .expect_err("recovery must not install while another writer holds the lock"); assert!(error.contains("项目正在被其他写操作占用")); assert!(read_ui_design_document_path(&primary).is_err()); - drop(project_lock); + let _ = release_sender.send(()); + holder.join().expect("join concurrent writer"); let recovered = load_ui_design_state_at(LoadUiDesignStateInput { project_path: directory.path().to_string_lossy().into_owned(), diff --git a/docs/project-memory/plans/【实施计划】项目客户端占用锁收敛-2026-09-14.md b/docs/project-memory/plans/【实施计划】项目客户端占用锁收敛-2026-09-14.md index f1ffb1b07..e579a6514 100644 --- a/docs/project-memory/plans/【实施计划】项目客户端占用锁收敛-2026-09-14.md +++ b/docs/project-memory/plans/【实施计划】项目客户端占用锁收敛-2026-09-14.md @@ -15,14 +15,16 @@ Milestone: `【里程碑】项目客户端占用锁收敛-2026-09-14.md` ## 修改顺序 1. 统一同进程嵌套调用的项目锁语义,禁止自等待。 -2. 盘点并迁移 Runner 的项目级 owner 文件到统一锁,保留诊断投影与跨 boot 恢复。 -3. 删除重复项目级锁路径及其专属调用,保留底层原子写和 Git 锁。 -4. 补齐同进程重入、跨进程占用、崩溃恢复和锁释放测试。 +2. 收窄复用判据:按 `pid` 放行会放过本进程其它线程的并行写,改为按“当前线程就是真实持锁线程”判定重入,并保住同进程跨线程的等待与终态占用。 +3. 盘点并迁移 Runner 的项目级 owner 文件到统一锁,保留诊断投影与跨 boot 恢复。 +4. 删除重复项目级锁路径及其专属调用,保留底层原子写和 Git 锁。 +5. 补齐同进程重入、同进程跨线程争用、跨进程占用、崩溃恢复和锁释放测试。 ## 验证命令 - `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` +- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --test-threads=1` +- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_write_lock --no-default-features` - Runner owner 与 response stream 相关定向测试 - `npm run check:encoding` - `git diff --check` @@ -31,4 +33,5 @@ Milestone: `【里程碑】项目客户端占用锁收敛-2026-09-14.md` - Runner 与 GUI 可能是不同进程;统一锁前必须验证同一客户端不会互相阻塞。 - 旧 `.agent/runtime/execution-owner.lock` 残留需要按 PID/启动身份安全回收,不能直接删除。 +- 复用判据按线程判定:出现同进程跨线程重入的现场时先按 `*_locked` 入口处置,不要把判据退回按 `pid` 一律放行(那会放过并行写,见里程碑「边界」末条)。 - 若跨 boot 恢复或 GUI/Runner 联动回归,回滚统一路径迁移,保留已验证的同进程重入修复。 diff --git a/docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md b/docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md index ed1c643a2..218b10182 100644 --- a/docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md +++ b/docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md @@ -7,22 +7,24 @@ Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施 ## 目标 -项目只保留一个面向客户端占用的项目级跨进程锁,防止多个客户端同时打开同一项目;同一客户端进程内的嵌套调用复用既有项目锁,不因自身持锁进入等待。 +项目只保留一个面向客户端占用的项目级跨进程锁,防止多个客户端同时打开同一项目;同一客户端进程内**同一条写调用链(同一线程)的嵌套调用**复用既有项目锁,不因自身持锁进入等待。 ## 边界 - 项目客户端占用锁与项目写入调用的职责统一,跨进程竞争仍返回占用语义。 - Agent DB、session lane、manifest 原子写和 Git 自身的底层一致性机制不在本里程碑删除范围内。 -- 不改变项目 revision、权限、幂等、恢复和数据格式合同。 +- 不改变项目 revision、权限、幂等、恢复和数据格式合同。**本进程其它线程的并发写入必须继续串行化**:按 `pid` 一律返回 advisory guard 会放过并行写,直接违反本边界(见验收标准第 2 条)。 ## 验收标准 -- 同一进程内嵌套取得项目锁立即返回 advisory guard,不等待、不删除真实持有者锁。 +- 同一线程(同一条写调用链)嵌套取得项目锁立即返回 advisory guard,不等待、不删除真实持有者锁。 +- 本进程另一条线程持锁(模拟“另一个写通道/另一个客户端”的既有用例形态)时仍保持等待与终态占用:项目 revision 侧车、steer 序号分配、一致快照读、pending sidecar 复核和恢复安装不得被复用判据放过。 - 不同进程持有项目锁时仍保持占用失败与残留回收判据。 - 客户端项目占用入口与 Runtime 写入入口不会各自维护第二个项目级锁文件。 - 锁释放后下一客户端可重新取得锁。 -- 定向 Rust 锁测试、`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。 +- 定向 Rust 锁测试、`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过;锁语义变更必须跑 `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --test-threads=1` 全量,定向用例覆盖不到 `project_tools` / `command_runtime` / `parallel_actions` / `runtime_state` / `response_stream` / `direct_tool_bridge` / `ui_editor::persistence` 里的锁不变量。 ## 未决事项 - Runner 的 `execution-owner.lock` 如何迁移到统一客户端占用锁,需要补充跨进程启动、恢复和诊断测试后再落地。 +- 同进程**跨线程**重入(持锁调用链在 `await` / `spawn_blocking` 之后于其它线程再次取锁)仍会走有界等待,预算耗尽时报“项目正在被其他写操作占用”。发现这类现场时按 2026-08-27 的既有处置改用 `*_locked` 入口复用已有 guard(`project-memory/shared-memory/pitfalls.md`「持锁调用链二次取锁」),不放宽整条锁的串行化语义。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ef516ae27..d197d7e06 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -8637,3 +8637,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 决策:DirectProject app-server thread 改为 `sandbox="danger-full-access"`,turn 改为 `sandboxPolicy.type="dangerFullAccess"`,不再发送 `writableRoots` 或 workspace 网络开关,原生命令网络随完整 sandbox 开放;app-server 交互请求不再按 grant root 做白名单裁剪,直接项目会话统一接受文件变更、命令执行和权限请求。首页只读对话、AGC `agc_tools` 业务授权、Provider 凭据隔离、Runtime 审计和客户端受控文件工具合同继续保留。 - 提示词同步:DirectProject 不再把路径范围描述成 Codex 原生能力禁区,但仍禁止主动输出 Token、Cookie、auth.json、`.env` 和 Runtime 私有控制面。 - 验证:Rust 定向单测覆盖 `danger-full-access` / `dangerFullAccess`、无 `writableRoots`、外部 grant root 仍接受,以及 DirectHome 继续只读拒绝。 + +## 2026-09-14 项目写锁的同进程复用收窄为同线程重入 + +- 背景:`write_lock.rs` 的 advisory 复用判据曾放宽为「`.agent/project.lock` 的 `pid` 等于当前进程」,使本进程所有写通道都不再等待。`Project CI` 的 Rust 全量门禁因此出现 12 条失败:另一线程持锁时一致快照读 / `project.diff` / `action_history` / `command.output_read` / steer 不再等待,4 路并行直写撞项目 revision 侧车(`File exists (os error 17)`),8 线程并发 steer 拿到重复序号,`file.write` 锁失败脱敏与恢复安装的失败关闭变成成功。 +- 决策:复用判据收窄为**同一条写调用链(同一线程)重入**——按锁路径登记真实持锁线程,只有当前线程就是持锁线程时才返回 advisory guard;本进程其它线程的争用继续走有界等待与终态占用。自主游戏构建流水线的并行专家动作豁免保持不变;跨进程占用、残留回收、权限分类、等待预算和错误文案不变。 +- 边界:锁定这些不变量的既有用例(`project_tools` / `command_runtime` / `parallel_actions` / `runtime_state` / `response_stream` / `direct_tool_bridge` / `ui_editor::persistence`)不得为了让锁语义通过而改写;用「同线程自持锁」模拟「另一个写者」的两条用例改为**在另一条线程持锁**,断言语义不变。同进程跨线程重入(持锁链在 `await` / `spawn_blocking` 后于其它线程再取锁)仍会等满预算,出现现场时按 2026-08-27 的既有处置改用 `*_locked` 入口,不放宽判据。 +- 关联文档:[项目客户端占用锁收敛里程碑](../plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md)、[踩坑记录](pitfalls.md)。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 4df507394..8e8eca7e1 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,14 @@ # 踩坑与排障记录 +## 2026-09-14 项目写锁的同进程复用判据不能只看 pid + +- **现象**:`master` 的 `Project CI / Native shell tests` 红在 `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --test-threads=1`,12 条用例失败(`2439 passed; 12 failed`)。断言分三类:① 另一线程持锁时快照读 / `project.diff` / `action_history` / `command.output_read` / steer 不再等待(`... must wait for the project consistency lock`);② 并发写不再串行化——4 路并行直写撞项目 revision 侧车报 `File exists (os error 17)`,8 线程并发 steer 拿到 `[1, 1, 1, 1, 1, 1, 1, 2]`;③ 别的写通道持锁时 `file.write` 与恢复安装必须失败关闭,实测变成 `ok` / 不再报占用。 +- **原因**:`project/write_lock.rs` 的 advisory 复用判据从「自主游戏构建流水线 + 本进程持锁」放宽成「本进程持锁」,而判据只比 `.agent/project.lock` JSON 里的 `pid`。`pid` 只能证明锁由本进程持有,分不清「同一条调用链再次取锁(必须放行,否则自己等自己)」和「本进程另一条写通道正在写(必须继续串行化)」;于是同进程其它线程的写通道也拿到 advisory guard。 +- **处理**:复用判据收窄到**同线程重入**。新增 `PROJECT_WRITE_LOCK_THREAD_OWNERS`(按锁路径登记真实持锁线程)与 `project_write_lock_reentered_by_current_thread`:登记在 `create_new` 成功处,注销在 guard `Drop` 里并且**按路径**注销(guard 会被移到别的线程再 Drop,例如写入路径交给阻塞线程池的持有者)。只有当前线程就是该路径的持锁线程(或自主游戏构建流水线)才返回 advisory guard;本进程其余争用继续走有界等待与终态占用。 +- **易错点**:① 用「同线程」近似重入后,靠**同线程自持锁 + 同线程调用**模拟「另一个写者」的用例会失去信号(`agent_runtime_file_write_lock_failure_redacts_project_path`、`recovery_install_respects_the_project_write_lock`):它们必须改成**在另一条线程持锁**,断言才有意义;② 不要用「同进程还有 guard 活着」当重入依据,那等于退回按 `pid` 放行;③ 同进程**跨线程**重入(持锁调用链在 `await` / `spawn_blocking` 之后于其它线程再次取锁)仍会等满预算并在耗尽时报占用,出现这类现场按 2026-08-27 的处置改用 `*_locked` 入口复用已有 guard,不要放宽判据。 +- **验证**:本地定向 36 条(`--test-threads=1`,过滤 `_after_project_lock` / `bridge_write_file` / `project_write_lock` 等):CI 那 12 条里 9 条转绿(覆盖 `project_tools` / `command_runtime` / `parallel_actions` / `runtime_state` / `response_stream` / `external_generation_state`),3 条在本机被 Windows 临时目录 owner/DACL 挡在 setup(与本次改动无关,见 2026-09-13 条);`project_write_lock_reuses_same_process_owner_and_releases_on_drop`(同线程重入)继续通过。`cargo fmt --check`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 全绿。 +- **关联**:`apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs`、`src/agent/runtime_actions/project_gates.rs`(有界等待预算)、`docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`「2026-09-14 项目客户端占用锁收敛」。 + ## 2026-09-14 UI 超时围栏只能放弃等待,不能放弃结果;排队闸门不能无限等 - **现象**:登录/建项在 UI 上"超时"后报错,用户重试仍然无效;界面停在原页面,而后端/Runner 其实已经接受了这次操作(登录后本机登录态已装好、项目目录已建好)。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 9a6daf398..7ba0d39da 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1372,5 +1372,5 @@ DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过 ## 2026-09-14 项目客户端占用锁收敛 -项目锁职责收敛为“客户端占用项目”这一事实:同一客户端进程内的嵌套项目写入调用复用已有项目锁并返回 advisory guard,不再等待自身持有的 `.agent/project.lock`;跨进程竞争继续沿用现有占用、残留回收和权限分类。Runner 的 `.agent/runtime/execution-owner.lock` 迁移到统一项目占用锁仍属于进行中的里程碑,完成前不改变其恢复诊断合同。 +项目锁职责收敛为“客户端占用项目”这一事实:跨进程竞争继续沿用现有占用、残留回收和权限分类。同进程复用的判据收窄到**同一条写调用链(同一线程)重入**——本线程已落盘持有该项目的 `.agent/project.lock` 时再次取锁,返回 advisory guard,不再等待自身持有的锁。本进程**其它线程**的写入通道仍走有界等待与终态占用:项目 revision 侧车、steer 序号分配、一致快照读、pending sidecar 复核和恢复安装都依赖这把锁把同进程的并发写入串行化,按 `pid` 一律放行会让它们静默竞态。自主游戏构建流水线沿用既有的并行专家动作豁免。Runner 的 `.agent/runtime/execution-owner.lock` 迁移到统一项目占用锁仍属于进行中的里程碑,完成前不改变其恢复诊断合同。 diff --git a/docs/【技术方案】AGC客户端稳定版生命周期大切换-2026-09-14.md b/docs/【技术方案】AGC客户端稳定版生命周期大切换-2026-09-14.md index f5abedfa0..ba403e9e9 100644 --- a/docs/【技术方案】AGC客户端稳定版生命周期大切换-2026-09-14.md +++ b/docs/【技术方案】AGC客户端稳定版生命周期大切换-2026-09-14.md @@ -104,4 +104,4 @@ - 原生(真实 Runner/IPC)与真实 Provider 下的同一批时序未执行:本轮结论来自 deterministic surface 与 mock 故障注入。 - `src-tauri/src/project/bootstrap.rs` 在 `npm install` 之前读取 `package-lock.json` 计算 `lockSha256`,安装后仍使用旧字节;疑似只影响审计准确性,未复现、未修改。 -- 同 PID 下的写入 advisory guard(`write_lock.rs` 的 `bypassed_same_process`)是否会放过并行写,尚未排除误报。 +- 同 PID 下的写入 advisory guard(`write_lock.rs` 的 `bypassed_same_process`)是否会放过并行写:**已确认会**。只比 `pid` 的豁免让同进程其它线程的写通道也跳过 `.agent/project.lock`,`Project CI` 的 Rust 全量门禁因此红了 12 条(4 路并行直写撞项目 revision 侧车报 `File exists`、8 线程并发 steer 序号重复、一致快照读 / pending 复核 / 恢复安装不再等待、写锁失败不再失败关闭)。已把复用判据收窄为**同线程重入**:本进程其它线程继续走有界等待与终态占用,详见 `docs/project-memory/shared-memory/pitfalls.md`「项目写锁的同进程复用判据不能只看 pid」与 `docs/project-memory/plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md`。