修复: AI 对话/工具可靠性(sanitize 三元组 + G2 签名重复 + handshake 不杀 loop + 空 tool_call id 兜底)

治 5 个对话停止/工具失败根因:sanitize 三元组按 id 配对治 400;G2 探索熔断从结果空
改签名重复判定(治误停正常探索);handshake 删越权强杀活 loop(generating 归 guard 单源);
空 tool_call id 兜底 gen_<index>(治 SenseNova 工具结果路由错位)。
This commit is contained in:
lxy
2026-08-02 02:21:30 +08:00
parent c76e77bd3c
commit 57d6a2d066
8 changed files with 991 additions and 185 deletions
+28 -60
View File
@@ -100,79 +100,47 @@ pub fn run() {
*last = std::time::Instant::now();
}
let mut session = session_arc.lock().await;
// F-260616-09 B 批8(设计 §3 batch8 + §5.2):遍历 per_conv(HashMap)清多 conv
// 残留 generating(HMR/dev 热载场景多 conv 并发跑 loop 致多 conv 卡 generating)。
// 批4:per_conv 唯一真相源,删顶层 session.generating 双写复位(顶层字段已退役)。
// 读点(B-Phase2):活跃 conv 列表改读无锁 conv_states(零锁竞争);不再 iter per_conv
// 判 conv_state.is_active()。
// BUG-2026-07-08: L0 握手清理 dirty conv 前,必须先 stop 旧 loop。
// 原逻辑直接把 ConvState 改 Idle,但后台 run_agentic_loop 仍在跑(等 LLM 响应)。
// 改 Idle 后用户重发 → can_accept_request 放行 → 新旧两个 loop 同操作一个 conv →
// 消息覆盖/guard 冲突/generating 紊乱。
// 修复:先设 stop_flag + notify_one()(旧 loop 的 stream_llm select! 即时唤醒)
// + 再改 ConvState。notify_one 让阻塞在 stream.next() 的 loop 即时检查 stop_flag 退出。
let app_state_ref = app_h.state::<AppState>();
// 职责分离(根治 BUG-2026-07-08 误杀活 loop):
// 生产环境前端 WebSocket 重连 / webview reload 会触发 ai-client-ready(非用户重发),
// 旧握手无脑强杀正在跑的 agentic loop(stop_flag + transition Idle + emit AiCompleted)
// → AI 中断不回复 → 用户「莫名其妙停止」(实证 cf5f44ed:seq26 后 loop 等下次 LLM 被杀)。
//
// 三道防线已就绪,握手无需也不应再强杀 loop:
// 1. guard.rs GeneratingGuard RAII:new→Generating,reset→Idle(正常),Drop→Idle(panic 兜底)。
// loop 自己复位 generating,握手强杀是多余且有害的旁路。
// 2. conv_state.rs can_accept_request 只 Idle|Error 可接 → generating 时 ai_chat_send
// 拒新消息 → 防「用户重发双 loop」(原 BUG-2026-07-08 的真实诉求)。
// 3. max_iterations 超时兜底 loop 卡死。
//
// 故握手职责收窄为「同步真实状态给前端」:读 conv_states 当前状态逐个 emit,
// 不改写 conv_states、不设 stop_flag、不收尾。loop 在跑则前端拿到 Generating,
// loop 已退出(guard 已复位)则拿到 Idle —— 前端状态与后端一致即可。
let dirty_convs: Vec<String> = app_state_ref.conv_states.active_convs();
let was_generating = !dirty_convs.is_empty();
// 先对每个 dirty conv 设 stop_flag + notify 让旧 loop 退出(防双 loop 并发)。
for cid in &dirty_convs {
if let Some(c) = session.per_conv.get_mut(cid) {
c.stop_flag.store(true, std::sync::atomic::Ordering::SeqCst);
c.notify.notify_one();
}
}
// 复位每个残留生成态的 conv(批8 多 conv 全覆盖)。
// B-Phase3:conv_state 写切 ConvStateStore 单源。
for cid in &dirty_convs {
if let Err(e) = app_state_ref.conv_states.transition(
cid,
crate::commands::ai::agentic::conv_state::ConvState::Idle,
) {
tracing::warn!(
conv_id = %cid,
error = %e,
"[ai] HMR 热载 conv_states→Idle 非法(不阻断热载)"
);
}
}
// 对话透明化 L1:收集每个 dirty conv 的 pinned_goals 快照(供 emit AiCompleted 携带)
let pinned_goals_map: std::collections::HashMap<String, Vec<commands::ai::GoalEntry>> = dirty_convs
.iter()
.filter_map(|cid| {
session.per_conv.get(cid).map(|c| (cid.clone(), c.pinned_goals.clone()))
})
.collect();
// BUG-260619-06 修复: clear 致冷启动 restore 重建审批丢失(restore 填充后 clear 无条件清空,
// 重启后待审批工具全丢)。改 retain 仅清非 recovered(本次会话/HMR 死 pending),
// 保留 restore 重建(recovered=true,audit.rs:331),对齐 switchConversation retain 保护意图。
// 阶段3a 单真相源合并:单表按 !recovered retain(kind 不区分,path 审批恢复恒无)。
session.pending_approvals.retain(|_, a| !a.recovered);
// 在 drop(session) 前快照 active_conversation_id,供下方 idle emit 用。
// 握手需推状态的 conv 集合:dirty conv(后端已知活跃)+ 当前 active_conv(用户正看的)。
// dirty_convs 已含活跃 conv;active_conv 若非活跃(Idling/Eror)也补推一次让前端同步。
let active_conv = session.active_conversation_id.clone();
drop(session);
// 根治:不论是否有 dirty conv,HMR 重连后始终向前端推 idle 事件,
// 防前端残留 streaming=true 导致发送按钮变停止、消息排队不发。
// dirty conv 走 AiCompleted 收尾路由(idle conv 无历史需收尾无需气泡)。
for cid in &dirty_convs {
let _ = app_h.emit(
"ai-chat-event",
commands::ai::AiChatEvent::AiCompleted {
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
incomplete: None,
conversation_id: Some(cid.clone()),
pinned_goals: pinned_goals_map.get(cid).cloned().unwrap_or_default(),
},
);
}
// 始终推 AiConvStateChanged{idle} 通知前端同步(即使后端已是 idle)。
// 前端 handleConvStateEvent → setConvState(convId, 'idle') → convStates 删项。
let mut convs_to_sync: Vec<String> = dirty_convs.clone();
if let Some(ref cid) = active_conv {
if !convs_to_sync.iter().any(|c| c == cid) {
convs_to_sync.push(cid.clone());
}
}
drop(session);
// 逐个 conv 读 conv_states 真实状态推给前端(loop 在跑推 Generating/Compressed,
// 已退出推 Idle)。ConvStateStore.get 同步无锁,不竞争 session lock。
for cid in &convs_to_sync {
let real_state = app_state_ref.conv_states.get(cid);
let _ = app_h.emit(
"ai-chat-event",
commands::ai::AiChatEvent::AiConvStateChanged {
conv_state: crate::commands::ai::agentic::conv_state::ConvState::Idle,
conv_state: real_state,
conversation_id: Some(cid.clone()),
},
);