From e3041d84e5a6a378b915fb124d74007a4dd08168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 1 Oct 2026 15:41:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AA=E7=BB=91=E5=AE=9A=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E5=99=A8=E4=B8=8E=E5=9B=9E=E5=8C=85=E5=A4=B1=E8=B4=A5=E7=9A=84?= =?UTF-8?q?=E7=95=99=E7=97=95=E5=8A=A0=E6=9C=89=E7=95=8C=E5=8E=BB=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 未绑定宿主执行器的交互兜底按进程级 (method, outcome) 只写第一行,新增 distinct_outcomes - 适配器拒绝留痕抽出共用的 log_once,回包未送达同样按回合内原因只记一次 - 上限沿用 MAX_DENIED_REASON_LOGS = 64,模型重试不再逐次刷日志 - 同步更新 shared-memory 决策记录里的去重口径 --- .../src/agent/codex_app_server/execution.rs | 60 +++++++++++++++---- .../shared-memory/decision-log.md | 2 +- 2 files changed, 49 insertions(+), 13 deletions(-) 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 1f37e656c..29806b1c9 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 @@ -9,7 +9,7 @@ use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, Weak}; +use std::sync::{Arc, Mutex, OnceLock, Weak}; use std::time::Duration; use tokio::sync::{watch, Notify}; @@ -19,6 +19,11 @@ const MAX_REQUEST_CACHE: usize = 512; /// /// 原因基本是固定的静态分类,上限只是防止宿主错误文本意外发散时无界增长。 const MAX_DENIED_REASON_LOGS: usize = 64; +/// 未绑定执行器路径的进程级去重集合。 +/// +/// 这条路径没有适配器实例可挂去重状态(那正是它存在的前提),只能用进程级集合;上限与 +/// [`MAX_DENIED_REASON_LOGS`] 同口径。 +static NO_ADAPTER_INTERACTION_LOGGED: OnceLock>> = OnceLock::new(); /// 逐次审批协议的版本门禁:发行构建只接受捆绑侧车的固定版本;开发构建用宿主自带的 Codex /// (Linux 与未 stage 侧车时没有固定版本可用),按 profile 直接跳过该门禁。 @@ -54,12 +59,29 @@ pub(super) fn denied_response(id: u64, method: &str) -> Value { "item/permissions/requestApproval" => "empty-permissions", _ => "unsupported-method", }; - app_log!( - "agent.direct_codex.interaction.no_adapter method={method} outcome={outcome} request_id={id}" - ); + log_no_adapter_interaction_once(method, outcome, id); denied(id, method) } +/// 未绑定执行器时的交互兜底留痕:进程级有界去重。 +/// +/// 模型被拒后常会反复重试同一个动作,逐次记录会把其它诊断刷掉;同一 `(method, outcome)` 只写 +/// 第一行,`distinct_outcomes` 保留"一共出现过几种"的信息。 +fn log_no_adapter_interaction_once(method: &str, outcome: &str, request_id: u64) { + let logged = NO_ADAPTER_INTERACTION_LOGGED.get_or_init(|| Mutex::new(HashSet::new())); + let Ok(mut logged) = logged.lock() else { + return; + }; + if logged.len() >= MAX_DENIED_REASON_LOGS || !logged.insert(format!("{method}\u{1}{outcome}")) { + return; + } + let distinct = logged.len(); + drop(logged); + app_log!( + "agent.direct_codex.interaction.no_adapter method={method} outcome={outcome} request_id={request_id} distinct_outcomes={distinct}" + ); +} + pub(super) struct ExecutionBinding { inner: Weak, pub(super) adapter: Arc, @@ -348,7 +370,18 @@ impl ExecutionAdapter { /// 有用的诊断刷掉;"这一轮被拒过哪些原因"仍然完整。 fn log_denied_reason(&self, method: &str, reason: &str) { let key = format!("{method}\u{1}{reason}"); - // 拒绝原因留痕不得反过来影响放行判定:锁不可用就放弃记录。 + let thread_id = &self.thread_id; + self.log_once(key, |distinct| { + format!( + "agent.direct_codex.approval.denied thread_id={thread_id} method={method} reason={reason} distinct_reasons={distinct}" + ) + }); + } + + /// 有界去重留痕:同一回合内同 `key` 只写第一行。 + /// + /// 共用一把独立于审批状态的锁;锁不可用就放弃记录,留痕不得反过来影响放行判定。 + fn log_once(&self, key: String, build_line: impl FnOnce(usize) -> String) { let Ok(mut logged) = self.denied_reasons_logged.lock() else { return; }; @@ -357,10 +390,7 @@ impl ExecutionAdapter { } let distinct = logged.len(); drop(logged); - app_log!( - "agent.direct_codex.approval.denied thread_id={} method={method} reason={reason} distinct_reasons={distinct}", - self.thread_id - ); + app_log!("{}", build_line(distinct)); } fn denied(&self, id: u64, method: &str, reason: &str) -> Value { @@ -761,9 +791,15 @@ impl ExecutionAdapter { pub(super) fn response_write_failed(self: &Arc) { // 回包写不出去时 Codex 侧等不到 decision,表现为「审批没有回应」;与主动 decline 分开留痕。 - app_log!( - "agent.direct_codex.approval.response_write_failed thread_id={}", - self.thread_id + // 同一回合内同一原因只留一行:这一步可能被重复触发。 + self.log_once( + "host-interaction\u{1}response-write-failed".to_string(), + |_| { + format!( + "agent.direct_codex.approval.response_write_failed thread_id={}", + self.thread_id + ) + }, ); let adapter = Arc::clone(self); tokio::spawn(async move { diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index f718c57e3..d9c28492e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4,7 +4,7 @@ - 决策:宿主拒绝 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`。 - 原因:线上对审批只回 `{"decision":"decline"}`,模型与用户都看不到是哪一道闸门(合同缺失 / 预算耗尽 / 阶段已收束 / item 不在途 / 无适配器)拦下的,只能靠复现。 -- 边界:`reason` 只接受 `execution.rs` 内的静态分类或宿主自己的错误文本,不带请求参数、上游正文、路径或凭据;同一回合内 `(method, reason)` 只记一次,最多 `MAX_DENIED_REASON_LOGS = 64` 条。 +- 边界:`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`。 - 验证:`cargo test --bin genarrative-ai-game-creator-shell agent::codex_app_server::execution::tests`(15 passed,含 `denied_approval_records_its_reason_once_per_turn`、`denied_reason_logging_is_bounded`)、`npm run check:encoding`、`git diff --check`。