diff --git a/docs/todo.md b/docs/todo.md index 0f5b72f..d9983e8 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -840,7 +840,7 @@ graph TD 902. [ ] 超时失败后的一键继续入口: 流式首字节超时(120s,服务收了不响应,不可自动重试)报「响应超时,请重试」后流程中断——轮次重入预算耗尽(3/3)即彻底停,用户只见裸文案。修:超时失败落停靠语义(复用 emit_task_parked),停靠卡带「继续」(ai_continue_loop parked 分流已修好)——错误后一键续跑而非中断。体验缺口实证 2026-10-07 晚 u-switch 故障窗口。 903. [ ] u-switch 20:38-21:02 故障窗口根因深查: 11 连败+120s 首字节超时风暴(多会话多 provider 同时),21:0x 自愈。查 moyu-ops 机 u-switch 日志定因(上游抖动/服务重启/连接池耗尽),根治或加监控告警。**【21:5x AI-Ops 定因已实锤,journalctl 实测】上游连环降级雪崩非服务故障:api.deepseek.com 402 余额不足(冷却10m)+enterprise.moyu.info 401 认证失效(冷却10m)+sensenova kimi-k3 429/SSE空响应体(30s冷却循环)+bigmodel glm 429/SSE截断缺 message_stop——多通道同时打光致无首字节,冷却走完自愈。剩余工作=根治项:①switch-gw 的 deepseek 系 4 通道确认摘除(用户指认已摘但配置仍在役)②429 风暴时客户端退避协同(30s 冷却对重试风暴无效)③故障窗口告警。**(10-08 已处置:moyu 令牌过期续期+主链切 ds-flash,见 900 区段外 905) -909. [~] **907 源头守静 + 无损归位(夹层根治)**(2026-10-10 devflow 接手登记,源自 907 补注遗留):现 907 修复走「未邻接即剥除」——保证不 400 但有损(夹层场景丢一轮工具交互)。 +909. [x] **907 源头守静 + 无损归位(夹层根治)**(2026-10-10 devflow 接手登记,源自 907 补注遗留):现 907 修复走「未邻接即剥除」——保证不 400 但有损(夹层场景丢一轮工具交互)。 - ✅ **① 源头守静(2026-10-10 实施完成,避让插入变体)**:df-ai 新增 `ContextManager::push_system_anchor`(context/mod.rs)——末条为悬空 `ToolCallHead` 时将 system 锚点插在**该头之前**(index=len-1),否则常规尾部追加;私有判定 `ends_with_dangling_tool_call_head`(`group==ToolCallHead`)。比原设计「推迟 push」更优:摘要/警示**不丢**,且楔入根本不产生。5 处尾部 push 全量收口(压缩成功 context_lifecycle:269 / 兜底 keyword_fallback_commit:372 / 警示 insert_system_notice helpers:44 / 主题标记 agentic/mod:1804 / 续轮 nudge :2399)。单测 `push_system_anchor_appends_on_normal_tail` + `push_system_anchor_avoids_dangling_tool_call_head` 锁定。验收:`cargo test -p df-ai --lib context` **155 passed/0 failed**;`cargo check -p devflow` EXIT 0。 - - [ ] **①-补 双取锁合并未做**:压缩失败路径 `set_compressing(false)` 与兜底提交仍两次取锁(context_lifecycle:312→:322),并发二次压缩窗口未消(低风险,留后续)。 + - ✅ **①-补 双取锁合并(2026-10-10 实施完成)**:`keyword_fallback_commit`(context_lifecycle.rs)内单锁一并 `set_compressing(false)` + 兜底提交,消「锁外复位→再取锁提交」的并发二次压缩窗口;复位置于 `KEYWORD_FALLBACK_ENABLED` 门控之前(开关关闭同样复位,否则标志永真致压缩停摆);两处调用点(provider 重建失败/LLM 失败)的独立复位块删除。验收:`cargo check -p devflow` EXIT 0 + `cargo test -p devflow --lib` **749 passed/0 failed**。 - ✅ **② 无损归位(2026-10-10 实施完成)**:`crates/df-ai/src/context/sanitize.rs` 新增 step 0.5 `reorder_interlopers_after_tool_run`——楔入 assistant 头与其 tool 响应之间的异类消息(压缩摘要/用户插话/警示 system)**整体移到该组响应之后**(只重排不删内容),归位后 step1 邻接判定即认闭合,工具轮**不再被剥除**;仅对「单头 + 夹层 + 紧随该头的连续 tool 段」生效,遇下一个 tool_call 头即停(不跨单元),其余畸形仍走剥除兜底。幂等。单测替换旧剥除用例为 `sanitize_reorders_interloper_to_keep_tool_round` + 新增 `sanitize_reorder_is_idempotent`。验收:`cargo test -p df-ai --lib context` **156 passed/0 failed**。 diff --git a/src-tauri/src/commands/ai/agentic/context_lifecycle.rs b/src-tauri/src/commands/ai/agentic/context_lifecycle.rs index 748f89e..7dceb4c 100644 --- a/src-tauri/src/commands/ai/agentic/context_lifecycle.rs +++ b/src-tauri/src/commands/ai/agentic/context_lifecycle.rs @@ -218,13 +218,8 @@ pub(super) async fn maybe_auto_compress( "[ai] 后台压缩 provider 重建失败,降级走关键词摘要兜底(KEYWORD_FALLBACK_ENABLED={})", KEYWORD_FALLBACK_ENABLED, ); - // 单次锁内 check+mutate:复位 is_compressing(防 conv 两次锁间被删惰性重建)。 - { - let mut session = session_arc.lock().await; - if session.per_conv.contains_key(&conv_id) { - session.conv(&conv_id).messages.set_compressing(false); - } - } + // 复位 is_compressing 收口进 keyword_fallback_commit 单锁(909①-补: + // 原先此处单独取锁复位,与提交之间留并发二次压缩窗口)。 keyword_fallback_commit( &session_arc, &conv_id, @@ -305,13 +300,8 @@ pub(super) async fn maybe_auto_compress( // 压缩失败不发 AiError——前端 AiError case 无条件 setStreaming(false) // 误判生成结束(后端 loop 实际继续),致"压缩后停止"。降级为 warn + 兜底释放 // token + build_for_request 裁剪,loop 继续 stream_llm,用户自然看到后续回复。 - // 单次锁内 check+mutate(防 conv 在两次锁间被删致惰性重建污染新 conv)。 - { - let mut session = session_arc.lock().await; - if session.per_conv.contains_key(&conv_id) { - session.conv(&conv_id).messages.set_compressing(false); - } - } + // 复位 is_compressing 收口进 keyword_fallback_commit 单锁(909①-补, + // 同 provider 重建失败路径;消锁外复位→再取锁提交的并发窗口)。 tracing::warn!( conv_id = %conv_id, error = %e, @@ -339,12 +329,18 @@ pub(super) async fn maybe_auto_compress( /// 关键词兜底提交单点(DRY #8 收敛,2026-10-01)。 /// /// 抽自 [`maybe_auto_compress`] 后台 task 内的逐字双胞胎块:压缩 provider 重建失败路径 -/// 与压缩 LLM 失败路径共用「KEYWORD_FALLBACK_ENABLED 门控 → 锁内 check+mutate -/// (标 compressed 释放 token + 关键词摘要尾部追加,非空才插)→ 成功插入打 info」。 +/// 与压缩 LLM 失败路径共用「复位 is_compressing → KEYWORD_FALLBACK_ENABLED 门控 → 锁内 +/// check+mutate(标 compressed 释放 token + 关键词摘要尾部追加,非空才插)→ 成功插入打 info」。 /// 两条失败路径只差日志正文,经 `log_head` 参数化(拼为 /// "[ai] {log_head}兜底(后台):关键词摘要已尾部追加(compressed 标记已扣 token)")。 /// -/// conv 已删(per_conv 无条目)→ 直接返回不兜底(对齐原 return 语义:防惰性重建污染新 conv)。 +/// **复位重入标志收口单锁(909①-补,2026-10-10)**:`set_compressing(false)` 与兜底提交在同一 +/// 次锁内原子完成——原调用方在锁外单独取锁复位,与这里的提交之间留出窗口(期间的压缩触发读到 +/// 的仍是 false,可并发二次压缩,放大楔入概率)。注意复位在 `KEYWORD_FALLBACK_ENABLED` 门控 +/// **之前**:开关关闭时同样必须复位,否则 is_compressing 永真 → 压缩永久停摆。 +/// +/// conv 已删(per_conv 无条目)→ 直接返回(标志随 conv 销毁无需复位;对齐原 return 语义: +/// 防惰性重建污染新 conv)。 async fn keyword_fallback_commit( session_arc: &Arc>, conv_id: &str, @@ -353,24 +349,28 @@ async fn keyword_fallback_commit( pre_tokens: u32, log_head: &str, ) { - if !KEYWORD_FALLBACK_ENABLED { - return; - } let inserted = { let mut session = session_arc.lock().await; if !session.per_conv.contains_key(conv_id) { return; } let conv = session.conv(conv_id); - let _compressed = conv.messages.compress_old_messages(protect_start); - if keyword_fallback.is_empty() { + // 复位与提交同锁原子(909①-补):消「锁外复位→再取锁提交」之间的并发二次压缩窗口。 + conv.messages.set_compressing(false); + if !KEYWORD_FALLBACK_ENABLED { + // 开关关闭 → 回退原裸裁剪行为:不压缩不插摘要,仅完成复位。 false } else { - // 尾部追加(而非 insert_at(0) 头部插入):头部插入位移整条前缀字节, - // 使 DeepSeek 等自动前缀缓存 provider 全部缓存失效(命中率掉到个位数)。 - // 尾部追加紧邻本轮内容 LLM 仍可见,前缀稳定命中率最大化。 - conv.messages.push_system_anchor(ChatMessage::system(keyword_fallback)); - true + let _compressed = conv.messages.compress_old_messages(protect_start); + if keyword_fallback.is_empty() { + false + } else { + // 尾部追加(而非 insert_at(0) 头部插入):头部插入位移整条前缀字节, + // 使 DeepSeek 等自动前缀缓存 provider 全部缓存失效(命中率掉到个位数)。 + // 尾部追加紧邻本轮内容 LLM 仍可见,前缀稳定命中率最大化。 + conv.messages.push_system_anchor(ChatMessage::system(keyword_fallback)); + true + } } }; if inserted {