Merge branch 'master' into feat/dev-all
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 2m43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 1m36s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m46s
Project CI / Frontend tests (pull_request) Successful in 2m40s
Project CI / Repository checks (pull_request) Successful in 3m23s
Project CI / Native shell tests (pull_request) Failing after 4m27s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 2m43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 1m36s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m46s
Project CI / Frontend tests (pull_request) Successful in 2m40s
Project CI / Repository checks (pull_request) Successful in 3m23s
Project CI / Native shell tests (pull_request) Failing after 4m27s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
This commit is contained in:
@@ -732,7 +732,9 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at(
|
||||
client_turn_id: Option<&str>,
|
||||
observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>,
|
||||
) -> Result<String, String> {
|
||||
direct_turn_trace("claude-executor-enter");
|
||||
let (mcp_url, mcp_token) = start_external_mcp_loopback(root, llm.web_search_enabled).await?;
|
||||
direct_turn_trace("claude-mcp-loopback-ready");
|
||||
let _mcp_guard = ExternalMcpStopGuard;
|
||||
let home = crate::project::resolve_local_project_path(root, ".agent/runtime/claude-code/home")?;
|
||||
std::fs::create_dir_all(&home)
|
||||
@@ -742,6 +744,7 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at(
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|sessions| sessions.get(&session_key).cloned());
|
||||
direct_turn_trace("claude-sidecar-start");
|
||||
let payload = serde_json::json!({
|
||||
"type": "turn",
|
||||
"id": client_turn_id.unwrap_or("claude-direct-turn"),
|
||||
@@ -769,6 +772,7 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at(
|
||||
client_turn_id,
|
||||
)
|
||||
.await?;
|
||||
direct_turn_trace("claude-sidecar-done");
|
||||
if let Some(session_id) = sidecar
|
||||
.result
|
||||
.get("session_id")
|
||||
@@ -785,6 +789,7 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at(
|
||||
.filter_map(|event| serde_json::to_string(&event).ok())
|
||||
.collect::<Vec<_>>();
|
||||
stream.push(String::new());
|
||||
direct_turn_trace("claude-parse-start");
|
||||
parse_direct_stream_result(stream.join("\n").as_bytes(), observer)
|
||||
}
|
||||
|
||||
|
||||
@@ -4294,6 +4294,24 @@ pub(crate) fn check_direct_turn_preconditions(
|
||||
.map_err(|detail| DirectTurnError::InputRejected { detail })?;
|
||||
Ok(())
|
||||
}
|
||||
/// Direct 回合在 `direct_execution::begin` 之后的分段 trace。
|
||||
///
|
||||
/// 起因:2026-10-02 连续四轮回合都停在 `begin` 之后——账本 `phase=working`、
|
||||
/// `nextSequence=0`、`active={}`、没有任何错误 sidecar、stderr 无 panic、连
|
||||
/// `agent.direct_codex.*` 都没有。窗口就夹在 `begin` 与执行器调用之间,需要知道
|
||||
/// 具体停在哪一步。测试态不写,避免污染本机 AppData。
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn direct_turn_trace(stage: &str) {
|
||||
let _ = crate::append_application_log(
|
||||
"info".to_string(),
|
||||
"direct-turn".to_string(),
|
||||
format!("agent.direct_turn.trace stage={stage}"),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn direct_turn_trace(_stage: &str) {}
|
||||
|
||||
pub(crate) async fn run_direct_game_creator_turn_at_with_creation_type_and_emitter(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
@@ -4401,6 +4419,7 @@ async fn run_direct_game_creator_turn_inner(
|
||||
})?;
|
||||
let execution_session = execution_guard.session();
|
||||
execution_session.set_analytics_capture(capture.clone());
|
||||
direct_turn_trace("session-ready");
|
||||
let started = execution_session
|
||||
.newly_accepted
|
||||
.then(std::time::Instant::now);
|
||||
@@ -4411,6 +4430,7 @@ async fn run_direct_game_creator_turn_inner(
|
||||
}
|
||||
}
|
||||
}
|
||||
direct_turn_trace("run-analytics-accepted");
|
||||
// 在 guard 仍存活时冻结整体结果,避免 Drop 的中断收尾覆盖真实失败原因。
|
||||
let result: Result<String, DirectTurnError> = async {
|
||||
if let Some(report) = super::direct_delivery::terminal_report(&execution_session) {
|
||||
@@ -4444,6 +4464,7 @@ async fn run_direct_game_creator_turn_inner(
|
||||
);
|
||||
}
|
||||
}
|
||||
direct_turn_trace("goal-analytics-accepted");
|
||||
emit_direct_game_creator_progress(root, "codex.turn", "陶泥儿正在处理这条消息");
|
||||
if let Some(emitter) = turn_emitter {
|
||||
emitter.emit("running", Some("preparing"));
|
||||
@@ -4460,6 +4481,7 @@ async fn run_direct_game_creator_turn_inner(
|
||||
DirectTurnError::turn_failed(DirectCodexFailureStage::CodeGeneration, error)
|
||||
},
|
||||
)?;
|
||||
direct_turn_trace("system-prompt-ready");
|
||||
// 三维请求:把"自选三维技术栈、解除 Phaser 固定约束"的合同放在系统提示最前,
|
||||
// 避免被长度上限截断,也不阻断任何工具。
|
||||
let engine_contract = direct_engine_three_dimensional_contract(root, prompt);
|
||||
@@ -4471,6 +4493,7 @@ async fn run_direct_game_creator_turn_inner(
|
||||
None => base_system_prompt,
|
||||
};
|
||||
let reply = if let Some(emitter) = turn_emitter {
|
||||
direct_turn_trace("executor-dispatch");
|
||||
let client_turn_id = emitter.turn_id().to_string();
|
||||
let emitter = emitter.clone();
|
||||
let mut observer = |observation: DirectCodexTurnObservation| {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/// 一次放行的占用。持有它就代表这一轮还没收口。
|
||||
@@ -84,13 +87,90 @@ 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 {
|
||||
// 项目侧也留一份脱敏诊断:用户在项目目录里就能看到这一轮的收场,
|
||||
// 不必只依赖 AppData 的应用日志。
|
||||
let failure = DirectTurnError::TurnFailed {
|
||||
stage: DirectCodexFailureStage::CodeGeneration,
|
||||
detail: format!(
|
||||
"DirectProject 宿主任务提前结束(panic、future 被丢弃或被取消),本轮未写下终态;threadId={} turnToken={}",
|
||||
self.thread_id, self.token
|
||||
),
|
||||
};
|
||||
let _ = record_direct_codex_failure(Path::new(&self.thread_id), &failure, None);
|
||||
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 +189,23 @@ 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;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/// 把 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// 放行之后这一段:取这一轮的调用身份 → 落盘用户条目 → 下发 → 跑整轮。
|
||||
@@ -149,16 +242,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`;这里只补最后一条回合更新。
|
||||
|
||||
Reference in New Issue
Block a user