`(路由页真实元素根,非 slot 透传) · 主窗口 + detached 两处同构 | ✅ |
+| ② ErrorBoundary display:contents/flex | 正常透明 + error 降级 | `ErrorBoundary.vue:6` 根固定 `` · `:38-40 .error-boundary-root{display:contents}`(正常布局透明,slot 直参与父 flex)· `:41-50 .is-error{display:flex...}`(error 降级 UI)· onErrorCaptured/reset 逻辑不变 | ✅ |
+| ③ 主+分离两处改 | 主窗口 + detached 同构 | App.vue diff 主窗口(router-view) + `#detached` 分离模板两处均改 ErrorBoundary 包 transition | ✅ |
+
+**对抗核验印证**:
+- **transition 不再包 ErrorBoundary**:新结构 ErrorBoundary 在 transition 外层(transition 是 ErrorBoundary 子),ErrorBoundary 根 div 不再是 transition 直接子 → 彻底消除「non-element root」警告源(双保险:结构移出 + ErrorBoundary 根固定元素)
+- **:key="route.path" 保留**:ErrorBoundary 仍带 route.path key,路由切换整重建(重置 error 态),合理
+- **vue-tsc**:CR-19 已全局 vue-tsc EXIT 0,CR-20 为 template 结构调整(类型无关),commit 声明 EXIT 0
- **根因**:c38cefe ErrorBoundary 接入嵌 transition 内部,slot 透传非元素根 → mode="out-in" 过渡卡死 → 点主菜单 URL 变但页面不切换(AI 面板不走 transition 故正常)
- **维度**:
1. transition 直接子是真实元素根(component :is 路由页面)
2. ErrorBoundary display:contents 正常态布局透明 + error 态 flex 降级
3. 主+分离窗口两处改
- **验证**:独立 grep/read 核验 7f1242c + vue-tsc
-- **状态**:🟡 待审
+- **状态**:✅ 已审(PASS·⚪0)
-### CR-260619-21 对话标题卡死"新对话"(ensure 跳过占位·commit ecf501d) — 🟡 待审
+### CR-260619-21 对话标题卡死"新对话"(ensure 跳过占位·commit ecf501d) — ✅ 已审(PASS·⚪0·独立 grep/read 核验)
- **范围**(2 文件):title.rs(:40 ensure 排除"新对话"占位)+ conversation.rs(:139 need_title_regen 含"新对话")
+- **结论(2026-06-20·独立 grep/read 核验 commit ecf501d + cargo check EXIT 0)**: ✅ **PASS** — 🔴0 🟡0 ⚪0
+
+**3 维度逐项核验(file:line 佐证)**:
+
+| 维度 | 核验点 | 佐证 | 判定 |
+|---|---|---|---|
+| ① ensure 排除"新对话" | title Some 且非空非"新对话"才 return | `title.rs:42-45` `if let Some(t)=&rec.title { if !t.is_empty() && t != "新对话" { return } }` · 边界完备:Some("")/Some("新对话") 继续生成 · Some("真标题") 跳过 · None 外层 if let 不进 | ✅ |
+| ② switch 触发含"新对话" | need_title_regen 含"新对话"占位 | `conversation.rs:141` `need_title_regen = record.title.is_none() 或 == Some("新对话")` · 切入"新对话"会话 spawn ensure | ✅ |
+| ③ 双重卡死链闭合 | ensure 不跳过 + switch 触发 | ensure :40 排除"新对话"(重新生成)+ conversation :141 switch 触发 spawn_ensure → "新对话"会话切入即重新生成 · 配合 `title.rs:84` extract 兜底落库 + LLM 覆盖 | ✅ |
+
+**对抗核验印证**:
+- **i18n 对抗解除**:"新对话"占位是**后端硬编码中文**(`title.rs:84` `unwrap_or_else(\|\| "新对话".to_string())` extract 兜底 + `:187` clean 空兜底"新对话"),非前端 t('newConversation')。后端 ensure/switch 匹配后端产生的占位串,一致自洽;英文 locale 下后端占位仍中文"新对话"(后端不随 locale 变)
+- **数据印证**:6 条"新对话"会话 msg_len 100K+(有 user,extract 应非 None)证实 ensure 未生成(声明,未独立查 DB)
+- **cargo check EXIT 0**(独立复跑,零 warning)
- **根因**:ensure :40 title.is_some() return,"新对话"占位 Some 永远跳过;switch need_title_regen 仅 None,"新对话"不触发 → 双重卡死
- **维度**:
1. ensure :40 排除"新对话"(title Some 且非空非"新对话"才 return)
2. conversation.rs :139 need_title_regen 含"新对话"(switch 触发)
3. 数据印证:6 条"新对话"会话 msg_len 100K+ 有 user(extract 应非 None,证实未生成)
- **验证**:独立 grep/read 核验 ecf501d + cargo check
-- **状态**:🟡 待审
+- **状态**:✅ 已审(PASS·⚪0)
+
+---
+
+### CR-260619-22 摘要/压缩不改 updated_at(防时间分组跳变·commit 49afffe) — ✅ 已审(PASS)
+
+- **结论(2026-06-20·独立 grep/read 核验 commit 49afffe)**:✅ PASS — 4 维度全对,调用方参数无错配无遗漏 — 🔴0 🟡0 ⚪0
+ - 维度1 签名:`conversation.rs:136-143` 第6参 `touch_updated_at: bool` 落在 `model: Option<&str>` 之后,类型正确 ✅
+ - 维度2 条件改:`conversation.rs:177-179` `if touch_updated_at { rec.updated_at = now_millis() }` 已落库 UPDATE 条件化;新会话首次 INSERT(`:213` `updated_at: now`)不受 touch 控制,保持 now ✅(:200 `let now = now_millis()` → :213 注入,新会话必带时间戳)
+ - 维度3 调用方:grep 全 15 处调用全含第6参(0 遗漏);**compress :870(chat.rs ai_chat_compress_context)= false**✅(系统摘要插入,LLM 压缩后 insert_at(0) system 消息,无用户活跃,正确不改 updated_at);其余 14 处全 true——agentic 6 处(loop 入口 stop :470/正常退出 :832/异常 :898/审批 :923/max_rounds :952/完成 spawn :982)+ chat.rs 7 处(regenerate :158/approve 拒 :405/approve 同意 :507/auth_dir 拒 :579/auth_dir 允 :638/clear :743/edit :958)+ conversation.rs 切换旧会话 :64——均为用户活跃路径,正确 ✅。对抗核验:title 生成(spawn_ensure_title)/知识提炼(maybe_spawn_extraction)走各自 repo 写库,不经 save_conversation,无遗漏的系统自动路径误标 true。
+ - 维度4 不破坏:messages 赋值(:176)/token 累加(:180-183)/model 补填+models 去重追加(:185-193)/新会话 insert(:201-217)全保留,diff 仅动签名+`:177-179` 条件+调用方加参 ✅
+ - 验证:`cargo check --workspace` EXIT 0(cached,无错误);grep save_conversation 调用 15 处全含第6参(已逐条核对上下文)
+- **范围**(4 文件):conversation.rs(save_conversation 加 touch_updated_at 参数 + :175 条件)+ chat.rs(compress :870 false)+ agentic/mod.rs(loop 退出 save true)+ commands/conversation.rs(切换 save true)
+- **根因**:save_conversation :175 每次改 updated_at=now。compress/摘要(chat.rs:870)走 save → updated_at=now → timeBucket 按 updated_at 分组 → 会话"昨天→今天"
+- **待修项回流 todo**:**无** 🔴/🟡 项
+
+---
+
+### CR-260619-23 消息级溯源 P2 b 批次A 读路径切读 ai_messages(commit 13e7953) — ✅ 已审(PASS)
+
+- **结论(2026-06-20·独立 grep/read 核验 commit 13e7953)**:✅ PASS — 映射 round-trip 严格 / fallback 条件正确 / status 归一化双向一致 / state 注册对齐 ai_conversations 模式 — 🔴0 🟡0 ⚪1
+ - 维度1 AppState(state.rs):`state.rs:265` `pub ai_messages: AiMessageRepo` + `:489` init `AiMessageRepo::new(&db)`,import `:14` 加 `AiMessageRepo`,完全对齐 `ai_conversations`(AiConversationRepo)模式 ✅
+ - 维度2 映射工具(conversation.rs):
+ - `record_to_message`(读,:77-97):role 字符串→枚举 / parts·tool_calls JSON→Vec(空/解析失败→None 防御,:78-85)/ status "active"→None 归一化(:88-92)/ id→Some(record.id 必非空主键)✅
+ - `message_to_record`(写预留,:115-146,`#[allow(dead_code)]` 标注批次B用):role 枚举→字符串 / parts·tool_calls→JSON / status None→"active" / id=None 兜底 `msg_{conv}_{seq}`(:117)✅
+ - **round-trip 8 测覆盖全字段**:`roundtrip_plain_text_status_none`(纯文本+status归一化)/ `roundtrip_multimodal_parts`(Image base64)/ `roundtrip_tool_calls`(assistant 多 tool_call,经 JSON 字符串比对因 ToolCall 无 PartialEq)/ `roundtrip_tool_message`(role=Tool+tool_call_id)/ `roundtrip_status_non_active_preserved`(truncated/compressed 原样)/ `roundtrip_id_none_fallback`/ `roundtrip_reasoning_content`/ `roundtrip_full_fields`(全字段混合)✅
+ - 对抗核验:`MessageRole` 仅 4 变体(System/User/Assistant/Tool,df-ai-core/types.rs:144-149 + rename_all="lowercase"),`role_from_str`(:46-55)全覆盖 + `_ => User` 防御;`role_to_str`(:61-68)穷尽 match ✅
+ - 字段对齐:ChatMessage(df-ai-core/types.rs:79-118)11 字段 ↔ AiMessageRecord(models.rs:235-262)13 字段(多 conversation_id/seq/created_at 持久化用),映射函数全字段赋值无遗漏 ✅
+ - 维度3 load 切读(conversation.rs):
+ - switch `:261` `state.ai_messages.list_by_conversation(&conversation_id)` → `:265` `records.iter().map(record_to_message).collect()` → `:307` `conv.messages.restore_from_messages(messages)` ✅
+ - export `:460` 同模式(切换/导出读路径一致)✅
+ - **messages_json 重序列化**:`:282` `serde_json::to_string(&messages)`(由映射后 Vec 重序列化),前端契约不变(仍吃 JSON 字符串)✅
+ - **fallback 条件正确**:switch `:266-279` / export `:464-475` —— 表空(`records.is_empty()`)且旧 messages JSON 非空 `!= "[]"` 且 `!is_empty()` 才 warn 回退读 JSON;`ai_conversations.messages` 列 DEFAULT `'[]'`(migrations.rs:671),空对话 messages="[]" 被 `!= "[]"` 排除走空 Vec,正确不误触发 fallback ✅
+ - **list_by_conversation 排序**:message_repo.rs:115 `ORDER BY seq ASC`,保证对话内时间序,restore_from_messages 顺序正确 ✅
+ - 维度4 不破坏:旧 messages JSON 列保留(migrations 不删);save 写路径(save_conversation)未碰(批次 B 留);`message_to_record` 标 `#[allow(dead_code)]` 不报未用警告 ✅
+ - 验证:`git show 13e7953 --stat` = 2 文件(conversation.rs +367 / state.rs +9);commit 自验 cargo test conversation 8 passed
+ - ⚪1 low:**id 兜底格式不一致**——`message_to_record`(:117)老数据兜底 `msg_{conv}_{seq}`,而 `new_message_id()`(df-ai-core/types.rs:134-139)新消息生成 `msg_{ts}_{counter}`。本批次读路径不触发(record.id 必非空直接用),仅批次 B 写路径 save 老数据(无 id)时兜底,需在批次 B 审查时确认老对话迁移已赋 id 后兜底是否仍会触发(理论上迁移跑完 ai_messages.id 全非空)。非本批次问题,标记供批次 B 跟进。
+- **范围**(2 文件):conversation.rs(映射工具 record_to_message/message_to_record + switch :261-282 切读 + export :460-475 切读 + 8 round-trip 测)+ state.rs(AppState 注册 ai_messages)
+- **设计**:读路径优先 ai_messages 表(每消息一行)替代 ai_conversations.messages 整对话 JSON;fallback 兜底老库未迁移/坏数据/批次B前窗口;写路径留批次 B(save_conversation 切 ai_messages.replace_conversation)
+- **待修项回流 todo**:**无** 🔴/🟡 项(⚪1 low id 兜底格式不一致属批次 B 写路径范畴,非本批次阻塞)
+
+---
+
+### CR-260619-24 消息级溯源 P2 b 批次B 写路径切 ai_messages(commit 7a1fced) — ✅ 已审(PASS·⚪0·独立 grep/read 核验)
+
+- **范围**(4 文件):message_repo.rs(replace_conversation 单事务 DELETE+INSERT + 4 测)+ conversation.rs(save 切 replace_conversation + 旧 messages 备份)+ chat.rs(ai_chat_clear 切 delete_range + 旧 clear 同步)+ commands/conversation.rs(映射启用 + 2 round-trip 测)
+- **结论(2026-06-20·独立 grep/read 核验 commit 7a1fced + cargo test EXIT 0)**: ✅ **PASS** — 🔴0 🟡0 ⚪0
+
+**5 维度逐项核验(file:line 佐证)**:
+
+| 维度 | 核验点 | 佐证 | 判定 |
+|---|---|---|---|
+| ① replace 单事务原子 | DELETE+INSERT OR IGNORE 单事务无空窗 | `message_repo.rs:210 transaction()` · `:214 DELETE WHERE conversation_id=?1` · `:221 INSERT OR IGNORE INTO ai_messages` · `:237 tx.commit()` · 4 测(:428 全量/492 空清/509 隔离/554 幂等) | ✅ |
+| ② save 切+备份 | replace_conversation 全量重写 + 旧 messages 保留 | `conversation.rs:219 replace_conversation(conv_id, records)` · `:193` 旧 messages JSON 不赋新值(备份) · `:225-227` 首次落库双写 fallback 兼容老库 reload | ✅ |
+| ③ clear 同步 | delete_range + 旧 clear_messages 防脏读 | `chat.rs:697 clear_messages(&id)`(旧 JSON 列同步清空)· `:701 ai_messages.delete_range`(新表全删)· `:687` 内存 messages.clear() | ✅ |
+| ④ 零回归 round-trip | 150 条全字段 + save overwrite | `cargo test -p df-storage` EXIT 0(message_repo 9 passed,42 filtered 全 51 过)· 4 单测 + 2 端到端 round-trip(commands/conversation.rs +184) | ✅ |
+| ⑤ 旧 messages 备份保留 | 不删 V22 才删 | `conversation.rs:193` 不赋新值保留旧值 · 双写兼容老库 reload fallback · clear 同步清空备份列防脏读 | ✅ |
+
+**对抗核验印证**:
+- **并发安全(F-09 真并发)**:replace 按 conversation_id,跨 conv 隔离(测 :509 isolation_between_convs 印证)+ 决策e per_conv 单 loop 同 conv 不并发两 save → 无后写赢覆盖风险
+- **全量重写写放大**:长对话每次 save DELETE+INSERT 全部消息,已知 trade-off(commit 声明「全量重写非 dirty 增量,P2.1 优化」),非缺陷
+- **INSERT OR IGNORE 幂等**:DELETE 后无残留,防 records 内 id 重复/竞态残留跳过不报错
+- **双写 fallback 渐进迁移**:首次落库 messages_json + ai_messages 双写,老库 reload 走 messages fallback,V22 删列
+
+**独立 grep/read 复核补遗(2026-06-20·commit 7a1fced 当前形态 + cargo test EXIT 0)**: 维度①-⑤ 全 PASS,df-storage 51+11 + devflow 149 测全过 + cargo build 零 warning。补 3 条 low 观察项(均非阻塞,P2 过渡期可接受):
+- ⚪1 low(`message_repo.rs:221` INSERT OR IGNORE 静默丢):schema V21(`migrations.rs:534`)含 `UNIQUE(conversation_id, seq)` + `id PRIMARY KEY`,重复冲突静默 IGNORE 丢行。seq 基于位置(`enumerate`)必唯一、id None 兜底 `msg_{conv}_{seq}`(`commands/conversation.rs:121`)基于位置也唯一 → 实际安全;仅极端(内存两条共享同显式 id 如重试/服务端 echo 重复)理论静默丢 → DB 条数 < 内存。留 P2.1 dirty 增量观察。
+- ⚪2 low(`chat.rs:697-704` ai_chat_clear 两步非原子):clear_messages(置 messages='[]' + token 清零 `conversation_repo.rs:317`)先于 delete_range,各自 `map_err(err_str)?`。若 delete_range 失败 → 旧 JSON 已清/token 已零但 ai_messages 仍在,reload 读 ai_messages 非空(`commands/conversation.rs:261`)→ 消息恢复(清空视觉失败,虽然已 Err 返)。建议理论可两操作同事务或先 delete_range。非阻塞(Mutex 串行化下概率低)。
+- ⚪3 low(`conversation.rs:215-221` 写路径 warn 吞错):已落库分支 update_full 成功但 replace_conversation 失败仅 `tracing::warn` 不中断 → 对话元数据(token/updated_at)持久化但 ai_messages 未更新(新真相源丢失)。P2 过渡期设计可接受(warn 不 fatal),V22 删旧列后需复核。
+- **待修项回流 todo**:无 🔴/🟡 项。⚪ 均为 P2.1/V22 复核观察,当前批次 B 无需改。
+- **待修项回流 todo**:**无** 🔴/🟡 项(⚪3 low:INSERT OR IGNORE 极端静默丢/ai_chat_clear 两步非原子/update_full 成功 replace 吞错 — P2 过渡期可接受,V22 删旧列后复核)
+
+---
+
+### CR-260619-25 AI Chat 跑题改进 P0(系统提示聚焦 + 意图接入 loop·commit 102d398+1606d5a) — ✅ 已审(PASS·🟡2 已修·⚪1)
+
+- **结论(2026-06-20·独立 grep/read 核验 commit 102d398)**: ⚠️ **ISSUES** — 🔴0 🟡2 ⚪1
+ - 改进1(聚焦段)+改进2(意图接入)+三重 fallback + 执行路径不变 **全 PASS**。df-ai filter 7/7 + devflow prompt 4/4 测试绿。
+ - **执行路径不变确证**:filter 后 `tool_defs`(mod.rs:124/173/771)仅喂 `stream_llm`(LLM `tools` 字段);`tools_arc` 完整 registry 走 `process_tool_calls`(mod.rs:962)。LLM 幻觉被滤掉的工具名 → 要么不在 LLM 可见列表(LLM 不会调),要么 audit/tools_arc.get 仍能查到/拒绝。三重 fallback 全到位(conf<0.7→全量 / filter 内 subset 空→全量 / 过滤<3→全量)。
+ - **🟡-1 Debug 意图过度收敛**:`intent.rs:344` Debug → `[File, Http]` 域,无 Data。SPECIFIC(Debug)优先级 > ENTITY(Task/Project/Idea)。模拟三例「调试任务为什么失败」「排查这个任务状态」「帮我debug任务推进问题」**全部 Debug 胜出 → Data 域工具(list_tasks/advance_task/run_workflow/create_project 等 18 个)丢失**。三重 fallback 不救(Debug 子集 11 工具 ≥3 且非空)。执行路径不变故非崩溃,属用户可见功能降级(LLM 看不到任务工具,会回「无法完成」或只用消息上下文答)。建议:`Debug` 加 `ToolDomain::Data`(调试常需查任务/项目/workflow 状态)。
+ - **🟡-2 文档失准「threshold=1.0 关闭收敛」**:`intent.rs:322` conf 截断到 1.0(`score.min(1.0)`),大量单关键词命中达 conf=1.0(代码/重构/bug/调试/grep/http/项目/任务/灵感/搜索等 weight=1.0)。故 `INTENT_CONF_THRESHOLD=1.0` 时 `conf >= 1.0` **仍通过过滤**,无法真正关闭收敛。agentic/mod.rs:437 注释 + commit message 声称「置 1.0 = 关闭收敛退回接入前」不准确。真正关闭需 threshold > 1.0(如 1.01)或加显式 `disable` 开关。
+ - **⚪-1 `< 3` 魔数无注释**(mod.rs:457 `if filtered.len() < 3`):为什么是 3 非 2/5,无常量名/rationale 注释。
+- **待修项回流 todo**: ✅ **2 项已修**(commit 1606d5a):① Debug 加 Data 域(intent.rs:344,防"调试任务"丢 Data 工具)② threshold 注释修正(mod.rs:437,conf 截断 1.0 致 =1.0 非关闭,真关闭需 >1.0)。CR-25 🟡 闭环,verdict 转 ✅ PASS。
+
+---
+
+### CR-260619-26 AI Chat 跑题改进 P1(压缩增强 + 工具结果压缩·commit 013ce21) — ✅ 已审(PASS·🟡1⚪1)
+
+- **结论(2026-06-20·独立 grep/read 核验 commit 013ce21)**: ✅ **PASS** — 🔴0 🟡1 ⚪1
+ - 维度①-⑤ 全 PASS:view-only **确认不改持久化**(build_for_request 返回 clone context.rs:125/150-153,extract_key_info 仅改 clone 视图 mod.rs:786-820)+ 常量开关兜底双到位(KEYWORD_FALLBACK_ENABLED/TOOL_RESULT_COMPRESS_ENABLED 默认 true,设 false 等价旧行为)+ 失败兜底与 LLM 成功路径**语义一致**(均 compress_old_messages+insert_at,持久化落 save_conversation)。
+ - **测试复跑**:df-ai context_helpers **14 passed** ✅ / devflow prompt **6 passed**(含 zh/en 主题锚点测,CR 登记"7"为虚指实际 6)✅ / cargo check -p devflow EXIT 0 零 warning ✅。
+ - **view-only 对抗核验**(CR 重点):build_for_request 只 `.clone()`/`all_messages_clone`,ContextManager.messages 未被 extract_key_info 触碰 → 原始 tool_result 仍在 DB(下次 build 全量重建自愈)✅。失败兜底分支(mod.rs:724-741)compress_old_messages+insert_at 改的是 ContextManager.messages(持久化层),**但与 LLM 成功路径(mod.rs:685-687)逐字同构**,loop 退出 save_conversation 落库 → 摘要(启发式 vs LLM)落 DB 合理,非 view-only 违规。**无误改 per_conv.messages 风险**(conv() 返回同 conv,无跨 conv 写)。
+- **范围**(3 文件):prompt.rs(compress prompt 主题保留 + 2 测)+ context_helpers.rs(extract_keyword_summary + should_summarize_tool_result + extract_key_info + 14 测)+ agentic/mod.rs(失败兜底关键词 + view-only 工具压缩接入 + 2 常量开关)
+
+**5 维度逐项核验**:
+
+| # | 维度 | 判定 |
+|---|------|------|
+| 1 | 改进3 compress prompt 主题保留 | ✅ prompt.rs:181-183(en)+ :205(zh-CN)加主题词/实体/技术名词锚点保留条,2 测覆盖 |
+| 2 | 改进3 失败兜底关键词 | ✅ extract_keyword_summary 纯函数(context_helpers:190-252)user 词素 top-10 去停用词;失败分支 mod.rs:724-741 仍 compress_old_messages 释放 token + insert_at(0, system) 插锚点;KEYWORD_FALLBACK_ENABLED 开关 mod.rs:67 |
+| 3 | 改进4 should_summarize/extract_key_info | ✅ should_summarize_tool_result(>2KB/占比>40%,0 history 不除零 context_helpers:341-343)+ extract_key_info(错误行 error/panic/失败/.rs:N context_helpers:420-433 + 首尾5行,短内容原样 :366-368),14 测全绿 |
+| 4 | 改进4 view-only(本 CR 重点) | ✅ build_for_request 返回 clone(context.rs:125/150-153),extract_key_info 仅改 clone(mod.rs:803-823),ContextManager.messages/DB 原始 tool_result 不变 |
+| 5 | 常量开关 | ✅ KEYWORD_FALLBACK_ENABLED(mod.rs:67)+ TOOL_RESULT_COMPRESS_ENABLED(mod.rs:77)默认 true,设 false 等价旧行为 |
+
+**🟡 建议改进 (1)**:
+
+| # | file:line | 问题 | 建议 |
+|---|-----------|------|------|
+| 1 | `context_helpers.rs:281-286` | **中文多字实体被拆单字过滤丢锚点**:`tokenize` 把每个汉字单独成词素(注释自述「汉字单独成词」),MIN_WORD_LEN=2(context_helpers:192)按 `chars().count()` 过滤 → 「压缩/架构/审批」等 2 字中文领域词被拆成 2 个单字词素,各 count=1 < 2 全滤掉。本 CR 主题改进目标正是中文 chat 跑题,**中文实体锚点基本捕获不到**(仅英文标识符如 build_for_request 因聚成 1 词素 count=17 入榜)。14 测中无多字中文实体保留用例印证(`keyword_summary_chinese_extracts_frequent_terms` 只验英文标识符)。 | (a) MIN_WORD_LEN 改按字节或对汉字放宽到 1(但单字虚词多需扩停用词);(b) 加 2-gram 汉字滑动窗口收集(轻量,无 jieba);(c) 接受现状文档标注「中文锚点依赖 LLM 成功路径,兜底仅保英文标识符」。非阻塞(LLM 成功路径不受影响,仅兜底场景中文覆盖弱)。 |
+
+**⚪ 可选优化 (1)**:
+
+| # | file:line | 问题 |
+|---|-----------|------|
+| 1 | `agentic/mod.rs:663-664` | active_msgs 空 → `Ok(None)` noop 分支(:700),但 keyword_fallback 已先算(:655-656)被丢弃。极小浪费(空消息 extract 早返空串,零成本),无需改。 |
+
+- **待修项回流 todo**: **无** 🔴/🟡 项(🟡1 中文 tokenize 锚点弱:每汉字单成词素,"压缩/架构/审批"2字词被拆滤,非阻塞 LLM 成功路径,批次3 P2 tokenize 2-gram 汉字滑窗一并改)
+
+---
+
+### CR-260619-27 AI Chat 跑题改进 P2(主题检测保守 + tokenize 2-gram·commit a2db5c7) — 🟢 通过
+
+- **结论**🟢 通过:6 维度全部核验通过,topic 字段无破坏(TrackedMessage 无 Serialize derive,只 ChatMessage 落库)+ 双高置信保守(任一 topic None 不标)+ tokenize 2-gram 正确修复中文锚点。df-ai 189 passed 独立复跑确认。无 high/med 风险,2 条 low 提示。
+- **范围**(3 文件):context.rs(TrackedMessage.topic 字段 + pending_topic_marker + push 推断 + last_user_topic/take_topic_marker + 5 主题测)+ context_helpers.rs(tokenize 2-gram 汉字滑窗 + 4 测)+ agentic/mod.rs(TOPIC_MARKER_ENABLED 常量开关 + loop 顶部消费 marker)
+- **根因**:多主题交织无检测/分段 + CR-26 🟡1 中文 tokenize 锚点弱
+- **维度核验**:
+ 1. ✅ topic 字段无破坏:`TrackedMessage`(context_helpers.rs:151)上方无任何 `#[derive(..., Serialize)]`(全文 `^#\[derive` 仅 27/104/136 三处均非本结构),`Option` 字段加于无 Serialize 结构体 → 无 DB/序列化影响。ChatMessage(104)仍单独 Serialize 落库,topic 不进库。
+ 2. ✅ push 推断 topic:context.rs:79-85 仅 User 消息走 `IntentRecognizer::recognize`,`conf >= 0.7 && !Unknown` 才 Some,低置信 None(intent.rs:289 签名匹配,Intent::Unknown 在 intent.rs:51)。
+ 3. ✅ 主题标记保守:context.rs:91-101 push 前 `last_user_topic()`(反向扫末条 user,跳过 assistant)取旧 topic,与本次推断 topic 双 Some 且不同才置位 marker("old|new");任一 None 不标(测试 `topic_marker_not_triggered_when_either_topic_none` 覆盖正反两向)。
+ 4. ✅ loop 软提示:agentic/mod.rs:560-610 `TOPIC_MARKER_ENABLED`(默认 true)门控,`take_topic_marker()` 一次性消费(take 后清空防重复 insert),splitn(2,'|') 安全解析(intent.as_str 标签 code/debug/.../chat 均不含 `|`);insert system 软提示非强制。loop 段含陈旧 conv 检测(562-569)与二次 lock 前的 `contains_key` 复检(598),防竞态。
+ 5. ✅ topic 不参与裁剪/压缩:`grep \.topic` 全 crate 仅 5 处(push 推断 79/540、insert_at None 583、测试 1400/1405),裁剪/压缩/build_for_request 路径零读取(测试 `topic_marker_not_polluting_compression_or_trim` 验证 cfg(80) 撑爆裁剪后仍正常)。
+ 6. ✅ tokenize 2-gram:context_helpers.rs:294-304 相邻汉字(prev_class==1 且 prev_han 有值)组 2-gram,非汉字边界/拉丁切换重置 prev_han 不跨边界组词;"压缩架构"→压缩/缩架/架构 并存单字,英文标识符 `build_for_request` 保持完整(测试 `tokenize_english_identifier_unchanged` + `tokenize_chinese_2gram_not_crossing_boundary` 覆盖)。
+- **findings**:
+ - [low] agentic/mod.rs:598 loop 第二次 lock 后仅 `contains_key` 复检即 insert_at(0),未重算 token 增量对裁剪阈值的边际影响——但 insert_at 内部已 `history_tokens += tokens`(context.rs:576-578),且软提示短(单行),无实际越界风险。提示性,无需改。
+ - [low] context.rs:91 marker 置位后立即 push 本条 user,若同轮内 loop 又触发 push(理论上单轮单 push)不会重复置位;多 user 连发场景 marker 只记最近一次 old→new,中间切换链丢失——但保守设计本意如此(只标最末切换),非缺陷。
+- **待修项回流 todo**:**无** 🔴/🟡 项(2 low 提示性:软提示 token 边际 + 多切换链只记末次,保守设计预期行为)
---