From 5cf4a018b3b6e6b05f9feaea946744cfadc7bbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 22 Sep 2026 17:12:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=BF=E4=B8=BB=E7=BB=88=E6=80=81=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=EF=BC=9A=E5=A4=B1=E8=B4=A5=E5=86=99=E8=BF=9B=20turn.c?= =?UTF-8?q?ompleted=20=E7=9A=84=20failure=20=E8=BD=BD=E8=8D=B7=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=9C=A8=20turn.started=20=E4=B9=8B=E5=90=8E=E6=AD=A6?= =?UTF-8?q?=E8=A3=85=E5=85=9C=E5=BA=95=E5=AE=88=E5=8D=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - codex_app_server 的 DirectProject 终态改用 direct_turn_failure 判定:失败走 turn_completed_failed(原因脱敏 + 截断后写进同一个事件),其余仍走 turn_completed(status) - 失败判定两个来源:collect_result 是 Err 时用错误本身当原因;collect_result 是交付报告但 status 已判成 failed 时用那份报告当原因 - turn.started 进入队列后立即武装 DirectTurnFailureGuard,写完终态 disarm:panic、回合 future 被丢弃、终态之前的早退都会补一条 host-dropped 失败终态,前端不会停在"还在跑" - 定向 `cargo test direct_`(438 passed,含 wire / manager / 失败策略模块) --- .../src/agent/codex_app_server/mod.rs | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index 35c52a8bc..138ae6871 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -3548,12 +3548,20 @@ impl CodexAppServerConnection { let direct_turn_user_item_id = direct_persisted_user_item .as_ref() .and_then(direct_thread_item_identity); + // 回合终态兜底:`turn.started` 进队列之后就武装,写完终态即解除。宿主在这两者之间任何 + // 提前收场(panic、future 被丢弃、以后新增的早退)都由它补一条失败终态,否则前端只能 + // 永远停在"还在跑"。 + let mut direct_turn_failure_guard: Option = None; if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { append_direct_thread_event( &direct_thread_id, DirectThreadEvent::turn_started(direct_turn_started_at_ms) .with_user_item_id(direct_turn_user_item_id.as_deref()), ); + direct_turn_failure_guard = Some(DirectTurnFailureGuard::arm( + direct_thread_id.clone(), + direct_turn_user_item_id.clone(), + )); if let Some(user_item) = direct_persisted_user_item.as_ref() { if let Some(entry_item) = direct_thread_event_item(history_root, user_item) { // 这里的条目时间可能是启动应答后的观测时间;前端按同一用户条目身份 @@ -4061,11 +4069,30 @@ impl CodexAppServerConnection { .map(|(_, at)| *at) .unwrap_or_else(direct_tool_call_now_ms) }; - append_direct_thread_event( - &direct_thread_id, - DirectThreadEvent::turn_completed(status, completed_at) - .with_user_item_id(direct_turn_user_item_id.as_deref()), + // 终态只有 `turn.completed` 一种事件:失败时同一个事件带 `failure` 载荷(原因由宿主 + // 脱敏 + 截断后写进去),其余(`completed` / `interrupted` / `aborted`)不带载荷。 + // 失败不再只写一个 `status="failed"`:那让失败与正常结束在协议上长得一样,前端只能 + // 另开一条通道(命令返回 / 另一条 IPC)去拿原因,也就等于承认事件流讲不清一轮怎么结束。 + let failure = direct_turn_failure( + &status, + collect_result.as_ref().map(String::as_str), + history_root, ); + match failure { + Some(failure) => append_direct_thread_event( + &direct_thread_id, + DirectThreadEvent::turn_completed_failed(failure, completed_at) + .with_user_item_id(direct_turn_user_item_id.as_deref()), + ), + None => append_direct_thread_event( + &direct_thread_id, + DirectThreadEvent::turn_completed(status, completed_at) + .with_user_item_id(direct_turn_user_item_id.as_deref()), + ), + }; + if let Some(guard) = direct_turn_failure_guard.as_mut() { + guard.disarm(); + } } let text = collect_result?; guard.armed = false;