回包失败留痕改为记录真实原因

- response_write_failed 改名为 response_delivery_failed,新增 ResponseDeliveryFailure 枚举
- 两个调用点分别传 Write(stdin 写入 / flush 失败)与 TurnBindMismatch(bind_turn 返回 false)
- 日志键改为 agent.direct_codex.approval.response_delivery_failed 并带 cause 字段,回合绑定失败不再被误报成写入失败
- 同步更新 shared-memory 决策记录里的日志键口径
This commit is contained in:
2026-10-01 15:44:25 +08:00
parent e3041d84e5
commit ce4ada8bb3
3 changed files with 32 additions and 9 deletions
@@ -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<Self>) {
// 回包写不出去时 Codex 侧等不到 decision,表现为「审批没有回应」;与主动 decline 分开留痕。
pub(super) fn response_delivery_failed(self: &Arc<Self>, 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()
)
},
);
@@ -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,
);
}
}
}
@@ -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`。