修复:封口复核的控制流不再被吞成 HostDropped

- direct_runtime 返修循环接住 chat 侧返回的 TurnError::RepairRequired(app-server 封口复核),写回 prompt 继续一轮,流式与非流式两条分支都补上
- 删除 thread_manager/dispatch.rs 的 finish_turn_failure:它的 ShouldContinue 静默分支正是控制流漏到收口后、被 Drop 兜成 HostDropped 的入口
- 两处回合失败收口改为就地对 classify 结果显式分流:ShouldStop 写失败终态,ShouldContinue 视为上游缺陷直接 unreachable
This commit is contained in:
2026-10-03 14:32:31 +08:00
parent 6550e9a4bb
commit 84cbe9b1a6
2 changed files with 36 additions and 16 deletions
@@ -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)
@@ -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}"
)
}
}
}
}
}