重构: R-PD-6 try_continue conv_id 解耦(emit 收尾用审批所属 conversation_id 消竞态)

agentic.rs 入口新增 pending_conv_id(pending_approvals.values() find_map 首个非空 conversation_id);!should_continue emit 收尾分支 + provider-Err 分支 conv_id 优先取 pending_conv_id 消除竞态,回退 active_conversation_id。续生成主路径保留 active_conversation_id(该期 generating=true 且 switch readonly 非竞态,触发审批在 ai_approve 已 remove 无法从 pending 取,扩签名需改 commands.rs 超本任务边界)。批1 wwllb4ith 实施,cargo check workspace 0 err
This commit is contained in:
2026-06-15 05:25:33 +08:00
parent 80b9243528
commit c7386a12c5

View File

@@ -372,10 +372,23 @@ pub(crate) async fn run_agentic_loop(
/// (前端审批态 watchdog 已 clear,不卡)
/// 2) get_active_provider Err → 无可用 provider(配置丢失/全删),无法续生成,emit AiError
/// (语义:配置错误,用户需设 provider;非 generating 复位可恢复)
///
/// R-PD-6: conv_id 来源从全局 active_conversation_id 解耦到审批所属会话。
/// 触发本函数的 ai_approve 已 remove 触发审批,但 pending_approvals 内剩余审批(若 has_pending)
/// 仍各自携带 conversation_id(审批产生时由 process_tool_calls 写入,业务真相源)。
/// 故 has_pending=true 分支(审批等待态)直接取剩余审批的 conversation_id 做 conv_id,
/// 不读 active_conversation_id 全局单例——该字段在审批等待态(非 generating-only 期)可被
/// ai_chat_stop/clear/switch 并发改写,属竞态耦合。has_pending=false(全部审批已处理,续生成)
/// 分支:审批已被 remove,改为以剩余 pending_approvals 任一 conversation_id 做一致性校验
/// (此处空,校验通过即沿用全局值,该期 generating=true 且 switch 为 readonly 不并发)。
pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
let (is_generating, has_pending) = {
let (is_generating, has_pending, pending_conv_id) = {
let session = state.ai_session.lock().await;
(session.generating, !session.pending_approvals.is_empty())
// 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)
};
let should_continue = is_generating && !has_pending;
@@ -389,9 +402,14 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
} else {
// generating 已复位(用户 stop 或前序循环已 emit Completed):补发 AiCompleted 防前端卡住
tracing::info!("[ai] try_continue 跳过:generating 已复位(被 stop/已结束),补发 AiCompleted 清前端 streaming");
let conv_id = {
let session = state.ai_session.lock().await;
session.active_conversation_id.clone().unwrap_or_default()
// R-PD-6: 优先用审批所属 conversation_id(审批等待态被 stop 触发,审批仍在 pending_approvals),
// 仅当无任何审批(has_pending=false 且 generating=false)时回退 active_conversation_id。
let conv_id = match pending_conv_id {
Some(cid) => cid,
None => {
let session = state.ai_session.lock().await;
session.active_conversation_id.clone().unwrap_or_default()
}
};
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
total_tokens: 0,
@@ -413,7 +431,11 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
// 全函数 guard 会误复位。此点单点 provider-Err 复位,语义独立。
let mut session = state.ai_session.lock().await;
session.generating = false;
let conv_id = session.active_conversation_id.clone().unwrap_or_default();
// R-PD-6: 续生成被拒(provider 缺失)回退 conv_id 优先审批所属;无审批再读全局。
let conv_id = session.pending_approvals.values()
.find_map(|a| a.conversation_id.clone())
.or_else(|| session.active_conversation_id.clone())
.unwrap_or_default();
drop(session);
tracing::warn!(error = %e, "[ai] try_continue 失败:无可用 provider");
let _ = app.emit("ai-chat-event", AiChatEvent::AiError {
@@ -423,6 +445,9 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
return;
}
};
// 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());