Skip to content

feat(sessions): search message text across sessions with a local FTS5 index - #7896

Open
Owlbay wants to merge 15 commits into
mainfrom
feat/session-content-search
Open

Owlbay wants to merge 15 commits into
mainfrom
feat/session-content-search

Conversation

@Owlbay

@Owlbay Owlbay commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary / 概述

会话页的搜索只索引元数据(会话 ID、标题、摘要、项目目录、源文件路径),只出现在对话中段的关键词永远搜不到,而手册把它叫「全文搜索」。本 PR 在 #7825 重写后的会话结构上加了正文搜索,并改写手册。

后端(session_manager/search.rs)

  • 独立索引文件 ~/.cc-switch/session-index.db:不进主库,不参与备份与云同步,删掉后会重建。
  • SQLite FTS5 + trigram 分词,每条消息一行;只收录用户与 Agent 的 Text 块,思考、工具参数与输出、注入内容(AGENTS.md、system-reminder 等)不收;单条消息最多 20,000 字符。
  • 增量:通过 load_messages 解析,按 source_fingerprint(mtime, len)跳过没变的会话;会话删除时同步删除索引行;一次重建 ≥50 个会话后 optimize + VACUUM。
  • 不足 3 个字符的查询(如「正交」)trigram 用不上,退回 LIKE;含 É、Ж 之类有大小写的非 ASCII 字母时改为 Rust 侧逐行折叠比较(SQLite LIKE 只折叠 ASCII)。
  • 新命令:sync_session_index(后台同步,推送 session-index-status 进度事件)、search_session_content、clear_session_index;新设置 sessionContentSearchEnabled(默认开)。

前端

  • 元数据搜索仍由 FlexSearch 即时完成,正文搜索防抖 250ms 后查询,两边结果按会话合并去重;查询中搜索框显示转圈。
  • 只在正文命中的会话,第二行显示「正文:」和最相关一处的摘录,其余命中处(最多再 2 条)列在行下方。
  • 点会话打开阅读页,查找栏自动展开并定位到最相关那条消息;点下方某条摘录则定位到那条消息。
  • 建索引期间搜索框下方显示进度;「更多」菜单里的「搜索消息正文」开关,关闭时删除索引文件。
  • zh / zh-TW / en / ja 文案。

手册:3.4-sessions.md 三种语言的「全文搜索」一节改为「搜索」,写清两部分范围、正文不收录的内容、索引位置和关闭方式。

Related Issue / 关联 Issue

Closes #7420
Related: #3721, #2019, #7399, #6711
Builds on: #7825(结构化 blocks 与源指纹缓存)

Prior art / 致谢

本 PR 在 #7825 重写后的会话结构上重新实现了正文搜索,设计参考了以下工作,在此致谢:

两个前置 PR 合并后,可由维护者决定是否关闭 #4976 / #6636。

Performance / 性能(release,本机真实数据)

1672 个会话(Claude Code、Codex、OpenCode 等),收录约 11 万条消息、约 30MB 正文:

首次建索引(后台) 约 19s
之后的增量检查(无变化) 约 45ms
索引文件 开发版实际建出(含 optimize + VACUUM)约 118MB;不整理时约 170MB
查询:≥3 字(trigram) 1–6ms(「会话列表」36 个会话 2.5ms)
查询:2 字中文(LIKE) 约 80–145ms(「正交」)
查询:2 字含非 ASCII 大小写(Rust 折叠) 约 185ms(「éc」)
高频字「的」(命中上限 200 个会话) 约 22ms

Screenshots / 截图

(待补)

Checklist / 检查清单

  • pnpm typecheck passes / 通过 TypeScript 类型检查
  • pnpm format:check passes / 通过代码格式检查
  • cargo clippy passes (if Rust code changed) / 通过 Clippy 检查(新代码无警告)
  • Updated i18n files if user-facing text changed / 已更新国际化文件(zh / zh-TW / en / ja)

验证:cargo test(全部通过;session_manager::search 10 个新用例)、pnpm test:unit(190 files, 2203 tests passed;新增页面用例 4 个、turns 用例 1 个);开发版与已安装版并行,在本机真实会话上实际使用。

Owlbay added 2 commits October 6, 2026 15:33
… index

The Sessions search only indexed session metadata (ID, title, summary,
project directory, source path), so a keyword that appears only in the
middle of a conversation could never be found.

Add a Rust-side full-text index built on the #7825 session model:

- Separate `~/.cc-switch/session-index.db` (not the main database, never
  backed up or synced); one FTS5 trigram row per user/assistant message,
  Text blocks only, injected content skipped, 20,000 chars per message
- Incremental: sources are re-parsed through `load_messages` only when the
  `source_fingerprint` (mtime, len) changes; deleted sessions drop their
  rows; optimize + VACUUM after large rebuilds
- Queries shorter than 3 characters fall back to LIKE, or to a Rust-side
  case-folded scan when they contain non-ASCII cased letters
- Up to 3 snippets per session; the reader opens with the find bar on the
  message behind the clicked snippet
- "Search message text" toggle in the Sessions menu; turning it off deletes
  the index file

Closes #7420
Refs #4976, #6636, #3721
The manual called the session search "Full-Text Search" and listed only
metadata fields. Rename the section to "Search", describe the metadata and
message text scopes, what the text index skips, where it is stored and how
to turn it off.

Refs #7420
@Owlbay
Owlbay requested a review from farion1231 as a code owner October 6, 2026 07:34
@github-actions github-actions Bot added documentation Improvements or additions to documentation frontend Frontend (React/TypeScript) backend Backend (Rust/Tauri) i18n Internationalization (zh/en/ja) labels Oct 6, 2026
@farion1231

Copy link
Copy Markdown
Owner

GPT-6 Astra 自动代码审查

审查提交:ecec305ff07166b62ca0ea8c122bd5d87aa17c09
Base:3af748231729d34d5dffdc49efda37188e35fc04

由 Codex 定时任务使用 GPT-6 Astra 子 agent 审查,并经主 agent 复核。

[P2] 在索引锁内重新检查开关,避免关闭后重新落盘正文

位置:src-tauri/src/session_manager/search.rs:536

在后台建索引期间关闭「搜索消息正文」时,with_index 可能先在第 536 行读到 enabled=true,然后线程被暂停;设置保存为 false、clear 持有 INDEX 锁关闭连接并删除文件后,后台线程才获得锁。这里没有锁内复查,会再次创建 session-index.db,并继续执行 index_source 已准备好的 replace_source,把会话正文重新写入磁盘。后续同步循环只会因关闭开关而退出,不会再删除这份索引,因此清理命令已成功返回、功能保持关闭时仍会留下正文副本。这是新的本地隐私清理竞态,不要求设置保存失败。

证据:

  • search.rs:535-554:enabled() 检查在获取 INDEX 锁之前,锁内 guard.is_none() 会无条件打开或创建索引并执行闭包。
  • search.rs:566-571:clear 在相同 INDEX 锁内置 None、删除文件后返回成功;没有等待/取消已通过开关检查的工作。
  • search.rs:697-700:后台加载正文后通过 with_index 执行 replace_source;search.rs:655-672 的后续 enabled 检查不会清理被重新创建的文件。
  • SessionManagerPage.tsx:300-306 的关闭流程先 autoSaveSettings(false) 再 clearContentIndex;commands/settings.rs:72-76 与 settings.rs:809-817 证实保存会更新后端开关。
  • 无文件无网络 V8 交错模型找到有效序列:worker 检查 true → 保存 false → clear 获锁/删文件/返回 → worker 获锁/重建/写正文;模拟加入锁内复查后相同模型未出现该终态。此为逻辑模型,不是 Rust 并发实测。

建议:至少在取得 INDEX 锁后、打开连接或调用闭包前再次检查 enabled;使关闭与清理成为可阻止旧工作的同步边界,必要时使用任务代次或取消标记。增加带屏障的回归测试,让后台工作暂停于锁前检查之后,完成关闭清理再恢复,断言索引及侧文件不再出现。

[P2] 在截断命中行之前应用 provider 过滤

位置:src-tauri/src/session_manager/search.rs:462

当未选应用对某个查询占满全局前 20,000 条命中时,当前应用的真实结果会全部丢失。三个查询分支都先按 MAX_HIT_ROWS 截断,直到这里才过滤 provider;例如 Claude 有 20,000 条「needle xy Éclair」,Codex 有一条「needle xy Éclair target」,仅搜索 Codex 时 needle、xy、éc 都返回零条,尽管 Codex 只有一个匹配会话,远未达到 200 会话上限。若关键词不在该会话元数据中,页面会错误显示没有匹配会话,且刷新或缩小到 Codex 都不能恢复它。

证据:

  • search.rs:405-440:FTS5、非 ASCII 短查询扫描、LIKE 三个分支都在应用过滤前限制为 MAX_HIT_ROWS=20,000。
  • search.rs:446-464:在候选 rowids 已截断后才读取来源表并执行 provider 过滤。
  • SessionManagerPage.tsx:252-265 将当前应用传给正文查询;src/lib/api/sessions.ts:50-56 将 providerIds 传给后端。
  • 标准库内存 SQLite 3.53.4 合成夹具:source id=1 为 Codex 唯一匹配行,id=2 为 Claude 的 20,000 行;FTS5 needle、LIKE xy 和短查询大小写折叠模型 éc 各取得 20,000 个候选,过滤后 Codex 命中均为 0,实际 Codex 匹配数为 1。
  • search.rs:872-900 的现有 provider 过滤测试只含三条匹配消息,没有覆盖候选行上限;新增 MSW 搜索模拟也未实现该全局截断。

建议:将 provider 条件下推到 FTS5/LIKE 候选查询,在 LIMIT 之前按 indexed_sources 过滤;短查询的逐行折叠也应只让选中应用消耗命中预算。添加未选应用占满 20,000 行、所选应用仍有匹配的三个分支回归用例。

[P2] 让成功刷新列表触发同步,而非仅监听元数据引用

位置:src/hooks/useSessionContentSearch.ts:112

首次索引完成后,如果正文变化而 SessionMeta 不变,用户点击刷新仍不会更新正文索引。一个具体场景是 Grok Build 一轮进行中只追加 chat_history.jsonl,summary.json 尚未更新:列表扫描仅从 summary.json 生成元数据,TanStack Query 默认结构共享保留相等数据的数组引用,页面又通过 useMemo 保留该引用,因此此 effect 的 [enabled, sessions] 不变,syncContentIndex 根本不会调用。新关键词不在标题/摘要中时会持续搜不到,直到其他元数据改变、重新进入页面或重开开关;后端已有的正文源指纹检测也无法补救,因为它从未被触发。

证据:

  • useSessionContentSearch.ts:102-115:索引同步唯一的列表刷新触发器依赖 enabled 和 sessions,没有使用成功刷新版本或显式刷新动作。
  • SessionManagerPage.tsx:152-153 保留 data 引用;667-672 的 refreshList 只 await refetch() 并提示成功,没有显式同步索引。
  • src/lib/query/queries.ts:308-314 及 src/lib/query/queryClient.ts 未关闭 TanStack Query 的默认 structuralSharing。
  • providers/grokbuild.rs:44-53、182-224 的扫描和元数据完全来自 summary.json;60-69 的正文来自同目录 chat_history.jsonl。
  • session_manager/cache.rs:112-116、512-543 明确处理并测试正文变而 summary 不变的场景;search.rs:686-694 只有在同步已经启动后才检查这个指纹。
  • 无文件无网络 V8 依赖模型:相同合成元数据刷新后保留引用,即使正文已变化,[enabled, sessions] 的 Object.is 比较全部相等。该验证仅模拟结构共享契约,并非 React/TanStack 项目实测。

建议:传入会话列表成功读取的版本(例如 dataUpdatedAt)作为同步依赖,或在成功 refetch 后显式请求同步,不要把元数据数组引用当作正文变更信号。用保持 list_sessions 返回内容不变、只修改合成正文的测试验证刷新会再次调用 syncContentIndex,并让新正文命中可见。

验证与范围

子 agent 完整审阅固定三点差异的 24 个文件和必要上下文,实际 merge-base 与指定 base 一致。主 agent 复核 INDEX 锁、后端设置缓存更新和关闭清理顺序,确认旧后台工作可在清理返回后重建正文索引;逐分支确认候选行截断先于 provider 过滤;核对列表刷新、正文源指纹和 effect 依赖。主 agent 另只读核对本机已安装且与本 PR lockfile 一致的 @tanstack/query-core 5.90.3 源码,确认默认 replaceEqualDeep 保留等值数组引用,补齐子 agent 未读取该实现的限制。三条 P2 均保留,发布前三类讨论为空。验证仅为静态和隔离逻辑模型,不表示项目测试已通过。

  • 范围与元数据核对;完成;首先 jq 仅提取指定 PR 字段、comments/reviews/inline/review_workspace。HEAD 等于指定 head;实际 merge-base 等于指定 base,全部审查范围为 BASE...HEAD,共 24 个变更文件,未混入 base 独有修改。
  • 静态审查;完成;完整阅读全部变更;补读索引源验证、指纹、消息加载/删除、设置保存/缓存、会话列表刷新与查询缓存、阅读器分轮/搜索/流式数据、备份与同步范围,以及新增测试和相关既有测试。被截断的关键内容已按范围补读。
  • 标准库内存 SQLite 逻辑模型(非项目实测);复现 provider 过滤漏报;python3 -I -B,仅导入 sqlite3/json,连接 :memory:,temp_store=MEMORY,不读会话/配置、不联网、不写文件。SQLite 3.53.4,以 20,001 条合成消息和项目等价的候选查询/过滤顺序验证:FTS5 与 LIKE 以及短非 ASCII 折叠模型均把实际存在的 Codex 唯一匹配过滤成 0。未调用仓库入口或 Rust 测试。
  • V8 纯逻辑交错模型(非项目实测);复现关闭清理后重新创建索引的有效交错;在 functions.exec 内以纯对象状态模拟 enabled、INDEX 锁、清理及工作线程步骤。发现 1 个关闭完成后仍留下正文的有效交错;增加锁内开关复查的模型为 0。无文件和网络调用。
  • V8 纯逻辑 effect 依赖模型(非项目实测);确认等值元数据引用不变时不会触发同步依赖;用合成 Grok 元数据模拟结构共享保留引用;正文变化不在依赖中,Object.is 检查显示所有 effect 依赖未改变。未运行 React 或实际 TanStack Query。
  • 只读与进程检查;完成;独立副本审查前、写结果前及写结果后 git status --porcelain=v1 --untracked-files=all 均为空;写结果后 git diff --exit-code HEAD -- 返回 0,HEAD 已核实为固定 head。所有 exec_command 均已返回退出码,没有后台 session。唯一文件写入为指定 results/7896.json,且 jq 已确认 JSON 可解析、必需字段齐全、finding 数为 3。

未验证或受限范围:

  • 未运行仓库测试、构建、钩子或应用入口:未建立可证明隔离真实配置/秘密读取、联网与副本外写入的执行环境;未安装依赖。作者正文中的测试通过声明未当作实测。
  • 独立副本没有 node_modules/@TanStack,未直接运行或读取已安装的 TanStack 实现;刷新问题依据默认结构共享契约和仓库调用链,V8 夹具只验证对应逻辑模型。
  • 并发关闭问题通过静态锁顺序及确定性交错模型验证,未测量真实线程调度下的发生概率;SQLite 夹具使用系统标准库 SQLite,而非项目绑定的 rusqlite 运行时。
  • coverage_complete=true 表示完整覆盖本 PR 差异及必要静态上下文,不代表全平台运行、UI 截图或完整测试套件已通过。

本评论仅针对以上提交的代码审查结果,不构成合并批准。

Owlbay added 2 commits October 7, 2026 10:52
- Check the "Search message text" switch after taking the index lock, so a
  background write that passed the check before the user turned the switch
  off can no longer recreate session-index.db after clear() deleted it
- Apply the app filter inside the candidate queries (FTS5, LIKE and the
  case-folded scan) instead of after the 20,000-row cap, so a busy app can
  no longer crowd out the selected app's only matches
- Sync the index after every successful session list read (dataUpdatedAt)
  rather than when the list array changes: structural sharing keeps the
  same array when only message text changed (e.g. a Grok Build turn in
  progress), so Refresh never reached the backend fingerprint check

Refs #7420
…eat/session-content-search

# Conflicts:
#	docs/user-manual/en/3-extensions/3.4-sessions.md
#	docs/user-manual/ja/3-extensions/3.4-sessions.md
#	docs/user-manual/zh/3-extensions/3.4-sessions.md

xue-ding-e commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

在当前 head 078031d 核查并复现了一个增量索引的失败恢复问题,和 Text-only 的范围选择无关:

search.rs 的 index_source 把 load_messages 的 Err 经 unwrap_or_default() 转成空正文,随后 replace_source 仍保存当前指纹。临时 I/O 失败恢复后,只要源文件指纹未变化,就会一直跳过重试;如果之前已有正文索引,本次失败还会删除旧正文。

用可注入 loader + 实际 SessionIndex/bundled SQLite 做了两个合成回归:

  1. 首次读取失败,第二次同指纹读取成功:原实现 loader 只调用 1 次(应为 2)。
  2. 已有正文后刷新读取失败:原实现旧关键词命中数变成 0(应保留 1)。

最小修复是读取失败时不调用 replace_source、不提交新指纹;成功返回空正文仍照常替换。修复候选连同现有真实 search/model 模块测试共 18 项通过。这里的测试是隔离模块验证,不是完整 Tauri 测试:当前环境缺 GTK/WebKit/libsoup,未把完整项目检查标为通过。

我正在准备单独的小补丁来补这两个回归,可针对本 PR 的 feature 分支提交,避免另做一套全文检索。

验证补充:已在隔离工作区补齐官方 Linux 系统依赖,真实项目的 cargo test session_manager::search --lib --locked 现已通过,14 passed / 0 failed,包含以上两项回归。该次使用真实 Tauri crate 和 GTK/WebKit/libsoup/OpenSSL 编译链接,没有 stub;尚未将全项目测试或 Clippy 标为通过。

后续回归补充(仍基于此 PR 的 078031d3,未改变 Text-only 范围):

  • 会话列表成功变为空时仍需同步,以删除最后一个会话的旧索引;加载/失败状态不能当作成功空列表。真实 hook 回归已验证。
  • 删除索引 DB/WAL/SHM 时,非 NotFound 错误不能静默返回成功;应尝试清理所有文件并报告失败。真实后端测试已验证。
  • 关闭设置保存后的托盘更新可能迟到:用户在此期间重新开启并重建,旧关闭 continuation 随后会 clear 新索引。用真实页面、useSettings 和 React Query 复现;以 QueryClient 范围单飞覆盖保存与清除,跨页面重挂载也保持锁定。7 项回归覆盖延迟保存/清除、连续切换及失败恢复。

四个小修复的独立组合验收已完成:前端 195 文件 / 2263 测试通过,typecheck 与 renderer build 通过;对应 Rust 源码的完整 lib 测试 3385 passed / 0 failed / 9 ignored,严格 Clippy 通过。未运行忽略项或真实桌面 E2E。补丁仍在本地,尚未发布 PR;会保留为针对本 feature 分支的独立小提交。

使用整串小写 shadow 索引和区分大小写的 trigram,短词在规范化正文上精确匹配。

摘录携带完整正文产生的 UTF-16 高亮范围,保留大小写上下文、空白压缩与多字节位置;补充检索、点击定位和安全渲染回归。
- 按提供商记录扫描完整性,仅清理确认扫描成功的源
- 保留读取失败时的正文和指纹,恢复后正常增量同步
- 覆盖目录故障、独立清理、数据库恢复和 UTF-8 尾部读取

@farion1231 farion1231 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢这版实现!先说核实过的部分:上一轮自动审查提的三处(锁内复查开关、应用过滤下推到截断之前、按 dataUpdatedAt 触发同步)都已修好,我逐条核过。索引只收正文,而阅读页的查找范围包含正文,所以列表里显示命中的会话,点进去一定能找到。和 main 合并后无冲突,cargo test --lib 与 pnpm typecheck / pnpm test:unit 全部通过。

下面两个 P2 修掉就可以合并,P3 和 nit 看你方便。

[P2] OpenCode / Hermes / MiniMax Code 的会话会在每次同步时全部重建索引

这三家的会话存在同一个 SQLite 数据库里,而指纹只看整个库文件(加 WAL)的修改时间和大小,不区分是哪个会话:

SourceLocation::Sqlite { db, .. } => {
let mut fp = Fingerprint::of_metadata(&fs::metadata(db)?);
if let Some(wal) = sqlite_wal_path(db) {
if let Ok(meta) = fs::metadata(wal) {
fp.merge(Fingerprint::of_metadata(&meta));
}
}
Ok(fp)

所以客户端只要写入一条消息,这个应用所有会话的指纹都会变。下一次同步会把它们全部重新解析、重写索引行;重建数一旦达到 50,还会触发 optimize + VACUUM:

let tidy = if changed >= COMPACT_AFTER_CHANGES {
with_index(|index| index.compact())
} else {
with_index(|index| index.checkpoint())
};

VACUUM 会重写整个索引文件(按 PR 描述约 118MB),期间持有索引锁,搜索要等它跑完。同步的触发时机是会话列表每次刷新成功(useSessionContentSearch.ts#L110-L123)。列表查询 30 秒后过期(queries.ts#L312),加上全局开了窗口获得焦点时重新拉取,所以用户一边用 OpenCode、一边切回会话页,每次切回都会重跑一遍。

建议对 SQLite 来源改用会话自己的最后活跃时间当指纹。扫描结果里已经有这个值:OpenCode 取的是会话和消息 time_updated 的最大值(opencode.rs#L218),MiniMax Code 是 updated_at_ms,Hermes 优先取 last_activity_at(hermes.rs#L229-L234)。Hermes 的旧版库如果没有这一列,会回落到 ended_at / updated_at,请确认会话进行中这个值也会更新。大致改法:run_sync 收集来源时把 last_active_at 一起带上,然后在 index_source 里:

let fingerprint = if matches!(source.location, content::SourceLocation::Sqlite { .. }) {
    // 这几家的会话共用一个数据库,整库指纹一写全变;改用会话自己的最后活跃时间
    StoredFingerprint { modified_ns: last_active_at, len: 0 }
} else {
    let Ok(fp) = cache::source_fingerprint(&source) else { return false };
    StoredFingerprint::from(fp)
};

另外,整理(optimize + VACUUM)建议不要只看「这次重建了多少个会话」:比如只在首次建库后整理(同步开始时 indexed_sources 为空),这样就不会每隔几次同步就把整个文件重写一遍。

[P2] 会话读取失败时不应写入指纹

search.rs#L733-L736 会把 load_messages 的错误当成零行正文,并照常记下指纹。之后只要指纹不变就不会重试;如果这个会话之前已经收录过,旧正文也会被清掉(@xue-ding-e 在上面的评论里也提到了)。

单看这一条影响有限:指纹是在读取之前取的,因为并发写入导致的读取失败,等写入结束后指纹通常会变,于是会重新读。但上一条改成「按会话最后活跃时间」做指纹之后,这条自动恢复的路就没了:一次失败要等这个会话来新消息才会重试。所以建议两条一起改:

let rows = match super::load_messages(&source.provider_id, &source.load_path()) {
    Ok(messages) => message_rows(&messages),
    Err(e) => {
        log::warn!("读取会话正文失败,下次同步重试: {e}");
        return false;
    }
};

真正坏掉的文件会在每次同步时重试一次解析,代价是一次失败的读取,可以接受。

[P3] 点正文摘录时可能定位到思考块

阅读页的查找始终包含思考内容(「包含思考」只影响复制),而思考步骤和正文带的是同一个消息序号。findMatchPositionForMessage 按文档顺序取第一个序号相同的命中,所以 assistant 消息是 [thinking("… needle …"), text("… needle …")] 时,会定位到思考块,而不是摘录所在的正文。这条消息作为最终回复、或作为中途说明,两种情况我都用最小用例复现了:返回 1,预期 2。建议优先选正文命中:

let sameMessage: number | null = null;
// …循环内命中同一条消息时:
const isText =
  match.target === "question" ||
  match.target === "final" ||
  turn.steps.some((s) => s.kind === "note" && s.id === match.targetId);
if (isText) return position;
sameMessage ??= position;
// …
return sameMessage ?? sameTurn ?? 1;

nit

  • run_sync 一开始会再调一次 scan_sessions()(search.rs#L672-L678),而它是在 list_sessions 刚扫完之后被触发的,等于每次刷新扫两遍。虽然在后台线程里跑,会话多的机器上还是能省一次。

修掉两个 P2 就可以合并,推上来后我会尽快复审。

Copy link
Copy Markdown
Contributor

补充这项性能 P2 的复现,供当前修订参考(AI 辅助、本地验证):基于 #8150 的 8546386,保留这里相同的整库指纹与 compact 阈值,50 个合成 SQLite/WAL 会话在无变化刷新时 loader 调用为 0;只改一个会话的正文行后,loader、索引替换及 changed 都变成 50,也会达到 compact 阈值。新增断言期待 1,实际为 50。

直接把 last_active_at 当作唯一指纹还需要一个可靠回退。Hermes 上游明确说明该字段是限频心跳,可能落后于新消息,因此活跃时间还要结合消息时间:固定提交的实现说明。时间缺失或不递增时,不能据此跳过正文变化;仅用 max(timestamp) 也不足以覆盖同时间戳编辑或删除。

#8150 已覆盖另一个读取失败 P2:保留旧正文和旧指纹,允许下次重试,同时保留扫描不完整时不清理旧索引的保护。性能项建议单独使用能反映正文变化的会话 revision,普通增量刷新不再仅因 changed >= 50 执行 VACUUM。

本次验证使用提取的生产 Rust 函数与 bundled SQLite 3.45.0、合成数据,结果为 10 项通过、1 项预期失败;属于隔离模块复现,未将其称为完整 Tauri、跨平台测试或大型索引延迟基准。

@Owlbay

Owlbay commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. The follow-ups are in the current head: SQLite-backed sources (OpenCode/Hermes/MiniMax Code) now fingerprint per session and only compact after the first build, a failed load_messages no longer stores a fingerprint or clears existing rows, body hits are preferred over thinking blocks when locating a match, and run_sync reuses the fresh scan. Regression tests are included, and the branch merges cleanly with current main. @farion1231 could you take another look when you have time?

…eat/session-content-search

# Conflicts:
#	src-tauri/src/session_manager/providers/hermes.rs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backend (Rust/Tauri) documentation Improvements or additions to documentation frontend Frontend (React/TypeScript) i18n Internationalization (zh/en/ja)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

会话搜索只覆盖会话元数据,消息正文从未入索引(Full-Text Search 的范围与命名不符)

3 participants