Direct 回合 panic 转成可诊断的分类失败
Project CI / AI game creator shell Rust smoke (push) Failing after 2m4s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 2m4s
Project CI / AI game creator shell Rust crates (push) Successful in 2m3s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 2m17s
Project CI / Backend tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled

dispatch 层用 AssertUnwindSafe/catch_unwind 接住回合体 panic,不再落到 HostDropped 兜底
panic 负载转成直接可读文本并写进脱敏诊断与应用日志,终态改为分类失败
补齐 futures::FutureExt 导入
This commit is contained in:
kdletters
2026-10-02 13:51:52 +08:00
parent fb9e25c90d
commit a6428228d1
@@ -13,13 +13,16 @@
use std::path::{Path, PathBuf};
use futures::FutureExt;
#[cfg(test)]
use crate::agent::PendingTurn;
use crate::agent::{
append_direct_project_user_message_at, claim_pending_turn, complete_turn_if_reserved,
direct_codex_user_item_to_prompt, direct_now_ms, redact_agent_runtime_error,
run_direct_game_creator_turn_at_with_creation_type_and_emitter, thread_id_for_project,
DirectGameCreatorTurnUpdateEmitter, DirectTurnError, DirectTurnTerminal, DispatchedTurn,
direct_codex_user_item_to_prompt, direct_now_ms, record_direct_codex_failure,
redact_agent_runtime_error, run_direct_game_creator_turn_at_with_creation_type_and_emitter,
thread_id_for_project, DirectCodexFailureStage, DirectGameCreatorTurnUpdateEmitter,
DirectTurnError, DirectTurnTerminal, DispatchedTurn,
};
/// 一次放行的占用。持有它就代表这一轮还没收口。
@@ -184,6 +187,17 @@ pub(crate) fn kick_queue_dispatch(root: &Path) {
}));
}
/// 把 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>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"未知 panic 负载".to_string()
}
}
/// 放行之后这一段:取这一轮的调用身份 → 落盘用户条目 → 下发 → 跑整轮。
///
/// 这一段的每个失败都是**回合失败**:占用与开始事件已经写好,只走占用对象的终态出口,不再回到任何
@@ -218,16 +232,38 @@ async fn run_dispatched_direct_turn(
crate::agent::codex_app_server::emit_direct_thread_user_item(&root, &canonical_user_item);
let capture = crate::analytics::gui::capture_writer_context();
let emitter = DirectGameCreatorTurnUpdateEmitter::new(&root, turn_id);
let outcome = run_direct_game_creator_turn_at_with_creation_type_and_emitter(
&root,
&prompt,
dispatched.pending.creation_type.as_deref(),
Some(&emitter),
Some(canonical_user_item.clone()),
capture,
release_identity_generation,
// 回合体 panic 不能落到 `TurnReservation` 的 Drop 兜底:那会把这一轮收成"说不出原因"的
// `HostDropped`,症状正是用户看到的「本轮执行已中断,请重试」,而项目侧诊断与应用日志
// **一行都不会有**(真实案例:2026-10-02 连续两轮)。这里把 panic 转成分类失败,
// 走与正常失败同一条收口:先写脱敏诊断,再写终态。
let outcome = match std::panic::AssertUnwindSafe(
run_direct_game_creator_turn_at_with_creation_type_and_emitter(
&root,
&prompt,
dispatched.pending.creation_type.as_deref(),
Some(&emitter),
Some(canonical_user_item.clone()),
capture,
release_identity_generation,
),
)
.await;
.catch_unwind()
.await
{
Ok(result) => result,
Err(payload) => {
let failure = DirectTurnError::turn_failed(
DirectCodexFailureStage::CodeGeneration,
format!(
"DirectProject 宿主任务 panic:{}",
direct_turn_panic_detail(payload.as_ref())
),
);
let stage = failure.turn_failure_stage();
let detail = record_direct_codex_failure(&root, &failure, Some(turn_id.as_str()));
Err(DirectTurnError::TurnFailed { stage, detail })
}
};
match outcome {
Ok(_) => {
// 深层的终态出口已经在 `run_turn` 里写出 `turn.completed`;这里只补最后一条回合更新。