DeepSeek-Reasonix 历史搜索 Catalog 架构解析:可丢弃 FTS 投影、索引策略与诊断命令
【免费下载链接】DeepSeek-ReasonixDeepSeek-native AI coding agent for your terminal. Engineered around prefix-cache stability — leave it running.项目地址: https://gitcode.com/GitHub_Trending/de/DeepSeek-Reasonix
历史搜索 Catalog 是 Reasonix 为会话历史检索构建的可丢弃 SQLite/FTS5 投影,存放在
<cache root>/history-search/v1.sqlite,它以会话 JSONL、event log、metadata、子 Agent transcript 与 archive 等权威文件为唯一数据源,索引与搜索完全解耦。本文从存储位置、token 化与 BM25 排序、增量索引与后台重建、运行态内存覆盖、可丢弃投影策略到doctor catalogs/catalogs reindex诊断命令,逐层解析该模块的设计与源码实现,帮助读者理解其"删库不丢会话、搜索不扫盘"的工程原理。
1. 为什么需要"投影":权威数据与检索索引的分离
Reasonix 的每条会话历史都以 JSONL 形式落盘,包含 session JSONL、event log、metadata、子 Agent transcript 与 archive。这些文件是唯一权威数据:任何时刻删除或重建历史搜索 Catalog 数据库,都不会删除会话本身;即使回退到旧版 Reasonix,也依然能读取原有权威文件。
历史搜索 Catalog 只是这些权威文件的一个可丢弃投影(disposable projection)。投影的核心价值在于:把"高成本、低频率"的全文索引过程与"高频、低延迟"的搜索过程解耦。搜索不再需要同步扫描目录、逐文件解析 JSONL,而是命中预先构建好的 FTS5 索引。
从源码看,internal/historycatalog包开头的注释即明确了这一契约(internal/historycatalog/types.go):
Package historycatalog maintains a disposable FTS projection of saved session history. Session JSONL/event/meta files remain authoritative.
投影数据库的默认路径由DefaultPath()计算得出:<CacheDir>/history-search/v1.sqlite(internal/historycatalog/types.go)。当 OS 缓存目录不可用时返回空字符串,Open()会自动退化为内存模式(InMemory),保证功能不中断。
2. 存储位置与 schema 设计
2.1 默认路径
// DefaultPath returns the disposable history FTS path under CacheDir. func DefaultPath() string { cache := strings.TrimSpace(config.CacheDir()) if cache == "" { return "" } return filepath.Join(cache, "history-search", "v1.sqlite") }即默认数据库文件为<cache root>/history-search/v1.sqlite。v1表示 Schema 版本(SchemaVersion = 1,见 internal/historycatalog/types.go)。
2.2 表结构
internal/historycatalog/schema.go定义了 v1 迁移的全部表:
| 表 | 作用 | 关键列 |
|---|---|---|
history_state | 全局状态:单调递增的revision与tokenizer_version,单行约束id=1 | revision、tokenizer_version |
history_roots | 每个被索引的根目录(session 目录、subagents 目录、archive 目录)的扫描状态 | signature(根目录指纹)、scan_generation、state(pending/scanning/ready)、indexed、total、completed_at |
history_sources | 每个会话源文件的元信息与健康状态 | content_fingerprint、meta_fingerprint、content_digest、message_count、indexed_message_count、custom_title、topic_id、topic_title、preview、health(ok/stale/corrupt/evicted/missing)、missing_since、seen_generation |
history_documents | 每条消息的每个 part 一条记录,外键指向 source | source_path、message_index、part_index、role、kind、tool_name、token_count |
history_fts | FTS5 虚拟表,contentless模式,只保存检索 token | terms(token 串)、rowid关联history_documents.id |
关键设计点:
- contentless FTS5:
history_fts声明为content=''的 contentless 表,配合contentless_delete=1。这意味着 FTS 表只存检索 token,不存完整消息正文——对应文档中"Catalog 的 FTS5 只保存规范化检索 token,不保存完整消息正文"的约定。 - tokenizer:
tokenize='unicode61 remove_diacritics 2',去掉变音符。CJK 的"重叠 bigram"由上层internal/retrieval在写入terms之前完成,而非依赖 SQLite tokenizer。 - 级联删除:
history_documents.source_path REFERENCES history_sources(path) ON DELETE CASCADE,删除 source 时自动清空其文档与 FTS 行。
2.3 snippet 与 around 上下文的懒读取
由于 FTS 表不存正文,SQLite 的 FTS 查询只负责选出最终候选(候选 id、source 路径、message index、part index、rank 等)。真正的snippet和around上下文在候选确定后,才从权威文件读取并生成:
desktop/history_search_collect.go的historyHitFromCandidate在拿到候选后调用retrieval.MakeSnippet(text, req.Query, queryTerms, 240)生成最多 240 个 rune 的 snippet;- snippet 生成逻辑见 internal/retrieval/bm25.go 的
MakeSnippet:压缩空白后,优先以原始 query 定位,其次以非单字符 token(跳过 ASCII 单字符,CJK 单字符保留)定位。
这种"索引只存 token、正文按需读"的架构,既保证了索引体积可控,也保证了 snippet 永远反映权威文件的最新内容。
3. 索引范围与 token 化规则
3.1 索引哪些消息
user、assistant、tool input、tool error、tool output都会被索引,即所有 role 与 kind 都进入history_documents与 FTS。但普通 tool output 不属于默认搜索种类——即默认搜索不返回 tool output 命中,这与文档中"普通 tool output 仍不属于默认搜索种类"的说明一致;SearchRequest.Kinds用于按种类过滤,前端可显式指定要检索的 kind(internal/historycatalog/types.go)。
3.2 英文:小写与代码符号语义
检索 token 化对英文采用小写化,并保留代码符号语义(CamelCase、snake_case 分段)。internal/retrieval/bm25.go的Tokens()将标识符FooBar、foo_bar拆分出foobar、foo、bar等分段 token,使搜索foo能命中foo_bar会话内容。V2 检索(shadow 模式,internal/retrieval/v2.go)进一步把 CamelCase 分段与 CJK unigram 作为召回超集,但生产默认仍走 V1。
3.3 CJK:重叠 bigram
CJK 内容按重叠 bigram分词:连续两个 CJK 字符组成一个 token,窗口每次滑动一个字符(如"历史搜索" → "历史"、"史搜"、"搜索")。retrieval.Tokens在处理 CJK run 时逐字符推进并两两组合。这样单字查询也能通过 unigram 兜底匹配,而多字查询有较好召回。
QueryTerms(internal/retrieval/bm25.go)对查询串去重并规范化,若剩余 token 为空则报错query must contain at least one letter or number——对应搜索框输入纯符号时的提示。
3.4 BM25 排序与分页游标
Search()(internal/historycatalog/catalog.go)构造history_fts MATCH ?查询,各 term 之间以OR连接,按bm25(history_fts)升序排序后联合history_documents、history_sources取回候选。排序键是稳定的 keyset:(rank, source_path, message_index, part_index, id),由SearchCursor承载,支持滚动分页(After游标参数),桌面端每批batchLimit = 200拉取后在内存层过滤再返回(desktop/history_search_collect.go)。
默认返回上限DefaultLimit = 50,硬上限MaxLimit = 200。
4. 增量更新、后台重建与并发模型
4.1 非阻塞索引提示
权威会话提交成功且文件锁释放后,保存路径只发送非阻塞、按 path 合并的索引提示。EnqueuePersist(root, event)根据SessionPersistEvent的Rewrite标志决定从何处追加:
- 普通 append:
appendFrom = event.AppendFrom,只读取 display index 的新增范围; - rewrite(重写):
appendFrom = -1,走全量重建。
enqueuePath对同一 path 的提示做合并:若已有排队项,取更早的appendFrom;queue通道容量默认 1024,队满时降级为把整个 root 标记为 dirty,由后台 reconcile 兜底(internal/historycatalog/catalog.go)。
4.2 连续 append 的增量路径
连续 append 时,indexPath先比对content_fingerprint、meta_fingerprint与content_digest:文件与 meta 未变则直接跳过(指纹廉价比对,避免无谓重解析);已变且具备追加条件(appendFrom >= 0且旧 health 非evicted)时,走tryAppendPath只补索引新增消息,避免整会话重载。
4.3 rewrite、通知丢失与外部写入:单 source 后台重建
出现以下情形时,单个 source 在后台整体重建:
- 会话日志被 rewrite(如格式 2 会话切换 head);
- 索引提示通知丢失;
- 外部进程直接写入权威文件(指纹不一致);
- 健康状态为
evicted的 source 无法追加,需全量重载。
重建前先将该 source 标记为health='stale'(隐藏过期 terms),随后在同一事务内删除旧 FTS 行与 documents、重插新行并bumprevision,保证"隐藏过期 + 原子替换"的可见性语义(internal/historycatalog/catalog.go 的indexPath)。
4.4 完整 transcript decoder 单并发与 checkpoint
完整 transcript decoder 始终单并发:Catalog内只有一个 worker goroutine 消费queue、rootCh、flushCh与定时 reconcile(ReconcileInterval默认 5 分钟),索引路径天然串行,避免多并发解码导致 CPU/内存尖峰。checkpoint 按 source 持久化在history_sources的seen_generation、content_revision、indexed_message_count等列,因此单个坏会话不会阻断其他历史:坏文件被标记health='corrupt'并记录last_error,其余 source 照常索引。
4.5 根目录指纹与缺失清理
reconcileRoot对根目录下所有会话 transcript 计算签名(路径 + size + mtime,含BranchMetaPath),签名未变且状态为ready时整轮扫描直接短路。会话被删除或移动后,经过MissingGrace(默认 30 秒)宽限期,超过missing_since截止的 FTS/document/source 行被清理,避免索引残留过期内容。
5. 格式 2 会话与 head 切换
格式 2 的会话日志(支持多 head 的日志格式)通过其选中 head 的派生 transcript建立索引。切换 head 会重写该派生 transcript,投影将其当作普通 rewrite 在后台重建,无需特殊逻辑。其他 head 在被设为当前之前不可搜索——索引只覆盖当前选中的 head 视图,这是"投影只反映当前权威状态"原则的自然延伸。
6. 运行态:内存覆盖而非 SQLite 持久化
open、running、current等运行态只从内存覆盖,SQLite 不保存也不恢复这些状态。原因很直接:运行态是易变的、进程相关的信息,若写入 SQLite 会造成陈旧所有权(如崩溃后残留 "running" 标记)。实现上:
- 桌面端
historySearchRootFilter与collectHistorySearchItems通过catalogRuntimeOverlays()获取内存运行态覆盖层; historyHitFromCandidate用historyStatusMatches(req.Status, overlay.open, current)过滤 open/current 状态(desktop/history_search_collect.go);Status结构中的Revision用于携带投影片段版本,前端据此判断结果是否过期(internal/historycatalog/types.go)。
7. 面向 provider 的稳定性契约
Agent 的history工具与 Desktop 历史管理器共享同一个投影(同一historycatalog.Catalog实例)。为保证两端的搜索结果一致,面向 provider 的工具名、描述、schema、默认值和顺序保持逐字节不变——desktop/host_command_owners.generated.json等生成契约用于约束这一稳定性,防止投影层升级意外改变 tool 协议对外暴露。
8. 可丢弃投影的数据库策略与降级
历史搜索 Catalog 遵循与 session catalog 相同的通用可丢弃投影策略(internal/projectiondb/projectiondb.go):
| 维度 | 策略 |
|---|---|
| journal 模式 | 本地盘使用WAL(PRAGMA journal_mode=WAL) |
| 同步级别 | PRAGMA synchronous=NORMAL |
| 外键 | PRAGMA foreign_keys=ON |
| busy timeout | 短超时busy_timeout=150ms(DSN 参数_pragma=busy_timeout%28150%29&_pragma=foreign_keys%281%29) |
| 权限 | 用户私有权限 |
| 远程/不可用缓存 | 自动退化为内存模式(PathLooksRemote或打开失败时ModeMemory) |
| 安全删除 | SecureDelete: true(覆盖删除敏感 token) |
| 自动 vacuum | AutoVacuum: true |
完整性或迁移失败只隔离并重建缓存:损坏的数据库被隔离(QuarantinedPath),状态进入degraded,重建替代库后发布。遇到未来 schema 时保留原库并进入 degraded,避免降级破坏未来版本的数据。
此外,投影有体积上限治理(internal/historycatalog/govern.go):默认DefaultMaxBytes = 256 << 20(256MB),可用用户配置history_search.max_mb覆盖(internal/config/history_search.go)。当磁盘索引超过上限 2 倍时,后台直接 wipe+rebuild(而非逐会话驱逐);否则按需驱逐到上限的 80%(evictTargetPercent),避免下一批持久化立即再次触发驱逐。被驱逐的 source 保持health='evicted',追加路径会退化为全量重载。
9. 首次索引未完成时的体验
搜索不会同步扫描目录。首次索引未完成时,Search立即返回已有结果,并通过Status().Pending > 0置位Partial标志,前端据此展示"部分结果 + 索引进行中"的明确进度(internal/historycatalog/catalog.go 的Search)。这与文档"首次索引未完成时立即返回已有结果和明确进度"一致,保证应用启动后立即可搜索,索引在后台渐进完成。
10. 诊断与安全重建命令
reasonix doctor catalogs [--json] reasonix catalogs reindex history [--dir PATH ...] [--json]10.1 doctor catalogs
doctorCatalogsCommand(internal/cli/catalogs.go)在 2 秒超时内对所有可丢弃投影执行projectiondb.Inspect:
- 覆盖
sessions与注册的所有 catalog 命令(history由 internal/cli/catalogs_history.go 注册); - 文本输出每项的状态(missing/ok/integrity/error)、schema 版本、大小与路径;
--json输出结构化诊断 JSON,便于脚本解析。
诊断不会输出 query、token、snippet、消息、tool arguments 或 provider 内容——只有路径、schema、大小、状态这类元信息,符合隐私与安全约束。
10.2 catalogs reindex history
reindexHistoryCatalog(internal/cli/catalogs_history.go):
--dir PATH可重复指定要重建的会话目录(Scope 为 global);缺省时对默认会话目录(含subagents子目录)与 archive 目录(config.ArchiveDir())全部重建;- 通过
historycatalog.Rebuild把每个权威 root 索引进一个校验过的兄弟数据库,成功后原子发布替换原库——重建期间原库仍可读,重建失败不影响原库; --json输出Status结构(state/mode/revision/indexed/total/pending/failed 等)。
reindex 只替换该可丢弃投影,不触碰任何权威会话文件;即使索引全毁,会话数据与旧版 Reasonix 兼容性均不受影响。
11. 关键文件索引
| 关注点 | 文件 |
|---|---|
| 投影默认路径、类型与搜索请求定义 | internal/historycatalog/types.go |
| schema、FTS5 contentless 表、tokenizer | internal/historycatalog/schema.go |
| 打开、注册 root、增量索引、重建、搜索、purge | internal/historycatalog/catalog.go |
| 体积治理、驱逐与上限 | internal/historycatalog/govern.go |
| reindex 原子重建流程 | internal/historycatalog/rebuild.go |
| token 化、QueryTerms、BM25 排序与 snippet | internal/retrieval/bm25.go |
| 通用投影数据库策略(WAL/降级/隔离) | internal/projectiondb/projectiondb.go |
用户配置history_search.max_mb | internal/config/history_search.go |
| doctor 命令与 reindex CLI 入口 | internal/cli/catalogs.go、internal/cli/catalogs_history.go |
| Desktop 搜索候选收集、snippet 与运行态过滤 | desktop/history_search_collect.go |
| 前端历史面板与目录桥接 | desktop/frontend/src/components/HistoryPanel.tsx、desktop/frontend/src/lib/historyCatalogBridge.ts |
12. 小结
Reasonix 历史搜索 Catalog 用一套"权威文件 + 可丢弃 FTS 投影"的架构解决了长会话历史检索的两难:会话数据永不因索引重建而丢失,搜索永远不扫描目录而是命中预构建的 contentless FTS5 索引。其核心工程取舍可总结为:
- 索引与正文分离:FTS 只存规范化 token,snippet/上下文按需从权威文件读取,索引体积可控;
- 异步增量 + 后台重建:非阻塞、按 path 合并的索引提示,连续 append 增量追加,rewrite/丢通知/外部写入由单并发 worker 后台重建,坏会话被隔离不阻断整体;
- 内存运行态:open/running/current 仅内存覆盖,SQLite 不持久化易变状态;
- 可丢弃与自愈:WAL/NORMAL/外键/短 busy timeout、远程缓存退化为内存、损坏隔离重建、未来 schema 进入 degraded 保留原库;
- 可运维:
reasonix doctor catalogs只读诊断、reasonix catalogs reindex history原子重建,均不触碰权威数据。
对需要理解"长历史下如何保证搜索可用性与数据安全性"的开发者而言,该模块是投影式索引设计的一个完整参考实现。
【免费下载链接】DeepSeek-ReasonixDeepSeek-native AI coding agent for your terminal. Engineered around prefix-cache stability — leave it running.项目地址: https://gitcode.com/GitHub_Trending/de/DeepSeek-Reasonix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考