From d86ea61a68a1d4ea22d3e97b7196ab4aa8ded508 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 22 Aug 2026 10:46:39 +0000 Subject: [PATCH] =?UTF-8?q?=E5=AE=A1=E6=89=B9=E6=B6=88=E8=B4=B9=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=94=B9=E7=94=A8=E7=94=9F=E4=BA=A7=E5=86=99=E6=B3=95?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=AD=96=E5=88=92=E5=AD=90=20Run=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=E4=B8=8A=E5=B9=82=E7=AD=89=E9=94=AE=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E7=9A=84=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit approval_receipt_consumes_submit_batch_and_folds_final_provider_usage_once 本该覆盖上一个提交修掉的那条链路,却一直是绿的。原因在夹具:它手搓 child_completed 再用 append_game_creator_agent_runtime_task 写终态,而这个入口 不往记录里写 actionId。生产侧走的是 ensure_project_planning_submit_child_completion_at,它用 append_game_creator_agent_runtime_task_projection_once 把 actionId 一起盖进去。 差别正好落在被测的那把幂等键上。磁盘上没有 (runId, actionId, phase=completed) 这一行,receipt 消费时的投影就找不到冲突对象,顺利追加——于是一条 100% 必现的 线上故障在单测里完全看不见。 改成调用生产入口本身,并按它的前置补齐 durable delivery(沿用同模块 2400 行 一带的既有写法,结果文案与生产的 last_response 一致,避免 publish 时身份不符)。 A/B 验证过测试确实抓得住:临时撤掉上一个提交的修复,此测试报 assertion failed: !first.recovery_pending,正是线上症状;装回修复即通过。 同批 planning_submit 并发跑有 6 条失败,逐条单跑全过,是本机 agent.db 文件锁 竞争,与本改动无关。 Co-Authored-By: Claude Opus 5 --- .../agent/runtime_protocol/planning_submit.rs | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) 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 dd614e229..3736cec90 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 @@ -4484,21 +4484,49 @@ mod tests { context.action_id = pending.action_id.clone(); context.action_fingerprint = pending.action_fingerprint.clone(); - execute_plan_submit_gdd(&root, &context, &input).expect("commit GDD"); + let submit_result = execute_plan_submit_gdd(&root, &context, &input).expect("commit GDD"); assert_eq!( fold_plan_provider_usage_into_session_at_locked(&root) .expect("submit batch must defer final usage"), PlanProviderUsageFoldOutcome::Deferred ); + // Close the child through the production writer rather than a + // hand-rolled terminal state. It stamps the task projection with the + // submit `actionId`, which is what makes the receipt consumption below + // collide on the (runId, actionId, phase) idempotency key. A plain + // `append_game_creator_agent_runtime_task` leaves no keyed row, so the + // collision never happens and this test passes over a lane that fails + // 100% of the time in production. + let delivery = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "root-session-001", + &context.root_run_id, + "parent-action-001", + &context.delegation_id, + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + &context.session_id, + &context.created_by_run_id, + ); + create_or_read_static_delegate_delivery_at(&root, &delivery) + .expect("create planning delivery"); + mark_static_delegate_delivery_ready_at( + &root, + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + &context.session_id, + &context.created_by_run_id, + &context.delegation_id, + "completed", + &format!("Fast GDD v{} 已提交。", submit_result.gdd_ref.version), + ) + .expect("close planning delivery"); let mut child_completed = child_runtime.clone(); - child_completed.status = "completed".to_string(); - child_completed.phase = "completed".to_string(); - child_completed.current_action = "Fast GDD 已提交".to_string(); - child_completed.pending_tool_action = Some(pending.summary()); - append_game_creator_agent_runtime_task(&root, &child_completed) - .expect("append completed planning child task"); - write_game_creator_agent_runtime_state(&root, &child_completed) - .expect("persist completed planning child state"); + ensure_project_planning_submit_child_completion_at( + &root, + &mut child_completed, + &pending, + &submit_result, + ) + .expect("close planning child at the submit point"); let gdd = read_plan_gdd_chain(&root) .expect("read submitted GDD")