优化: 边界加固(AI loop竞态根治+数据/审批/反馈/并发/错误分类)

AI loop 竞态(P0):per-conv epoch/owner token + 存活心跳治 force_send 双loop + stop 3s兜底误判;旧loop stale 全跳过(guard/emit/save)

agentic 收尾(A2-B8):Fatal 退出落库user消息(镜像Exhausted)+ 入口早退补save + usage is_estimated 打标 + emit_ai_completed_once 单点收敛清审批残留

聊天清理(A2-B9):clearChat 先停loop→DB单事务→内存清(clear_conversation_atomic)+ 前端错误气泡

循环并发(A2-B11):三态 ProviderAcquire(NotConfigured/Acquired/Exhausted)+ 候选循环非阻塞+防抖3次饱和降级+单测

错误分类(A2-B12):stream error帧接入 classify_status_or_class + 关键词保守降级 + 7单测

数据(G1.2/G1.4):purge_with_descendants 级联补全(11表单事务+存在性守卫)+ move_task_queue 单事务收口(两调用方共用)

git只读(G3.1):run_git_status/diff/log success判定(exit_code差异语义,失败结构化{success:false,error})

安全(G5.2/G5.6):create_project 目录Err+name校验 + module.rs 路径遍历DRY(分段匹配修a..b.rs误伤)

幂等(V2/V32):裸ALTER全守卫化 + v1..v40全链重跑幂等测试(16过)

附:remote_bridge await 临时引用修(E0716)+ agentic emit 收敛 E0716 app_state 绑定修
This commit is contained in:
lxy
2026-08-05 22:10:32 +08:00
parent ec9f0bf1ea
commit 5667da6cf4
16 changed files with 1696 additions and 215 deletions
+89 -10
View File
@@ -62,6 +62,8 @@ pub async fn try_continue_agent_loop(
let ev = AiChatEvent::AiCompleted {
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
prompt_cache_hit_tokens: 0, prompt_cache_miss_tokens: 0, reasoning_tokens: 0,
// 零 token 收敛信号,非估算(无真实 LLM 调用)。
is_estimated: false,
incomplete: None,
conversation_id: Some(emit_conv_id),
pinned_goals: snap.pinned_goals_snapshot.clone(),
@@ -113,6 +115,8 @@ pub async fn try_continue_agent_loop(
let ev = AiChatEvent::AiCompleted {
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
prompt_cache_hit_tokens: 0, prompt_cache_miss_tokens: 0, reasoning_tokens: 0,
// 零 token 收敛信号,非估算(无真实 LLM 调用)。
is_estimated: false,
incomplete: None,
conversation_id: Some(conv_id_owned.clone()),
pinned_goals: snap.pinned_goals_snapshot.clone(),
@@ -129,8 +133,15 @@ pub async fn try_continue_agent_loop(
round: 0, conversation_id: Some(conv_id_owned.clone()),
});
// F1 并发 epoch:续跑也是新 loop 生命周期,递增 owner token(旧 loop 已 return/disarm)。
// conv_id_owned 是 spawn 前 clone,此处仍可在 scope 内借用 state 锁。
let loop_epoch = {
let mut session = state.ai_session.lock().await;
session.conv(&conv_id_owned).loop_epoch.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1
};
tauri::async_runtime::spawn(async move {
super::run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id_owned, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override).await;
super::run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id_owned, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override, loop_epoch).await;
});
}
@@ -320,6 +331,30 @@ pub(crate) fn is_repetitive_exploration(signatures: &[String]) -> bool {
counts.values().any(|&c| c >= REPETITION_SINGLE_MAX)
}
/// 纯问候判定:纯社交短文本(你好/谢谢/在吗等)→ true。机制层治弱模型把问候当指令
/// 擅自调工具(会话 b4d6b4e0:用户"你好"→ list_project_modules + 探索源码)。
/// 规则:去空白后 ≤8 字,且不含动作词/实体引用(@[)。"你好,看看 moyu 项目"含动作词+超长 → false。
pub(crate) fn is_pure_greeting(msg: &str) -> bool {
// 问候/社交短文本词表(命中任一即可;长度约束兜底)
const GREETINGS: &[&str] = &[
"你好", "hello", "hi", "哈喽", "", "在吗", "谢谢", "感谢", "",
"好的", "ok", "没问题", "辛苦", "拜拜", "再见", "👋", "你好呀",
];
// 动作/请求意图词:出现任一即非纯问候(放行工具)
const ACTION_WORDS: &[&str] = &[
"", "", "", "创建", "新增", "修改", "更新", "删除", "分析",
"读取", "", "执行", "运行", "测试", "构建", "推进", "检查", "搜索",
"列出", "绑定", "如何", "怎么", "为什么", "什么是", "有哪些", "怎么办",
];
let t = msg.trim();
if t.is_empty() { return true; }
if t.chars().count() > 8 { return false; }
if t.contains("@[") { return false; }
let lower = t.to_lowercase();
if !GREETINGS.iter().any(|g| lower.contains(g)) { return false; }
!ACTION_WORDS.iter().any(|a| lower.contains(a))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -398,15 +433,15 @@ mod tests {
tool_call_signature("grep", &serde_json::json!({"pattern": "MAX", "path": "src"})),
tool_call_signature("grep", &serde_json::json!({"pattern": "truncate", "path": "src"})),
tool_call_signature("grep", &serde_json::json!({"pattern": "fetch_url", "path": "src"})),
tool_call_signature("grep", &serde_json!({"pattern": "MessageRole", "path": "crates"})),
tool_call_signature("grep", &serde_json!({"pattern": "pub enum", "path": "src"})),
tool_call_signature("grep", &serde_json::json!({"pattern": "MessageRole", "path": "crates"})),
tool_call_signature("grep", &serde_json::json!({"pattern": "pub enum", "path": "src"})),
tool_call_signature("read_file", &serde_json::json!({"path": "a.rs", "offset": 0, "limit": 50})),
tool_call_signature("read_file", &serde_json!({"path": "b.rs", "offset": 0, "limit": 50})),
tool_call_signature("read_file", &serde_json!({"path": "c.rs", "offset": 0, "limit": 50})),
tool_call_signature("read_file", &serde_json!({"path": "d.rs", "offset": 100, "limit": 50})),
tool_call_signature("list_dir", &serde_json!({"path": "src-tauri"})),
tool_call_signature("grep", &serde_json!({"pattern": "STALL", "path": "src"})),
tool_call_signature("read_file", &serde_json!({"path": "e.rs", "offset": 0, "limit": 50})),
tool_call_signature("read_file", &serde_json::json!({"path": "b.rs", "offset": 0, "limit": 50})),
tool_call_signature("read_file", &serde_json::json!({"path": "c.rs", "offset": 0, "limit": 50})),
tool_call_signature("read_file", &serde_json::json!({"path": "d.rs", "offset": 100, "limit": 50})),
tool_call_signature("list_dir", &serde_json::json!({"path": "src-tauri"})),
tool_call_signature("grep", &serde_json::json!({"pattern": "STALL", "path": "src"})),
tool_call_signature("read_file", &serde_json::json!({"path": "e.rs", "offset": 0, "limit": 50})),
];
assert_eq!(sigs.len(), 12);
assert!(!is_repetitive_exploration(&sigs), "正常代码审查不应判重复");
@@ -461,7 +496,7 @@ mod tests {
fn boundary_two_alternating_below_ratio() {
let a = tool_call_signature("grep", &serde_json::json!({"pattern": "a", "path": "x"}));
let b = tool_call_signature("grep", &serde_json::json!({"pattern": "b", "path": "x"}));
let sigs = vec![a, b.clone(), a.clone(), b.clone(), a, b];
let sigs = vec![a.clone(), b.clone(), a.clone(), b.clone(), a, b];
assert!(is_repetitive_exploration(&sigs), "两签名交替唯一率低应判重复");
}
@@ -474,4 +509,48 @@ mod tests {
.collect();
assert!(!is_repetitive_exploration(&sigs));
}
// ── is_pure_greeting 纯问候判定测试 ──
#[test]
fn greeting_pure_hello_is_greeting() {
assert!(is_pure_greeting("你好"));
}
#[test]
fn greeting_thanks_is_greeting() {
assert!(is_pure_greeting("谢谢"));
}
#[test]
fn greeting_with_action_is_not_greeting() {
// 含动作词"看"+ 超 8 字 → 放行工具
assert!(!is_pure_greeting("你好,看看 moyu 项目"));
}
#[test]
fn greeting_view_tasks_is_not_greeting() {
assert!(!is_pure_greeting("查看任务"));
}
#[test]
fn greeting_entity_ref_is_not_greeting() {
assert!(!is_pure_greeting("@[项目]"));
}
#[test]
fn greeting_empty_string_is_greeting() {
assert!(is_pure_greeting(""));
assert!(is_pure_greeting(" "));
}
#[test]
fn greeting_help_query_is_not_greeting() {
assert!(!is_pure_greeting("你好,帮我查询项目"));
}
#[test]
fn greeting_ok_is_greeting() {
assert!(is_pure_greeting("ok"));
}
}