修复: AI工具OOM/路径遍历+续跑锁收敛+UX交互批
This commit is contained in:
@@ -941,21 +941,32 @@ pub(crate) async fn run_agentic_loop(
|
||||
/// 分支:审批已被 remove,改为以剩余 pending_approvals 任一 conversation_id 做一致性校验
|
||||
/// (此处空,校验通过即沿用全局值,该期 generating=true 且 switch 为 readonly 不并发)。
|
||||
pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, start_iteration: usize) {
|
||||
let (is_generating, has_pending, pending_conv_id) = {
|
||||
// BUG-260617-05: 原 5 次独立 lock().await 造成 TOCTOU 竞态——should_continue=true 判出后、
|
||||
// spawn 前用户点 stop(ai_chat_stop 复位 generating=false),续跑仍按过时快照继续 spawn。
|
||||
// 修复:单次 lock 取结构化快照(所有续跑判定所需字段),无锁态判定;spawn 前单次 lock 原子
|
||||
// 重检 generating 仍为 true 才续跑,stop 后中途插入的直接收敛退出。
|
||||
let snap = {
|
||||
let session = state.ai_session.lock().await;
|
||||
// pending_approvals 中任一审批的 conversation_id:审批等待态(has_pending)下作为 conv_id 来源,
|
||||
// 取第一个非空值(同一对话的审批 conversation_id 一致,见 process_tool_calls 写入路径)。
|
||||
let pending_conv_id = session.pending_approvals.values()
|
||||
.find_map(|a| a.conversation_id.clone());
|
||||
(session.generating, !session.pending_approvals.is_empty(), pending_conv_id)
|
||||
ContinueSnapshot {
|
||||
is_generating: session.generating,
|
||||
has_pending: !session.pending_approvals.is_empty(),
|
||||
pending_conv_id,
|
||||
active_conversation_id: session.active_conversation_id.clone(),
|
||||
agent_language: session.agent_language.clone(),
|
||||
model_override: session.model_override.clone(),
|
||||
}
|
||||
};
|
||||
let should_continue = is_generating && !has_pending;
|
||||
let should_continue = snap.is_generating && !snap.has_pending;
|
||||
|
||||
if !should_continue {
|
||||
// generating=false(被 stop)或仍有审批(pending_approvals 非空):
|
||||
// 统一 emit AiCompleted 标当前轮收敛,清前端 streaming。
|
||||
// 轮 token 已在前序 AiCompleted/AiApprovalResult 流程落库,此处零 token 上报仅作收敛信号。
|
||||
if is_generating {
|
||||
if snap.is_generating {
|
||||
// pending_approvals 非空但 generating 仍 true:转审批态,前端审批态 watchdog 已 clear,不卡
|
||||
tracing::info!("[ai] try_continue 跳过:仍有待审批,转审批等待态");
|
||||
} else {
|
||||
@@ -963,12 +974,9 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s
|
||||
tracing::info!("[ai] try_continue 跳过:generating 已复位(被 stop/已结束),补发 AiCompleted 清前端 streaming");
|
||||
// R-PD-6: 优先用审批所属 conversation_id(审批等待态被 stop 触发,审批仍在 pending_approvals),
|
||||
// 仅当无任何审批(has_pending=false 且 generating=false)时回退 active_conversation_id。
|
||||
let conv_id = match pending_conv_id {
|
||||
let conv_id = match snap.pending_conv_id.clone() {
|
||||
Some(cid) => cid,
|
||||
None => {
|
||||
let session = state.ai_session.lock().await;
|
||||
session.active_conversation_id.clone().unwrap_or_default()
|
||||
}
|
||||
None => snap.active_conversation_id.clone().unwrap_or_default(),
|
||||
};
|
||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||
total_tokens: 0,
|
||||
@@ -1009,13 +1017,9 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s
|
||||
};
|
||||
// R-PD-6: 续生成路径 conv_id 解耦——has_pending=false 时审批已 remove,无审批 conversation_id 可取;
|
||||
// 此期 generating=true 且 switchConversation 为 readonly 不并发改 active_conversation_id,
|
||||
// 故读全局值安全(非竞态期);若 has_pending=true 已在上面 return,不会到此。
|
||||
let (lang, conv_id) = {
|
||||
let session = state.ai_session.lock().await;
|
||||
let lang = session.agent_language.clone().unwrap_or_else(|| "zh-CN".to_string());
|
||||
let conv_id = session.active_conversation_id.clone().unwrap_or_default();
|
||||
(lang, conv_id)
|
||||
};
|
||||
// 故读快照值安全(非竞态期);若 has_pending=true 已在上面 return,不会到此。
|
||||
let lang = snap.agent_language.clone().unwrap_or_else(|| "zh-CN".to_string());
|
||||
let conv_id = snap.active_conversation_id.clone().unwrap_or_default();
|
||||
let system_prompt = build_system_prompt(state, &lang).await;
|
||||
|
||||
let session_arc = state.ai_session.clone();
|
||||
@@ -1029,10 +1033,22 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s
|
||||
// F-260616-07: 流式失败重试次数快照
|
||||
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
||||
// F-01 阶段6: 续跑沿用同一主对话的 model_override(审批续跑/达 max 续跑保持一致)。
|
||||
let model_override = {
|
||||
let session = state.ai_session.lock().await;
|
||||
session.model_override.clone()
|
||||
};
|
||||
let model_override = snap.model_override.clone();
|
||||
|
||||
// BUG-260617-05 续: provider 解析/build_system_prompt 期间用户可能点 stop。
|
||||
// spawn 前单次 lock 原子重检 generating——若已被 stop 复位,收敛退出而非覆盖用户的 stop。
|
||||
// (run_agentic_loop 入口 GeneratingGuard 会再次置 generating=true,若不重检会抹掉 stop。)
|
||||
if !state.ai_session.lock().await.generating {
|
||||
tracing::info!("[ai] try_continue 终止:spawn 前重检 generating 已被 stop 复位,补发 AiCompleted");
|
||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||
total_tokens: 0,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复循环前通知前端新建 assistant 消息:审批(通过/拒绝)后新一轮文本
|
||||
// 不应追加到发起工具调用的旧消息,用 AiAgentRound 隔开
|
||||
@@ -1045,3 +1061,15 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, s
|
||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// BUG-260617-05: try_continue_agent_loop 续跑判定所需 session 字段的一次性快照。
|
||||
/// 单次 lock 取出后无锁态判定,消除多 lock 间其他 IPC(ai_chat_stop/clear/switch)改写 session
|
||||
/// 致续跑判断基于过时快照的 TOCTOU 竞态。
|
||||
struct ContinueSnapshot {
|
||||
is_generating: bool,
|
||||
has_pending: bool,
|
||||
pending_conv_id: Option<String>,
|
||||
active_conversation_id: Option<String>,
|
||||
agent_language: Option<String>,
|
||||
model_override: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user