新增: 写删同路径破坏性回环检测
写后紧删同路径是自毁循环直接证据,签名熔断对写删交替漏网(每步签名不同) 按工具与规范化路径对查,独立计数器与签名缓冲并行互不干扰 其他工具占位计入间隔,防写后多次探索再删的正常迭代误判
This commit is contained in:
@@ -352,6 +352,68 @@ pub(crate) fn is_repetitive_exploration(signatures: &[String]) -> bool {
|
|||||||
counts.values().any(|&c| c >= REPETITION_SINGLE_MAX)
|
counts.values().any(|&c| c >= REPETITION_SINGLE_MAX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// G2 补充:破坏性回环检测(治「写完就删」自毁循环,G2 签名熔断对此漏网)
|
||||||
|
//
|
||||||
|
// 漏网根因:write_file/delete_file 走 tool_call_signature 兜底分支(name + args 全 JSON),
|
||||||
|
// 写→删→写交替时每步签名都不同(唯一率≈0.83、单签名最高 2 次),is_repetitive_exploration
|
||||||
|
// 全程不触发。但「同路径写后紧跟删(或反向)」本身就是自毁/反悔循环的直接证据——正常流程
|
||||||
|
// 写后验证后保留,不会紧凑间隔内删同路径。本检测不比签名,直接按 (工具, 路径) 对查。
|
||||||
|
// 调用点:check_stall_breaker 采样处与签名缓冲并行维护,独立计数器互不干扰。
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/// 回环缓冲样本:每个工具调用占一位,write_file/delete_file 记 (工具名, 规范化路径),
|
||||||
|
/// 其他工具 None 占位。占位让「GAP 步内」的步按全部工具调用序位算——写删间穿插的
|
||||||
|
/// 探索调用也算间隔,避免把「写→多次探索→删」的长间隔正常迭代误判为紧凑回环。
|
||||||
|
pub(crate) type DestructiveLoopSample = Option<(String, String)>;
|
||||||
|
|
||||||
|
/// 路径规范化:分隔符统一为 / + 小写,用于写删同路径判定(Windows 路径 \ / 混用与大小写不敏感)。
|
||||||
|
pub(crate) fn normalize_loop_path(p: &str) -> String {
|
||||||
|
p.replace('\\', "/").to_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从工具调用提取回环样本:write_file/delete_file 取 path 参数规范化,其他工具 None 占位。
|
||||||
|
/// path 缺失/非字符串 → None(无路径无从判同路径,退化为占位)。
|
||||||
|
pub(crate) fn destructive_loop_sample(name: &str, args: &serde_json::Value) -> DestructiveLoopSample {
|
||||||
|
match name {
|
||||||
|
"write_file" | "delete_file" => {
|
||||||
|
let p = arg_str(args, "path");
|
||||||
|
if p.is_empty() { None } else { Some((name.to_string(), normalize_loop_path(&p))) }
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 破坏性回环检测:窗口内「write_file(P) 后 GAP 步内 delete_file(P)」或反向 → 命中路径。
|
||||||
|
///
|
||||||
|
/// GAP=6(全工具调用序位):写删交替紧凑才判回环,间隔远的写与删是正常迭代修复不算。
|
||||||
|
/// 返回首个命中对的规范化路径(警示/熔断文本可引用),无命中 → None。
|
||||||
|
pub(crate) fn detect_destructive_loop(samples: &[DestructiveLoopSample]) -> Option<String> {
|
||||||
|
/// 回环对最大间隔步数(按全部工具调用序位)。
|
||||||
|
const DESTRUCTIVE_LOOP_GAP: usize = 6;
|
||||||
|
|
||||||
|
for i in 0..samples.len() {
|
||||||
|
let (na, pa) = match samples[i].as_ref() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let opposite = match na.as_str() {
|
||||||
|
"write_file" => "delete_file",
|
||||||
|
"delete_file" => "write_file",
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
// j 限 (i, i+GAP]:紧凑间隔内找反向同路径操作。
|
||||||
|
for j in (i + 1)..samples.len().min(i + 1 + DESTRUCTIVE_LOOP_GAP) {
|
||||||
|
if let Some((nb, pb)) = samples[j].as_ref() {
|
||||||
|
if pb == pa && nb == opposite {
|
||||||
|
return Some(pa.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// 纯问候判定:纯社交短文本(你好/谢谢/在吗等)→ true。机制层治弱模型把问候当指令
|
/// 纯问候判定:纯社交短文本(你好/谢谢/在吗等)→ true。机制层治弱模型把问候当指令
|
||||||
/// 擅自调工具(会话 b4d6b4e0:用户"你好"→ list_project_modules + 探索源码)。
|
/// 擅自调工具(会话 b4d6b4e0:用户"你好"→ list_project_modules + 探索源码)。
|
||||||
/// 规则:去空白后 ≤8 字,且不含动作词/实体引用(@[)。"你好,看看 moyu 项目"含动作词+超长 → false。
|
/// 规则:去空白后 ≤8 字,且不含动作词/实体引用(@[)。"你好,看看 moyu 项目"含动作词+超长 → false。
|
||||||
@@ -505,6 +567,99 @@ mod tests {
|
|||||||
assert!(is_repetitive_exploration(&sigs), "反复读同段应判重复");
|
assert!(is_repetitive_exploration(&sigs), "反复读同段应判重复");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 破坏性回环检测(detect_destructive_loop)测试 ──
|
||||||
|
|
||||||
|
/// 测试辅助:(工具名, args) 序列 → 回环样本缓冲。
|
||||||
|
fn loop_samples_of(calls: &[(&str, serde_json::Value)]) -> Vec<DestructiveLoopSample> {
|
||||||
|
calls.iter().map(|(n, a)| destructive_loop_sample(n, a)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 核心回归(真实案例:清理任务 rename→delete→write→list→delete→write 自毁循环):
|
||||||
|
/// write(P)→delete(P)→write(P) 签名各不相同(G2 唯一率≈0.83 漏网),回环检测应命中。
|
||||||
|
#[test]
|
||||||
|
fn loop_write_delete_write_detected() {
|
||||||
|
let samples = loop_samples_of(&[
|
||||||
|
("write_file", serde_json::json!({"path": "out.txt", "content": "x"})),
|
||||||
|
("delete_file", serde_json::json!({"path": "out.txt"})),
|
||||||
|
("write_file", serde_json::json!({"path": "out.txt", "content": "y"})),
|
||||||
|
]);
|
||||||
|
assert_eq!(detect_destructive_loop(&samples), Some("out.txt".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 反向对:delete(P) 后紧跟 write(P)(删了又写回)同属回环。
|
||||||
|
#[test]
|
||||||
|
fn loop_delete_then_write_detected() {
|
||||||
|
let samples = loop_samples_of(&[
|
||||||
|
("delete_file", serde_json::json!({"path": "a.rs"})),
|
||||||
|
("write_file", serde_json::json!({"path": "a.rs", "content": "x"})),
|
||||||
|
]);
|
||||||
|
assert_eq!(detect_destructive_loop(&samples), Some("a.rs".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 不误报:对同/不同文件连续 write(A)→write(B) 正常序列,无反向操作不判回环。
|
||||||
|
#[test]
|
||||||
|
fn loop_normal_writes_no_false_positive() {
|
||||||
|
let samples = loop_samples_of(&[
|
||||||
|
("write_file", serde_json::json!({"path": "a.rs"})),
|
||||||
|
("write_file", serde_json::json!({"path": "b.rs"})),
|
||||||
|
("write_file", serde_json::json!({"path": "a.rs"})),
|
||||||
|
]);
|
||||||
|
assert_eq!(detect_destructive_loop(&samples), None, "纯写入交替不算回环");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 路径规范化:分隔符(\ /)与大小写差异视为同路径。
|
||||||
|
#[test]
|
||||||
|
fn loop_path_normalized() {
|
||||||
|
let samples = loop_samples_of(&[
|
||||||
|
("write_file", serde_json::json!({"path": "SRC\\Lib.RS"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "x", "path": "y"})),
|
||||||
|
("delete_file", serde_json::json!({"path": "src/lib.rs"})),
|
||||||
|
]);
|
||||||
|
assert_eq!(detect_destructive_loop(&samples), Some("src/lib.rs".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 间隔语义:中间穿插的其他工具调用计入步数,None 占位跳过判定但不缩间隔。
|
||||||
|
#[test]
|
||||||
|
fn loop_interleaved_tools_counted() {
|
||||||
|
let samples = loop_samples_of(&[
|
||||||
|
("write_file", serde_json::json!({"path": "p.txt"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "x", "path": "y"})),
|
||||||
|
("delete_file", serde_json::json!({"path": "p.txt"})),
|
||||||
|
]);
|
||||||
|
assert_eq!(detect_destructive_loop(&samples), Some("p.txt".to_string()), "间隔 2 步 ≤ GAP=6 应命中");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 间隔超 GAP 不判回环:写后隔 6 个其他调用才删(第 8 位,间隔 7)是正常迭代修复。
|
||||||
|
#[test]
|
||||||
|
fn loop_gap_beyond_threshold_not_detected() {
|
||||||
|
let samples = loop_samples_of(&[
|
||||||
|
("write_file", serde_json::json!({"path": "p.txt"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "1", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "2", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "3", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "4", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "5", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "6", "path": "y"})),
|
||||||
|
("delete_file", serde_json::json!({"path": "p.txt"})),
|
||||||
|
]);
|
||||||
|
assert_eq!(detect_destructive_loop(&samples), None, "间隔 7 步 > GAP=6 不应命中");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 间隔恰为 GAP(第 7 位,间隔 6)仍是回环边界内。
|
||||||
|
#[test]
|
||||||
|
fn loop_gap_exactly_at_threshold_detected() {
|
||||||
|
let samples = loop_samples_of(&[
|
||||||
|
("write_file", serde_json::json!({"path": "p.txt"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "1", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "2", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "3", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "4", "path": "y"})),
|
||||||
|
("grep", serde_json::json!({"pattern": "5", "path": "y"})),
|
||||||
|
("delete_file", serde_json::json!({"path": "p.txt"})),
|
||||||
|
]);
|
||||||
|
assert_eq!(detect_destructive_loop(&samples), Some("p.txt".to_string()), "间隔 6 步 = GAP 应命中");
|
||||||
|
}
|
||||||
|
|
||||||
/// 场景样本不足:仅 3 个签名(即使全同)。判 false(不判)。
|
/// 场景样本不足:仅 3 个签名(即使全同)。判 false(不判)。
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_insufficient_sample_not_repetitive() {
|
fn scenario_insufficient_sample_not_repetitive() {
|
||||||
|
|||||||
Reference in New Issue
Block a user