From a59d068c1d641c95ab5bc3f11bbea0f9c092acf1 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:47:36 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=99=20Direct=20=E5=9B=9E=E5=90=88?= =?UTF-8?q?=E7=9A=84=E9=9D=99=E9=BB=98=E6=97=A9=E9=80=80=E8=A1=A5=E4=B8=8A?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TurnReservation 兜底收口时写应用日志,并用 thread::panicking 区分 panic 与 future 被丢弃/取消 新增 Direct 回合 panic hook:task-local 携带回合身份,panic 时记录位置与负载 两处均 cfg(not(test)),不污染用例与本机 AppData --- .../src/agent/thread_manager/dispatch.rs | 75 ++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) 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 5eabb23b6..4bfcadd2e 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 @@ -84,13 +84,80 @@ impl Drop for TurnReservation { fn drop(&mut self) { // 兜底:任务 panic、future 被丢弃、或今后在终态之前新增的 `?` 早退。 // 这类失败说不出原因,只给分类;能说清原因的错误必须由调用方在更早的地方显式收口。 - let _ = self.finish_if_unfinished(DirectTurnTerminal::host_dropped()); + let finalized_by_guard = self.finish_if_unfinished(DirectTurnTerminal::host_dropped()); + // 兜底一旦真的收口,就说明这一轮**从未写下终态**:深层既没成功也没失败地退出了。 + // 这条必须留应用日志,否则离线只剩一个 `phase=working` 的账本,无从判断是哪一层 + // 提前退出(真实案例:2026-10-02 连续两轮只留下 working 账本,errors/ 与 + // application.log 全空)。测试态不写,避免用例污染本机 AppData。 + #[cfg(not(test))] + if finalized_by_guard { + let _ = crate::append_application_log( + "error".to_string(), + "direct-turn".to_string(), + format!( + "agent.direct_turn.host_dropped threadId={} turnToken={} userItemId={} panicking={}", + self.thread_id, + self.token, + self.user_item_id.as_deref().unwrap_or("-"), + // true = 这一轮是被 panic 带走的;false = future 被丢弃或取消。 + std::thread::panicking() + ), + ); + } // 占用的释放就是"这个项目腾出了跑回合的位置":踢一脚,队里排着的消息不必等下一次用户动作。 // panic 也走这里,所以队列不会因为一个任务炸掉而永久停住。 kick_queue_dispatch(Path::new(&self.thread_id)); } } +// 运行段经 task-local 携带回合身份,panic hook 据此把 panic 位置与负载写进应用日志。 +// task-local 而非 thread-local:多线程 runtime 下 future 会跨 worker 迁移。 +tokio::task_local! { + static DIRECT_TURN_PANIC_CONTEXT: Option<(String, String)>; +} + +/// 兜底诊断:Direct 回合任务 panic 时,把位置与负载写进应用日志。 +/// +/// 没有它时,panic 只会让 `TurnReservation::drop` 把回合收成 `HostDropped`——那是"说不出原因" +/// 的分类,离线只留一个 `phase=working` 的账本,`.agent/runtime/errors/` 与 `application.log` +/// 全空,无法判断是哪一层退出的(真实案例:2026-10-02 连续两轮如此)。 +fn ensure_direct_turn_panic_hook() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + if let Ok(Some((thread_id, token))) = DIRECT_TURN_PANIC_CONTEXT.try_with(Clone::clone) { + let location = info + .location() + .map(|location| { + format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ) + }) + .unwrap_or_else(|| "未知位置".to_string()); + #[cfg(not(test))] + let _ = crate::append_application_log( + "error".to_string(), + "direct-turn".to_string(), + format!( + "agent.direct_turn.panic threadId={} turnToken={} location={} payload={}", + thread_id, + token, + location, + info.payload_as_str().unwrap_or("未知 panic 负载") + ), + ); + #[cfg(test)] + let _ = (thread_id, token, location); + } + previous(info); + })); + }); +} + /// 踢一脚:让队首那条待发消息有机会变成一轮真的回合。 /// /// 幂等,三个调用点:**入队之后**(队列空且没有回合在跑时,放行就是这一脚,不必再等一个调度周期)、 @@ -109,10 +176,12 @@ pub(crate) fn kick_queue_dispatch(root: &Path) { crate::platform_session::current_platform_session_write_state().identity_generation; let reservation = TurnReservation::resume(&thread_id, &dispatched); let root = root.to_path_buf(); - tauri::async_runtime::spawn(async move { + ensure_direct_turn_panic_hook(); + let panic_context = Some((thread_id.clone(), dispatched.token.clone())); + tauri::async_runtime::spawn(DIRECT_TURN_PANIC_CONTEXT.scope(panic_context, async move { run_dispatched_direct_turn(root, dispatched, reservation, release_identity_generation) .await; - }); + })); } /// 放行之后这一段:取这一轮的调用身份 → 落盘用户条目 → 下发 → 跑整轮。