From 8d5338ce82525097bc5d4a7b544212f9ff753b8c Mon Sep 17 00:00:00 2001 From: Linghong Date: Mon, 17 Aug 2026 04:28:49 +0000 Subject: [PATCH] =?UTF-8?q?P2=EF=BC=9Areject=20=E4=B8=8D=E6=98=AF=E6=AD=BB?= =?UTF-8?q?=E8=B7=AF=EF=BC=8C=E9=94=81=E4=BD=8F=E7=9C=9F=E6=AD=A3=E7=9A=84?= =?UTF-8?q?=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原判定「reject 后同一 session 无法再次提交,因为 CAS 门只放行 collecting / revision_requested」不成立。提交门在 phase 判据之前先要求 session 的 activeRunId 等于 当前策划子 run,而 schema 不变量禁止 rejected / revision_requested / approved / awaiting_* / recovery_required 保留 activeRunId——两者互斥,终态 phase 根本到不了那条 phase 判据。把 rejected 加进允许集只会多一个不可达分支,续跑仍然起不来;连既有的 revision_requested 也已经是不可达的。 真正决定 reject 能否重做的是 M1C-2b 的 continuation 起点 writer:技术方案 §8.6 要求它 「以新 activeRunId 写 revision+1 successor」,而唯一能同时带 activeRunId 又过提交门的 phase 只有 collecting。本提交不改行为,只把这条因果写进门旁注释,并加一条回归锁住它: 终态 session 挂 activeRunId 在指纹阶段就被 PLAN_INVALID_SCHEMA 拒绝,而落回 collecting 的 continuation 能过提交门。 Co-Authored-By: Claude Opus 5 --- .../agent/runtime_protocol/planning_submit.rs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs index 5a7d4da17..07f17d13a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs @@ -1252,6 +1252,14 @@ fn validate_current_session_cas( "plan.submit_gdd 必须绑定当前活跃策划子 Run", )); } + // 这条 phase 判据实际只可能看到 `collecting`:上面的 activeRunId 判据要求 + // session 绑着当前策划子 run,而 schema 不变量禁止 `awaiting_user_input`、 + // `awaiting_gdd_approval`、`revision_requested`、`approved`、`rejected`、 + // `recovery_required` 保留 activeRunId(planning_storage.rs 的 + // 「session 进入审批/终态/recovery_required 后不得保留 activeRunId」)。 + // 因此 revise/reject 之后能不能重做,不由这条门决定,而由 M1C-2b 的 continuation + // 起点 writer 决定——它必须以新 activeRunId 写 revision+1 successor,phase 只能落回 + // `collecting`。这里保留 `revision_requested` 作为既有冗余,不再新增更多不可达分支。 if !matches!(session.phase.as_str(), "collecting" | "revision_requested") { return Err(submit_error( "PLAN_PENDING_GDD_EXISTS", @@ -3535,6 +3543,66 @@ mod tests { cleanup_fixture(root); } + /// reject 之后能不能在同一 lineage 重做,**不由提交门的 phase 判据决定**。 + /// + /// 提交门要求 session 的 activeRunId 等于当前策划子 run,而 schema 不变量禁止 + /// `rejected`(以及 `revision_requested`、`approved`、`awaiting_*`)保留 + /// activeRunId——两者互斥,所以终态 phase 永远到不了那条 phase 判据。真正决定重做 + /// 能力的是 M1C-2b 的 continuation 起点 writer:技术方案 §8.6 要求它「以新 + /// activeRunId 写 revision+1 successor」,而唯一能同时带 activeRunId 又过提交门的 + /// phase 只有 `collecting`。 + /// + /// 这条测试把该约束锁住,免得日后有人以为「把 rejected 加进提交门允许集」就能让 + /// reject 重做——那只会多一个不可达分支,真正的续跑仍然起不来。 + #[test] + fn rejected_session_needs_a_collecting_continuation_not_a_wider_submit_gate() { + let (root, context, input) = submit_fixture(); + execute_plan_submit_gdd(&root, &context, &input).expect("submit v1"); + let gdd = read_plan_gdd_chain(&root) + .expect("read submitted GDD") + .pop() + .expect("GDD exists"); + create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending"); + decide_plan_gdd_at( + &root, + &approval_input( + &gdd, + "reject", + "gdd-response-00000000-0000-4000-8000-000000000040", + Some("当前方向需要重新梳理".to_string()), + ), + ) + .expect("commit reject receipt"); + + let session = read_plan_session_with_recovery(&root) + .expect("read session after reject") + .expect("session exists after reject"); + assert_eq!(session.phase, "rejected"); + assert!(session.active_run_id.is_none()); + + // 终态 session 直接挂 activeRunId 连指纹都算不出来——schema 层就禁止。 + let mut forged = session.clone(); + forged.active_run_id = Some(context.created_by_run_id.clone()); + let error = + plan_session_fingerprint(&forged).expect_err("终态 session 不得保留 activeRunId"); + assert_eq!(error.code(), "PLAN_INVALID_SCHEMA"); + + // continuation 起点把 phase 落回 collecting 之后,同一 lineage 才能继续提交。 + let mut continuation = session.clone(); + continuation.session_revision += 1; + continuation.previous_fingerprint = Some(session.session_fingerprint.clone()); + continuation.phase = "collecting".to_string(); + continuation.active_run_id = Some(context.created_by_run_id.clone()); + continuation.session_fingerprint = + plan_session_fingerprint(&continuation).expect("continuation session fp"); + let mut next_context = context.clone(); + next_context.source_session_revision = continuation.session_revision; + next_context.source_session_fingerprint = continuation.session_fingerprint.clone(); + validate_current_session_cas(&continuation, &next_context, &input) + .expect("reject 之后的 continuation 必须能提交同一 lineage 的下一版本"); + cleanup_fixture(root); + } + #[test] fn approval_actions_rebuild_receipt_aware_index_and_are_idempotent() { for (action, comment, expected_status) in [