diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs index d185e2aa4..c5dfd1a9c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs @@ -161,10 +161,39 @@ pub(super) fn game_creator_agent_final_reply_error_allows_fallback(error: &str) } fn plan_submit_error_is_business_rejection(error: &PlanningStorageError) -> bool { - matches!( - error.code(), - "PLAN_INVALID_REQUEST" | "PLAN_SIZE_LIMIT" | "PLAN_VERSION_LIMIT_REACHED" - ) + matches!(error.code(), "PLAN_INVALID_REQUEST" | "PLAN_SIZE_LIMIT") +} + +/// A malformed Fast GDD is useful feedback for the planning child, but it +/// must not let one run replay an ever-growing prompt forever. Keep this +/// counter on the durable Runtime state rather than only in the in-memory +/// continuation: a process restart is part of the failure chain we bound. +const PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT: u32 = 5; + +fn plan_submit_business_rejection_limit_reached(rejection_count: u32) -> bool { + rejection_count >= PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT +} + +fn next_plan_submit_business_rejection_count(current: u32) -> (u32, bool) { + let next = current.saturating_add(1); + (next, plan_submit_business_rejection_limit_reached(next)) +} + +fn finish_plan_submit_business_rejection_limit_at( + root: &Path, + runtime: &AgentRuntimeState, +) -> Result { + let error = format!( + "Fast GDD 连续 {} 次未通过 Runtime 校验,已停止自动续跑;请检查最后一次拒绝 observation 后重新发起策划。", + PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT + ); + let failed = fail_game_creator_agent_runtime_turn_at(root, runtime.clone(), &error)?; + let _ = append_game_creator_agent_background_task_failed_audit( + root, + &failed, + AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_PLAN_SUBMIT_REJECTION_LIMIT, + ); + Ok(AgentBackgroundTaskOutcome::Finished) } /// A strict submit payload rejection is a normal planning observation, not a @@ -204,6 +233,9 @@ fn project_plan_submit_business_rejection_at( summary: "Fast GDD 提交被 Runtime 拒绝,请根据 observation 修正后重新提交。".to_string(), detail: Some(public_error), }; + let (rejection_count, exhausted) = + next_plan_submit_business_rejection_count(runtime.plan_submit_gdd_rejection_count); + runtime.plan_submit_gdd_rejection_count = rejection_count; let mut rejected = pending.clone(); rejected.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string(); rejected.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED.to_string(); @@ -221,6 +253,9 @@ fn project_plan_submit_business_rejection_at( &rejected, &observation, )?; + if exhausted { + return finish_plan_submit_business_rejection_limit_at(root, runtime); + } let continuation = continuation_for_game_creator_agent_runtime_steer( runtime, &AgentRuntimeToolPlan::default(), @@ -454,6 +489,8 @@ pub(in crate::agent) fn prepare_game_chat_single_round_convergence_at( } const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_TOOL_PLAN: &str = "tool-plan-failed"; +const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_PLAN_SUBMIT_REJECTION_LIMIT: &str = + "plan-submit-validation-retries-exhausted"; const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_BUDGET: &str = "loop-budget-exhausted"; const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_FINAL_REPLY: &str = "final-reply-failed"; const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_FINALIZATION: &str = "finalization-failed"; @@ -913,6 +950,23 @@ async fn run_game_creator_agent_background_task_pass_without_deadline( let mut converged = false; let mut context_stalled = continuation.context_stalled; + // `project_game_creator_agent_runtime_provider_batch_abort` persists the + // rejection counter together with the rejected observation before this + // terminal transition. A crash between those two durable steps must not + // turn the fifth rejection into a sixth Provider request after recovery. + if plan_submit_business_rejection_limit_reached(runtime.plan_submit_gdd_rejection_count) { + return match finish_plan_submit_business_rejection_limit_at(&root, &runtime) { + Ok(outcome) => outcome, + Err(error) => fail_game_creator_agent_background_context_at( + &root, + &agent_id, + &session_id, + runtime, + &format!("收束已耗尽的 Fast GDD 提交拒绝失败:{error}"), + ), + }; + } + if continuation.applied_steer_cursor < runtime.applied_steer_cursor { return fail_game_creator_agent_background_context_at( &root, @@ -4329,4 +4383,49 @@ mod plan_gdd_blocker_projection_tests { )) .is_none()); } + + /// The fifth rejected Fast-GDD payload is recorded, then stops the run; + /// it must not schedule a sixth Provider turn after a restart or a long + /// series of invalid serializations. + #[test] + fn plan_submit_business_rejection_limit_stops_on_the_fifth_rejection() { + for current in 0..PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT - 1 { + let (next, exhausted) = next_plan_submit_business_rejection_count(current); + assert_eq!(next, current + 1); + assert!(!exhausted); + } + assert_eq!( + next_plan_submit_business_rejection_count(PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT - 1), + (PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT, true) + ); + } + + #[test] + fn plan_submit_business_rejection_limit_stays_terminal_after_recovery() { + assert!(!plan_submit_business_rejection_limit_reached( + PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT - 1 + )); + assert!(plan_submit_business_rejection_limit_reached( + PLAN_SUBMIT_GDD_BUSINESS_REJECTION_LIMIT + )); + assert!(plan_submit_business_rejection_limit_reached(u32::MAX)); + } + + #[test] + fn plan_submit_version_limit_is_an_authority_boundary_not_provider_feedback() { + let version_limit = PlanningStorageError::new( + "PLAN_VERSION_LIMIT_REACHED", + "不能继续创建第 129 个 GDD 版本", + ); + assert!(plan_submit_error_is_business_rejection( + &PlanningStorageError::new("PLAN_INVALID_REQUEST", "候选 GDD 缺少标题") + )); + assert!(plan_submit_error_is_business_rejection( + &PlanningStorageError::new("PLAN_SIZE_LIMIT", "候选 GDD 超出大小上限") + )); + assert!( + !plan_submit_error_is_business_rejection(&version_limit), + "版本上限由既有 lineage 决定,重试相同 Provider submit 不会改变它" + ); + } } 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 1ccbff9dc..d97306edc 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 @@ -910,6 +910,27 @@ fn submit_error(code: &'static str, detail: impl Into) -> PlanningStorag PlanningStorageError::new(code, detail) } +/// A limit derived from existing immutable authority is not feedback the +/// Provider can correct by submitting the same turn again. Preserve input-side +/// limits, but turn the durable-authority branch into reconciliation before it +/// reaches the main-loop retry classifier. +fn existing_planning_authority_error( + error: PlanningStorageError, + authority: &str, +) -> PlanningStorageError { + match error.code() { + "PLAN_SIZE_LIMIT" => submit_error( + "PLAN_NEEDS_RECONCILIATION", + format!("{authority} 读取超出大小上限,不能作为 Provider submit 重试处理"), + ), + "PLAN_VERSION_LIMIT_REACHED" => submit_error( + "PLAN_NEEDS_RECONCILIATION", + format!("{authority} 已达版本上限,不能作为 Provider submit 重试处理"), + ), + _ => error, + } +} + fn session_recovery_error(error: &PlanningStorageError) -> PlanningStorageError { submit_error( "PLAN_SESSION_RECOVERY_REQUIRED", @@ -1745,8 +1766,10 @@ pub(crate) fn execute_plan_submit_gdd( // commit point. validate_durable_child_binding(root, context)?; - let chain = read_plan_gdd_chain_locked(root)?; - let approvals = read_plan_gdd_approvals_locked(root)?; + let chain = read_plan_gdd_chain_locked(root) + .map_err(|error| existing_planning_authority_error(error, "既有 GDD 权威事实"))?; + let approvals = read_plan_gdd_approvals_locked(root) + .map_err(|error| existing_planning_authority_error(error, "既有 GDD approval receipt"))?; // Keep a session read error until after durable action identity replay is // resolved. The GDD create is the commit point: if session projection was // lost/corrupted after that point, a retry must still return the committed @@ -1843,9 +1866,12 @@ pub(crate) fn execute_plan_submit_gdd( .map(|latest| latest.version.saturating_add(1)) .unwrap_or(1); if version > PLAN_MAX_VERSIONS { - return Err(submit_error( - "PLAN_VERSION_LIMIT_REACHED", - "不能继续创建第 129 个 GDD 版本", + return Err(existing_planning_authority_error( + submit_error( + "PLAN_VERSION_LIMIT_REACHED", + "不能继续创建第 129 个 GDD 版本", + ), + "既有 GDD lineage", )); } let approval_request_id = context @@ -3111,6 +3137,103 @@ mod tests { cleanup_fixture(root); } + #[test] + fn submit_routes_oversized_existing_gdd_to_reconciliation_not_provider_retry() { + let (root, context, input) = submit_fixture(); + fs::write( + root.join(".agent/planning/gdd.v1.json"), + vec![b'x'; 256 * 1024 + 1], + ) + .expect("seed oversized immutable GDD authority"); + + let error = execute_plan_submit_gdd(&root, &context, &input) + .expect_err("oversized existing authority must block new submit"); + assert_eq!(error.code(), "PLAN_NEEDS_RECONCILIATION"); + cleanup_fixture(root); + } + + #[test] + fn submit_routes_exhausted_existing_lineage_to_reconciliation_not_provider_retry() { + let (root, context, input) = submit_fixture(); + let approvals_root = root.join(PLAN_GDD_APPROVAL_DIR); + fs::create_dir_all(&approvals_root).expect("create immutable approval authority directory"); + for version in 1..=PLAN_MAX_VERSIONS { + let mut gdd = build_plan_gdd_from_submit_input( + &input, + &context, + version, + &format!("gdd-approval-00000000-0000-4000-8000-{version:012x}"), + ) + .expect("build fixture GDD version"); + gdd.submission_id = format!("action-{version:024x}"); + gdd.action_fingerprint = format!("{version:064x}"); + gdd.fingerprint = plan_gdd_fingerprint(&gdd).expect("fixture GDD fingerprint"); + fs::write( + root.join(format!("{PLAN_STORAGE_ROOT}/gdd.v{version}.json")), + canonical_plan_gdd_bytes(&gdd).expect("fixture GDD canonical bytes"), + ) + .expect("write fixture GDD authority"); + + let decision_input = PlanGddApprovalDecisionInputV1 { + project_id: gdd.project_id.clone(), + gdd_id: gdd.gdd_id.clone(), + version, + fingerprint: gdd.fingerprint.clone(), + pending_action_id: gdd.submission_id.clone(), + action_fingerprint: gdd.action_fingerprint.clone(), + approval_request_id: gdd.approval_request_id.clone(), + response_id: format!("gdd-response-00000000-0000-4000-8000-{version:012x}"), + action: "approve".to_string(), + comment: None, + }; + let decision_fingerprint = plan_gdd_approval_decision_fingerprint_for_identity( + &decision_input, + PLAN_GDD_APPROVAL_SOURCE, + &gdd.run_profile, + &gdd.run_profile_binding_fingerprint, + &gdd.session_id, + &gdd.created_by_run_id, + None, + ) + .expect("fixture approval decision fingerprint"); + let mut approval = PlanGddApprovalV1 { + schema_version: PLAN_GDD_APPROVAL_SCHEMA_VERSION.to_string(), + project_id: gdd.project_id.clone(), + gdd_id: gdd.gdd_id.clone(), + version, + fingerprint: gdd.fingerprint.clone(), + pending_action_id: gdd.submission_id.clone(), + action_fingerprint: gdd.action_fingerprint.clone(), + approval_request_id: gdd.approval_request_id.clone(), + response_id: decision_input.response_id, + decision_fingerprint, + source: PLAN_GDD_APPROVAL_SOURCE.to_string(), + run_profile: gdd.run_profile.clone(), + run_profile_binding_fingerprint: gdd.run_profile_binding_fingerprint.clone(), + session_id: gdd.session_id.clone(), + run_id: gdd.created_by_run_id.clone(), + action: "approve".to_string(), + comment: None, + decided_at_utc: gdd.created_at_utc.clone(), + receipt_fingerprint: String::new(), + }; + approval.receipt_fingerprint = plan_gdd_approval_receipt_fingerprint(&approval) + .expect("fixture receipt fingerprint"); + fs::write( + approvals_root.join(format!("v{version}.json")), + canonical_plan_gdd_approval_bytes(&approval) + .expect("fixture approval canonical bytes"), + ) + .expect("write fixture approval authority"); + } + + let error = execute_plan_submit_gdd(&root, &context, &input) + .expect_err("the 129th version is a durable lineage boundary"); + assert_eq!(error.code(), "PLAN_NEEDS_RECONCILIATION"); + assert!(error.to_string().contains("既有 GDD lineage")); + cleanup_fixture(root); + } + #[test] fn submit_handler_rejects_second_submission_while_first_is_pending() { let (root, context, input) = submit_fixture(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs index 4c68c1d0b..4a15b1a69 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs @@ -1600,6 +1600,7 @@ pub(crate) fn default_game_creator_agent_runtime_state( waiting_on: "开发者输入".to_string(), next_step: "等待输入".to_string(), loop_iteration: 0, + plan_submit_gdd_rejection_count: 0, max_loop_iterations: AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT as u32, tool_action_budget: AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT as u32, plan_revision: 0, diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 9099ef240..5bf03bf1c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -245,6 +245,11 @@ struct AgentRuntimeState { next_step: String, #[serde(default)] loop_iteration: u32, + /// Consecutive strict Fast-GDD submit rejections for this exact planning + /// child run. It is Runtime-owned durable state so a restart cannot turn + /// an invalid-provider-output loop back into an unbounded retry. + #[serde(default)] + plan_submit_gdd_rejection_count: u32, #[serde(default)] max_loop_iterations: u32, #[serde(default)] diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c3a58bea7..52f7d4d76 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -30,6 +30,14 @@ - **验证**:`appSurface.test.ts` **381 passed / 0 failed**(378 既有 + 3 新增);`agentTraceSummary` 与 `rememberCommand`(另两个 import `src/App` 的用例文件)13 passed;`agc:typecheck` 通过;6 个改动/新增文件 ESLint `--max-warnings 0` 通过;`check:encoding` 5409 文件通过。不改 Rust——三条全在前端,后端语义已经正确。 - **对既有记录的更正**:M1D-1 与 M1D-2 两条记录分别称「Shell TypeScript typecheck 仍被仓库既有依赖缺失阻断」「appSurface UI suite 受仓库现有缺失 Tauri plugin 依赖阻断,未把该基线失败归因于本包」,在原分支主工作树上都不成立:`agc:typecheck` 干净退出,appSurface 378 条全绿;两道门分别位于 CI 的 `check:native-shells`(且 typecheck 排在 cargo test 之前)与 Frontend tests 内,一直是活的。实际情况与记录相反——`bf2185fba` 改名 `taskGroupLabels.design` 后,appSurface 有 8 个用例文件的断言变红,随后由 `c6a08ef98` 修复;把该套件记为「基线阻断、不归因本包」正是让这条自带回归合入的原因。**隔离工作树的依赖缺失不能作为跳过门禁的依据,须回原工作树复跑后再下结论。** - **未修的审查发现(本次不并入,单列后续)**:① hydrate 在校验 GDD/session 的 projectId 与 manifest 一致之前,已执行 `reconcile_plan_gdd_approval_projections_at`、session previous 提升与 index 重建等落盘修复,违反第 18.3 节固定顺序,其中 `session.previous.json` 的提升+删除不可逆(触发需外部篡改 `.agent/`,App 自身流程造不出该分歧);② 项目写锁竞争时 `acquire_project_write_lock` 的错误原文内嵌项目绝对路径,被原样回传前端,违反第 18.3 节「返回值不包含绝对路径」,常态可达;③ design 组展示名只改了 `taskGroupLabels` 一本字典,`agentPresentation.ts` 的 `groupConfigs` 与 `view/project-development/index.tsx` 的 `summarizeAgent` 仍硬编码「策划 Agent」,与新阶段「立项策划」同屏共存,违反第 18.2 节;④ 阶段进度「轮次 X/3」直接透传 0-indexed 的 `clarificationRound` 未 +1(后端自己用的是 `current_round + 1`),最后一轮显示「轮次 2/3」,字面暗示还剩一轮。 +## 2026-08-18 M1E 隔离工作树:Fast GDD submit 有界拒绝与覆盖审计 + +- **范围与结论**:在 `codex/genarrative-isolated` 上按 M1E 只接受具备完整触发链的缺陷。确认 Provider 连续输出不合法 `plan.submit_gdd` 时,Runtime 原有「rejected observation → 同 child run 续跑」链没有次数上限,模型可反复请求 tool-plan 并累积历史 observation;这是可达的 prompt 膨胀与资源消耗链。 +- **有界收束**:为 Runtime state 新增 durable `planSubmitGddRejectionCount`。仅本次 Provider input / 候选 GDD 触发的 `PLAN_INVALID_REQUEST`、`PLAN_SIZE_LIMIT` 计数;前四次维持既有 batch abort、observation 与 same-run continuation,第五次仍先完整落 rejected observation,再将该 planning child 终态失败并记录专用失败 audit,不发起第六次 Provider tool-plan。字段以 serde default 向后兼容,进程重启不会重置;新 child run 才从零开始,普通工具 observation 不计入。 +- **自审修复**:`PLAN_SIZE_LIMIT` 也可能来自读取既有不可变 GDD/receipt,而非 Provider 输入;`PLAN_VERSION_LIMIT_REACHED` 则由既有 lineage 已达 128 版或其读取异常决定。若只按 error code 分类,会把 durable authority 异常误当可纠正模型输出,最多多跑五次。现将这些读取/版本边界转为 `PLAN_NEEDS_RECONCILIATION`;Provider input/候选 GDD 的大小限制仍可按上项重试,不扩大改变其它 authority 错误语义。 +- **覆盖审计**:第 21 节要求的澄清三轮/回答绑定/continuation、submit→receipt 的 replay 与投影恢复、审批后修订、hydrate 空态与恢复均已有真实 Runtime/存储回归。未发现能低成本构成完整新断链的前端或跨层缺陷,因而未为拼接既有单测新增大而脆的 E2E。 +- **自审收口**:复核第五次 rejected observation 已持久化、但终态失败写入前进程中断的窗口:原先恢复会再次进入 Provider loop,形成第六次请求。恢复入口现读取 durable counter,达到 5 时直接复用同一终态失败与 audit 收束,不依赖内存 continuation;该修复与原有计数边界完全同域。未发现其它具备完整触发链、且可在 M1E 或此前范围内明确修复的问题;不扩展至 M2 的 `approvedGddRef` 或完整构建绑定。 +- **已验证**:新增“第五次终止”“第五次后恢复仍终止”“超限既有 GDD 转 reconciliation”及“lineage 版本上限不作 Provider feedback”四条回归;随后 Rust `planning_` 定向组 **157 passed / 0 failed**,`cargo check --offline --all-targets`、`cargo fmt --check`、`npm run check:encoding`(6608 files)和 `git diff --check` 均通过。仓库既有 Rust warnings 未在本包扩修。M1E 完成。 ## 2026-08-18 M1D-2 隔离工作树实现:入口分流与阶段进度 diff --git a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md index 71fc01504..ce32ed9e0 100644 --- a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md +++ b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md @@ -3,7 +3,7 @@ - 日期:2026-08-10 - 状态:2026-08-12 **M0 代码工作包全部完成**(`M0A-2`、`M0B-1`、`M0B-2` 已合入 M0 集成分支并通过各自门禁,见第 23.4 节);同日 **D6 作废、拓扑改变**,`M0A-1` 交付的文档基线随之失效,需以工作包 `M0A-3` 修订,**修订完成前 M0 不计完整完成**(见第 1.1 节)。2026-08-13 **D9 二次作废、D10 作废,由 D11 取代**:立项策划节点改为 Project Supervisor 通过 `agent.delegate` 发起的静态委派子 Agent,问询复用 PR #165 中转链路(见第 1.1 节「D11 新拓扑」);D11 依赖 WP1(静态委派澄清轮次与返工深度拆分)为强制前置,**该前置已于 2026-08-13 落地并合入**(`WP1` 生产代码 + `WP2` 回归,完成状态与门禁见第 23.5 节),澄清轮次上限现为 3(game-chat source 仍为 1)。随后 `M1A-1`、`M1A-2`、`M1A-3`、`M1A-4` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界,`M1A-4` 收窄 plan 根 run 的子 Agent 创建面。**2026-08-14 `M1B-1` 已通过门禁并合入本分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 门禁已完成。**2026-08-15 `M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支**:已落地 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复;本包不包含 `gdd-approval` planning pending、审批等待、receipt、审批命令或 UI。**2026-08-14 `M1C-0` 已合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。**2026-08-15 `M1C-0b` 已通过定向门禁并完成**:只改静态委派 durable status 的前向兼容读路径,未知字符串显式保留为 `Unknown(raw)` 并最大化阻塞;不含审批写入方。`M1C-1` 与 `M1C-2a` 已提供审批核心、固定 Goal Contract、完整分页 `file.read` evidence、claim 后三态 acceptance gate、审批 pending 恢复及 completion/finalization 门。**`M1C-2b` 已完成本包实现并通过门禁,现已快进合回 `feat/five_min_design`**:planning 澄清中转、确定性 continuation/session 投影、审批后用户修订谱系、Provider 活跃时间预算与末次 submit usage fold 已实现,11 条 `planning_clarification_*` 回归及关联 Rust 门禁通过。审批 UI、hydrate、构建准入与下游完整构建仍后置,M1 整体不可交付(见第 23.6、23.8 节)。 - 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入 -- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-4` 已提供 plan source、两层工具面、角色 brief 与子 Agent 创建面收窄的 Runtime 基础,`M1C-0` 已提供用户修订 lineage 分类,已合入的 `M1B-1` 提供 storage 基础与写入隔离;`M1B-2` 已提供提交点与恢复,`M1C-0b` 已补齐静态委派未知 durable status 的前向兼容读路径,`M1C-1` 已提供 receipt/审批核心、投影恢复和 plan 根完成门,`M1C-2a` 已补齐固定 Goal Contract、验收图证据与生产 acceptance gate。`M1C-2b` 已实现 planning 澄清中转、三轮确定性 session/continuation 投影、审批后修订谱系和 Provider 活跃时间预算,并已合回 `feat/five_min_design`;`M1D-1` 已提供 hydrate/read model 与审批卡,`M1D-2` 已接入新项目入口分流、阶段进度和实际项目总控页面挂载;approved GDD 构建绑定与完整下游仍留给 M2/M1E +- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-4` 已提供 plan source、两层工具面、角色 brief 与子 Agent 创建面收窄的 Runtime 基础,`M1C-0` 已提供用户修订 lineage 分类,已合入的 `M1B-1` 提供 storage 基础与写入隔离;`M1B-2` 已提供提交点与恢复,`M1C-0b` 已补齐静态委派未知 durable status 的前向兼容读路径,`M1C-1` 已提供 receipt/审批核心、投影恢复和 plan 根完成门,`M1C-2a` 已补齐固定 Goal Contract、验收图证据与生产 acceptance gate。`M1C-2b` 已实现 planning 澄清中转、三轮确定性 session/continuation 投影、审批后修订谱系和 Provider 活跃时间预算,并已合回 `feat/five_min_design`;`M1D-1` 已提供 hydrate/read model 与审批卡,`M1D-2` 已接入新项目入口分流、阶段进度和实际项目总控页面挂载;`M1E` 已完成 submit 连续拒绝的有界收束、重启恢复边界与既有回归覆盖审计。M1 策划闭环完成;approved GDD 构建绑定与完整下游留给 M2。 ## 1. 背景与目标 @@ -1162,6 +1162,8 @@ GDD handler 只能从已验证 batch binding 复制 `sourceSessionRevision/sourc 4. Provider transient failure/物理中断但 session、context 和 request slot 未变时,才沿用同一 base ID 的 attempt 派生规则。已知 retryable transport/upstream failure 先把旧 attempt durable 闭合为 `failed`;Runner/进程恢复只有在 boot/owner/lease 证据证明旧物理请求不再存活且无 handoff/batch 时,才闭合为 `interrupted`。旧终态写入、同步并回读成功后,才能创建 attempt N+1 的新 `started`;不能原地复用同一 providerRequestId,也不能让两个 started attempt 并存。无法证明旧请求已终止时进入 recovery required,不自动重发。每个 attempt 始终有独立 `started → completed|failed|interrupted` lifecycle。 5. 除第 1~2 项明确允许的同 binding `started + ready batch` 崩溃组合,以及上文 delivery 问题落盘/答案绑定的已消费证明外,binding 缺失/损坏、lifecycle 与 batch 不一致、同 revision 下 requestContextFingerprint 漂移、session 不是合法 successor,或 batch 已进入执行/等待状态时返回 `PLAN_NEEDS_RECONCILIATION`。此路径不自动删除、不补默认 binding、不重绑、不重试。 +严格 submit input 被 Runtime 以 `PLAN_INVALID_REQUEST` 或由该输入导出的候选 GDD `PLAN_SIZE_LIMIT` 拒绝时,当前策划子 run 最多产生 **5 次** `plan.submit_gdd / rejected` observation:前 4 次关闭原 sole-action batch 后可在同一 run 续跑,让 Provider 根据最后一条 observation 修正;第 5 次仍须先完整落盘 rejected observation,再把该 run 终态失败,**不得**请求第 6 次 Provider tool-plan。该分类只针对本次 Provider input / 候选 GDD;读取既有不可变 GDD 或 receipt 时出现同名大小上限、既有 lineage 已达版本上限,或任何其它 durable authority 异常,一律是 `PLAN_NEEDS_RECONCILIATION`,不得消耗 Provider 重试额度。计数是 Runtime state 的 durable、每个 child run 独立的字段,进程重启不能清零;只有新建的策划 child run 才从 0 开始。它不依赖前端、Prompt 文字或 Provider 自报,且普通工具 observation 不计入。 + 第 2 项的自动前滚必须与 session successor、batch supersede/cleanup 和 replacement request 的 started 写入都在项目锁内按幂等步骤恢复;任一断点重启后只能继续相同步骤。这样合法 steer 能确定性替换旧输出,而身份污染不会被“自动恢复”掩盖。 **(2026-08-13 删除)**原此处一整段规定 `plan-decision-checkpoint` 的 stale/repair/superseded 状态机(`repair-{K+1}` 转换、传递闭包数组、handoff 与 successor 的应用边界)。该请求 kind 随 D10 作废,整段无对应物:D11 下续跑是「新 run、同 session」,其 stale 判据就是普通 tool-plan 的那一套(本节第 1~5 项),不需要第二套。 @@ -2025,7 +2027,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` | `M1C-2c` | 决策卡 A/B 语义(第 23.9 节,2026-08-18 实现完成并合回):选项 → 台账映射改为 A/B 均 `confirmed/user_option`、信封 label 形状校验、planning role brief 与 Supervisor playbook/final-reply 文案(B 必须是真实岔路、第 3 项恒定且 description 须给出可执行验证方式、改口转述规则、提问纪律) | `M1C-2b` | **实现与门禁完成,已由 `6e4bd9703` 合回 `feat/five_min_design`**:Runtime 已实现 A/B/固定第三项校验、B 不再生成 `default_pending`、`answerSummary` 逐字保真;非法 C/缺项 fail-closed,A/B/自由填写回归已通过。`planning_clarification_*` 13、`project_planning` prompt 5、`planning_submit` 定向回归、prompt bundle、格式、编码、diff、offline all-targets 均通过;不含 M1D-1 前端、hydrate、构建准入或下游完整构建 | | `M1D-1` | 前端 hydrate 与 GDD 审批卡;决策卡按第 23.9 节实现(label 动态渲染、默认焦点 A、Other 槽不变) | `M1C-2b` | **已完成并合入 `feat/five_min_design`(落地 `0052a80da`,其后 ESLint 修正 `5b11a0530`)**:新增严格 `{projectPath}` hydrate command、`plan-gdd-state-view.v1` Rust read model、审批卡与独立 GDD 正文详情弹层;页面只消费 hydrate,决定 responseId 按审批请求/动作复用,`recoveryPending` 仅提供恢复重试;审批前置 pending 与错绑 session 继续 fail-closed。 | | `M1D-2` | 入口分流与阶段进度 | `M1D-1` | **已完成并以 `bf2185fba` 合入 `feat/five_min_design`**:游戏新项目默认 `standard + project-supervisor-plan`,显式“直接开建”保持 `autonomous-game-build`;阶段进度显示轮次 x/3、当前版本和状态徽章;实际项目总控页面挂载 hydrate/审批卡,并将 `project-planning` / 设计组展示名收口。未接 M2 approved-GDD 构建绑定或完整构建按钮。 | -| `M1E` | 端到端与故障注入收口 | `M1D-2` | 第 21 节测试矩阵中跨层场景 | +| `M1E` | 端到端与故障注入收口 | `M1D-2` | **已完成**:planning 覆盖审计与 submit 拒绝上限收口完成。`PLAN_INVALID_REQUEST` / Provider input 或候选 GDD 的 `PLAN_SIZE_LIMIT` 每 child run 最多 5 次 rejected observation,第 5 次在 observation durable 后终态失败;counter durable,重启不清零。若在第五条 rejected observation 落盘与终态失败之间崩溃,恢复入口会按 durable counter 直接终态失败,不请求第六次 Provider tool-plan。既有不可变 GDD/receipt 的超限读取及 lineage 版本已耗尽均改走 reconciliation,不误耗 Provider 重试额度。第 21 节已存在的三轮、续跑、receipt/replay、投影恢复及 hydrate 回归复核通过;不为“拼接已有单测”新增脆弱大 E2E。 | **2026-08-18 M1D 审查修复快照**:对 `14c00017c..bf2185fba` 做规格对照审查后,修复三条决定链路缺陷并补齐回归。① `decidePlanGdd` 的失败分支原来不 hydrate,命中后端任一 `PLAN_STALE_APPROVAL` 分支后卡片会停在已失效的 pending 身份上、`recoveryPending` 永不翻真导致「重试恢复」入口不渲染,现已按第 18.3 节在失败分支同样重灌(顺序钉死:`hydratePlanGddState` 入口会清空错误,必须先 hydrate 再写决定错误)。② responseId 复用键原为 `approvalRequestId:action`,不含 comment,违反第 13.2 节「改变 action/comment 必须换新 responseId」,现改为比对 `{action, comment}` 完整意图,判据方向为宁可多换不可少换。③ 第 18.2 节「`recoveryPending` 时不允许提交决定」原来只作用于三个触发按钮,已打开的评论弹层仍可提交,现已同门控并保留用户已输入内容。回归位于 `tests/appSurface/plan-gdd.suite.ts`,三条均经变异验证(逆转对应修复即变红);`appSurface.test.ts` 381 passed,`agc:typecheck`、ESLint、编码检查通过。其中锁错误回传绝对路径、阶段进度轮次差一格与 design 组展示名收口三条已于同日补修(见 decision-log 同日两条);仅 hydrate 身份校验与落盘投影修复的顺序一条单列后续,未并入。 @@ -2058,7 +2060,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` **为什么不进 `M1C-2b`、由 `M1C-2c` 承接**:`M1C-2b` 的合入门禁(3 轮上限、continuation 重放幂等、答案绑定冲突被拒)与选项文案无关;它按旧映射表完成了澄清中转,并已于 2026-08-17 快进合回。`M1C-2c` 已在隔离 worktree 翻映射、改信封形状校验并更新 role brief、Supervisor playbook 与 final-reply 提示;`planning_clarification_*` 13 条、`project_planning` prompt 5 条、`planning_submit` 定向回归、prompt bundle、格式、编码、diff 和 offline all-targets 门禁均通过。**`M1D-1` 前端决策卡必须直接按本节实现**(label 动态渲染、默认焦点在 A、Other 槽不变),避免做两遍;本包不接前端、hydrate、构建准入或下游完整构建。 -**顺带核对项(归 `M1E`)**:`plan.submit_gdd` 连续校验失败必须有**次数**上限。本地实测无界时,模型对大载荷序列化出错后连续 40 余次重试,每次重放全部历史,单次 prompt 涨到 15 万 token;生产的 240/300 秒预算注入兜不住「硬超时注入后仍连续校验失败」的情形。若第 12 节 retry 状态机没有该上限,补一个(原型取 5 次)。 +**顺带核对项(`M1E` 已完成)**:`plan.submit_gdd` 连续校验失败有**5 次**上限。本地实测无界时,模型对大载荷序列化出错后连续 40 余次重试,每次重放全部历史,单次 prompt 涨到 15 万 token;生产的 240/300 秒预算注入兜不住「硬超时注入后仍连续校验失败」的情形。第 12 节 retry 状态机现以 durable counter 收束,第五次 rejected observation 落盘后终态失败;恢复入口同样按该 counter 终态收束,不能因两步之间崩溃而发起第六次请求。 ## 24. 最终不变量摘要