diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs index 29806b1c9..cd6b98452 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs @@ -291,6 +291,26 @@ fn accepted(id: u64, method: &str) -> Value { } } +/// app-server 交互回包未能送达的原因。 +/// +/// 两种失败共用同一条收束路径,但来源不同:写失败是 Codex 根本没收到 decision,回合绑定不一致 +/// 是回包已经产出、宿主无法确认执行作用域。留痕必须写真实原因,不能按函数名反推。 +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum ResponseDeliveryFailure { + /// stdin 写入 / flush 失败。 + Write, + /// `bind_turn` 返回 false:回合绑定不一致。 + TurnBindMismatch, +} +impl ResponseDeliveryFailure { + fn as_str(self) -> &'static str { + match self { + Self::Write => "response-write-failed", + Self::TurnBindMismatch => "turn-bind-mismatch", + } + } +} + impl ExecutionAdapter { pub(super) async fn for_turn( root: &Path, @@ -789,15 +809,16 @@ impl ExecutionAdapter { .await; } - pub(super) fn response_write_failed(self: &Arc) { - // 回包写不出去时 Codex 侧等不到 decision,表现为「审批没有回应」;与主动 decline 分开留痕。 + pub(super) fn response_delivery_failed(self: &Arc, cause: ResponseDeliveryFailure) { + // 回包没送达 Codex 时线上表现为「审批没有回应」;与主动 decline 分开留痕。 // 同一回合内同一原因只留一行:这一步可能被重复触发。 self.log_once( - "host-interaction\u{1}response-write-failed".to_string(), + format!("host-interaction\u{1}response-delivery:{}", cause.as_str()), |_| { format!( - "agent.direct_codex.approval.response_write_failed thread_id={}", - self.thread_id + "agent.direct_codex.approval.response_delivery_failed thread_id={} cause={}", + self.thread_id, + cause.as_str() ) }, ); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index 3c00d1410..a9b0a3560 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -18,7 +18,7 @@ mod execution; mod model_catalog; pub(crate) use direct_project_identity::thread_id_for_project; use direct_project_identity::*; -use execution::ExecutionAdapter; +use execution::{ExecutionAdapter, ResponseDeliveryFailure}; const GAME_CREATOR_CODEX_APP_SERVER_PROVIDER_ID: &str = "genarrative_agc"; const GAME_CREATOR_CODEX_APP_SERVER_API_KEY_ENV: &str = "GENARRATIVE_AGC_CODEX_API_KEY"; @@ -4884,7 +4884,7 @@ async fn read_game_creator_codex_app_server_stdout( }; if !sent { if let Some(adapter) = adapter { - adapter.response_write_failed(); + adapter.response_delivery_failed(ResponseDeliveryFailure::Write); } } }); @@ -4916,7 +4916,9 @@ async fn read_game_creator_codex_app_server_stdout( let adapter = inner.execution.lock().ok().and_then(|slot| slot.clone()); if let Some(adapter) = adapter { if !adapter.bind_turn(turn_id) { - adapter.response_write_failed(); + adapter.response_delivery_failed( + ResponseDeliveryFailure::TurnBindMismatch, + ); } } } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d9c28492e..a00fa21cc 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2,7 +2,7 @@ ## 2026-10-01 DirectProject 审批拒绝原因留痕 -- 决策:宿主拒绝 app-server 的审批 / 交互请求时,原因必须落 AppData `RUST` 日志。稳定键 `agent.direct_codex.approval.denied`,字段为 `thread_id` / `method` / `reason` / `distinct_reasons`;未绑定宿主执行器时另记 `agent.direct_codex.interaction.no_adapter`(`method` / `outcome`,`outcome` 区分 `decline` / `empty-permissions` / `unsupported-method`),回包未送达另记 `agent.direct_codex.approval.response_write_failed`。 +- 决策:宿主拒绝 app-server 的审批 / 交互请求时,原因必须落 AppData `RUST` 日志。稳定键 `agent.direct_codex.approval.denied`,字段为 `thread_id` / `method` / `reason` / `distinct_reasons`;未绑定宿主执行器时另记 `agent.direct_codex.interaction.no_adapter`(`method` / `outcome`,`outcome` 区分 `decline` / `empty-permissions` / `unsupported-method`);回包未送达另记 `agent.direct_codex.approval.response_delivery_failed`,`cause` 区分 `response-write-failed` 与 `turn-bind-mismatch`。 - 原因:线上对审批只回 `{"decision":"decline"}`,模型与用户都看不到是哪一道闸门(合同缺失 / 预算耗尽 / 阶段已收束 / item 不在途 / 无适配器)拦下的,只能靠复现。 - 边界:`reason` 只接受 `execution.rs` 内的静态分类或宿主自己的错误文本,不带请求参数、上游正文、路径或凭据;有界去重一律按 `MAX_DENIED_REASON_LOGS = 64` 条封顶——适配器路径按回合内 `(method, reason)`、未绑定执行器路径按进程级 `(method, outcome)`、回包未送达按回合内原因各只记一次。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs`。