diff --git a/crates/df-ai/src/context.rs b/crates/df-ai/src/context.rs index 3def50f..caafd1a 100644 --- a/crates/df-ai/src/context.rs +++ b/crates/df-ai/src/context.rs @@ -802,9 +802,13 @@ impl ContextManager { /// 不参与二次压缩,幂等)。`protect_start` 为保护区起点(如 `len - PROTECT_COUNT`)。 pub fn has_compressible_messages(&self, protect_start: usize) -> bool { let end = protect_start.min(self.messages.len()); + // BUG-260624-05:排除 system 角色(压缩摘要 / 话题切换锚点)。这些是上下文锚点非压缩目标—— + // 若计入,压缩摘要 insert_at(0) 落在可压缩区 [0..protect_start) 且 is_active(status=None), + // 致每轮 has_compressible 恒 true → 无限循环压缩(用户报"压缩后每轮提示已压缩并停止")。 + // compress_old_messages 不改:被调用时仍标旧 system 摘要 compressed(被新摘要替代,防堆积)。 self.messages[..end] .iter() - .any(|t| t.message.is_active()) + .any(|t| t.message.is_active() && !matches!(t.message.role, MessageRole::System)) } /// 把保护区 `[0, compress_end)` 范围内的 active 消息标记为 `status="compressed"`, diff --git a/docs/todo.md b/docs/todo.md index b1a86d8..a2fd166 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -973,6 +973,15 @@ > - 修复:`grep` Medium→**Low**(对齐只读同类)+ tool_display_hint 加 `"grep" => Some("搜索代码内容")`(文案准确)+ 注释更正。cargo check exit 0。 > - 全量核对:28 工具 risk_level 分级,**除 `restore_project`: High 过保守(恢复非破坏,对齐 create/update 该 Medium;但 High 安全方向不算错,未改)** 外全合理。 > +> **BUG-260624-05 压缩后每轮停止根治**(用户「上下文压缩后,后续所有对话都提示已压缩然后停止」): +> - 根因(诊断 workflow 4 维度·DB 铁证 top.1216.devflow 库 565 compressed/4246 active,ead0f7e5 seq=0 是 active 兜底摘要,12 会话前7条有 active system): +> - **#1(主因·无限循环)**:压缩摘要 `insert_at(0, ChatMessage::system(summary))`(agentic/mod.rs:923)→ status=None → is_active=true,落在可压缩区 [0..protect_start) → 每轮 `has_compressible_messages` 恒 true → 无限循环压缩。topic-switch system(DB 3eafdb85 三条堆 seq 0/1/2)同理。 +> - **#2(次因·前端停止)**:压缩失败 emit AiError(useAiEvents:648 无条件 setStreaming(false))→ 前端误判生成结束(后端 loop 实际继续)。 +> - 修复(P0 根治循环 + P1 前端停止): +> - **P0**(context.rs:803 `has_compressible_messages` 排除 system 角色,用 `!matches!(role, System)` 因 MessageRole 无 derive PartialEq):摘要/话题锚点是上下文锚点非压缩目标,排除后不再因 system 触发。compress_old_messages 不改(被调用时仍标旧 system 摘要 compressed 被新摘要替代,防堆积)。DB 自愈:排除 system 不限 status,旧 active 摘要也被排除,**无需 migration**。 +> - **P1**(agentic/mod.rs:985 压缩失败不发 AiError):前端 AiError 无条件停流,降级 warn + build_for_request 裁剪兜底,loop 继续 stream_llm。 +> - **纠正 workflow 方案 A 矛盾**:workflow 建议 is_active 排除 summarized,但会致 build_for_request 不送摘要进 LLM 丢历史;改用 has_compressible 排除 system(摘要仍 active 进 LLM)。 +> > **剩余低优 [ ](非阻断)**: > - [x] ✅(2026-06-24·ebba973) BUG-260624-01 残留:① MessageList:503 scrollToFirstPending return 过早修复(遍历全实例调,各内部 querySelector 无 pending 则 no-op,治 pending 落后续实例滚不到)+ ② ToolCardList listener 加 props.toolCalls.some(pending) 过滤短路(降大列表 N 实例×M emit 的 DOM 查询)。**scrollToFirstPending 时间窗去抖评估后不做**:同 id 连发被上游守卫拦截(AiDirAuthRequired/AiApprovalRequired useAiEvents:359/472 some 去重 + AiToolCallStarted:427 findToolCall 守卫),不同 id 连发是多 pending 合理场景每次滚到首个合理,vue-tsc EXIT 0。 > - [ ] BUG-260624-02 残留:审批超时倒计时 UI(5min 无倒计时,ToolCard pending 态显 mm:ss 到期前 60s 变红)+ 核 detached 窗口 emit 作用域(分离窗口是否独立 startListener,否则 ai-pending-arrived 收不到) diff --git a/src-tauri/src/commands/ai/agentic/mod.rs b/src-tauri/src/commands/ai/agentic/mod.rs index eb66dee..e364b79 100644 --- a/src-tauri/src/commands/ai/agentic/mod.rs +++ b/src-tauri/src/commands/ai/agentic/mod.rs @@ -982,14 +982,10 @@ pub(crate) async fn run_agentic_loop( ); } } - let ev = AiChatEvent::AiError { - error: format!("自动上下文压缩失败,已降级为普通裁剪: {}", e), - error_type: Some(ErrorType::Unknown), - conversation_id: Some(conv_id.clone()), - }; - let _ = app_handle.emit("ai-chat-event", ev.clone()); - // L3 emit 双写:tunnel subscriber(阶段2)透传 miniapp - let _ = app_handle.state::().ai_event_bus.publish_event(ev); + // BUG-260624-05:压缩失败不发 AiError——前端 AiError case 无条件 setStreaming(false) + // 误判生成结束(后端 loop 实际继续),致"压缩后停止"。降级为 warn(上方 line 957 已有) + // + compress_old_messages 兜底释放 token + build_for_request 裁剪,loop 继续 stream_llm, + // 用户自然看到后续回复。压缩降级对用户透明(非致命错误,不该停流)。 } } }