修复: 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:
+28
-60
@@ -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()),
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user