补齐 M1C-1 漏掉的 agent_db 守恒断言,并让审批投影恢复区分瞬时错误

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 04:55:38 +00:00
parent a60f95b554
commit 34bcd61824
2 changed files with 68 additions and 4 deletions
@@ -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<bool, String> {
// 瞬时错误绝不能收敛成 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")
));
}
}
@@ -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)