未绑定执行器与回包失败的留痕加有界去重

- 未绑定宿主执行器的交互兜底按进程级 (method, outcome) 只写第一行,新增 distinct_outcomes
- 适配器拒绝留痕抽出共用的 log_once,回包未送达同样按回合内原因只记一次
- 上限沿用 MAX_DENIED_REASON_LOGS = 64,模型重试不再逐次刷日志
- 同步更新 shared-memory 决策记录里的去重口径
This commit is contained in:
2026-10-01 15:41:26 +08:00
parent c34e45e0c4
commit e3041d84e5
2 changed files with 49 additions and 13 deletions
@@ -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 {