diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs index 9db5f7eac..3ff8e559a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs @@ -519,7 +519,7 @@ fn start_game_creator_agent_background_task_with_link_in_session_lane_with_proje let error = redact_agent_runtime_project_paths(root, &error, 500); let failed_task = AgentRuntimeTaskRecord { status: "failed".to_string(), - phase: "planning-session-projection-failed".to_string(), + phase: AGENT_RUNTIME_TASK_PHASE_PLANNING_SESSION_PROJECTION_FAILED.to_string(), current_action: "Fast GDD session 未能安全绑定,后台任务未执行".to_string(), terminal_detail: Some(error.clone()), error: Some(error.clone()), 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 2eaccbcbd..f0b760e52 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 @@ -4,6 +4,12 @@ static AGENT_RUNTIME_EVENT_ID_SEQUENCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); pub(crate) const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX: &str = "runtime-public-status-"; +/// task journal 只在**读**的时候校验 phase 白名单,写侧不校验。所以一个没登记的 +/// phase 落盘之后,整份 journal 从那一行起再也读不出来:`agent.run_status` 对该 +/// Agent 永久失败,父 run 只能瞎转到 needs-reconciliation。实测就是这么炸的。 +/// 让写方和白名单引用同一个常量,两边不可能再漂移。 +pub(crate) const AGENT_RUNTIME_TASK_PHASE_PLANNING_SESSION_PROJECTION_FAILED: &str = + "planning-session-projection-failed"; fn game_creator_agent_runtime_public_status_message_id( agent_id: &str, @@ -4089,6 +4095,7 @@ fn validate_game_creator_agent_runtime_task_status_phase( | "completion-contract-failed" | "conversation-write-failed" | "public-status-write-failed" + | AGENT_RUNTIME_TASK_PHASE_PLANNING_SESSION_PROJECTION_FAILED | "parent-terminal" | "parent-link-missing" ) { diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs index 1e3b140d1..b77477437 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs @@ -235,19 +235,33 @@ fn runtime_task_reader_accepts_rare_persisted_phases() { "failed", "public-status-write-failed", ); + // 写侧不校验 phase,只有读侧校验:白名单漏登记一个 phase,落盘之后整份 journal + // 从那一行起再也读不出来,`agent.run_status` 对该 Agent 永久失败。这里用写方 + // 引用的同一个常量,漏登记会在这条用例上先红。 + let planning_session_projection_failed = runtime_task_json_line( + "planning-session-projection-failed-run", + "failed", + AGENT_RUNTIME_TASK_PHASE_PLANNING_SESSION_PROJECTION_FAILED, + ); fs::write( &path, - format!("{brief}\n{parent_link_missing}\n{public_status_write_failed}\n"), + format!( + "{brief}\n{parent_link_missing}\n{public_status_write_failed}\n{planning_session_projection_failed}\n" + ), ) .expect("write rare persisted task phases"); let records = read_all_game_creator_agent_runtime_tasks(&path) .expect("known persisted task phases must remain readable"); - assert_eq!(records.len(), 3); + assert_eq!(records.len(), 4); assert_eq!(records[0].phase, "brief"); assert_eq!(records[1].phase, "parent-link-missing"); assert_eq!(records[2].phase, "public-status-write-failed"); + assert_eq!( + records[3].phase, + AGENT_RUNTIME_TASK_PHASE_PLANNING_SESSION_PROJECTION_FAILED + ); fs::remove_dir_all(root).ok(); }