@modusensus/dsh-mneme 0.8.13 → 0.8.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +655 -626
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +689 -689
  5. package/bin/dsh-mneme-mcp.mjs +510 -510
  6. package/bin/dsh-mneme-serve.mjs +151 -0
  7. package/cordis.patch.yml +21 -21
  8. package/docs/CONFIGURATION.md +263 -0
  9. package/docs/CONTINUITY.md +124 -0
  10. package/docs/DAEMON.md +46 -0
  11. package/docs/ENTITIES.md +245 -0
  12. package/docs/LOCAL_MODEL.md +222 -0
  13. package/docs/SEMANTIC.md +256 -0
  14. package/docs/SLEEP.md +164 -0
  15. package/docs/STORAGE.md +89 -0
  16. package/lib/api-standalone.js +673 -572
  17. package/lib/api.js +1437 -1433
  18. package/lib/bootstrap.js +211 -211
  19. package/lib/client.js +5444 -5295
  20. package/lib/commands.js +95 -95
  21. package/lib/config.js +880 -836
  22. package/lib/content-hash.js +54 -54
  23. package/lib/continuity.js +220 -220
  24. package/lib/document-index.js +94 -94
  25. package/lib/document.js +303 -303
  26. package/lib/dream/clustering.js +118 -118
  27. package/lib/dream/decisions.js +674 -674
  28. package/lib/dream/narratives.js +44 -44
  29. package/lib/dream/sleep.js +819 -819
  30. package/lib/dream.js +1501 -1501
  31. package/lib/embedding.js +177 -177
  32. package/lib/entities/extractor.js +252 -252
  33. package/lib/graph/anchoring.js +179 -179
  34. package/lib/guide.js +74 -74
  35. package/lib/heat.js +144 -144
  36. package/lib/hot-memory.js +46 -46
  37. package/lib/index.js +535 -671
  38. package/lib/inject.js +464 -450
  39. package/lib/lang.js +248 -229
  40. package/lib/local-embedder.js +358 -358
  41. package/lib/maintenance.js +142 -142
  42. package/lib/mirror.js +330 -330
  43. package/lib/organize.js +450 -450
  44. package/lib/peak-hours.js +117 -117
  45. package/lib/placement.js +13 -0
  46. package/lib/pre-inject-gate.js +340 -0
  47. package/lib/quality-filter.js +126 -126
  48. package/lib/recall-stats.js +175 -175
  49. package/lib/reranker.js +269 -269
  50. package/lib/runtime/adopt.js +176 -176
  51. package/lib/runtime/closure.js +172 -172
  52. package/lib/runtime/download.js +247 -247
  53. package/lib/runtime/layout.js +265 -265
  54. package/lib/runtime/loader.js +252 -252
  55. package/lib/runtime/model-fetch.js +361 -361
  56. package/lib/runtime/provision.js +237 -237
  57. package/lib/runtime/tarball.js +130 -130
  58. package/lib/runtime/verify.js +223 -204
  59. package/lib/scope.js +146 -146
  60. package/lib/search/adaptive.js +60 -60
  61. package/lib/search/bm25.js +96 -96
  62. package/lib/semantic.js +229 -0
  63. package/lib/sensitive-scan.js +209 -209
  64. package/lib/serve.js +215 -0
  65. package/lib/service.js +2663 -2596
  66. package/lib/settings.js +517 -509
  67. package/lib/store.js +3007 -3007
  68. package/lib/summarize.js +891 -856
  69. package/lib/tools.js +743 -737
  70. package/lib/vector-index.js +110 -110
  71. package/lib/version-check.js +122 -122
  72. package/lib/write-admission.js +402 -402
  73. package/locale/en.json +6 -6
  74. package/locale/zh.json +6 -6
  75. package/package.json +8 -4
  76. package/runtime-manifest.json +526 -526
  77. package/scripts/benchmark-embed.js +201 -201
  78. package/scripts/benchmark-recall.js +249 -249
  79. package/scripts/benchmark-rerank.js +166 -166
  80. package/scripts/build-runtime-manifest.mjs +224 -224
  81. package/scripts/changelog-prep.mjs +36 -0
  82. package/scripts/check-sync.js +42 -42
  83. package/scripts/e2e-dsh.js +220 -220
  84. package/scripts/mneme-runtime.mjs +277 -277
  85. package/scripts/stress-dsh.js +255 -255
  86. package/scripts/sync-lib.js +47 -47
  87. package/scripts/sync-test-badge.mjs +69 -69
  88. package/scripts/test-count-sync.mjs +44 -44
  89. package/src/api-standalone.js +673 -572
  90. package/src/api.js +1437 -1433
  91. package/src/bootstrap.js +211 -211
  92. package/src/commands.js +95 -95
  93. package/src/config.js +880 -836
  94. package/src/content-hash.js +54 -54
  95. package/src/continuity.js +220 -220
  96. package/src/document-index.js +94 -94
  97. package/src/document.js +303 -303
  98. package/src/dream/clustering.js +118 -118
  99. package/src/dream/decisions.js +674 -674
  100. package/src/dream/narratives.js +44 -44
  101. package/src/dream/sleep.js +819 -819
  102. package/src/dream.js +1501 -1501
  103. package/src/embedding.js +177 -177
  104. package/src/entities/extractor.js +252 -252
  105. package/src/graph/anchoring.js +179 -179
  106. package/src/guide.js +74 -74
  107. package/src/heat.js +144 -144
  108. package/src/hot-memory.js +46 -46
  109. package/src/index.js +535 -671
  110. package/src/inject.js +464 -450
  111. package/src/lang.js +248 -229
  112. package/src/local-embedder.js +358 -358
  113. package/src/maintenance.js +142 -142
  114. package/src/mirror.js +330 -330
  115. package/src/organize.js +450 -450
  116. package/src/peak-hours.js +117 -117
  117. package/src/placement.js +13 -0
  118. package/src/pre-inject-gate.js +340 -0
  119. package/src/quality-filter.js +126 -126
  120. package/src/recall-stats.js +175 -175
  121. package/src/reranker.js +269 -269
  122. package/src/runtime/adopt.js +176 -176
  123. package/src/runtime/closure.js +172 -172
  124. package/src/runtime/download.js +247 -247
  125. package/src/runtime/layout.js +265 -265
  126. package/src/runtime/loader.js +252 -252
  127. package/src/runtime/model-fetch.js +361 -361
  128. package/src/runtime/provision.js +237 -237
  129. package/src/runtime/tarball.js +130 -130
  130. package/src/runtime/verify.js +223 -204
  131. package/src/scope.js +146 -146
  132. package/src/search/adaptive.js +60 -60
  133. package/src/search/bm25.js +96 -96
  134. package/src/semantic.js +229 -0
  135. package/src/sensitive-scan.js +209 -209
  136. package/src/serve.js +215 -0
  137. package/src/service.js +2663 -2596
  138. package/src/settings.js +517 -509
  139. package/src/store.js +3007 -3007
  140. package/src/summarize.js +891 -856
  141. package/src/tools.js +743 -737
  142. package/src/vector-index.js +110 -110
  143. package/src/version-check.js +122 -122
  144. package/src/write-admission.js +402 -402
  145. package/test/adaptive-inject-budget.test.js +85 -85
  146. package/test/api-routes-gaps.test.js +218 -218
  147. package/test/api.test.js +1702 -1688
  148. package/test/audit.test.js +448 -448
  149. package/test/baseline-persist.test.js +248 -248
  150. package/test/benchmark.test.js +94 -94
  151. package/test/bootstrap.test.js +220 -220
  152. package/test/changelog-prep.test.js +60 -0
  153. package/test/client.test.js +1230 -1169
  154. package/test/clustering.test.js +100 -100
  155. package/test/commands.test.js +106 -106
  156. package/test/config.test.js +66 -66
  157. package/test/conflict-freeze.test.js +300 -300
  158. package/test/content-hash.test.js +204 -204
  159. package/test/continuity.test.js +333 -333
  160. package/test/document-dir.test.js +233 -233
  161. package/test/document.test.js +671 -671
  162. package/test/dream-archive-guardrail.test.js +116 -116
  163. package/test/dream-candidate-hybrid.test.js +281 -281
  164. package/test/dream-coverage-degrade.test.js +86 -86
  165. package/test/dream-failure-backoff.test.js +173 -173
  166. package/test/dream-peak-hours.test.js +194 -194
  167. package/test/dream.test.js +1385 -1383
  168. package/test/embedding-ready.test.js +196 -196
  169. package/test/entities.test.js +627 -547
  170. package/test/entity-recall.test.js +179 -179
  171. package/test/epistemic.test.js +325 -325
  172. package/test/fnew-0112.test.js +311 -311
  173. package/test/fnew-03.test.js +422 -422
  174. package/test/graph-anchoring.test.js +184 -184
  175. package/test/graph-api.test.js +175 -175
  176. package/test/graph-inject-budget.test.js +103 -103
  177. package/test/graph-passive-confirm.test.js +91 -91
  178. package/test/graph-weight-evolution.test.js +104 -104
  179. package/test/heat.test.js +175 -175
  180. package/test/helpers/dream-mock.js +82 -82
  181. package/test/helpers/peer-worker.mjs +25 -25
  182. package/test/helpers/runtime-source.js +60 -60
  183. package/test/helpers/tar-builder.js +80 -80
  184. package/test/helpers/write-admission-samples.js +308 -308
  185. package/test/hot-memory.test.js +145 -145
  186. package/test/inject-content-cap.test.js +91 -91
  187. package/test/inject-first-round-bm25.test.js +89 -89
  188. package/test/inject-heat.test.js +77 -61
  189. package/test/inject-parent-gate.test.js +106 -106
  190. package/test/inject-pools.test.js +194 -194
  191. package/test/inject-recall.test.js +105 -105
  192. package/test/inject-rotation.test.js +161 -161
  193. package/test/inject.test.js +420 -420
  194. package/test/lang.test.js +87 -87
  195. package/test/lastrunat-persist.test.js +101 -101
  196. package/test/lib-smoke.test.js +96 -96
  197. package/test/llm-audit.test.js +598 -598
  198. package/test/llm-message-source.test.js +207 -207
  199. package/test/local-embedder.test.js +292 -292
  200. package/test/maintenance.test.js +391 -391
  201. package/test/mcp-stdio.test.js +385 -385
  202. package/test/mirror-dirty.test.js +424 -424
  203. package/test/mirror-edit-digest.test.js +187 -187
  204. package/test/mirror-generation.test.js +499 -499
  205. package/test/mirror-types.test.js +208 -208
  206. package/test/mirror.test.js +249 -249
  207. package/test/narrative-bars.test.js +162 -162
  208. package/test/organize.test.js +418 -418
  209. package/test/peer-blockers.test.js +193 -193
  210. package/test/placement.test.js +36 -0
  211. package/test/policy-epoch.test.js +259 -259
  212. package/test/pre-inject-gate.test.js +434 -0
  213. package/test/quality-filter.test.js +186 -186
  214. package/test/reasoning-effort.test.js +752 -752
  215. package/test/recall-anti-update.test.js +82 -82
  216. package/test/recall-evals.test.js +253 -253
  217. package/test/recall-layer.test.js +326 -326
  218. package/test/recall-stats.test.js +189 -189
  219. package/test/receipt-chain.test.js +451 -451
  220. package/test/reflection.test.js +226 -226
  221. package/test/reindex-backfill.test.js +62 -62
  222. package/test/reranker-single-logit.test.js +73 -73
  223. package/test/reranker.test.js +197 -197
  224. package/test/resident-status.test.js +83 -83
  225. package/test/runtime-adopt.test.js +206 -206
  226. package/test/runtime-cli.test.js +89 -89
  227. package/test/runtime-closure.test.js +193 -193
  228. package/test/runtime-download.test.js +302 -302
  229. package/test/runtime-layout.test.js +163 -163
  230. package/test/runtime-loader.test.js +255 -255
  231. package/test/runtime-manifest.test.js +159 -159
  232. package/test/runtime-model-fetch.test.js +348 -348
  233. package/test/runtime-provision.test.js +101 -101
  234. package/test/runtime-tarball.test.js +81 -81
  235. package/test/runtime-verify.test.js +319 -279
  236. package/test/scope-candidates.test.js +239 -239
  237. package/test/scope-explicit.test.js +452 -452
  238. package/test/scope-inject-soft.test.js +106 -106
  239. package/test/scope-retrieval.test.js +197 -197
  240. package/test/scope-storage.test.js +255 -255
  241. package/test/scope-strict.test.js +367 -343
  242. package/test/search-fusion.test.js +90 -90
  243. package/test/semantic.test.js +124 -124
  244. package/test/sensitive-scan.test.js +207 -207
  245. package/test/serve-bin.test.js +121 -0
  246. package/test/serve.test.js +196 -0
  247. package/test/service-search.test.js +174 -174
  248. package/test/service.test.js +405 -405
  249. package/test/settings.test.js +261 -261
  250. package/test/skip-invalid-restore.test.js +262 -262
  251. package/test/sleep-action-set.test.js +422 -422
  252. package/test/sleep-heat.test.js +126 -126
  253. package/test/sleep-scheduler-rearm.test.js +89 -89
  254. package/test/sleep.test.js +527 -527
  255. package/test/standalone-api.test.js +751 -544
  256. package/test/store-vector-cache.test.js +98 -98
  257. package/test/store.test.js +396 -396
  258. package/test/stress.test.js +209 -209
  259. package/test/summarize-occurred-at.test.js +127 -0
  260. package/test/summarize-reasoning-effort.test.js +189 -189
  261. package/test/summarize.test.js +1465 -1465
  262. package/test/supersede-state-tracking.test.js +394 -0
  263. package/test/test-count-sync.test.js +60 -60
  264. package/test/tools-dto-schema-matrix.test.js +325 -325
  265. package/test/tools-memory-get-schema.test.js +65 -65
  266. package/test/tools.test.js +428 -374
  267. package/test/updated-at-semantics.test.js +113 -113
  268. package/test/vector-index.test.js +221 -221
  269. package/test/version-check.test.js +152 -152
  270. package/test/webServer-optional.test.js +82 -82
  271. package/test/write-admission.test.js +668 -668
@@ -0,0 +1,256 @@
1
+ # dsh-mneme 语义增强架构 — 设计文档
2
+
3
+ - **日期**:2026-08-15
4
+ - **状态**:✅ 已实现(本地 Embedding / Rerank / 聚类随 v0.2+ 落地;本文档转为历史设计存档,当前能力以 [README](../README.md) 正文为准)
5
+ - **范围**:为 dsh-mneme 增加"完全离线的语义记忆引擎",覆盖本地 Embedding、Rerank 精排、autoDream 向量聚类三个阶段
6
+ - **相关文档**:[本地模型部署指南](LOCAL_MODEL.md) · [从 v0.1 升级说明](MIGRATION.md)
7
+
8
+ ## 1. 背景与目标
9
+
10
+ dsh-mneme v0.1 已具备**可选的向量搜索**:通过 `/api/dsh-mneme/vector-config` 配置 OpenAI 兼容 `/embeddings` 端点,命中"字面不同但语义相近"的记忆;未配置或失败时降级为 LIKE 子串搜索。
11
+
12
+ v0.2 的目标是把这条**可选、依赖外部 API** 的向量链路升级为**默认可用、完全离线**的语义记忆引擎:
13
+
14
+ 1. **本地 Embedding**:ONNX(`Xenova/bge-small-zh-v1.5`)或 Ollama,记忆向量不再依赖外网 API
15
+ 2. **Rerank 精排**:向量初筛后交叉编码精排,把 Top-K 准确率再抬一档
16
+ 3. **autoDream 语义增强**:对记忆向量做聚类,让自动巩固(去重/合并/冲突裁决)从"主题相近"升级为"语义相近"
17
+
18
+ ## 2. 总体架构
19
+
20
+ ```
21
+ ┌──────────────────────────────────────────────────────────────┐
22
+ │ 语义引擎(v0.2,完全离线可选) │
23
+ ├──────────────────────────────────────────────────────────────┤
24
+ │ ① Embedder 层(三选一,统一接口,可自动降级) │
25
+ │ LocalEmbedder(ONNX) │ OllamaEmbedder │ OpenAIEmbedder │
26
+ │ init() / embed(texts) / embedSingle() / dimension / hash │
27
+ ├──────────────────────────────────────────────────────────────┤
28
+ │ ② VectorIndex 层 │
29
+ │ SQLite memories.embedding 列(JSON 向量) │
30
+ │ + 余弦相似度检索(score 0..1)+ 缺失向量增量补建 reindex │
31
+ ├──────────────────────────────────────────────────────────────┤
32
+ │ ③ Rerank 层(可选) │
33
+ │ LocalReranker(Xenova/bge-reranker-base,交叉编码) │
34
+ │ 召回候选 → 精排 → Top-K │
35
+ ├──────────────────────────────────────────────────────────────┤
36
+ │ ④ autoDream 聚类(可选) │
37
+ │ clusterMemories(向量 K-Means) / findPotentialConflicts │
38
+ │ 语义分组 + 疑似矛盾检测 → 巩固决策更精准 │
39
+ └──────────────────────────────────────────────────────────────┘
40
+ ```
41
+
42
+ - **① Embedder**:`src/local-embedder.js`,三后端统一接口,`createEmbedderByProvider(provider, opts)` 按名创建
43
+ - **② VectorIndex**:复用 v0.1 的 `memories.embedding` 列与 `store.js` 的余弦检索,无新存储层
44
+ - **③ Rerank**:`src/reranker.js`(Phase 2),可选开启
45
+ - **④ 聚类**:`src/dream/clustering.js`(Phase 3),autoDream 调度器集成
46
+
47
+ ### 目录结构(新增/变化部分)
48
+
49
+ ```
50
+ src/
51
+ ├── local-embedder.js # [新增] Local/Ollama/OpenAI 三 Embedder + createEmbedderByProvider
52
+ ├── reranker.js # [新增·Phase2] LocalReranker 交叉编码精排
53
+ ├── dream/
54
+ │ └── clustering.js # [新增·Phase3] clusterMemories / findPotentialConflicts / kMeans
55
+ └── embedding.js # [保留] v0.1 OpenAI 兼容客户端(作为降级路径之一复用)
56
+ scripts/
57
+ ├── benchmark-embed.js # [新增] Embedding 吞吐量基准
58
+ └── benchmark-rerank.js # [新增] Rerank 延迟基准
59
+ docs/
60
+ ├── SEMANTIC.md # 本文档
61
+ ├── LOCAL_MODEL.md # 本地模型部署指南
62
+ └── MIGRATION.md # 从 v0.1 升级说明
63
+ ```
64
+
65
+ ## 3. 搜索流水线
66
+
67
+ `memory_search` 在 v0.2 的完整链路(对应 `src/tools.js` 的 `mode: auto | keyword | vector | hybrid`):
68
+
69
+ ```
70
+ query
71
+ │
72
+ ┌───────────┴───────────┐
73
+ ▼ ▼
74
+ keyword 召回 vector 召回
75
+ (LIKE 子串 title/ (Embedder.embed(query)
76
+ content/tags) → 余弦相似度 top-N
77
+ → 缺失向量自动补建)
78
+ │ │
79
+ └───── 混合融合 ─────────┘
80
+ (auto: 关键词命中优先 + 向量补足;分数归一化合并)
81
+ │
82
+ ▼
83
+ [Rerank 开启?]
84
+ ├─ 是 → LocalReranker.rerank(query, candidates, topK)
85
+ │ 交叉编码精排,取 topK
86
+ └─ 否 → 按 importance/score 直接截取 topK
87
+ │
88
+ ▼
89
+ memory_search 结果
90
+ ```
91
+
92
+ - **召回阶段(候选)**:混合召回,宁多勿漏,候选集通常取向量 top-N(默认远超 Top-K,供精排筛选)
93
+ - **精排阶段(Rerank)**:对候选逐条打分(query 与候选的交互编码),取 Top-K
94
+ - **结果阶段**:返回条目 + `score`(召回分/精排分)+ `source` 时间戳,与 v0.1 返回结构保持一致
95
+
96
+ ## 4. 模块接口
97
+
98
+ ### 4.1 Embedder 统一接口(`src/local-embedder.js`)
99
+
100
+ 三个后端共享同一接口,方法失败**抛错**,由上层编排降级链(与 v0.1 `embedding.js` 的"失败返回 null"不同)。
101
+
102
+ | 成员 | 说明 |
103
+ |------|------|
104
+ | `init()` | 加载模型 / 探测服务,失败抛错 |
105
+ | `embed(texts: string[])` | 批量嵌入,返回 `number[][]`(本地模型 mean pooling + L2 归一化) |
106
+ | `embedSingle(text)` | 单条嵌入 → `number[]` |
107
+ | `dimension` | 向量维度(getter) |
108
+ | `modelHash` | 模型指纹(`<model>#<hash>`),用于向量索引一致性校验,模型换名/换维度时提示重建索引 |
109
+ | `dispose()` | 释放模型资源 |
110
+
111
+ **LocalEmbedder(ONNX)**
112
+
113
+ ```js
114
+ new LocalEmbedder({
115
+ model: "Xenova/bge-small-zh-v1.5", // 默认;中文优化,全离线
116
+ dimension: 512, // 默认与模型匹配
117
+ device: "cpu", // cpu | wasm | gpu(onnxruntime 后端)
118
+ batchSize: 8, // 分批嵌入,控制峰值内存
119
+ cacheDir: "", // 自定义模型缓存目录,空则用 HF 默认缓存
120
+ useDtype: "q8", // 量化精度 q8/fp32/fp16
121
+ logger: null
122
+ })
123
+ ```
124
+
125
+ **OllamaEmbedder**
126
+
127
+ ```js
128
+ new OllamaEmbedder({
129
+ baseUrl: "http://localhost:11434", // 默认
130
+ model: "nomic-embed-text", // 默认;维度从首次响应自动推断
131
+ logger: null
132
+ })
133
+ ```
134
+
135
+ **OpenAIEmbedder(兼容 v0.1 行为)**
136
+
137
+ ```js
138
+ new OpenAIEmbedder({
139
+ baseUrl: "", // 例如 https://api.openai.com/v1,也支持 SiliconFlow/智谱/Ollama 代理
140
+ apiKey: "",
141
+ model: "",
142
+ timeoutMs: 15000
143
+ })
144
+ ```
145
+
146
+ **工厂**
147
+
148
+ ```js
149
+ import { createEmbedderByProvider } from "../src/local-embedder.js";
150
+ const embedder = createEmbedderByProvider("local", { device: "cpu" }); // local | ollama | openai
151
+ await embedder.init();
152
+ const vecs = await embedder.embed(["你好", "记忆库"]);
153
+ ```
154
+
155
+ ### 4.2 Reranker(`src/reranker.js`,Phase 2)
156
+
157
+ | 成员 | 说明 |
158
+ |------|------|
159
+ | `constructor({ model, device, batchSize, cacheDir, logger })` | `model` 默认 `Xenova/bge-reranker-base` |
160
+ | `init()` | 加载交叉编码模型 |
161
+ | `rerank(query, candidates, topK?)` | 对候选逐条打分,返回按相关性降序的候选(附 `score`),`topK` 缺省返回全量排序 |
162
+
163
+ 示例:
164
+
165
+ ```js
166
+ import { LocalReranker } from "../src/reranker.js";
167
+ const reranker = new LocalReranker({ model: "Xenova/bge-reranker-base" });
168
+ await reranker.init();
169
+ const top = await reranker.rerank("怎么部署本地模型", candidates, 5);
170
+ ```
171
+
172
+ ### 4.3 autoDream 聚类(`src/dream/clustering.js`,Phase 3,已实现)
173
+
174
+ | 成员 | 说明 |
175
+ |------|------|
176
+ | `cosineSimilarity(a, b)` | 余弦相似度,零向量/长度不齐返回 0,数值稳定 |
177
+ | `kMeans(vectors, k, opts)` | K-Means(k-means++ 播种 + 空簇修复),返回每簇索引数组 |
178
+ | `clusterMemories(memories, vectors, k)` | 按向量把记忆分组,返回簇(保留原对象引用) |
179
+ | `findPotentialConflicts(memories, vectors, threshold = 0.85)` | 同类型、高相似(可能矛盾)的记忆对 |
180
+
181
+ autoDream 调度器在触发巩固时:对候选记忆做 `clusterMemories` 分组 → 每组送 LLM 决策(merge/archive/conflict),并用 `findPotentialConflicts` 预先标出疑似矛盾对,减少漏判。
182
+
183
+ ## 5. 降级策略
184
+
185
+ ```
186
+ 首选 回退 1 回退 2 兜底
187
+ LocalEmbedder(ONNX) ──失败──► OllamaEmbedder ──失败──► OpenAIEmbedder ──失败/未配置──► 关键词 LIKE
188
+ ```
189
+
190
+ - Embedder 方法**抛错**,由上层 `createEmbedderByProvider` 编排的降级链逐级捕获切换;全链路失败则回退到 v0.1 的 LIKE 子串关键词搜索(本库无 FTS5,关键词召回即 LIKE 子串扫描)
191
+ - **原则**:语义链路任何一环失败都**静默降级、不阻断记忆读写**——写入照常落库,只是当次不补向量
192
+ - **降级是每请求动态的**:同一会话内 ONNX 偶发失败,下一次请求自动重试首选路径,不固化到降级档
193
+ - **索引一致性**:Embedder 的 `modelHash` 变化(换模型/换维度)时,旧向量与新向量不可混算余弦,需触发「重建索引」(复用 v0.1 的 `reindexMissing` 补建缺失向量)
194
+
195
+ ## 6. 模型清单
196
+
197
+ | 用途 | 模型 | 后端 | 维度 | 说明 |
198
+ |------|------|------|------|------|
199
+ | Embedding | `Xenova/bge-small-zh-v1.5` | ONNX(默认) | 512 | 中文优化,全离线,体积小 |
200
+ | Embedding | `bge-m3` | Ollama | 1024 | 多语言(中/英/日等),质量高 |
201
+ | Embedding | `nomic-embed-text` | Ollama | 768 | 英文为主,通用 |
202
+ | Embedding | `text-embedding-3-small` | OpenAI 兼容 | 1536 | v0.1 云端默认,升级后仍可用 |
203
+ | Rerank | `Xenova/bge-reranker-base` | ONNX(Phase 2) | — | 交叉编码,首候选精排 |
204
+ | Rerank | `bge-reranker-v2-m3` | 云/本地 | — | 更强精排,多语言 |
205
+
206
+ > 维度随模型而定:换用不同维度的模型后,已存向量与模型指纹不匹配,需重建索引。
207
+
208
+ ## 7. 配置新增项
209
+
210
+ 见 [MIGRATION.md](MIGRATION.md) 完整表格。核心:`embedProvider`(`local` / `ollama` / `openai`,默认 `openai` 保持 v0.1 行为)、`localEmbedModel`、`localEmbedDimension`、`localEmbedDevice`、`localEmbedBatchSize`、`ollamaBaseUrl`、`ollamaModel`、`embedModelCacheDir`、`rerankEnabled`、`rerankModel`。
211
+
212
+ ## 8. 测试与基准
213
+
214
+ - 单元:`src/local-embedder.js` 三后端接口一致性、`clustering.js` 聚类正确性(node:test)
215
+ - 降级链:mock 各后端抛错,断言逐级回退到 LIKE
216
+ - 基准:`scripts/benchmark-embed.js`(嵌入延迟/吞吐)、`scripts/benchmark-rerank.js`(Rerank 延迟 vs 候选规模)
217
+
218
+ ## 9. 反思更新(v0.2.1 `update` 决策 + 失败追踪)
219
+
220
+ ### 9.1 `update` 决策类型
221
+
222
+ autoDream 整理时,LLM 可输出第 5 种决策 `update`,直接修正单条记忆的过时/错误内容:
223
+
224
+ - **约束**:`ids` 只能含一个 id;必须产生实际字段变化;不能更新 `summary`;新建 < 24h 的记忆不可 update
225
+ - **频率限制**:每次 autoDream 最多 `reflectionUpdateMaxPerRun`(默认 2)个 update,超限整单拒绝
226
+ - **幂等**:字段已与目标一致时跳过(可重放无副作用)
227
+ - **审计**:`dream_runs` 记录 update 的 `_before` 快照,可回溯变更前内容
228
+ - **向量同步**:update 后删除旧向量、按新内容重嵌入,保持索引一致
229
+
230
+ ### 9.2 失败追踪(failure_memories)
231
+
232
+ `failure_memories` 表记录记忆纠正/失败事件,为后续反思进化积累数据:
233
+
234
+ ```sql
235
+ CREATE TABLE failure_memories (
236
+ id TEXT PRIMARY KEY,
237
+ query TEXT, -- 用户原始查询/意图(可选)
238
+ expected TEXT, -- 期望结果
239
+ actual TEXT, -- 实际结果
240
+ failure_type TEXT, -- outdated / miss / wrong / user_correction
241
+ memory_id TEXT, -- 关联的记忆 id
242
+ created_at TEXT NOT NULL
243
+ );
244
+ ```
245
+
246
+ - **触发**:用户调用 `memory_update` 且内容变化时,若 `reflectionFailureTracking` 开启则记录 `user_correction` 行(actual=旧值、expected=新值)
247
+ - **查询**:`store.listFailures`(过滤/分页)、`store.getFailureStats`(按类型计数)
248
+
249
+ ### 9.3 配置项
250
+
251
+ ```javascript
252
+ reflectionUpdateEnabled: true, // update 决策总开关
253
+ reflectionFailureTracking: true, // 失败追踪总开关
254
+ reflectionUpdateMaxPerRun: 2, // 每次整理最多 update 数
255
+ reflectionUpdateMinAgeHours: 24 // 新建记忆保护期(小时)
256
+ ```
package/docs/SLEEP.md ADDED
@@ -0,0 +1,164 @@
1
+ # dsh-mneme Sleep Mode 系统级睡眠设计文档
2
+
3
+ ## 概述
4
+ 从被动整理到主动维护:在系统空闲时段自动执行深度记忆压缩、冲突消解、模式发现与关系补全,实现知识库的长效健康治理。
5
+
6
+ ## 动机与设计原则
7
+ - **非侵入性**:仅在用户空闲(Idle)时触发,不干扰正常读写与实时推理。
8
+ - **可中断**:用户一旦产生活动,立即中止当前睡眠周期,保障实时体验。
9
+ - **分层压缩**:按访问热度与时间衰减进行阶梯式降级,保留核心语义,释放存储压力。
10
+ - **模式发现**:利用 LLM 挖掘长期记忆中的隐性规律,生成结构化 Pattern 实体。
11
+ - **审计延续**:与 `autoDream` 共享 `dream_runs` 审计表,通过 `run_type` 区分,保障全链路可追溯。
12
+
13
+ ## 架构图
14
+ ```mermaid
15
+ flowchart TD
16
+ User[用户写入/读取] -->|noteWrite| Scheduler[SleepScheduler]
17
+ Scheduler -->|clearTimeout + 重置计时器| Timer[Idle Timer]
18
+ Timer -->|>= sleepIdleMinutes| Check[maybeSchedule]
19
+
20
+ Check -->|条件满足| Queue[service.enqueue]
21
+ Check -->|条件不满足| Wait[等待下一次 noteWrite]
22
+
23
+ subgraph Sleep Run [串行执行]
24
+ Q[Queue] --> P1[Phase 1: conflict_resolution]
25
+ P1 --> P2[Phase 2: archival_demotion]
26
+ P2 --> P3[Phase 3: pattern_discovery]
27
+ P3 --> P4[Phase 4: relation_completion]
28
+ end
29
+
30
+ P4 -->|独立 try/catch 隔离| Receipt[buildReceipt]
31
+ Receipt -->|saveDreamRun run_type='sleep'| Audit[(dream_runs)]
32
+
33
+ User -->|活动触发| Abort[AbortController.abort]
34
+ Abort -->|中断当前 Run| Scheduler
35
+
36
+ AutoDream[autoDream 实时轻量触发] -.共享队列不重叠.-> Queue
37
+ ```
38
+
39
+ ## 配置项说明
40
+ | 配置项 | 默认值 | 说明 |
41
+ |:---|:---|:---|
42
+ | `sleepModeEnabled` | `false` | 全局开关(Opt-in),开启后激活调度器 |
43
+ | `sleepIdleMinutes` | `5` | 连续无操作触发睡眠的阈值(分钟) |
44
+ | `sleepMinIntervalHours` | `8` | 两次 Sleep Run 的最小时间间隔(小时) |
45
+ | `sleepConflictStrictness` | `'normal'` | 冲突消解严格度:`'gentle'`(0.92) / `'normal'`(0.85) / `'aggressive'`(0.75) |
46
+ | `sleepArchiveDays` | `30` | 进入冷存储/摘要降级的天数阈值 |
47
+ | `sleepCompressDays` | `90` | 进入完全归档(Archive)的天数阈值 |
48
+ | `sleepPatternMinMemories` | `100` | 模式发现阶段扫描的最近记忆条数下限 |
49
+ | `sleepPatternLookbackDays` | `30` | 模式发现回溯的时间窗口(天) |
50
+ | `sleepMaxPatternPerRun` | `3` | 单次运行最多创建的模式数量上限 |
51
+ | `sleepProvider` | `''` | 睡眠模式专用 LLM Provider(留空则复用默认) |
52
+ | `sleepModel` | `''` | 睡眠模式专用 LLM Model(留空则复用默认) |
53
+ | `sleepReasoningEffort` | (未配置) | 睡眠 LLM 推理强度透传:`off` / `low` / `medium` / `high` / `none`。未配置 = 自动取模型支持的最低档(#135:避免思考模型用自带默认档烧光预算返回空体);显式 `none` = 不传该字段,使用模型默认(与 `dreamReasoningEffort` 语义一致) |
54
+ | `sleepMaxTokens` | `8192` | 冲突消解与模式发现阶段的 LLM 输出预算上限(token,#257:原硬编码 2048——`sleepActionSet: full` 实测 24 对需约 7000,截断即整轮裁决失败)。流式计费按实际用量,调大不增加成本 |
55
+
56
+ ## 四阶段详解
57
+ 每个阶段独立包裹 `try/catch`,任一阶段失败仅跳过该阶段,不阻塞后续流程。
58
+
59
+ ### 1. `conflict_resolution`(冲突消解)
60
+ - **目标**:清理向量空间中的重复、矛盾或过时记忆。
61
+ - **输入**:全量/增量记忆向量索引、`sleepConflictStrictness` 阈值。
62
+ - **算法**:
63
+ 1. 向量检索 `findPotentialConflicts` 获取高相似度对。
64
+ 2. LLM 仲裁:基于严格度阈值生成 `keep/merge/discard` 决策。
65
+ 3. `validateDecisions`:严格校验决策合法性,缺省策略为 `keep`。
66
+ 4. `applyDecisions`:批量执行状态变更。
67
+ - **输出**:冲突消解报告(决策明细)。
68
+ - **验收**:无合法记忆被误删;仲裁结果符合配置的严格度阈值;决策可回滚。
69
+
70
+ ### 2. `archival_demotion`(归档降级)
71
+ - **目标**:按时间衰减对记忆进行分层压缩,释放热存储压力。
72
+ - **输入**:`last_accessed_at` 排序的记忆列表、`sleepArchiveDays`、`sleepCompressDays`。
73
+ - **算法**(纯规则、无 LLM,确定性执行):
74
+ - `>= sleepCompressDays`:调用 `setArchived` 完全归档。
75
+ - `>= sleepArchiveDays && < sleepCompressDays`:调用 `demoteToSummary(m.id, 摘要, {minRefTimeMs: archiveCut})` 转为摘要态——原内容存入 `_full_content`,摘要由前 120 字截断生成(LLM 未配置时的 fallback)。
76
+ - `< sleepArchiveDays`:保持原状不动。
77
+ - **minRefTimeMs 关键点**:传入快照时刻 `archiveCut`,`demoteToSummary` 在事务内复查 `last_accessed_at`——快照后被召回 touch 的记忆视为"重新活跃",跳过不降级。
78
+ - **输出**:降级操作清单与状态快照。
79
+ - **验收**:严格遵循分层阈值;`minRefTimeMs` 机制生效,防止快照后 `touch` 导致的误降级;摘要保留 `_full_content` 可无损恢复。
80
+
81
+ ### 3. `pattern_discovery`(模式发现)
82
+ - **目标**:从近期记忆中提炼可复用的隐性规律或知识模式。
83
+ - **输入**:最近 `sleepPatternLookbackDays` 内的 `sleepPatternMinMemories` 条记忆。
84
+ - **算法**:
85
+ 1. LLM 扫描分析,提取候选模式。
86
+ 2. **证据校验**:强制 `evidence` 列表与数据库真实 ID 求交集(`intersect`),过滤伪造/幻觉引用。
87
+ 3. 输出 `type=pattern` 创建指令,受 `sleepMaxPatternPerRun` 限制。
88
+ - **输出**:新创建的 Pattern 实体及关联证据链。
89
+ - **验收**:无孤立证据;Pattern 创建数不超上限;证据 100% 可溯源。
90
+
91
+ ### 4. `relation_completion`(关系补全)
92
+ - **目标**:修复知识图谱中的断链与孤立节点。
93
+ - **输入**:`listEntities` 获取的实体列表、关系图谱。
94
+ - **算法**:
95
+ 1. 筛选 `getRelations` 返回为空的孤立实体。
96
+ 2. 基于上下文推断关系:
97
+ - 语义/实体共现 → `related_to`
98
+ - 同属项目/模块 → `part_of`
99
+ - 技术栈/流程依赖 → `depends_on`
100
+ 3. 批量写入关系边。
101
+ - **输出**:新增关系边列表。
102
+ - **验收**:关系推断符合领域常识;不引入循环依赖;图谱连通性提升。
103
+
104
+ ## 分层压缩策略表
105
+ | 记忆层级 | 判定条件 (`last_accessed_at`) | 处理策略 | 存储形态 |
106
+ |:---|:---|:---|:---|
107
+ | **活跃 (Active)** | `< sleepArchiveDays` | 保持原状,正常搜索可见 | 全文索引 + 向量 |
108
+ | **冷 (Cold)** | `>= sleepArchiveDays` 且 `< sleepCompressDays` | `demoteToSummary` 压成摘要,原文进 `_full_content` | 摘要 + `_full_content` 分离 |
109
+ | **睡眠 (Archived)** | `>= sleepCompressDays` | `setArchived` 移出热查询域 | 仅保留元数据与归档标记 |
110
+
111
+ ## 调度器与可中断机制
112
+ - **`noteWrite()` 心跳重置**:每次用户写入/读取记忆时触发。必须先 `clearTimeout` 清除旧计时器,再重新设置 Idle Timer,彻底杜绝 Stale Timer 导致的误触发。
113
+ - **`maybeSchedule()` 准入控制**:串行检查 `sleepModeEnabled` → 空闲时长 `>= sleepIdleMinutes` → 距上次运行 `>= sleepMinIntervalHours` → 状态非 `running/disposed`。任一不满足则放弃调度。
114
+ - **`service.enqueue` 串行队列**:所有 Sleep Run 必须进入全局串行队列执行,与 `autoDream` 严格互斥,避免并发写入冲突与资源争抢。
115
+ - **`AbortController` 可中断**:调度器持有 AbortController 实例。一旦检测到用户活动(`noteWrite` 或显式交互),立即调用 `abort()` 中断当前 `runSleep` 的执行上下文,保证实时性优先。
116
+
117
+ ## 数据迁移与存储层变更
118
+ - **幂等迁移**:
119
+ - `memories` 表新增列:`last_accessed_at` (DATETIME)、`_full_content` (TEXT/BLOB)。
120
+ - `dream_runs` 表新增列:`run_type` (VARCHAR, 默认 'auto',用于区分 'sleep')。
121
+ - 迁移脚本需保证幂等(`IF NOT EXISTS` / 检查列是否存在)。
122
+ - **Store 新增方法**:
123
+ - `demoteToSummary(id, summary, opts)`:执行降级并记录 `minRefTimeMs`。
124
+ - `restoreContent(id)`:按需从归档/摘要恢复原文。
125
+ - `touchLastAccess(id)`:更新 `last_accessed_at` 至当前时间。
126
+ - `getUnrecalledSince(days)`:按时间窗口拉取未召回记忆。
127
+ - `listEntities()`:获取实体列表用于关系分析。
128
+ - **Service 扩展**:
129
+ - `enqueue(task)`:串行任务队列入口。
130
+ - `setSleepHook(fn)`:注入 `noteWrite` 钩子。
131
+ - `touchRecalled(id)`:在召回路径中自动更新访问时间。
132
+
133
+ ## 与 autoDream 的协作关系
134
+ | 维度 | autoDream (实时) | Sleep Mode (系统级) |
135
+ |:---|:---|:---|
136
+ | **触发时机** | 写入阈值/实时事件 | 系统空闲 + 定时周期 |
137
+ | **执行深度** | 轻量级、局部关联补全 | 深度扫描、全局压缩与模式提炼 |
138
+ | **资源占用** | 低延迟、短时 | 允许较高延迟、长时运行 |
139
+ | **队列关系** | 共享 `service.enqueue` 串行队列 | 共享 `service.enqueue` 串行队列(严格互斥不重叠) |
140
+ | **审计记录** | 共用 `dream_runs`,`run_type='auto'` | 共用 `dream_runs`,`run_type='sleep'` |
141
+
142
+ ## 实现要点与避坑(v0.4.1 教训)
143
+ 1. **Stale Timer 防护**:`noteWrite()` 中必须严格遵循 `clearTimeout(oldTimer) -> setNewTimer()` 顺序,否则快速连续写入会累积多个 Timer 导致频繁误触发。
144
+ 2. **`minRefTimeMs` 防误降级**:调用 `demoteToSummary` 时必须传入快照时刻 `{minRefTimeMs: archiveCut}`。否则在 Sleep Run 执行期间被用户 `touch` 的记忆会因时间窗口漂移被错误降级。
145
+ 3. **证据强过滤**:Pattern Discovery 阶段 LLM 返回的 `evidence` 必须与 DB 真实 ID 列表求交集。未通过 `intersect` 校验的证据一律丢弃,严防幻觉伪造关联。
146
+ 4. **严格串行化**:所有 Sleep Run **必须**走 `service.enqueue`。禁止直接调用异步函数启动,确保与 `autoDream` 绝对不重叠,避免写入锁冲突。
147
+ 5. **Fail-Safe 阶段隔离**:四阶段必须各自独立 `try/catch`。LLM 服务抖动或网络异常时,仅跳过当前阶段(如跳过 Pattern 发现),后续阶段(如 Demotion、Relation)必须继续执行并记录 Receipt。
148
+ 6. **无 LLM 降级路由**:当 `resolveSleepRoute` 返回 `undefined`(如 Provider 未配置或模型不可用)时,系统应自动跳过所有依赖 LLM 的阶段,但 `archival_demotion`(纯规则)仍需照常执行,保障基础压缩能力不丢失。
149
+
150
+ ## 测试清单
151
+ - [ ] **调度器**:验证 `sleepIdleMinutes` 触发准确性;验证 `clearTimeout` 防 Stale;验证 `sleepMinIntervalHours` 冷却生效。
152
+ - [ ] **中断机制**:运行中触发 `noteWrite`,验证 `abort()` 立即终止 Run 且状态回滚至安全点。
153
+ - [ ] **阶段隔离**:Mock LLM 500 错误,验证仅对应阶段跳过,Receipt 仍生成且 `run_type='sleep'`。
154
+ - [ ] **降级策略**:构造不同 `last_accessed_at` 的记忆,验证 `minRefTimeMs` 防误降级逻辑及分层归档正确性。
155
+ - [ ] **模式发现**:注入含虚假 ID 的 LLM 响应,验证 `intersect` 过滤生效且创建数 `<= sleepMaxPatternPerRun`。
156
+ - [ ] **迁移幂等**:重复执行迁移脚本,验证无报错、无重复列/数据。
157
+ - [ ] **队列互斥**:并发触发 `autoDream` 与 Sleep,验证 `service.enqueue` 串行执行无重叠。
158
+
159
+ ## 启用指南(Opt-in)
160
+ 1. **配置开启**:在 `config.js` 或环境变量中设置 `sleepModeEnabled: true`。
161
+ 2. **资源评估**:建议生产环境配置独立的 `sleepProvider` 与 `sleepModel`,避免与实时推理争抢额度/并发。
162
+ 3. **初始运行**:首次启用后,系统将在首次空闲 `>= sleepIdleMinutes` 时触发全量扫描。可通过查看 `dream_runs` 表中 `run_type='sleep'` 的记录监控执行结果。
163
+ 4. **调优建议**:初期可设置 `sleepConflictStrictness: 'gentle'`,观察 `conflict_resolution` 决策准确率后再逐步收紧至 `'normal'` 或 `'aggressive'`。
164
+ 5. **关闭恢复**:设置 `sleepModeEnabled: false` 后,调度器将在 `dispose()` 阶段清理 Timer 并释放 Hook,已归档数据可通过 `restoreContent` 按需恢复。
@@ -0,0 +1,89 @@
1
+ # dsh-mneme 存储无损回收(#275 第一批)
2
+
3
+ ## 概览
4
+
5
+ 库大了以后有两类内容占着体积却零信息增量:
6
+
7
+ - `dream_runs.input`:每次巩固时的**记忆库快照**,可由记忆库重建;
8
+ - 归档行的向量:检索 SQL 恒带 `archived = 0`,按定义不可达。
9
+
10
+ 这个入口把这两样回收掉。一条记忆都不删、一次价值判断都不做。
11
+
12
+ 它**不自动跑**:两项都是不可逆的内容丢弃,只在人看得见数字的时候按下(维护者 2026-09-23 拍板:不挂启动路径、不开 `auto_vacuum`)。
13
+
14
+ ## 用法
15
+
16
+ ```bash
17
+ dsh-mneme reclaim # dry-run:会清几行、库多大,一个字节都不改
18
+ dsh-mneme reclaim --apply # 执行清理(不带 --vacuum 文件不会变小)
19
+ dsh-mneme reclaim --apply --vacuum # 清理 + 整库 VACUUM
20
+ dsh-mneme reclaim --older-than 30 --apply # 输入快照保留窗口 30 天(默认 7;0 = 全部)
21
+ dsh-mneme reclaim --json # 原始 JSON,给脚本用
22
+ ```
23
+
24
+ 前置:插件设置里打开「外部 API」(默认 `http://127.0.0.1:8790`),token 用 `dsh-mneme config set <url> <token>` 配一次。命令走 standalone 数据面 `POST /maintenance/reclaim`;服务端要求显式 `confirm`,不带就是干跑。
25
+
26
+ ## 清什么、不清什么
27
+
28
+ | 对象 | 动作 | 为什么零损失 |
29
+ |---|---|---|
30
+ | `dream_runs.input`(窗口外) | 置空 | 它是当时的记忆库快照,可由记忆库重建 |
31
+ | `dream_runs` 的骨架 / `decisions` / `outcome` / `receipt` | 不动 | 不可重建:拆了就回放不了那次裁决 |
32
+ | `memories.embedding`(`archived = 1`) | 置空 | 检索恒带 `archived = 0`,按定义不可达 |
33
+ | `memories` 的行 | 一条不删 | 本入口止体积不止条数;条数与重复归 #254 与整理动作面 |
34
+ | 活跃行的向量 | 不动 | 检索正在用 |
35
+
36
+ 取消归档时(`memory_archive` 还原)服务端会重新排队嵌入,所以被回收的向量能拿回来。
37
+
38
+ ## 代价与实测
39
+
40
+ VACUUM 是 O(库大小),且需要排他写锁:库越大越慢,期间的写要等锁。
41
+
42
+ 实测方法:把一份真实库**复制**到临时目录后跑本入口(不动活库),命令即 `reclaim` 的 dry-run 与 `--apply --vacuum`。样本是一次单机实测,不代表全部用户。
43
+
44
+ | 项 | 读数 |
45
+ |---|---:|
46
+ | 清理前磁盘足迹(主文件 65.5 MiB + WAL 3.9 MiB) | 69.5 MiB |
47
+ | 清 `dream_runs.input` | 177 个 run,列文本 7.24 MiB |
48
+ | 清归档行向量 | 295 行,列文本 3.04 MiB |
49
+ | 清理 + VACUUM 后足迹 | 50.8 MiB(降 18.66 MiB,约 27%;页数 16800 到 12983) |
50
+ | VACUUM(含收尾 checkpoint) | 1.2 s |
51
+ | dry-run | 48 ms |
52
+
53
+ 两点值得注意:体积降幅**大于**列文本合计(7.24 + 3.04 MiB),差额来自溢出页与索引页的回收——这就是报告口径按「VACUUM 前后体积」而不按列字节估的原因;另外体积量的是**磁盘足迹**(主文件 + `-wal` + `-shm`),WAL 模式下只量主文件会量到「还没落盘」的旧值。
54
+
55
+ 报数字以自己这次的 dry-run 与 apply 输出为准(dry-run 里的列文本大小只是上界,空闲页字节是「立刻能收回来」的那部分)。
56
+
57
+ ## 已知坑
58
+
59
+ - `--vacuum` 必须配 `--apply`:dry-run 不改数据,也就没有可回收的页。
60
+ - 只清理不 VACUUM,文件不会变小(空闲页留在文件里);体积收益要等一次 VACUUM。VACUUM 内部收尾会跑一次 `wal_checkpoint(TRUNCATE)`,否则 WAL 模式下主文件可能一动不动。
61
+ - VACUUM 拿不到排他锁(别的进程占着库)会失败:**清理已经完成且不可逆**,命令会照旧打印「清了 N 行」并把失败原因写进报告与回执,重跑一次 `reclaim --apply --vacuum` 即可(清理幂等)。
62
+ - 窗口是每次调用各自按当时的时间算的:`--older-than 0` 这类窗口在 dry-run 与 apply 之间新建的 run 也会被算进去。要严格对齐就先看数字、紧接着执行。
63
+ - 还原归档行后需要一次重嵌入才回到向量检索(排队的语义与写入路径一致:事务中或嵌入器未就绪会延后)。
64
+ - `llmAudit.enabled=false` 时回执不落库(那个开关连审计行的启动期清理一起关掉),但清理照做。
65
+ - 窗口内(默认 7 天)的输入快照不动:近期 run 仍可能在离线回放里被用到。
66
+ - `run_type='organize'` 的行任何窗口都不清:它们的 `input` 存的是 apply 的重放载荷(`src/organize.js` 直接读它重建候选索引),不是可重建的快照。置空会让那次 apply 静默空转并把报告锁死。
67
+
68
+ ## 升格吸收的 evidence(#230 × #275)
69
+
70
+ `memory_register_document` 注册成功后,这份文档引用的 evidence 行会**在同一事务里**翻成归档(absorbed)。一次吸收 20 条,活跃面就少 20 行——而不是「21 行不是 1 行」。
71
+
72
+ - 只翻标志位:内容、`content_history`、审计行一条不删,随时可以还原;
73
+ - `constraint` / `preference` 永不自动归档(#249 的逐字保真池),要归档得手动来;
74
+ - 只吸收**原子条**:别的 `document` 行与 `summary` 行不碰——前者本体是磁盘上的文件(归档它会让同一路径下次注册留下第二行),后者是 dream 总览与叙述(按 source 身份去重、按注入档位常驻);
75
+ - 想保留活跃面:工具带 `keep_evidence_active: true`(注册器参数是 `archiveEvidence: false`);
76
+ - 重注册同一份文档(出新版)时,那些已归档的 evidence 行仍可为**它所属的那份文档**背书;换个文档拿归档 id 当证据,照旧按捏造拒绝。
77
+
78
+ ## 第五指标:归档净增速率 + 可压掉行数
79
+
80
+ 面板「记忆复用」卡与 `GET /api/dsh-mneme/recall-stats` 的返回里多了一个 `archive` 块(#275 拍板:与既有指标同位):
81
+
82
+ - `total` / `addedInWindow` / `perDay`:归档区现有行数、窗口内新进的、以及日均净增速率。归档时刻取行的 `updated_at`(`setArchived` 会刷它)——代理口径:再改一次已归档行就会被算进本窗口,精确口径要一个 `archived_at` 列(属第二批题材);
83
+ - `compressible.rows` / `.groups`:**可压掉行数**——同 type、同 scope、内容归一化后逐字节相同的归档行里,每组留下的那一行之外的那些。判据用内容哈希(与写入准入同一把尺),**不**建在向量近重复上:回收动作本身就会清掉归档行的向量,指标不能指望自己的输入还在。
84
+
85
+ 这是观察口,不是动作触发:真删仍属第二批(未立项)。
86
+
87
+ ## 与 #254 的分界
88
+
89
+ 本入口止体积、不止重复。归档区里同主题条目堆积是**出口**问题,归 #254 的写入准入与后续整理动作面;这里一条记忆都不碰(维护者拍板 4)。