未绑定执行器与回包失败的留痕加有界去重
- 未绑定宿主执行器的交互兜底按进程级 (method, outcome) 只写第一行,新增 distinct_outcomes - 适配器拒绝留痕抽出共用的 log_once,回包未送达同样按回合内原因只记一次 - 上限沿用 MAX_DENIED_REASON_LOGS = 64,模型重试不再逐次刷日志 - 同步更新 shared-memory 决策记录里的去重口径
This commit is contained in:
@@ -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<Mutex<HashSet<String>>> = 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<CodexAppServerInner>,
|
||||
pub(super) adapter: Arc<ExecutionAdapter>,
|
||||
@@ -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<Self>) {
|
||||
// 回包写不出去时 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 {
|
||||
|
||||
@@ -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`。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user