From 84cbe9b1a6dcc84c017b2da4246a509edc87d59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 3 Oct 2026 14:32:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=B0=81=E5=8F=A3?= =?UTF-8?q?=E5=A4=8D=E6=A0=B8=E7=9A=84=E6=8E=A7=E5=88=B6=E6=B5=81=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=A2=AB=E5=90=9E=E6=88=90=20HostDropped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - direct_runtime 返修循环接住 chat 侧返回的 TurnError::RepairRequired(app-server 封口复核),写回 prompt 继续一轮,流式与非流式两条分支都补上 - 删除 thread_manager/dispatch.rs 的 finish_turn_failure:它的 ShouldContinue 静默分支正是控制流漏到收口后、被 Drop 兜成 HostDropped 的入口 - 两处回合失败收口改为就地对 classify 结果显式分流:ShouldStop 写失败终态,ShouldContinue 视为上游缺陷直接 unreachable --- .../src-tauri/src/agent/direct_runtime/mod.rs | 14 +++++++ .../src/agent/thread_manager/dispatch.rs | 38 +++++++++++-------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 95d4fc299..dc71492c0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -4631,6 +4631,15 @@ async fn run_direct_game_creator_turn_inner( Err(error) => break Err(error), } } + // 封口复核要求继续当前返修批次(app-server 封口复核):**控制流,不是失败**。 + // 这是 `direct_delivery::review_reply` 之外的第二条返修来源,处理与上面的 + // 返修要求完全一样——把要求写回 prompt 再跑一轮;次数上限在产生侧(执行账本 + // 的批次上限),这里不另设计数。不接住它,回合会漏到终态收口被静默吞掉。 + Err(TurnError::RepairRequired { detail }) => { + emitter.emit("running", Some("host-review")); + feedback_prompt = + format!(prompt_text!("direct.deliveryFeedback"), detail = detail); + } // 交付报告的兜底只读一次:guard 与取值各调一次会在两次之间换出不同结果, // 第二次拿到 `None` 时还会把空串当成回复返回。 Err(error) => match super::direct_delivery::terminal_report(&execution_session) @@ -4688,6 +4697,11 @@ async fn run_direct_game_creator_turn_inner( Err(error) => return Err(error), } } + // 同流式分支:封口复核的返修要求是控制流,写回 prompt 再跑一轮。 + Err(TurnError::RepairRequired { detail }) => { + feedback_prompt = + format!(prompt_text!("direct.deliveryFeedback"), detail = detail); + } // 同上:交付报告只读一次,避免两次调用之间换出不同结果(第二次拿到 `None` // 时还会绕过下面那条"未返回结果"的兜底错误)。 Err(error) => match super::direct_delivery::terminal_report(&execution_session) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs index 7cb48aebb..ec68232d3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs @@ -192,20 +192,6 @@ pub(crate) fn kick_queue_dispatch(root: &Path) { })); } -/// 放行之后的回合失败写点:typed 错误按 [`TurnError::classify`] 投影,真失败才写终态。 -/// -/// 这些路径上的错误都是**回合失败**(写历史失败 / 回合体返回的 `Err`);控制流(返修 / 复核要求 -/// 继续)在 `direct_runtime` 的返修循环里就被消化,不会到这里。真漏到这里也不写终态——这一轮还 -/// 没结束,不能伪造一条失败。 -fn finish_turn_failure(reservation: &TurnReservation, error: &TurnError, history_root: &Path) { - match error.classify(history_root) { - TurnErrorClassified::ShouldStop(payload) => { - reservation.finish_if_unfinished(TurnCompletion::failed(payload)); - } - TurnErrorClassified::ShouldContinue { .. } => {} - } -} - /// 把 panic 负载转成可读文本:`panic!("…")` 的负载是 `&str`,`panic!("{x}")` 是 `String`。 fn direct_turn_panic_detail(payload: &(dyn std::any::Any + Send)) -> String { if let Some(text) = payload.downcast_ref::<&str>() { @@ -238,7 +224,15 @@ async fn run_dispatched_direct_turn( // 消息的助手回复,而且失败会被静默掉。 // 原文不在这里脱敏:`classify` 在投影成失败载荷时统一脱敏 + 截断(错误文案的脱敏保留)。 let failure = TurnError::environment_not_ready(format!("写入本项目对话历史失败:{error}")); - finish_turn_failure(&reservation, &failure, &root); + // 这条错误是本地构造的**真失败**:classify 只可能给 ShouldStop。 + match failure.classify(&root) { + TurnErrorClassified::ShouldStop(payload) => { + reservation.finish_if_unfinished(TurnCompletion::failed(payload)); + } + TurnErrorClassified::ShouldContinue { detail } => { + unreachable!("environment_not_ready 只可能是回合失败,不该分类成控制流:{detail}") + } + } return; } // 用户条目落盘成功即下发:这一轮从"放行"到"起 codex"之间的一切失败(连不上 app-server、执行器 @@ -292,7 +286,19 @@ async fn run_dispatched_direct_turn( Err(error) => { // 放行之后的失败一律是回合失败:失败诊断与失败说明已由上层写过,这里补终态事件。 // 深层已经写出终态时它不覆盖(同一轮只允许一条终态)。 - finish_turn_failure(&reservation, &error, &root); + // 按 `classify` 显式分流,不留静默分支:控制流(返修 / 复核要求继续)本应被 + // `direct_runtime` 的返修循环消化成下一轮;两条来源(交付复核、app-server 封口复核) + // 都在循环里接住了。漏到这里就是上游缺陷——直接炸出来,不再静默吞掉。 + match error.classify(&root) { + TurnErrorClassified::ShouldStop(payload) => { + reservation.finish_if_unfinished(TurnCompletion::failed(payload)); + } + TurnErrorClassified::ShouldContinue { detail } => { + unreachable!( + "控制流错误到达回合失败收口(应在 direct_runtime 的返修循环内消化):{detail}" + ) + } + } } } }