From 34bcd618241216f82591c8ea362dc045810e0fa8 Mon Sep 17 00:00:00 2001 From: Linghong Date: Mon, 17 Aug 2026 04:55:38 +0000 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=20M1C-1=20=E6=BC=8F=E6=8E=89?= =?UTF-8?q?=E7=9A=84=20agent=5Fdb=20=E5=AE=88=E6=81=92=E6=96=AD=E8=A8=80?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E8=AE=A9=E5=AE=A1=E6=89=B9=E6=8A=95=E5=BD=B1?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=8C=BA=E5=88=86=E7=9E=AC=E6=97=B6=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M1C-1 给 agent_db 新增了 planning 决策预留车道并从普通追加额度里扣除,但 ordinary_append_soft_limit_preserves_action_receipt_record_slots 仍断言两车道的守恒 律,差额恰好是新车道的 2_097_280 字节;记录额度同样少算 128 条。master 没有这个常量, 所以 master CI 绿而本分支必红。断言补到三车道,记录额度改为 999_680。 审批投影恢复的收敛此前不分错误性质:`.agent/project.lock` 正被占用这类瞬时错误也会 把策划根 run 永久标成 needs-reconciliation——比收敛出现之前的强传播更糟。现在先过 static_delegate_parent_wake_error_is_transient(复用委派唤醒的既有判据,避免两处分类 漂移),瞬时错误照旧上抛,由调用方转成 recovery_pending 下一轮重试;只有持久不一致 才收敛到那个 run。新增回归覆盖这条分支。 Co-Authored-By: Claude Opus 5 --- .../src/agent/runtime_driver/recovery_scan.rs | 62 ++++++++++++++++++- .../src/project/agent_db/security_tests.rs | 10 ++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs index 2fc3e0f43..f42df38e3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs @@ -822,9 +822,17 @@ fn durable_process_session_recovery_exists_at(root: &Path) -> bool { /// Fast GDD approval 投影恢复失败时,把 fail-closed 收敛到受影响的那个 run。 /// /// 返回 `Ok(true)` 表示已经把策划根 Supervisor 标成 needs-reconciliation,调用方可以 -/// 继续扫描其余 Agent;`Ok(false)` 表示当前没有可归属的 run,无法精确收敛,调用方必须 -/// 把原错误照旧上抛,保持全局 fail-closed。 +/// 继续扫描其余 Agent;`Ok(false)` 表示不该、或无法精确收敛,调用方必须把原错误照旧 +/// 上抛,保持全局 fail-closed。 fn contain_plan_gdd_approval_recovery_failure_at(root: &Path, error: &str) -> Result { + // 瞬时错误绝不能收敛成 needs-reconciliation。最常见的就是 `.agent/project.lock` + // 正被另一个写操作占用——什么都没坏,下一轮扫描重试即可;把它标成人工核对等于 + // 用一次转瞬即逝的锁争用永久停掉策划根 run,比原来的强传播更糟。这里照旧上抛, + // 调用方把它变成 recovery_pending 并在下一轮重试,与本函数出现之前的行为一致。 + // 判据复用委派唤醒那条既有的瞬时特征串,避免两处各写一套导致分类漂移。 + if static_delegate_parent_wake_error_is_transient(error) { + return Ok(false); + } let Some(_runtime_lock) = try_acquire_game_creator_agent_runtime_task_lock( root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, @@ -1967,4 +1975,54 @@ mod plan_gdd_approval_wait_recovery_tests { .count(); assert_eq!(audit_count, 1); } + + /// 瞬时错误不能被收敛成 needs-reconciliation。 + /// + /// `.agent/project.lock` 正被另一个写操作占用时什么都没坏,下一轮扫描重试即可; + /// 把它标成人工核对,等于用一次转瞬即逝的锁争用永久停掉策划根 run——那比这条 + /// 收敛出现之前的强传播还糟。此时必须照旧上抛,由调用方转成 recovery_pending。 + #[test] + fn transient_plan_gdd_approval_recovery_failure_is_retried_instead_of_reconciled() { + let temporary = + crate::tests::canonical_test_tempdir("plan-gdd-approval-recovery-transient-"); + let root = temporary.path(); + init_local_game_project_at(root, "plan-gdd-approval-transient", "审批投影恢复瞬时失败") + .expect("init project"); + let runtime = start_game_creator_agent_runtime_task_at( + root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "推进立项策划", + "plan-gdd-approval-transient-run", + "agent-ready-task-scheduler", + "准备推进立项策划", + vec!["推进立项策划".to_string()], + ) + .expect("start plan-root supervisor runtime"); + + let held = acquire_project_write_lock(root, "test.hold-project-write-lock") + .expect("hold the project write lock"); + let error = resume_game_creator_agent_background_tasks_at(root) + .expect_err("锁争用属于瞬时错误,必须照旧上抛而不是就地收敛"); + assert!( + error.contains("恢复 GDD approval 投影失败"), + "unexpected error: {error}" + ); + drop(held); + + let contained = read_game_creator_agent_runtime_at(root, &runtime.agent_id) + .expect("read supervisor runtime after transient failure"); + assert_ne!( + contained.state.phase, "needs-reconciliation", + "瞬时锁争用不得把策划根 run 永久停掉" + ); + assert!(contained.state.error.is_none()); + assert!(!read_agent_db_records_bounded(root, 1024 * 1024) + .expect("read approval recovery audit") + .0 + .iter() + .any( + |record| record.get("recordType").and_then(|value| value.as_str()) + == Some("agent.runtime.plan.gdd.approval_recovery.needs_reconciliation") + )); + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/agent_db/security_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/agent_db/security_tests.rs index 345d00975..cd6314dc6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/agent_db/security_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/agent_db/security_tests.rs @@ -2465,13 +2465,19 @@ fn agent_db_capacity_reserves_terminal_receipt_space_without_rotation() { #[test] fn ordinary_append_soft_limit_preserves_action_receipt_record_slots() { + // 守恒律必须覆盖全部预留车道。M1C-1 新增了 planning 决策车道 + // (`AGENT_DB_PLAN_GDD_DECISION_RESERVE_BYTES`,(16 KiB+1)×128)并从 + // `AGENT_DB_MAX_ORDINARY_APPEND_BYTES` 里扣掉,这条断言却还停在两车道, + // 差额恰好是新车道的 2_097_280 字节。少一条车道就意味着这条断言不再能证明 + // 「普通追加不会吃掉任何终态预留」——它才是这个测试存在的理由。 assert_eq!( AGENT_DB_MAX_ORDINARY_APPEND_BYTES + AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_BYTES - + AGENT_DB_TERMINAL_RESERVE_BYTES, + + AGENT_DB_TERMINAL_RESERVE_BYTES + + AGENT_DB_PLAN_GDD_DECISION_RESERVE_BYTES, AGENT_DB_MAX_ACTION_RECEIPT_SCAN_BYTES ); - assert_eq!(AGENT_DB_MAX_ORDINARY_APPEND_RECORDS, 999_808); + assert_eq!(AGENT_DB_MAX_ORDINARY_APPEND_RECORDS, 999_680); assert_eq!( AGENT_DB_MAX_SCAN_RECORDS - usize::try_from(AGENT_DB_TERMINAL_RESERVE_RECORDS)