Skip to content

fix(ai): 修复本地检索进度事件导致 ai.sqlite3 无限膨胀 - #162

Merged
2977094657 merged 2 commits into
LifeArchiveProject:mainfrom
C-Li:fix/ai-events-unbounded-growth
Sep 27, 2026
Merged

2977094657 merged 2 commits into
LifeArchiveProject:mainfrom
C-Li:fix/ai-events-unbounded-growth

Conversation

@C-Li

@C-Li C-Li commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

问题

启用本地检索并建立索引时,output/local_search/ai.sqlite3 会在任务运行期间持续写入。
本机实测约 2.9–3.5 行/秒,48 小时增长到了 144 GB(events 表 699,215 行,占 99.9%)。

根因是每次进度变化都向 events 表追加一份约 216 KB 的完整任务快照
(含 599 个群的 config / coverage / segments),既无 unique_key 去重,
也无 TTL 或条数保留策略;delivered 字段仅用于通知,进度事件永不清理。

以相邻两条记录 id=348397 / 348398 为例:31 个字段中 28 个完全相同,
单条 216 KB 里只有约 2 KB 是新信息,冗余率约 99%。

根因

  • LocalSearch.update() 每次都会调用 AIStore.event() 无条件 INSERT 新行。
  • events 表只增不减,没有 unique_key,也没有任何保留策略。
  • 该文件只存业务元数据(records = 任务配置/历史,events = 进度快照),
    真实数据在 databases/ 与 local_search/indexes/,因此进度快照完全可以丢弃。

改动

提交 说明
8312046 阻止进度事件无限增长
a52aeca 自动重建已膨胀的旧库
ad5820e 桌面与后端日志轮转

1. 8312046 阻止无限增长

  • 进度事件改用 replace=True 原地替换,每个任务只保留最新一行;
    INSERT OR REPLACE 会删除旧行并写入新行,自增 id 仍单调递增,
    依赖 Last-Event-ID 重连的 SSE 依然能收到最新进度。
  • 通知保持 INSERT OR IGNORE 去重语义,重复 key 不会重置 delivered,
    避免已确认的提醒被重新投递。
  • 进度事件剔除可由 records 重建的大字段
    (config / coverage / segments / read_starts),完整快照仍以
    records 为准;前端改为合并增量字段,不再整体替换任务对象。
  • 新增启动维护:折叠旧版遗留的重复快照、按 24 小时 TTL 回收事件、
    空闲页超过 64 MB 时才执行 VACUUM,并在后台线程对 summary 与 search
    两个 AI 库执行,避免大型遗留库拖慢启动。
  • 初始化不再对 events 同步建索引,避免在遗留巨型库上阻塞后端启动。

2. a52aeca 自动重建已膨胀的旧库

  • 库超过 512 MB 时改为重建:把 records 与未投递提醒复制到新库,
    丢弃可再生的 events;耗时与库体积无关,不会把 WAL 放大到库体积。
  • 先原子替换主库、成功后再清理旧 WAL/SHM;替换失败自动重试,仍失败则
    保留原库并告警,应用照常运行,下次启动再试。
  • 保留 sqlite_sequence,重建后事件 id 继续递增,SSE 不断档、前端无感。

影响与兼容性

  • 存量巨型库升级后首次启动自动收敛到几十 MB,且保留本地检索配置
    (records 中的 active 索引指针),无需重新整理/重新向量化。
  • 新库稳态极小;重建分支只在超阈值时触发,之后不再进入。
  • 用户数据不在该文件中:聊天与索引位于 databases/、local_search/indexes/,
    events 只是可再生产的过程快照。

测试

  • 新增 tests/test_ai_storage_retention.py:原地替换、非 replace 去重、
    遗留快照折叠、TTL、VACUUM、超大库重建。
  • 更新 tests/test_ai_message_pages.py:改为运行中捕获事件,验证“读取中实时
    进度”仍在,同时断言库内每个任务只剩一行且不含大字段。
  • 实测:合成的 117 MB 垃圾库重建为约 32 KB / 0.03s,records 与未投递提醒完好;
    相关 AI、前端、桌面用例通过。

复现

  1. 启用本地检索并选择群聊,触发索引任务;
  2. 监控 output/local_search/ai.sqlite3 与 -wal 的合计大小;
  3. 任务运行期间事件表持续追加,任务结束后仍不回收。

数据安全

删除该文件不影响任何用户数据。仅当在应用外手动删除 local_search/ai.sqlite3
时会丢失本地检索配置、需要重新整理;本 PR 的自动重建会保留配置与未投递提醒。

本地检索每次进度变化都会向 events 表追加一份约 216 KB 的完整任务快照,
且没有 unique_key、没有保留策略,任务运行期间以约 3 行/秒持续写入,
使 output/local_search/ai.sqlite3 增长到 144 GB。

- 进度事件改用 replace=True 原地替换,每个任务只保留最新一行;自增 id 仍
  递增,依赖 Last-Event-ID 重连的 SSE 依然能收到最新进度。
- 提醒类通知保留 INSERT OR IGNORE 去重语义,重复 key 不会重置 delivered,
  避免已确认的提醒被重新投递。
- 进度事件剔除可从 records 重建的大字段(config/coverage/segments/
  read_starts),完整快照仍以 records 表为准;前端改为合并增量字段,
  不再整体替换任务对象。
- 新增启动维护:折叠旧版遗留的重复快照、按 24 小时 TTL 回收事件、空闲页
  超过 64 MB 时执行 VACUUM,并在后台线程对 summary 与 search 两个 AI 库
  执行,避免大型遗留库拖慢启动;初始化不再同步建 events 索引,杜绝阻塞启动。
- 新增 tests/test_ai_storage_retention.py 覆盖原地替换、非 replace 去重、
  遗留快照折叠、TTL 与 VACUUM;tests/test_ai_message_pages.py 同步更新断言。
升级前已膨胀的库只靠 TTL 回收会残留很久,直接删除重建又会丢失本地检索配置
(records 中的 active 索引指针),导致需要重新整理/重新向量化。

- 库超过 512 MB 时改为重建:把 records 与未投递提醒复制到新库,
  丢弃可再生的 events;耗时与库体积无关。
- 先原子替换主库、成功后再清理旧 WAL/SHM;替换失败自动重试,
  仍失败则保留原库并告警,应用照常运行,下次启动再试。
- 保留 sqlite_sequence,重建后事件 id 继续递增,SSE 不断档、前端无感。
- maintain() 在超大库上走重建分支,不再原地删除/VACUUM。
- tests/test_ai_storage_retention.py 增加超大库重建用例。
@2977094657

Copy link
Copy Markdown
Member

正在查看相关问题

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants