@modusensus/dsh-mneme 0.13.0 → 0.14.0-beta.2

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 (543) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +830 -828
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +964 -964
  5. package/bin/dsh-mneme-choro.mjs +262 -254
  6. package/bin/dsh-mneme-mcp.mjs +510 -510
  7. package/bin/dsh-mneme-serve.mjs +151 -151
  8. package/cordis.patch.yml +21 -21
  9. package/docs/CONFIGURATION.md +407 -402
  10. package/docs/CONTINUITY.md +139 -139
  11. package/docs/DAEMON.md +57 -57
  12. package/docs/DIRECTORIES.md +114 -114
  13. package/docs/DISTILL.md +397 -371
  14. package/docs/DSH-GO.md +97 -97
  15. package/docs/ENTITIES.md +247 -247
  16. package/docs/FORGET.md +77 -77
  17. package/docs/KB.md +110 -110
  18. package/docs/LOCAL_MODEL.md +222 -222
  19. package/docs/SEMANTIC.md +256 -256
  20. package/docs/SHARE.md +552 -552
  21. package/docs/SLEEP.md +202 -202
  22. package/docs/STORAGE.md +122 -122
  23. package/docs/WORKFLOWS.md +366 -366
  24. package/lib/api-standalone.js +1007 -1007
  25. package/lib/api.js +3600 -3394
  26. package/lib/attachment-text.js +122 -122
  27. package/lib/bootstrap-http.js +105 -105
  28. package/lib/bootstrap.js +215 -215
  29. package/lib/candidates.js +454 -454
  30. package/lib/capabilities.js +819 -819
  31. package/lib/choro-command.js +111 -111
  32. package/lib/client.js +14420 -13436
  33. package/lib/commands.js +95 -95
  34. package/lib/config-keys.js +144 -144
  35. package/lib/config.js +1361 -1317
  36. package/lib/content-hash.js +54 -54
  37. package/lib/continuity.js +258 -258
  38. package/lib/date-hint.js +32 -0
  39. package/lib/diary/auto.js +190 -190
  40. package/lib/diary/compose.js +440 -440
  41. package/lib/diary/dir.js +59 -59
  42. package/lib/diary/export.js +97 -97
  43. package/lib/diary/index.js +26 -26
  44. package/lib/diary/library.js +189 -189
  45. package/lib/diary/qq-inbox.js +142 -142
  46. package/lib/diary/rollover.js +61 -61
  47. package/lib/diary/session-provider.js +122 -122
  48. package/lib/document-index.js +94 -94
  49. package/lib/document.js +322 -322
  50. package/lib/dream/clustering.js +123 -123
  51. package/lib/dream/decisions.js +945 -907
  52. package/lib/dream/narratives.js +44 -44
  53. package/lib/dream/sleep.js +2027 -1887
  54. package/lib/dream/triage.js +139 -0
  55. package/lib/dream.js +1879 -1764
  56. package/lib/dsh-go/host.js +162 -162
  57. package/lib/dsh-go/panel.js +2091 -2091
  58. package/lib/dsh-go/text.js +342 -342
  59. package/lib/dsh-go/views/common.js +68 -68
  60. package/lib/dsh-go/views/entity.js +48 -48
  61. package/lib/dsh-go/views/memory.js +198 -198
  62. package/lib/dsh-go/views/root.js +30 -30
  63. package/lib/dsh-go/views/settings.js +398 -398
  64. package/lib/dsh-go/views/status.js +51 -51
  65. package/lib/embedding.js +342 -342
  66. package/lib/entities/extractor.js +285 -285
  67. package/lib/fence.js +24 -24
  68. package/lib/graph/anchoring.js +184 -184
  69. package/lib/guide.js +74 -74
  70. package/lib/heat.js +144 -144
  71. package/lib/hot-memory.js +119 -107
  72. package/lib/image-describe.js +158 -0
  73. package/lib/index.js +852 -822
  74. package/lib/inject.js +656 -639
  75. package/lib/kb/index.js +309 -309
  76. package/lib/kb/routes.js +219 -219
  77. package/lib/kb/tools.js +148 -148
  78. package/lib/lang.js +607 -584
  79. package/lib/local-embedder.js +358 -358
  80. package/lib/maintenance.js +162 -162
  81. package/lib/memory-source.js +9 -9
  82. package/lib/mirror.js +330 -330
  83. package/lib/organize.js +456 -456
  84. package/lib/peak-hours.js +172 -172
  85. package/lib/pipeline-activities.js +178 -178
  86. package/lib/pipeline-attribution.js +165 -165
  87. package/lib/pipeline-command.js +279 -279
  88. package/lib/pipeline-confirm.js +142 -142
  89. package/lib/pipeline-exec.js +174 -174
  90. package/lib/pipeline-nudge-auto.js +129 -129
  91. package/lib/pipeline-nudge.js +1623 -1623
  92. package/lib/pipeline-observations.js +181 -181
  93. package/lib/pipeline-observe.js +133 -133
  94. package/lib/pipeline-runner.js +361 -361
  95. package/lib/pipeline-runs.js +442 -442
  96. package/lib/pipeline.js +716 -716
  97. package/lib/placement.js +13 -13
  98. package/lib/pre-inject-gate.js +354 -354
  99. package/lib/profile-extract.js +164 -164
  100. package/lib/profile-facts.js +356 -356
  101. package/lib/provenance-http.js +251 -251
  102. package/lib/quality-filter.js +162 -162
  103. package/lib/recall-stats.js +190 -190
  104. package/lib/reclaim-http.js +150 -150
  105. package/lib/reranker.js +269 -269
  106. package/lib/runtime/adopt.js +176 -176
  107. package/lib/runtime/closure.js +172 -172
  108. package/lib/runtime/download.js +247 -247
  109. package/lib/runtime/layout.js +265 -265
  110. package/lib/runtime/loader.js +252 -252
  111. package/lib/runtime/model-fetch.js +361 -361
  112. package/lib/runtime/provision.js +237 -237
  113. package/lib/runtime/tarball.js +130 -130
  114. package/lib/runtime/verify.js +223 -223
  115. package/lib/safe-slice.js +18 -18
  116. package/lib/scope-state.js +277 -277
  117. package/lib/scope.js +187 -187
  118. package/lib/search/adaptive.js +60 -60
  119. package/lib/search/bm25.js +96 -96
  120. package/lib/semantic.js +572 -572
  121. package/lib/sensitive-scan.js +209 -209
  122. package/lib/serve.js +228 -228
  123. package/lib/service.js +3268 -3196
  124. package/lib/session-gate.js +91 -91
  125. package/lib/session-usage.js +350 -350
  126. package/lib/settings.js +827 -802
  127. package/lib/share-http.js +281 -281
  128. package/lib/share-import.js +234 -234
  129. package/lib/share.js +335 -335
  130. package/lib/store.js +5895 -5662
  131. package/lib/summarize.js +1388 -1343
  132. package/lib/tools.js +1370 -1356
  133. package/lib/user-settings.js +230 -230
  134. package/lib/vector-index.js +180 -180
  135. package/lib/version-check.js +122 -122
  136. package/lib/wiki/claims.js +267 -0
  137. package/lib/wiki/cli-hint.js +90 -90
  138. package/lib/wiki/inject.js +164 -164
  139. package/lib/wiki/partition.js +200 -200
  140. package/lib/wiki/plan.js +214 -214
  141. package/lib/wiki/queries.js +640 -474
  142. package/lib/wiki/render.js +261 -187
  143. package/lib/wiki/scan.js +371 -371
  144. package/lib/wiki/t0.js +500 -463
  145. package/lib/wiki/t1-jobs.js +232 -232
  146. package/lib/wiki/t1.js +609 -502
  147. package/lib/write-admission.js +404 -404
  148. package/locale/en.json +6 -6
  149. package/locale/zh.json +6 -6
  150. package/package.json +15 -1
  151. package/runtime-manifest.json +526 -526
  152. package/scripts/benchmark-embed.js +201 -201
  153. package/scripts/benchmark-recall.js +249 -249
  154. package/scripts/benchmark-rerank.js +166 -166
  155. package/scripts/build-runtime-manifest.mjs +224 -224
  156. package/scripts/changelog-prep.mjs +91 -91
  157. package/scripts/check-sync.js +44 -44
  158. package/scripts/e2e-dsh.js +220 -220
  159. package/scripts/mneme-runtime.mjs +277 -277
  160. package/scripts/publish-gate.mjs +197 -197
  161. package/scripts/release-docs.mjs +210 -204
  162. package/scripts/release-notes.mjs +164 -161
  163. package/scripts/stress-dsh.js +255 -255
  164. package/scripts/sync-lib.js +47 -47
  165. package/scripts/sync-test-badge.mjs +69 -69
  166. package/scripts/test-count-sync.mjs +48 -48
  167. package/src/api-standalone.js +1007 -1007
  168. package/src/api.js +3600 -3394
  169. package/src/attachment-text.js +122 -122
  170. package/src/bootstrap-http.js +105 -105
  171. package/src/bootstrap.js +215 -215
  172. package/src/candidates.js +454 -454
  173. package/src/capabilities.js +819 -819
  174. package/src/choro-command.js +111 -111
  175. package/src/commands.js +95 -95
  176. package/src/config-keys.js +144 -144
  177. package/src/config.js +1361 -1317
  178. package/src/content-hash.js +54 -54
  179. package/src/continuity.js +258 -258
  180. package/src/date-hint.js +32 -0
  181. package/src/diary/auto.js +190 -190
  182. package/src/diary/compose.js +440 -440
  183. package/src/diary/dir.js +59 -59
  184. package/src/diary/export.js +97 -97
  185. package/src/diary/index.js +26 -26
  186. package/src/diary/library.js +189 -189
  187. package/src/diary/qq-inbox.js +142 -142
  188. package/src/diary/rollover.js +61 -61
  189. package/src/diary/session-provider.js +122 -122
  190. package/src/document-index.js +94 -94
  191. package/src/document.js +322 -322
  192. package/src/dream/clustering.js +123 -123
  193. package/src/dream/decisions.js +945 -907
  194. package/src/dream/narratives.js +44 -44
  195. package/src/dream/sleep.js +2027 -1887
  196. package/src/dream/triage.js +139 -0
  197. package/src/dream.js +1879 -1764
  198. package/src/dsh-go/host.js +162 -162
  199. package/src/dsh-go/panel.js +2091 -2091
  200. package/src/dsh-go/text.js +342 -342
  201. package/src/dsh-go/views/common.js +68 -68
  202. package/src/dsh-go/views/entity.js +48 -48
  203. package/src/dsh-go/views/memory.js +198 -198
  204. package/src/dsh-go/views/root.js +30 -30
  205. package/src/dsh-go/views/settings.js +398 -398
  206. package/src/dsh-go/views/status.js +51 -51
  207. package/src/embedding.js +342 -342
  208. package/src/entities/extractor.js +285 -285
  209. package/src/fence.js +24 -24
  210. package/src/graph/anchoring.js +184 -184
  211. package/src/guide.js +74 -74
  212. package/src/heat.js +144 -144
  213. package/src/hot-memory.js +119 -107
  214. package/src/image-describe.js +158 -0
  215. package/src/index.js +852 -822
  216. package/src/inject.js +656 -639
  217. package/src/kb/index.js +309 -309
  218. package/src/kb/routes.js +219 -219
  219. package/src/kb/tools.js +148 -148
  220. package/src/lang.js +607 -584
  221. package/src/local-embedder.js +358 -358
  222. package/src/maintenance.js +162 -162
  223. package/src/memory-source.js +9 -9
  224. package/src/mirror.js +330 -330
  225. package/src/organize.js +456 -456
  226. package/src/peak-hours.js +172 -172
  227. package/src/pipeline-activities.js +178 -178
  228. package/src/pipeline-attribution.js +165 -165
  229. package/src/pipeline-command.js +279 -279
  230. package/src/pipeline-confirm.js +142 -142
  231. package/src/pipeline-exec.js +174 -174
  232. package/src/pipeline-nudge-auto.js +129 -129
  233. package/src/pipeline-nudge.js +1623 -1623
  234. package/src/pipeline-observations.js +181 -181
  235. package/src/pipeline-observe.js +133 -133
  236. package/src/pipeline-runner.js +361 -361
  237. package/src/pipeline-runs.js +442 -442
  238. package/src/pipeline.js +716 -716
  239. package/src/placement.js +13 -13
  240. package/src/pre-inject-gate.js +354 -354
  241. package/src/profile-extract.js +164 -164
  242. package/src/profile-facts.js +356 -356
  243. package/src/provenance-http.js +251 -251
  244. package/src/quality-filter.js +162 -162
  245. package/src/recall-stats.js +190 -190
  246. package/src/reclaim-http.js +150 -150
  247. package/src/reranker.js +269 -269
  248. package/src/runtime/adopt.js +176 -176
  249. package/src/runtime/closure.js +172 -172
  250. package/src/runtime/download.js +247 -247
  251. package/src/runtime/layout.js +265 -265
  252. package/src/runtime/loader.js +252 -252
  253. package/src/runtime/model-fetch.js +361 -361
  254. package/src/runtime/provision.js +237 -237
  255. package/src/runtime/tarball.js +130 -130
  256. package/src/runtime/verify.js +223 -223
  257. package/src/safe-slice.js +18 -18
  258. package/src/scope-state.js +277 -277
  259. package/src/scope.js +187 -187
  260. package/src/search/adaptive.js +60 -60
  261. package/src/search/bm25.js +96 -96
  262. package/src/semantic.js +572 -572
  263. package/src/sensitive-scan.js +209 -209
  264. package/src/serve.js +228 -228
  265. package/src/service.js +3268 -3196
  266. package/src/session-gate.js +91 -91
  267. package/src/session-usage.js +350 -350
  268. package/src/settings.js +827 -802
  269. package/src/share-http.js +281 -281
  270. package/src/share-import.js +234 -234
  271. package/src/share.js +335 -335
  272. package/src/store.js +5895 -5662
  273. package/src/summarize.js +1388 -1343
  274. package/src/tools.js +1370 -1356
  275. package/src/user-settings.js +230 -230
  276. package/src/vector-index.js +180 -180
  277. package/src/version-check.js +122 -122
  278. package/src/wiki/claims.js +267 -0
  279. package/src/wiki/cli-hint.js +90 -90
  280. package/src/wiki/inject.js +164 -164
  281. package/src/wiki/partition.js +200 -200
  282. package/src/wiki/plan.js +214 -214
  283. package/src/wiki/queries.js +640 -474
  284. package/src/wiki/render.js +261 -187
  285. package/src/wiki/scan.js +371 -371
  286. package/src/wiki/t0.js +500 -463
  287. package/src/wiki/t1-jobs.js +232 -232
  288. package/src/wiki/t1.js +609 -502
  289. package/src/write-admission.js +404 -404
  290. package/test/adaptive-inject-budget.test.js +85 -85
  291. package/test/api-hardening.test.js +173 -173
  292. package/test/api-nudge-route.test.js +259 -259
  293. package/test/api-routes-gaps.test.js +1092 -1092
  294. package/test/api.test.js +3007 -2988
  295. package/test/attachment-text.test.js +139 -139
  296. package/test/audit.test.js +448 -448
  297. package/test/baseline-persist.test.js +248 -248
  298. package/test/benchmark.test.js +94 -94
  299. package/test/bm25-corpus-cache.test.js +145 -145
  300. package/test/bootstrap.test.js +228 -228
  301. package/test/candidates.test.js +345 -345
  302. package/test/capabilities.test.js +577 -577
  303. package/test/capacity-warning.test.js +262 -262
  304. package/test/changelog-prep.test.js +125 -125
  305. package/test/choro-command.test.js +218 -218
  306. package/test/cli-enums.test.js +37 -37
  307. package/test/cli-flag-value.test.js +86 -86
  308. package/test/cli-receive.test.js +390 -390
  309. package/test/cli-share.test.js +437 -437
  310. package/test/client-share.test.js +225 -225
  311. package/test/client.test.js +4595 -4184
  312. package/test/clustering.test.js +100 -100
  313. package/test/commands.test.js +106 -106
  314. package/test/config-keys-routes.test.js +233 -233
  315. package/test/config.test.js +153 -153
  316. package/test/conflict-freeze.test.js +344 -344
  317. package/test/conflict-ledger-enforce.test.js +452 -0
  318. package/test/conflict-ledger.test.js +242 -0
  319. package/test/conflict-reviewed.test.js +79 -79
  320. package/test/content-hash.test.js +204 -204
  321. package/test/continuity.test.js +452 -452
  322. package/test/contrast.test.js +432 -320
  323. package/test/date-hint.test.js +156 -0
  324. package/test/dedupe-candidate-window.test.js +180 -180
  325. package/test/dependency-overrides.test.js +78 -78
  326. package/test/diary-library.test.js +187 -187
  327. package/test/diary.test.js +576 -576
  328. package/test/differentiate-feedback.test.js +248 -248
  329. package/test/distill-tx-embed-gap.test.js +110 -110
  330. package/test/docs-config-keys.test.js +65 -65
  331. package/test/document-dir.test.js +233 -233
  332. package/test/document.test.js +703 -703
  333. package/test/dream-archive-guardrail.test.js +116 -116
  334. package/test/dream-baseline-clamp.test.js +174 -0
  335. package/test/dream-candidate-hybrid.test.js +281 -281
  336. package/test/dream-coverage-degrade.test.js +86 -86
  337. package/test/dream-decision-evidence.test.js +314 -314
  338. package/test/dream-failure-backoff.test.js +173 -173
  339. package/test/dream-fence.test.js +207 -207
  340. package/test/dream-maxtokens-fallback.test.js +132 -132
  341. package/test/dream-merge-guardrail.test.js +219 -219
  342. package/test/dream-peak-hours.test.js +194 -194
  343. package/test/dream.test.js +1528 -1528
  344. package/test/dsh-go-host.test.js +134 -134
  345. package/test/dsh-go-panel-actions.test.js +854 -854
  346. package/test/dsh-go-panel.test.js +251 -251
  347. package/test/embed-ready-poll.test.js +94 -94
  348. package/test/embed-text-reasons.test.js +112 -112
  349. package/test/embedding-cache-f32.test.js +276 -276
  350. package/test/embedding-ready.test.js +226 -226
  351. package/test/entities.test.js +711 -711
  352. package/test/entity-recall.test.js +179 -179
  353. package/test/epistemic.test.js +325 -325
  354. package/test/failure-query.test.js +147 -147
  355. package/test/failure-retention.test.js +105 -105
  356. package/test/fnew-0112.test.js +311 -311
  357. package/test/fnew-03.test.js +422 -422
  358. package/test/forget-corrections.test.js +70 -70
  359. package/test/graph-anchoring.test.js +184 -184
  360. package/test/graph-api.test.js +333 -209
  361. package/test/graph-boosted-relations.test.js +189 -189
  362. package/test/graph-inject-budget.test.js +103 -103
  363. package/test/graph-multipath.test.js +269 -269
  364. package/test/graph-passive-confirm.test.js +91 -91
  365. package/test/graph-weight-evolution.test.js +104 -104
  366. package/test/heat.test.js +175 -175
  367. package/test/helpers/dream-mock.js +86 -86
  368. package/test/helpers/peer-worker.mjs +25 -25
  369. package/test/helpers/runtime-source.js +60 -60
  370. package/test/helpers/tar-builder.js +80 -80
  371. package/test/helpers/write-admission-samples.js +312 -312
  372. package/test/hot-memory.test.js +259 -211
  373. package/test/image-description.test.js +271 -0
  374. package/test/image-memory.test.js +170 -170
  375. package/test/inject-content-cap.test.js +91 -91
  376. package/test/inject-first-round-bm25.test.js +89 -89
  377. package/test/inject-heat.test.js +77 -77
  378. package/test/inject-parent-gate.test.js +134 -106
  379. package/test/inject-pin-explicit.test.js +300 -300
  380. package/test/inject-pin-vector-crowd.test.js +225 -225
  381. package/test/inject-pools.test.js +195 -195
  382. package/test/inject-recall.test.js +172 -172
  383. package/test/inject-rotation.test.js +161 -161
  384. package/test/inject.test.js +553 -494
  385. package/test/issue-617-step1-query-and-fusion.test.js +361 -361
  386. package/test/kb-api-wiring.test.js +128 -128
  387. package/test/kb-collections.test.js +272 -272
  388. package/test/kb-routes.test.js +379 -379
  389. package/test/lang.test.js +98 -98
  390. package/test/lastrunat-persist.test.js +101 -101
  391. package/test/lib-smoke.test.js +116 -116
  392. package/test/llm-audit.test.js +815 -815
  393. package/test/llm-message-source.test.js +207 -207
  394. package/test/local-embedder.test.js +292 -292
  395. package/test/maintenance.test.js +501 -501
  396. package/test/mcp-stdio.test.js +385 -385
  397. package/test/mint-source.test.js +311 -311
  398. package/test/mirror-dirty.test.js +424 -424
  399. package/test/mirror-edit-digest.test.js +187 -187
  400. package/test/mirror-generation.test.js +499 -499
  401. package/test/mirror-types.test.js +208 -208
  402. package/test/mirror.test.js +249 -249
  403. package/test/narrative-bars.test.js +162 -162
  404. package/test/organize-tool.test.js +232 -232
  405. package/test/organize.test.js +418 -418
  406. package/test/panel-chart-scale.test.js +90 -0
  407. package/test/panel-tokens.test.js +245 -0
  408. package/test/peak-hours-observability.test.js +70 -70
  409. package/test/peer-blockers.test.js +193 -193
  410. package/test/persona-addressing.test.js +83 -83
  411. package/test/persona-api.test.js +172 -172
  412. package/test/persona-by-session.test.js +568 -568
  413. package/test/persona-flag.test.js +36 -36
  414. package/test/persona-inject.test.js +145 -145
  415. package/test/persona-settings.test.js +198 -198
  416. package/test/pipeline-activities.test.js +155 -155
  417. package/test/pipeline-attribution.test.js +175 -175
  418. package/test/pipeline-command.test.js +243 -243
  419. package/test/pipeline-confirm.test.js +148 -148
  420. package/test/pipeline-exec.test.js +172 -172
  421. package/test/pipeline-nudge-auto.test.js +248 -248
  422. package/test/pipeline-nudge.test.js +1278 -1278
  423. package/test/pipeline-observations.test.js +157 -157
  424. package/test/pipeline-observe.test.js +127 -127
  425. package/test/pipeline-r1r2.test.js +278 -278
  426. package/test/pipeline-runner.test.js +468 -468
  427. package/test/pipeline-runs.test.js +767 -767
  428. package/test/pipeline.test.js +636 -636
  429. package/test/placement.test.js +36 -36
  430. package/test/policy-epoch.test.js +259 -259
  431. package/test/pre-inject-gate.test.js +444 -444
  432. package/test/profile-extract.test.js +269 -269
  433. package/test/profile-facts.test.js +492 -492
  434. package/test/prompt-stance.test.js +128 -128
  435. package/test/provenance-http.test.js +367 -367
  436. package/test/publish-gate.test.js +272 -272
  437. package/test/quality-backfill.test.js +290 -290
  438. package/test/quality-filter.test.js +295 -295
  439. package/test/readme-claims.test.js +113 -113
  440. package/test/reasoning-effort.test.js +752 -752
  441. package/test/recall-anti-update.test.js +82 -82
  442. package/test/recall-evals.test.js +253 -253
  443. package/test/recall-layer.test.js +326 -326
  444. package/test/recall-retention.test.js +128 -128
  445. package/test/recall-stats.test.js +274 -274
  446. package/test/receipt-chain.test.js +451 -451
  447. package/test/reclaim-http.test.js +209 -209
  448. package/test/reflection-update-gate.test.js +129 -129
  449. package/test/reflection.test.js +226 -226
  450. package/test/reindex-backfill.test.js +475 -475
  451. package/test/reindex-scheduler.test.js +369 -369
  452. package/test/release-docs.test.js +156 -136
  453. package/test/release-notes.test.js +265 -240
  454. package/test/release-workflow.test.js +93 -93
  455. package/test/reranker-single-logit.test.js +73 -73
  456. package/test/reranker.test.js +197 -197
  457. package/test/resident-status.test.js +83 -83
  458. package/test/route-policy.test.js +113 -113
  459. package/test/runtime-adopt.test.js +206 -206
  460. package/test/runtime-cli.test.js +89 -89
  461. package/test/runtime-closure.test.js +193 -193
  462. package/test/runtime-download.test.js +302 -302
  463. package/test/runtime-layout.test.js +163 -163
  464. package/test/runtime-loader.test.js +255 -255
  465. package/test/runtime-manifest.test.js +159 -159
  466. package/test/runtime-model-fetch.test.js +348 -348
  467. package/test/runtime-provision.test.js +101 -101
  468. package/test/runtime-tarball.test.js +81 -81
  469. package/test/runtime-verify.test.js +319 -319
  470. package/test/scope-candidates.test.js +491 -491
  471. package/test/scope-distill-stamp.test.js +134 -134
  472. package/test/scope-explicit.test.js +452 -452
  473. package/test/scope-inject-soft.test.js +106 -106
  474. package/test/scope-optional-service.test.js +110 -110
  475. package/test/scope-retrieval.test.js +197 -197
  476. package/test/scope-state.test.js +610 -610
  477. package/test/scope-storage.test.js +255 -255
  478. package/test/scope-strict.test.js +367 -367
  479. package/test/search-fusion.test.js +90 -90
  480. package/test/security-doc.test.js +188 -178
  481. package/test/semantic.test.js +124 -124
  482. package/test/sensitive-scan.test.js +251 -251
  483. package/test/serve-bin.test.js +121 -121
  484. package/test/serve.test.js +258 -258
  485. package/test/service-search.test.js +174 -174
  486. package/test/service.test.js +410 -410
  487. package/test/session-gate.test.js +264 -264
  488. package/test/session-usage.test.js +317 -317
  489. package/test/settings.test.js +273 -273
  490. package/test/share-foreign-rows.test.js +144 -144
  491. package/test/share-http.test.js +144 -144
  492. package/test/share-import.test.js +577 -577
  493. package/test/share-imported-guard.test.js +311 -311
  494. package/test/share-imported-identity.test.js +111 -111
  495. package/test/share-pack.test.js +511 -511
  496. package/test/share-receive-http.test.js +756 -756
  497. package/test/share-routes.test.js +337 -337
  498. package/test/share-store.test.js +133 -133
  499. package/test/share-validate.test.js +101 -101
  500. package/test/skip-invalid-restore.test.js +262 -262
  501. package/test/sleep-action-set.test.js +441 -441
  502. package/test/sleep-catchup.test.js +315 -315
  503. package/test/sleep-failure-backoff.test.js +156 -156
  504. package/test/sleep-heat.test.js +174 -174
  505. package/test/sleep-scheduler-rearm.test.js +89 -89
  506. package/test/sleep.test.js +1455 -1418
  507. package/test/standalone-api.test.js +1344 -1344
  508. package/test/store-vector-cache.test.js +98 -98
  509. package/test/store-write-path-columns.test.js +191 -191
  510. package/test/store.test.js +744 -744
  511. package/test/stress.test.js +209 -209
  512. package/test/summarize-occurred-at.test.js +127 -127
  513. package/test/summarize-parse-diagnostics.test.js +75 -75
  514. package/test/summarize-reasoning-effort.test.js +189 -189
  515. package/test/summarize.test.js +2251 -2251
  516. package/test/supersede-state-tracking.test.js +394 -394
  517. package/test/test-count-sync.test.js +71 -71
  518. package/test/tool-state-ignore.test.js +65 -65
  519. package/test/tools-dto-schema-matrix.test.js +398 -398
  520. package/test/tools-memory-get-schema.test.js +65 -65
  521. package/test/tools.test.js +585 -585
  522. package/test/updated-at-semantics.test.js +113 -113
  523. package/test/user-settings-preview.test.js +127 -127
  524. package/test/user-settings-segments.test.js +527 -527
  525. package/test/vector-index.test.js +334 -334
  526. package/test/version-check.test.js +152 -152
  527. package/test/webServer-optional.test.js +82 -82
  528. package/test/wiki-api.test.js +734 -627
  529. package/test/wiki-claims.test.js +194 -0
  530. package/test/wiki-cli-hint.test.js +118 -118
  531. package/test/wiki-inject.test.js +265 -265
  532. package/test/wiki-partition.test.js +112 -112
  533. package/test/wiki-plan.test.js +80 -80
  534. package/test/wiki-provenance.test.js +202 -0
  535. package/test/wiki-queries.test.js +398 -277
  536. package/test/wiki-scan.test.js +208 -208
  537. package/test/wiki-store.test.js +190 -190
  538. package/test/wiki-t0.test.js +456 -380
  539. package/test/wiki-t1-jobs.test.js +125 -125
  540. package/test/wiki-t1.test.js +460 -235
  541. package/test/wiki-tools.test.js +176 -176
  542. package/test/workflow-guards.test.js +155 -155
  543. package/test/write-admission.test.js +668 -668
package/src/index.js CHANGED
@@ -1,822 +1,852 @@
1
- import { createStore } from "./store.js";
2
- import { createMirror, TYPE_FILE } from "./mirror.js";
3
- import { createDocumentIndex } from "./document-index.js";
4
- import { resolveDocumentDir } from "./document.js";
5
- // #368 v1:profile 作用域落点检测(只提示,不改行为)。
6
- import { findProfileScopeSegment } from "./placement.js";
7
- import { createService } from "./service.js";
8
- // #254 写入准入(第一阶段只计量,不拦截):见 src/write-admission.js 的文件头。
9
- import { createWriteAdmission } from "./write-admission.js";
10
- // #164 A2:写入边界的密钥 / PII 判据,注入给上面的写入准入。
11
- import { createSensitiveScan } from "./sensitive-scan.js";
12
- import { createTools } from "./tools.js";
13
- import { createInjector } from "./inject.js";
14
- import { createPreInjectGate } from "./pre-inject-gate.js";
15
- import { createContinuityRescue } from "./continuity.js";
16
- import { createSummarizer } from "./summarize.js";
17
- import { createSessionGate } from "./session-gate.js";
18
- import { createDreamScheduler, describeStreamFailure } from "./dream.js";
19
- import { createSleepScheduler, runSleep } from "./dream/sleep.js";
20
- // #622 可观测性:错峰时段串解析失败是「静默未配置」(parsePeakSpec 契约不救错),
21
- // 启动装配后对非空且解析失败的键出一条 warn——只补可见性,不改语义。
22
- import { invalidPeakSpecs } from "./peak-hours.js";
23
- import { createApi } from "./api.js";
24
- import { createStandaloneApi } from "./api-standalone.js";
25
- // #275 存储生命周期第一批:无损回收(手动入口,不挂启动路径)。
26
- import { createMaintenance } from "./maintenance.js";
27
- import { createChoroQueries } from "./wiki/queries.js";
28
- // chorography M3:开局注入块与 /choro 只读回执。两者都只吃 queries 读侧,不写任何东西。
29
- import { createChoroInject } from "./wiki/inject.js";
30
- import { registerChoroCommand } from "./choro-command.js";
31
- // 收尾档:面板重建按钮走 runChoroT0(P34;T1 不上 HTTP)。
32
- import { runChoroT0 } from "./wiki/t0.js";
33
- // T1 后台任务账(契约 §3.2):面板点「补语义正文」走 HTTP 起一个 spawn 子进程,
34
- // 进程账不能住在 api.js(HTTP 面刻意不摸 child_process),装配点在这里。
35
- import { createT1Jobs } from "./wiki/t1-jobs.js";
36
- import { createSettings } from "./settings.js";
37
- // 「Agent 眼中的你」自动事实(2026-11-06):逐卡事实清单 + dream 后自动维护轮。
38
- import { createProfileFacts } from "./profile-facts.js";
39
- // 知识库第二层(契约 §3.4):装配与开关无关(createKb 只建对象,读侧零 IO),
40
- // 闸在路由与工具两层——这样「没开」与「没装配」在 HTTP 面上是两个可区分答案。
41
- import { createKb } from "./kb/index.js";
42
- import { createCommandManager } from "./commands.js";
43
- import { createExecutionRecorder } from "./pipeline-runner.js";
44
- import { registerMoRunCommand } from "./pipeline-command.js";
45
- // M.O. 自动沉淀的触发侧(冷却 / 排队 / 兜底)。挖掘与落盘在 pipeline-nudge.js,
46
- // 这里只管「什么时候跑」。两者分开的理由见 src/pipeline-nudge-auto.js 文件头。
47
- import { createNudgeAutoRun } from "./pipeline-nudge-auto.js";
48
- // QQ 日记本:合成管线(compose)与触发侧(auto)分开,理由见 src/diary/index.js 文件头。
49
- import { createDiaryComposer, createDiaryAutoRun } from "./diary/index.js";
50
- import { createVectorIndex } from "./vector-index.js";
51
- // semantic(embedder/reranker/boot 回填)纯搬移至 src/semantic.js(PR2):宿主与
52
- // daemon(dsh-mneme-serve,#363)共用同一套装配,原文件保留 barrel 出口。
53
- import { createSemantic } from "./semantic.js";
54
- import { Config, applyLightModePreset, injectChildEnabled } from "./config.js";
55
- import { langOf } from "./lang.js";
56
- import { extractEntities } from "./entities/extractor.js";
57
- import { createProfileExtractor } from "./profile-extract.js";
58
- import { mkdirSync } from "node:fs";
59
- import { join } from "node:path";
60
- import { homedir } from "node:os";
61
-
62
- export const name = "dsh-mneme";
63
- // webServer 在 inject 声明中:cordis 会等宿主 webServer 服务激活后才 apply 本
64
- // 插件,保证 apply 时路由注册不落时序(v0.7.23 曾移出 inject 想支持 headless,
65
- // 结果 cordis 不再等待,apply 时宿主 webServer 未就绪 → 桌面端 "cannot get
66
- // property without inject" 崩溃)。守卫用 ctx.reflect.get(免 inject 读取,
67
- // 未提供返回 undefined)而非 if (ctx.webServer):直接访问未注入属性在 cordis
68
- // Proxy 下会抛错而非返回 undefined。
69
- export const inject = ["tools", "systemPrompt", "llm", "agentDefaultModel", "commands", "webServer"];
70
- export { Config };
71
-
72
- // Arrow (not function declaration): cordis 4 treats any apply with a
73
- // prototype as a class constructor (`new apply(...)`) and discards its return
74
- // value, so a `function apply` disposer would never run on unload. An arrow
75
- // has no prototype, is called normally, and its returned disposer is collected
76
- // and run by the fiber on unload.
77
- // Entity-extraction LLM adapter (issue #108/#109): maps the extractor's
78
- // options (provider/model override + reasoningEffort) onto a real dsh-llm
79
- // stream route and retries once without the effort when the first attempt is
80
- // rejected. Extracted from apply() so the effort-fallback branch is
81
- // unit-testable; the extractor only ever sees a callLLM(messages, options)
82
- // => Promise<string>. The route always carries a real provider/model (dsh-llm
83
- // GenerateOptions requires both) — never a bare stream.
84
- //
85
- // Issue #250: it also accounts for itself. Entity extraction is the background
86
- // LLM path that runs on every memory write, yet it never reached
87
- // llm_audit_logs — the adapter only ever held an llm handle, so it had no way
88
- // to call service.saveLlmAudit. It now takes service (+ config for the shared
89
- // llmAudit.enabled gate) and writes one row per stream attempt, matching the
90
- // contract of runAuditedLlm in dream.js: a rejected effort attempt records its
91
- // own error row and the retry records its own success row.
92
- export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, service, config }) {
93
- return async function streamEntityText(messages, options = {}) {
94
- let route = {};
95
- if (options.provider) route.provider = options.provider;
96
- if (options.model) route.model = options.model;
97
- if (!route.provider || !route.model) {
98
- try {
99
- const sel = agentDefaultModel?.currentSelection?.();
100
- if (sel?.provider && sel?.model) {
101
- route.provider ??= sel.provider;
102
- route.model ??= sel.model;
103
- }
104
- } catch { /* fall through to whatever route we already have */ }
105
- }
106
- const effort = options.reasoningEffort;
107
- // #405: 审计行的 related_memory_ids 此前恒空、error_message 又被压成一句常量,
108
- // 于是「哪条记忆没抽到实体、为什么」在库里查不出来(主库 735 条 error 行全是
109
- // 同一句话)。id 由 extractor 顺 options 传下来——它只认 memory.content,不认
110
- // id。口径同 write-admission:350:拿不到 id 就留空,绝不挂不存在的 id 造虚账。
111
- const relatedMemoryIds = options.memoryId ? [options.memoryId] : [];
112
- // 没有解析出 provider/model 就没有可归属的模型——与 dream 的 resolveRoute
113
- // 无路由早退同口径,那种情况不写审计行。
114
- const modelId = route.provider && route.model ? `${route.provider}:${route.model}` : "";
115
- const tryStream = (withEffort) => {
116
- let text = "";
117
- let inputTokens = 0;
118
- let outputTokens = 0;
119
- const startedAt = Date.now();
120
- const timestamp = new Date(startedAt).toISOString();
121
- // 记账是 best-effort:写审计行失败只 warn,绝不反噬抽取本身(CONTRIBUTING
122
- // 的 fail-safe 硬约定)。
123
- const writeAudit = (status, errorMessage, relatedMemoryIds = []) => {
124
- if (config?.llmAudit?.enabled === false || typeof service?.saveLlmAudit !== "function") return;
125
- // #372: modelId 为空 = 路由解析失败——此前整个 writeAudit 静默 return,
126
- // llm.stream 抛错也留不下任何行,正是 #108 要消灭的「零实体零日志」同款盲区。
127
- // 空路由无法记账,但至少要留一条可解释的 warn,别让失败面完全不可见。
128
- if (!modelId) {
129
- if (status !== "ok") logger?.warn?.(`dsh-mneme: entity extraction ${status} without audit row (llm route unresolved: ${errorMessage ?? "no route"})`);
130
- return;
131
- }
132
- try {
133
- service.saveLlmAudit({
134
- timestamp,
135
- trigger_source: "entityExtract",
136
- operation_type: "entity_extract",
137
- model_id: modelId,
138
- input_tokens: inputTokens,
139
- output_tokens: outputTokens,
140
- total_tokens: inputTokens + outputTokens,
141
- cost_usd: 0,
142
- duration_ms: Date.now() - startedAt,
143
- status,
144
- error_message: errorMessage,
145
- related_memory_ids: relatedMemoryIds
146
- });
147
- } catch (auditError) {
148
- logger?.warn?.(`dsh-mneme: entity extraction llm audit write failed: ${String(auditError)}`);
149
- }
150
- };
151
- return (async () => {
152
- for await (const chunk of llm.stream({
153
- ...route,
154
- maxTokens: 4096,
155
- ...(withEffort && effort ? { reasoningEffort: effort } : {}),
156
- messages
157
- })) {
158
- if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
159
- // DSH 的 StreamChunk 契约把用量嵌在 chunk.usage(TokenUsage);兼容直接
160
- // 平铺在 chunk 上的旧形态。归一放在这里,下面只需认平铺形状——与
161
- // dream.js 的 streamText 同一处理。
162
- if (chunk.type === "usage") {
163
- const u = chunk.usage ?? chunk;
164
- const i = u.input_tokens ?? u.inputTokens ?? u.prompt_tokens ?? u.promptTokens;
165
- const o = u.output_tokens ?? u.outputTokens ?? u.completion_tokens ?? u.completionTokens;
166
- if (Number.isFinite(i)) inputTokens = i;
167
- if (Number.isFinite(o)) outputTokens = o;
168
- }
169
- if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
170
- writeAudit("error", describeStreamFailure(chunk.reason) || "llm stream aborted or errored", relatedMemoryIds);
171
- return undefined;
172
- }
173
- }
174
- // 第三参必须显式传进去:writeAudit 的形参默认值 [] 是给"没有 id 可归因"
175
- // 留的,省略第三参等于宣称"这条成功与任何记忆无关"——实测活库 6256 条
176
- // success 行 100% 空数组,正是这个省略造成的。#405 只修了 error 半边。
177
- writeAudit("success", null, relatedMemoryIds);
178
- return text;
179
- })().catch((err) => {
180
- logger?.warn?.(`[dsh-mneme] entity extraction llm stream failed: ${String(err)}`);
181
- writeAudit("error", String(err?.message ?? err), relatedMemoryIds);
182
- return undefined;
183
- });
184
- };
185
- let text = await tryStream(true);
186
- if (text === undefined && effort) {
187
- // Mirror dream's effort fallback: a provider rejecting the reasoning
188
- // effort must not sink the whole extraction — retry once without it.
189
- logger?.warn?.(`[dsh-mneme] entity extraction: reasoningEffort "${effort}" rejected, retrying without it`);
190
- text = await tryStream(false);
191
- }
192
- return text;
193
- };
194
- }
195
-
196
- // backfillMissingEmbeddings 已随语义装配整体搬至 src/semantic.js(纯搬移);
197
- // 保留 barrel 再出口 —— test/reindex-backfill.test.js 仍从本模块 import,调用方零改动。
198
- export { backfillMissingEmbeddings } from "./semantic.js";
199
-
200
- export const apply = (ctx, config) => {
201
- const rawCfg = Config(config);
202
-
203
- // Resolve memoryDir: expand leading "~"
204
- const memoryDir = rawCfg.memoryDir.startsWith("~")
205
- ? join(homedir(), rawCfg.memoryDir.slice(1))
206
- : rawCfg.memoryDir;
207
- mkdirSync(memoryDir, { recursive: true });
208
-
209
- // #218 末楼的实际事故:profile 被清理,落在其中的记忆库连带丢失。命中
210
- // profile 作用域时给一条可操作的告警(只提示、不搬库、不加配置键),启发
211
- // 式的边界与取舍见 src/placement.js 文件头。
212
- const profileScope = findProfileScopeSegment(memoryDir);
213
- if (profileScope) {
214
- ctx.logger?.warn?.(
215
- `[dsh-mneme] memoryDir is inside a profile scope ("${profileScope}"): ${memoryDir} — the library may be lost when the host cleans or migrates profiles. Prefer a location outside profiles/ (default: ~/.dsh/memory; config key: memoryDir).`
216
- );
217
- }
218
-
219
- const store = createStore(join(memoryDir, "memory.db"));
220
-
221
- // User-configurable settings (profile, rules, panel mode, standalone API
222
- // token) share the same SQLite file in dedicated tables, isolated from
223
- // memories. Created before the config is finalized: the persisted
224
- // panel_mode participates in light-mode resolution below.
225
- const settings = createSettings(store.db);
226
-
227
- // Light mode (v0.7.12): the bundle config flag OR a persisted panel_mode of
228
- // "light" (the panel switch wins over the bundle config so it survives
229
- // config redeploys). applyLightModePreset turns every heavy background /
230
- // semantic feature off and keeps the core loop (autoInject, autoSummarize,
231
- // hot memory, quality filter).
232
- const lightMode = rawCfg.lightMode === true || settings.getPanelMode() === "light";
233
- // 功能开关合并顺序即优先级:用户显式开关(feature_flags kv,面板写入)>
234
- // 轻量预设(applyLightModePreset 批量置关的重型能力)> bundle 配置。预设必须
235
- // 先应用、用户开关后展开,否则 LIGHT_MODE_OFF 会把用户显式打开的开关再次
236
- // 压掉。合并结果只作用于本次启动:面板改开关后与 panel_mode 一样在下次
237
- // 启动生效。
238
- // 嵌套对象开关按首个点号拆开(kv 里平铺存的 "memoryQualityFilter.enabled" →
239
- // cfg.memoryQualityFilter.enabled),点号键不原样留在 cfg 顶层属性里。
240
- const flags = settings.getFeatureFlags();
241
- const flatFlags = {};
242
- const nestedFlags = {};
243
- for (const [key, value] of Object.entries(flags)) {
244
- const dot = key.indexOf(".");
245
- if (dot > 0) {
246
- const objKey = key.slice(0, dot);
247
- const subKey = key.slice(dot + 1);
248
- nestedFlags[objKey] = { ...(nestedFlags[objKey] ?? {}), [subKey]: value };
249
- } else {
250
- flatFlags[key] = value;
251
- }
252
- }
253
- const cfg = { ...applyLightModePreset({ ...rawCfg, lightMode }), ...flatFlags };
254
- for (const [objKey, sub] of Object.entries(nestedFlags)) {
255
- cfg[objKey] = { ...(cfg[objKey] ?? {}), ...sub };
256
- }
257
- // #622:错峰时段串(summarizePeakHours / dreamPeakHours)解析失败 = 静默未配置
258
- // (parsePeakSpec 的契约:排程是省钱手段,绝不该因写错格式停掉蒸馏/巩固)。但
259
- // 「不救错」不等于「不吭声」——配置写错的唯一症状是功能悄悄没生效(#424 的排查
260
- // 只能靠「高峰期还在巩固」倒推)。这里对**非空**且解析为 null 的键各出一条 warn:
261
- // 空串是合法的未配置,不告警(否则每个没配错峰的用户开机都被警告一遍)。helper
262
- // 只转述 parsePeakSpec:等价类归一(PR #621,评审中)落地后,救得了的写法自然
263
- // 不再走到这。行为零变化:只 log,不改 cfg。
264
- for (const [key, spec] of Object.entries(invalidPeakSpecs(cfg))) {
265
- ctx.logger?.warn?.(
266
- `[dsh-mneme] ${key} "${spec}" 不是合法的高峰时段串,将按未配置处理(错峰不生效、全天候运行)。` +
267
- `写法:09:00-18:00,多段用逗号分隔,可带星期前缀(mon-fri 08:00-12:00),支持跨零点;24:00 越界请写 00:00`
268
- );
269
- }
270
- const sessionGate = createSessionGate(cfg);
271
-
272
- // 失败表的启动期清理也放在装配之后(此前它在 createStore 之后、装配之前,写死
273
- // 90 天)。保留期现在是显式配置键 reflectionFailureRetentionDays;等它进面板
274
- // 白名单的那天,这里读的就是装配后的值,不用再改第二处。best-effort 语义不变:
275
- // 清理失败不挡启动,否则一次锁表就能让插件起不来——但「不挡启动」不等于「可以
276
- // 悄悄失败」:这一族的 recall_runs 清理当年就是静默蒸发,表涨到两万余行无人
277
- // 察觉,所以本处与 llm_audit / distill_records / recall_runs 三处一样出声。
278
- try {
279
- const failureRetentionDays = Number.isInteger(cfg.reflectionFailureRetentionDays)
280
- ? cfg.reflectionFailureRetentionDays
281
- : 90;
282
- store.deleteOldFailures(new Date(Date.now() - failureRetentionDays * 86400000).toISOString());
283
- } catch (err) {
284
- ctx.logger?.warn?.(`[dsh-mneme] failure-table retention purge failed: ${String(err)}`);
285
- }
286
-
287
- // Bug8 的启动期清理放在装配之后:面板把 llmAudit.* 写进 kv、经 nestedFlags 合进
288
- // cfg,而写入侧(dream / summarize / 写入准入)读的都是装配后的 cfg——清理若读
289
- // rawCfg,面板改了保留期它不认,更糟的是「开不开审计」与写入侧可能取到不同的值
290
- // (raw 说关 → 不清理,cfg 说开 → 照写,审计表就无保留期地长)。保留期默认 90 天、
291
- // 失败只 warn,与失败表清理同款:账本清理绝不许挡住插件启动。
292
- try {
293
- if (cfg.llmAudit?.enabled !== false) {
294
- const retentionMs = Number.isInteger(cfg.llmAudit?.retentionDays) ? cfg.llmAudit.retentionDays : 90;
295
- store.deleteOldLlmAudits(new Date(Date.now() - retentionMs * 86400000).toISOString());
296
- }
297
- } catch (err) {
298
- ctx.logger?.warn?.(`[dsh-mneme] llm_audit retention purge failed: ${String(err)}`);
299
- }
300
-
301
- // #399 的启动期裁剪:与上面同款(读装配后的 cfg,理由同上——面板写的保留期必须
302
- // 与写入侧取到同一个值)。不同的一点:蒸馏原文留存默认关,而「默认关」在写入侧
303
- // 的含义就是零写入,所以这里也只在 enabled === true 时才动手——否则一张空表被
304
- // 每次启动扫一遍,白付一次全表 DELETE 的代价。只裁 distill_records;链接
305
- // (memory_provenance)保留,指向空区间即为「来源已过期」。
306
- try {
307
- if (cfg.distillRecords?.enabled === true) {
308
- const retentionMs = Number.isInteger(cfg.distillRecords?.retentionDays)
309
- ? cfg.distillRecords.retentionDays
310
- : 90;
311
- store.clearDistillRecords(new Date(Date.now() - retentionMs * 86400000).toISOString());
312
- }
313
- } catch (err) {
314
- ctx.logger?.warn?.(`[dsh-mneme] distill_records retention purge failed: ${String(err)}`);
315
- }
316
-
317
- // recall_runs 的滚动清理(v0.7.0 原设计,2026-10-14 找回):与 llm_audit 同策略,
318
- // 按 cfg.recallRetentionDays(默认 90 天)启动时清一次。读装配后的 cfg,理由与上面
319
- // 两处一致 —— 写入侧取的是 cfg,清理侧若取 rawCfg,面板改的保留期就不生效。
320
- // 这一对曾在 dd0ed78(v0.7.12 发布提交)里被整块弄丢,配置键与文档却一直留着,
321
- // 实测 2026-10-13 那天从 16,041 涨到 20,510 行(一天四千余行,只增不减;
322
- // 单给一个静态行数会随表腐坏,故带日期的区间才是可核对的说法)。注入行开始写真实 query 之后,这条从「表膨胀」
323
- // 升级为「原话无限期留存」,所以必须与那次改动同批交付。
324
- // 保留期语义是「天」(函数内部乘 86400000),清理失败只 warn,绝不挡插件启动。
325
- try {
326
- const recallRetentionDays = Number.isInteger(cfg.recallRetentionDays) ? cfg.recallRetentionDays : 90;
327
- store.purgeRecallRunsOlderThan(recallRetentionDays);
328
- } catch (err) {
329
- // 这里必须真的出声。这一对当初是**静默**蒸发的:函数与调用一起没了、没人发现,
330
- // 表就这么涨到两万余行。清理再失败一次而仍然不出声,同样的回归照样看不见——
331
- // 「只 warn」不是修辞,是这族清理唯一的事后线索。
332
- ctx.logger?.warn?.(`[dsh-mneme] recall_runs retention purge failed: ${String(err)}`);
333
- }
334
-
335
- // 记忆语言(memory.language):本实例逐层传入 inject / summarize / dream /
336
- // sleep / mirror,多实例(如 agent preset 内挂载)互不影响。
337
- const mirror = createMirror(memoryDir, langOf(cfg));
338
-
339
- // #296 第二批:managed 文档目录。解析与 memoryDir 同一套(空 = 跟随 memoryDir 的
340
- // <memoryDir>/documents/,~ 展开,相对路径落在 memoryDir 下)。索引对象总是建:
341
- // 闸关时它只承担「清掉陈旧 index.md」这一件事,与 documents.md 在闸关时被镜像删
342
- // 掉同一口径。建目录与写索引失败都只 warn,不阻断插件加载(它们是机器产物)。
343
- const documentDir = resolveDocumentDir(memoryDir, cfg.documentDir);
344
- const documentIndex = createDocumentIndex(documentDir, langOf(cfg));
345
- if (cfg.documentMemoryEnabled === true) {
346
- const ensured = documentIndex.ensure();
347
- if (!ensured.ok) {
348
- ctx.logger?.warn?.(`[dsh-mneme] documentDir is not writable: ${documentDir}: ${ensured.error}`);
349
- }
350
- // 启动即渲染一次:索引不该等到第一次业务写才存在(删掉它之后重启也能自愈)。
351
- const synced = documentIndex.sync(store.list({ type: "document", limit: null }));
352
- if (!synced.ok) ctx.logger?.warn?.(`[dsh-mneme] document index sync failed: ${synced.error}`);
353
- } else {
354
- documentIndex.remove();
355
- }
356
-
357
- // 写入准入实例(#254)。两个开关 writeAdmission.enabled / .enforce 都默认关;
358
- // 关时它的返回与只计量那一阶段逐字段一致,写入路径完全不碰。既有的
359
- // llmAudit.enabled 关掉时它同样既不判也不写(那个开关连审计行的启动期清理一起
360
- // 关掉,在无保留期的表里按写入频次增长是不能接受的)。
361
- //
362
- // sensitiveScan 是密钥 / PII 那一类判据的注入点。按 #254 验收第 4 条它是 #164 A2
363
- // 的判据来源,现在由 src/sensitive-scan.js 实现(维护者 09-28 把 A2 的认领转给
364
- // 本侧)。这里只做接线:判据开不开由它自己的键 sensitiveScanEnabled 决定,工厂在
365
- // 关时返回 null,闸门的行为就与 #332 合并时逐字段一致(那一版根本没有这个函数);
366
- // 命中之后是仅告警还是真拦,仍是 writeAdmission.enforce 的事(#164 口径:
367
- // 默认仅告警、拦截 opt-in),判据不碰决策。
368
- const writeAdmission = createWriteAdmission({
369
- store,
370
- config: cfg,
371
- logger: ctx.logger,
372
- sensitiveScan: createSensitiveScan({ config: cfg })
373
- });
374
- const service = createService({ store, mirror, config: cfg, logger: ctx.logger, documentIndex, writeAdmission });
375
-
376
- // F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
377
- // a safe re-render at boot so a stale mirror converges without needing a
378
- // business write. Bounded: single attempt; on failure dirty stays for the
379
- // next boot. Never throws.
380
- service.recoverMirror();
381
-
382
- // Recall-layer receipt: when searchMemories runs with recordRecall=true, the
383
- // retrieval scene (query/mode/topK/threshold + candidates) is persisted to
384
- // recall_runs for audit/replay — the sibling of the dream_runs judgment trail.
385
- // Best-effort: a failed recall write must never break the search.
386
- service.setRecallRecorder((recall) => {
387
- try {
388
- store.saveRecallRun({
389
- query: recall.query,
390
- mode: recall.mode,
391
- topK: recall.topK,
392
- threshold: recall.threshold ?? null,
393
- candidates: recall.candidates ?? [],
394
- created_at: recall.createdAt
395
- });
396
- } catch { /* non-fatal: recall recording is bookkeeping */ }
397
- });
398
-
399
- // Semantic pipeline: a local/ollama embedder when configured, otherwise the
400
- // legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
401
- // the store's embedding column and tracks the active model fingerprint. A
402
- // slow embedder init (model download) never blocks plugin boot — failures
403
- // degrade to keyword search.
404
- const vectorIndex = createVectorIndex({ store, logger: ctx.logger });
405
- service.setVectorIndex(vectorIndex);
406
-
407
- // Human edits in mirror files win on every sync; merge them back first.
408
- // TYPE_FILE maps each memory type to its mirror filename. Read every type's
409
- // edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
410
- // a per-type read-then-merge loop would overwrite edits in files not yet read
411
- // (e.g. preferences.md merging would clobber unsynced projects.md edits).
412
- const humanEdits = new Map();
413
- for (const type of Object.keys(TYPE_FILE)) {
414
- humanEdits.set(type, mirror.readHumanEdits(type));
415
- }
416
- const applyHumanEdits = () => {
417
- for (const [type, edits] of humanEdits) {
418
- if (edits.length) service.mergeHumanEdits(type, edits);
419
- }
420
- };
421
-
422
- // embedder/reranker/boot 回填装配已整体搬至 src/semantic.js(纯搬移,宿主与 daemon
423
- // 共用):调用时序(applyHumanEdits 在各分支的触发点、#118 重试、autoReindexOnBoot)
424
- // 原样保留在 createSemantic 内部,这里只拿引用。init 全失败的 embedder 引用仍会进入
425
- // dream/sleep 的 semantic 面 —— 与搬移前一致:检索侧 setEmbedder(null) 降级关键词。
426
- const semantic = createSemantic({
427
- store, service, settings, cfg, logger: ctx.logger, vectorIndex, applyHumanEdits, lightMode
428
- });
429
- const { embedder, reranker } = semantic;
430
-
431
- // Custom commands: register persisted commands into the DSH command registry
432
- // on boot; add/remove re-register live through the API.
433
- let commands = null;
434
- // M.O. 执行记账器:默认 null(旗标关),开了才装配。
435
- let recorder = null;
436
- if (ctx.commands) {
437
- commands = createCommandManager({ ctx, settings, logger: ctx.logger, language: langOf(cfg) });
438
- commands.sync();
439
- }
440
-
441
- // M.O. 执行记账(v0.9.0 第二批):`/mo-run` 显式开一次执行窗口,窗口内的命令执行
442
- // 才可能被记成一条 run。挂的是 `session/event`——本仓已有三处消费者(continuity /
443
- // summarize / api),不是新引入的宿主依赖。
444
- //
445
- // 为什么必须显式开窗:工作流的 trigger 是人话,拿它去猜「用户这句话是不是要开始走
446
- // 某条流程」只能猜错,而猜错会把一条从没跑过的流程记成跑过。所以窗口边界由人的
447
- // 显式动作划,**没开窗时一个字都不记**。
448
- //
449
- // 门控:默认关(actionRunEnabled),并且它挂热路径,轻量档也关。
450
- if (cfg.actionRunEnabled === true) {
451
- recorder = createExecutionRecorder({ ctx, logger: ctx.logger, sessionGate });
452
- const disposeMoRun = registerMoRunCommand({
453
- ctx,
454
- recorder,
455
- enabled: true,
456
- language: () => langOf(cfg)
457
- });
458
- if (disposeMoRun) {
459
- disposers.push(disposeMoRun);
460
- } else {
461
- // 注册不上(宿主没有命令注册表)时别留一个只开着窗口却没人能操作的记录器。
462
- recorder.dispose();
463
- recorder = null;
464
- ctx.logger?.warn?.("[dsh-mneme] actionRunEnabled=true 但宿主没有命令注册表,M.O. 记账未启用");
465
- }
466
- }
467
-
468
- // 「Agent 眼中的你」自动事实(2026-11-06,opt-in:profileFactsEnabled):dream 巩固
469
- // 轮成功后的逐卡维护 + 面板手动轮(api.js)+ 注入事实段(factsProvider 传注入器与
470
- // 预览路由)。装配与开关无关——闸在消费侧(注入渲染 / 自动轮判空 / 路由 409),
471
- // 与 profileExtractor 同款:503 只该指装配缺失,不该被读成「用户没开」。
472
- // 位置必须在 dream 调度器之前:onRun 的 fire-and-forget 收尾要引用它。
473
- const profileFacts = createProfileFacts({ ctx, service, store, config: cfg, logger: ctx.logger });
474
-
475
- // Dream scheduler: automatic consolidation + summary runs, triggered by
476
- // store growth. Writes through the service fire the dream hook, which asks
477
- // the scheduler to (re)schedule a run once absolute and since-last-run
478
- // thresholds are both exceeded. onRun is deferred through `dream` so the
479
- // closure sees the assigned scheduler; the null guard keeps a run safe even
480
- // if the hook fires before assignment or after dispose.
481
- let dream = null;
482
- if (cfg.autoDream) {
483
- dream = createDreamScheduler({
484
- thresholdCount: cfg.dreamThresholdCount,
485
- thresholdChars: cfg.dreamThresholdChars,
486
- delayMs: cfg.dreamDelayMs,
487
- minIntervalMs: (cfg.dreamMinIntervalMinutes ?? 0) * 60000,
488
- // Issue #292:连续失败指数退避(opt-in,默认关 = 行为不变)。
489
- failureBackoff: cfg.autoDreamFailureBackoff === true,
490
- logger: ctx.logger,
491
- semantic: { embedder, vectorIndex },
492
- lastRunAtSeed: store.lastDreamRunAt("auto"),
493
- // Issue #89(基线半边):阈值基线同样跨重启恢复——只恢复 lastRunAt 的话,
494
- // 重启后的第一条写入仍会绕过阈值(库里记忆多于阈值时 overBase 恒成立)开跑
495
- // 整轮。种子取上次成功轮的库规模,口径与运行期刷新一致;读不到时调度器退回
496
- // 零基线(= 升级前行为)。
497
- baselineSeed: store.lastDreamBaseline("auto"),
498
- // Issue #239(第 4 项)镜像到巩固:高峰期不做梦,顺延到最近的高峰结束时刻。
499
- peakHours: cfg.dreamPeakHours ?? "",
500
- peakMaxDeferMinutes: cfg.dreamPeakMaxDeferMinutes ?? 120,
501
- // 跳过时的审计行在这里落地(调度器只拿到 service,拿不到 config 的
502
- // llmAudit 开关与巩固模型路由)。口径与 runAuditedLlm 一致:审计关掉就
503
- // 不写;写失败只 warn,绝不反噬调度(CONTRIBUTING 的 fail-safe 硬约定)。
504
- auditPeakSkip: ({ count, chars }) => {
505
- if (cfg?.llmAudit?.enabled === false || typeof service?.saveLlmAudit !== "function") return;
506
- const modelId = cfg.dreamProvider && cfg.dreamModel ? `${cfg.dreamProvider}:${cfg.dreamModel}` : "";
507
- service.saveLlmAudit({
508
- timestamp: new Date().toISOString(),
509
- trigger_source: "autoDream",
510
- operation_type: "dream_consolidate",
511
- model_id: modelId,
512
- input_tokens: 0,
513
- output_tokens: 0,
514
- total_tokens: 0,
515
- cost_usd: 0,
516
- duration_ms: 0,
517
- status: "skipped",
518
- error_message: "peak-hours",
519
- related_memory_ids: [],
520
- // 观测用:跳过时窗口里积了多少(阈值继续累积,不是丢弃)。
521
- metadata: JSON.stringify({ count, chars })
522
- });
523
- },
524
- onRun: () => {
525
- const run = dream
526
- ? dream.runDream(ctx, service, cfg)
527
- : Promise.resolve({ ok: true, skipped: true });
528
- // 画像事实自动维护:挂在巩固轮之后 fire-and-forget。刻意不并进 onRun 的
529
- // promise——profileFacts 的失败不能被计入 dream 的失败退避
530
- // (autoDreamFailureBackoff 的口径是巩固轮自身);逐卡经 service.enqueue
531
- // 串行,不与 sleep/下一轮巩固抢模型。单卡失败只 warn,账本已有 failed 回执。
532
- run.then(async (rep) => {
533
- if (cfg.profileFactsEnabled !== true || (rep && rep.ok === false)) return;
534
- for (const key of profileFacts.autoPassCandidates()) {
535
- try {
536
- await service.enqueue(() => profileFacts.runFor(key, { trigger: "auto" }));
537
- } catch (error) {
538
- ctx.logger?.warn?.(`[dsh-mneme] profile-facts auto pass failed for card "${key || "default"}": ${error?.message ?? error}`);
539
- }
540
- }
541
- }).catch(() => { /* dream 自身失败走调度器退避,这里不做第二遍日志 */ });
542
- return run;
543
- }
544
- });
545
- service.setDreamHook(() => dream.maybeSchedule(service));
546
- }
547
-
548
- // Sleep scheduler (v0.4.0): idle-triggered deep maintenance. Fires when the
549
- // store has been quiet for sleepIdleMinutes and re-arms on every write via
550
- // noteWrite (hooked to the service's write path). Runs go through
551
- // service.enqueue so they serialize with autoDream — the two never overlap.
552
- // Abortable on user activity; audited into dream_runs with run_type='sleep'.
553
- let sleep = null;
554
- if (cfg.sleepModeEnabled) {
555
- sleep = createSleepScheduler({
556
- service,
557
- config: cfg,
558
- logger: ctx.logger,
559
- lastRunAtSeed: store.lastDreamRunAt("sleep"),
560
- onRun: (signal) => (sleep ? runSleep(ctx, service, cfg, ctx.logger, { embedder, vectorIndex }, signal) : Promise.resolve({ ok: true, skipped: true }))
561
- });
562
- service.setSleepHook(() => sleep.noteWrite());
563
- }
564
-
565
- // Entity gene extraction (v0.3.0): wire the extractor into the service as a
566
- // hook so saveWithDedupe can fire-and-forget an extraction pass on fresh
567
- // writes. The service never sees ctx.llm — index.js adapts it here into the
568
- // callLLM(messages, options) => Promise<string> contract the extractor
569
- // expects, reusing the same ctx.llm.stream consumption pattern as dream.js.
570
- // Explicit opt-in only (entityExtractionEnabled defaults to false); any LLM
571
- // failure degrades inside the extractor to { ok:false }, never a write error.
572
- if (cfg.entityExtractionEnabled) {
573
- if (!ctx.llm) {
574
- // Issue #108: an enabled-but-unwired extractor failed silently before —
575
- // zero entities, zero llm_audit_logs, no log line anywhere. Make the
576
- // missing dependency visible so a user can tell "extractor not installed"
577
- // from "extraction failed".
578
- ctx.logger?.warn?.("[dsh-mneme] entityExtractionEnabled=true but ctx.llm unavailable — entity extractor NOT installed");
579
- } else {
580
- const streamEntityText = createEntityStreamAdapter({
581
- llm: ctx.llm,
582
- agentDefaultModel: ctx.agentDefaultModel,
583
- logger: ctx.logger,
584
- // Issue #250: the adapter needs service to write its llm_audit_logs row
585
- // (it never had it) and config for the shared llmAudit.enabled gate.
586
- service,
587
- config: cfg
588
- });
589
- service.setEntityExtractor((memory) =>
590
- extractEntities(memory, { store, config: cfg, callLLM: streamEntityText, logger: ctx.logger })
591
- .catch((err) => {
592
- ctx.logger?.warn?.(`[dsh-mneme] entity extraction failed: ${String(err)}`);
593
- return { ok: false, error: String(err) };
594
- })
595
- );
596
- }
597
- }
598
-
599
- const disposers = [];
600
-
601
- try {
602
- const disposeSessionGate = ctx.on?.("session/event", (session) => sessionGate.observe(session));
603
- if (typeof disposeSessionGate === "function") disposers.push(disposeSessionGate);
604
- } catch {
605
- // 宿主没有该事件钩子时,注入、工具与蒸馏消费点仍直接检查当前 Session。
606
- }
607
-
608
- // Issue #380:注入前判定闸门。判定 LLM 走 entity adapter 同款装配(ctx.llm +
609
- // agentDefaultModel 回退),gate 自持 enabled/usable 判定(未启用/llm 缺席/
610
- // llmAudit 关闭时内部恒 off,注入行为与现状逐字节一致)。
611
- const preInjectGate = createPreInjectGate({
612
- llm: ctx.llm,
613
- agentDefaultModel: ctx.agentDefaultModel,
614
- service,
615
- config: cfg,
616
- logger: ctx.logger
617
- });
618
- disposers.push(() => preInjectGate.clear());
619
-
620
- // #118: never let a pending embedder init retry fire after unload and touch
621
- // a torn-down context.
622
- disposers.push(() => semantic.dispose()); // #118 重试计时器 + boot 回填计时器(搬入 semantic.js 后由它自持)
623
-
624
- // chorography M3:地志提纲折进 memory context(不新开 context——宿主侧同 order 无
625
- // 兜底排序,多一块就多一份拼装抖动;spec §7 也要求「合并成一次拼装」)。档位由
626
- // chorographyInject 决定,off / 未匹配到仓库 / 渲染失败一律返回空串,宿主会过滤
627
- // 掉空文本,整块消失且不占位。
628
- //
629
- // 预算是 0:注入块只在 memory context 里多几行,本身不做检索、不 fork git——
630
- // 真过期判定留给 /choro(P29)。所以这里不需要额外的「热路径」闸,chorographyEnabled
631
- // 与 chorographyInject 两个开关在 resolveInjectTier 里已经判完。
632
- const choroInject = createChoroInject({
633
- choro: createChoroQueries({ store }),
634
- config: cfg,
635
- logger: ctx.logger,
636
- language: () => langOf(cfg)
637
- });
638
- disposers.push(() => choroInject.dispose());
639
-
640
- ctx.inject(["systemPrompt"], (promptCtx) => {
641
- if (cfg.autoInject) {
642
- disposers.push(createInjector(promptCtx, service, settings, cfg, preInjectGate, sessionGate, choroInject, profileFacts.factsProvider));
643
- }
644
- });
645
-
646
- // #249 N3:压缩边缘双落点。触发靠宿主自己落的压缩事件(订阅 + pre-step 追加),
647
- // 不需要 systemPrompt / tools 的任何能力,所以不塞进上面的 inject 回调;父/子
648
- // 闸门在挂载点判一次,与 inject.js 共用同一个 injectChildEnabled 判据。
649
- if (cfg.autoInject && injectChildEnabled(cfg, "continuityRescueEnabled")) {
650
- disposers.push(createContinuityRescue(ctx, store, sessionGate));
651
- }
652
-
653
- // 知识库第二层(契约 §3.4):装配与开关无关(createKb 只建对象,读侧零 IO),闸在
654
- // 路由与工具两层——这样「没开」(409)与「没装配」(503)在对外两面都是可区分的。
655
- // 位置必须在 tools/api 两处消费点之前:cordis 的 inject 回调可能在任一之后才触发,
656
- // 在消费点里现建就会造出第二份实例(两份缓存、两套 dispose)。
657
- const kb = createKb({ store, embedder, cfg });
658
- disposers.push(() => kb.dispose?.());
659
-
660
- ctx.inject(["tools"], (toolsCtx) => {
661
- // choro(M1):只读查询对象,chorographyEnabled 关着时工具压根不注册(闸在
662
- // createTools 的 hiddenTools,与 organizeEnabled 同款反向语义)。
663
- disposers.push(createTools(toolsCtx, service, cfg, embedder, sessionGate, createChoroQueries({ store }), kb));
664
- });
665
-
666
- // chorography M3:/choro 只读回执。与 /mo-run 同形(ctx.commands.register),
667
- // 但这里**不持有任何状态**、不写盘、不重建——spec §2 D6「只手动触发,过期只提示」
668
- // 决定了这条命令只能看,不能改。位置必须在 disposers 声明之后(同 actionNudge 的
669
- // TDZ 注释),否则插件加载时会直接抛 ReferenceError。
670
- if (cfg.chorographyEnabled === true) {
671
- const disposeChoro = registerChoroCommand({
672
- ctx,
673
- choro: createChoroQueries({ store }),
674
- config: cfg,
675
- enabled: true,
676
- language: () => langOf(cfg)
677
- });
678
- if (disposeChoro) disposers.push(disposeChoro);
679
- else ctx.logger?.info?.("[dsh-mneme] chorographyEnabled=true 但宿主没有命令注册表,/choro 未注册");
680
- }
681
-
682
- const summarizer = createSummarizer(ctx, service, cfg, {}, sessionGate);
683
- disposers.push(summarizer.dispose);
684
-
685
- // QQ 日记本(v0.13,diaryEnabled):构造期零副作用(只闭包住依赖,不建目录、不读盘、
686
- // 不调模型),所以位置可以放在这里——它必须早于下面的 createApi,面板的
687
- // 「写一篇」按钮要转调同一个 composer;而自动触发侧在 webServer 块之后挂(同
688
- // actionNudge 的 TDZ 理由)。关闸时 composer 是 null:路由回 409、座位不注册,
689
- // 与「装配缺失」的 503 严格区分(#497 先例)。
690
- const diary = cfg.diaryEnabled === true
691
- ? createDiaryComposer({ ctx, config: cfg, logger: ctx.logger, service, settings, memoryDir })
692
- : null;
693
-
694
- // #275 存储生命周期第一批:无损回收的手动入口。刻意不挂启动路径、不接定时器——
695
- // 这一步是不可逆的内容丢弃,只由人显式触发(面板的维护卡、`dsh-mneme reclaim`)。
696
- // 实例建在插件进程内,是为了让 CLI 也能走这条路:VACUUM 要排他锁,跟宿主抢锁的
697
- // 那条路走不通。
698
- // 位置在 webServer 块之前:面板的 in-process API(createApi)与 standalone 数据面
699
- // 都要用它,而 webServer 是可选的——headless 宿主不注册面板路由,构造点若留在块后
700
- // 就永远晚于 createApi(#386 第 4 条)。构造期无副作用(只返回 { reclaim }),前移安全。
701
- const maintenance = createMaintenance({ store, config: cfg, logger: ctx.logger });
702
-
703
- // webServer 可选依赖:cordis 4 的 ctx 是 Proxy,直接访问未在 inject 声明的
704
- // 属性会抛 "cannot get property without inject"(不会返回 undefined),所以
705
- // 不能用 if (ctx.webServer) 守卫。ctx.reflect.get 是 cordis 提供的免 inject
706
- // 读取(未提供时返回 undefined);对象字面量 mock ctx(测试)没有 reflect,
707
- // 退回直接属性访问。headless/无 UI 宿主无 webServer 时跳过 API 注册,其余
708
- // 功能(工具/注入/dream)照常。
709
- const webServer = typeof ctx.reflect?.get === "function" ? ctx.reflect.get("webServer") : ctx.webServer;
710
- if (webServer) {
711
- // 画像提取草稿(#497):装配与开关无关——闸在路由那一侧(关着回 409 disabled,
712
- // 本模块一次都不会被调用)。这样「没启用」与「没装配」在 HTTP 面上是两个可区分的
713
- // 答案(503 unavailable 只该出现在旧宿主/装配缺失上,不该被读成「用户没开」)。
714
- const extractProfileDraft = createProfileExtractor({ ctx, service, config: cfg, logger: ctx.logger });
715
- // chorography 读侧(M1):与 service 刻意分开——所有权模型相反(spec §3)。
716
- // 闸在路由/工具两层(关着 409 / 不注册),queries 本体装配与开关无关。
717
- // 收尾档:choro 对象加两件 host 侧能力——embedder(向量惰性物化,P37)与
718
- // runT0Apply(面板重建按钮的写路径,P34;T1 不上 HTTP,CLI 仍是它的入口)。
719
- // T1 任务账随 choro 装配建立(与开关无关:开关在路由那侧判,没开时这些方法一次
720
- // 都不会被调到,符合「503=装配缺件 / 409=用户没开」的分叉)。
721
- const t1Jobs = createT1Jobs({ store, dbPath: join(memoryDir, "memory.db") });
722
- disposers.push(() => t1Jobs.dispose());
723
- const choro = {
724
- ...createChoroQueries({ store }),
725
- embedder,
726
- runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }),
727
- startT1Job: (args) => t1Jobs.start(args),
728
- t1JobStatus: (jobId) => t1Jobs.status(jobId),
729
- };
730
- // The semantic surface handed to the API is createSemantic()'s own return
731
- // value, plus the vectorIndex it does not carry. Passing the bare literal
732
- // `{ vectorIndex, reranker }` used to drop embeddingDiagnostics, so the
733
- // panel's `data.embeddings` was permanently null on a real install — the
734
- // typeof guard in api.js fell through every time, and no test noticed
735
- // because none asserted on `data.embeddings`.
736
- const api = createApi(ctx, service, settings, commands ?? {
737
- add: () => { throw new Error("commands unavailable"); },
738
- remove: () => false,
739
- list: () => []
740
- }, embedder, { ...semantic, vectorIndex, reranker }, cfg.apiToken, cfg, maintenance, extractProfileDraft, choro, profileFacts, kb, diary);
741
- disposers.push(api.dispose);
742
-
743
- // dsh-go 原生面板:只有宿主提供 dshGo 服务(dsh-go 手机 App)时才挂载。
744
- // 动态 import 是刻意的——静态导入会让「没有桥就整块不加载」这句承诺落空。
745
- // cfg.apiToken 一并交给面板:createApi 给 token / token-write 路由包了鉴权,
746
- // 进程内调用不带令牌会被自家路由拦成 401,不为此在服务端开白名单。
747
- ctx.inject(["dshGo"], (dshCtx) => {
748
- import("./dsh-go/panel.js")
749
- .then((panel) => panel.mountDshGoPanel(dshCtx, { apiToken: cfg.apiToken }))
750
- .catch((error) => ctx.logger?.warn?.(`[dsh-mneme] dsh-go 面板未加载:${error?.message ?? error}`));
751
- });
752
- }
753
-
754
- // M.O. 自动沉淀(v0.9.3):项目里重复走的那套动作,自动变成 memoryDir 里的一条
755
- // 「提议」,等人确认后才可能进仓库。默认关(actionNudgeEnabled)。
756
- //
757
- // 为什么位置在这里而不是和 M.O. 记账同段:`disposers` 声明在 :532,而记账块在它
758
- // 之前(TDZ)——照抄过去会在插件加载时直接抛 ReferenceError。这里已在其后。
759
- //
760
- // 为什么挂 session/event 却又要进轻量档黑名单:它读的是 zstd 会话日志(同步解压),
761
- // 是本插件里最重的一条后台路径;且它只产「提议」,关掉不影响记忆本身的正确性。
762
- if (cfg.actionNudgeEnabled === true) {
763
- disposers.push(createNudgeAutoRun({ ctx, memoryDir, logger: ctx.logger, sessionGate }));
764
- }
765
-
766
- // 日记的触发侧(v0.13):turn/end 跨天判定 + 启动补写。
767
- //
768
- // 启动补写为什么在这里而不是 webServer 块里:它不依赖面板,headless 宿主也该补。
769
- // 补哪个项目只能靠状态文件里记过的项目根——宿主刚起来时没有任何会话,
770
- // 而「上次是哪个项目、哪天写的」就在那儿。补写不 await:启动路径上吊住一次
771
- // 模型调用,等于让整个插件加载等模型回来(本机 p90 五十秒级)。
772
- if (diary) {
773
- // diaryWriteEveryMinutes > 0 时额外起一根定时器(见 auto.js 的 tick):日记不该
774
- // 取决于「谁先开口」,整天不说话也要有一篇。listProjects 用 knownProjects ——
775
- // 定时器唤醒时没有会话,只能靠状态文件里记过的项目根。
776
- const everyMinutes = Number(cfg.diaryWriteEveryMinutes) || 0;
777
- const diaryAuto = createDiaryAutoRun({
778
- ctx,
779
- composer: diary,
780
- logger: ctx.logger,
781
- sessionGate,
782
- everyMs: everyMinutes > 0 ? everyMinutes * 60 * 1000 : 0,
783
- listProjects: () => diary.knownProjects()
784
- });
785
- disposers.push(diaryAuto);
786
- const projects = diary.knownProjects();
787
- if (projects.length) {
788
- Promise.resolve(diaryAuto.catchUp(projects)).catch((error) => {
789
- ctx.logger?.warn?.("[dsh-mneme] diary startup catch-up failed: " + String(error));
790
- });
791
- }
792
- }
793
-
794
- // Standalone external API (v0.7.12): plain node:http server for ecosystem
795
- // integrations outside the DSH host. Persisted external_api settings win
796
- // over the bundle config (enabled/port); the Bearer token lives in the same
797
- // kv and is auto-generated on first boot by createStandaloneApi. Binding a
798
- // non-loopback host is the operator's documented responsibility.
799
- if ((settings.getExternalApi?.()?.enabled ?? cfg.externalApiEnabled) === true) {
800
- // embedder 交给 /context 的查询嵌入(issue #370):与 service.setEmbedder 同一
801
- // 实例(semantic.js 装配产物),lightMode 下为 null,路由侧降级规则档。
802
- const standalone = createStandaloneApi({ service, store, config: cfg, logger: ctx.logger, settings, maintenance, embedder });
803
- disposers.push(() => standalone.server.close());
804
- standalone.ready.catch((error) => {
805
- ctx.logger?.warn?.(`[dsh-mneme] standalone API failed to start: ${String(error)}`);
806
- });
807
- }
808
-
809
- // Async disposer: cordis awaits the returned promise on unload (runDisposable),
810
- // so an in-flight dream run is allowed to finish before the SQLite store is
811
- // closed — dream.dispose() resolves only after its current run settles.
812
- return async () => {
813
- for (const dispose of disposers) {
814
- if (typeof dispose === "function") dispose();
815
- }
816
- commands?.dispose();
817
- recorder?.dispose();
818
- if (dream) await dream.dispose();
819
- if (sleep) sleep.dispose();
820
- store.close();
821
- };
822
- };
1
+ import { createStore } from "./store.js";
2
+ import { createMirror, TYPE_FILE } from "./mirror.js";
3
+ import { createDocumentIndex } from "./document-index.js";
4
+ import { resolveDocumentDir } from "./document.js";
5
+ // #368 v1:profile 作用域落点检测(只提示,不改行为)。
6
+ import { findProfileScopeSegment } from "./placement.js";
7
+ import { createService } from "./service.js";
8
+ // #254 写入准入(第一阶段只计量,不拦截):见 src/write-admission.js 的文件头。
9
+ import { createWriteAdmission } from "./write-admission.js";
10
+ // #164 A2:写入边界的密钥 / PII 判据,注入给上面的写入准入。
11
+ import { createSensitiveScan } from "./sensitive-scan.js";
12
+ import { createTools } from "./tools.js";
13
+ import { createInjector } from "./inject.js";
14
+ import { createPreInjectGate } from "./pre-inject-gate.js";
15
+ import { createContinuityRescue } from "./continuity.js";
16
+ import { createSummarizer } from "./summarize.js";
17
+ import { createSessionGate } from "./session-gate.js";
18
+ import { createDreamScheduler, describeStreamFailure } from "./dream.js";
19
+ import { createSleepScheduler, runSleep } from "./dream/sleep.js";
20
+ // #622 可观测性:错峰时段串解析失败是「静默未配置」(parsePeakSpec 契约不救错),
21
+ // 启动装配后对非空且解析失败的键出一条 warn——只补可见性,不改语义。
22
+ import { invalidPeakSpecs } from "./peak-hours.js";
23
+ import { createApi } from "./api.js";
24
+ import { createStandaloneApi } from "./api-standalone.js";
25
+ // #275 存储生命周期第一批:无损回收(手动入口,不挂启动路径)。
26
+ import { createMaintenance } from "./maintenance.js";
27
+ import { createChoroQueries } from "./wiki/queries.js";
28
+ // chorography M3:开局注入块与 /choro 只读回执。两者都只吃 queries 读侧,不写任何东西。
29
+ import { createChoroInject } from "./wiki/inject.js";
30
+ import { registerChoroCommand } from "./choro-command.js";
31
+ // 收尾档:面板重建按钮走 runChoroT0(P34;T1 不上 HTTP)。
32
+ import { runChoroT0 } from "./wiki/t0.js";
33
+ // T1 后台任务账(契约 §3.2):面板点「补语义正文」走 HTTP 起一个 spawn 子进程,
34
+ // 进程账不能住在 api.js(HTTP 面刻意不摸 child_process),装配点在这里。
35
+ import { createT1Jobs } from "./wiki/t1-jobs.js";
36
+ import { createSettings } from "./settings.js";
37
+ // 「Agent 眼中的你」自动事实(2026-11-06):逐卡事实清单 + dream 后自动维护轮。
38
+ import { createProfileFacts } from "./profile-facts.js";
39
+ // 知识库第二层(契约 §3.4):装配与开关无关(createKb 只建对象,读侧零 IO),
40
+ // 闸在路由与工具两层——这样「没开」与「没装配」在 HTTP 面上是两个可区分答案。
41
+ import { createKb } from "./kb/index.js";
42
+ import { createCommandManager } from "./commands.js";
43
+ import { createExecutionRecorder } from "./pipeline-runner.js";
44
+ import { registerMoRunCommand } from "./pipeline-command.js";
45
+ // M.O. 自动沉淀的触发侧(冷却 / 排队 / 兜底)。挖掘与落盘在 pipeline-nudge.js,
46
+ // 这里只管「什么时候跑」。两者分开的理由见 src/pipeline-nudge-auto.js 文件头。
47
+ import { createNudgeAutoRun } from "./pipeline-nudge-auto.js";
48
+ // QQ 日记本:合成管线(compose)与触发侧(auto)分开,理由见 src/diary/index.js 文件头。
49
+ import { createDiaryComposer, createDiaryAutoRun } from "./diary/index.js";
50
+ import { createVectorIndex } from "./vector-index.js";
51
+ // semantic(embedder/reranker/boot 回填)纯搬移至 src/semantic.js(PR2):宿主与
52
+ // daemon(dsh-mneme-serve,#363)共用同一套装配,原文件保留 barrel 出口。
53
+ import { createSemantic } from "./semantic.js";
54
+ import { Config, applyLightModePreset, injectChildEnabled } from "./config.js";
55
+ import { langOf } from "./lang.js";
56
+ import { extractEntities } from "./entities/extractor.js";
57
+ import { createProfileExtractor } from "./profile-extract.js";
58
+ import { mkdirSync } from "node:fs";
59
+ import { join } from "node:path";
60
+ import { homedir } from "node:os";
61
+
62
+ export const name = "dsh-mneme";
63
+ // webServer 在 inject 声明中:cordis 会等宿主 webServer 服务激活后才 apply 本
64
+ // 插件,保证 apply 时路由注册不落时序(v0.7.23 曾移出 inject 想支持 headless,
65
+ // 结果 cordis 不再等待,apply 时宿主 webServer 未就绪 → 桌面端 "cannot get
66
+ // property without inject" 崩溃)。守卫用 ctx.reflect.get(免 inject 读取,
67
+ // 未提供返回 undefined)而非 if (ctx.webServer):直接访问未注入属性在 cordis
68
+ // Proxy 下会抛错而非返回 undefined。
69
+ export const inject = ["tools", "systemPrompt", "llm", "agentDefaultModel", "commands", "webServer"];
70
+ export { Config };
71
+
72
+ // Arrow (not function declaration): cordis 4 treats any apply with a
73
+ // prototype as a class constructor (`new apply(...)`) and discards its return
74
+ // value, so a `function apply` disposer would never run on unload. An arrow
75
+ // has no prototype, is called normally, and its returned disposer is collected
76
+ // and run by the fiber on unload.
77
+ // Entity-extraction LLM adapter (issue #108/#109): maps the extractor's
78
+ // options (provider/model override + reasoningEffort) onto a real dsh-llm
79
+ // stream route and retries once without the effort when the first attempt is
80
+ // rejected. Extracted from apply() so the effort-fallback branch is
81
+ // unit-testable; the extractor only ever sees a callLLM(messages, options)
82
+ // => Promise<string>. The route always carries a real provider/model (dsh-llm
83
+ // GenerateOptions requires both) — never a bare stream.
84
+ //
85
+ // Issue #250: it also accounts for itself. Entity extraction is the background
86
+ // LLM path that runs on every memory write, yet it never reached
87
+ // llm_audit_logs — the adapter only ever held an llm handle, so it had no way
88
+ // to call service.saveLlmAudit. It now takes service (+ config for the shared
89
+ // llmAudit.enabled gate) and writes one row per stream attempt, matching the
90
+ // contract of runAuditedLlm in dream.js: a rejected effort attempt records its
91
+ // own error row and the retry records its own success row.
92
+ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger, service, config }) {
93
+ return async function streamEntityText(messages, options = {}) {
94
+ let route = {};
95
+ if (options.provider) route.provider = options.provider;
96
+ if (options.model) route.model = options.model;
97
+ if (!route.provider || !route.model) {
98
+ try {
99
+ const sel = agentDefaultModel?.currentSelection?.();
100
+ if (sel?.provider && sel?.model) {
101
+ route.provider ??= sel.provider;
102
+ route.model ??= sel.model;
103
+ }
104
+ } catch { /* fall through to whatever route we already have */ }
105
+ }
106
+ const effort = options.reasoningEffort;
107
+ // #405: 审计行的 related_memory_ids 此前恒空、error_message 又被压成一句常量,
108
+ // 于是「哪条记忆没抽到实体、为什么」在库里查不出来(主库 735 条 error 行全是
109
+ // 同一句话)。id 由 extractor 顺 options 传下来——它只认 memory.content,不认
110
+ // id。口径同 write-admission:350:拿不到 id 就留空,绝不挂不存在的 id 造虚账。
111
+ const relatedMemoryIds = options.memoryId ? [options.memoryId] : [];
112
+ // 没有解析出 provider/model 就没有可归属的模型——与 dream 的 resolveRoute
113
+ // 无路由早退同口径,那种情况不写审计行。
114
+ const modelId = route.provider && route.model ? `${route.provider}:${route.model}` : "";
115
+ const tryStream = (withEffort) => {
116
+ let text = "";
117
+ let inputTokens = 0;
118
+ let outputTokens = 0;
119
+ const startedAt = Date.now();
120
+ const timestamp = new Date(startedAt).toISOString();
121
+ // 记账是 best-effort:写审计行失败只 warn,绝不反噬抽取本身(CONTRIBUTING
122
+ // 的 fail-safe 硬约定)。
123
+ const writeAudit = (status, errorMessage, relatedMemoryIds = []) => {
124
+ if (config?.llmAudit?.enabled === false || typeof service?.saveLlmAudit !== "function") return;
125
+ // #372: modelId 为空 = 路由解析失败——此前整个 writeAudit 静默 return,
126
+ // llm.stream 抛错也留不下任何行,正是 #108 要消灭的「零实体零日志」同款盲区。
127
+ // 空路由无法记账,但至少要留一条可解释的 warn,别让失败面完全不可见。
128
+ if (!modelId) {
129
+ if (status !== "ok") logger?.warn?.(`dsh-mneme: entity extraction ${status} without audit row (llm route unresolved: ${errorMessage ?? "no route"})`);
130
+ return;
131
+ }
132
+ try {
133
+ service.saveLlmAudit({
134
+ timestamp,
135
+ trigger_source: "entityExtract",
136
+ operation_type: "entity_extract",
137
+ model_id: modelId,
138
+ input_tokens: inputTokens,
139
+ output_tokens: outputTokens,
140
+ total_tokens: inputTokens + outputTokens,
141
+ cost_usd: 0,
142
+ duration_ms: Date.now() - startedAt,
143
+ status,
144
+ error_message: errorMessage,
145
+ related_memory_ids: relatedMemoryIds
146
+ });
147
+ } catch (auditError) {
148
+ logger?.warn?.(`dsh-mneme: entity extraction llm audit write failed: ${String(auditError)}`);
149
+ }
150
+ };
151
+ return (async () => {
152
+ for await (const chunk of llm.stream({
153
+ ...route,
154
+ maxTokens: 4096,
155
+ ...(withEffort && effort ? { reasoningEffort: effort } : {}),
156
+ messages
157
+ })) {
158
+ if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
159
+ // DSH 的 StreamChunk 契约把用量嵌在 chunk.usage(TokenUsage);兼容直接
160
+ // 平铺在 chunk 上的旧形态。归一放在这里,下面只需认平铺形状——与
161
+ // dream.js 的 streamText 同一处理。
162
+ if (chunk.type === "usage") {
163
+ const u = chunk.usage ?? chunk;
164
+ const i = u.input_tokens ?? u.inputTokens ?? u.prompt_tokens ?? u.promptTokens;
165
+ const o = u.output_tokens ?? u.outputTokens ?? u.completion_tokens ?? u.completionTokens;
166
+ if (Number.isFinite(i)) inputTokens = i;
167
+ if (Number.isFinite(o)) outputTokens = o;
168
+ }
169
+ if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
170
+ writeAudit("error", describeStreamFailure(chunk.reason) || "llm stream aborted or errored", relatedMemoryIds);
171
+ return undefined;
172
+ }
173
+ }
174
+ // 第三参必须显式传进去:writeAudit 的形参默认值 [] 是给"没有 id 可归因"
175
+ // 留的,省略第三参等于宣称"这条成功与任何记忆无关"——实测活库 6256 条
176
+ // success 行 100% 空数组,正是这个省略造成的。#405 只修了 error 半边。
177
+ writeAudit("success", null, relatedMemoryIds);
178
+ return text;
179
+ })().catch((err) => {
180
+ logger?.warn?.(`[dsh-mneme] entity extraction llm stream failed: ${String(err)}`);
181
+ writeAudit("error", String(err?.message ?? err), relatedMemoryIds);
182
+ return undefined;
183
+ });
184
+ };
185
+ let text = await tryStream(true);
186
+ if (text === undefined && effort) {
187
+ // Mirror dream's effort fallback: a provider rejecting the reasoning
188
+ // effort must not sink the whole extraction — retry once without it.
189
+ logger?.warn?.(`[dsh-mneme] entity extraction: reasoningEffort "${effort}" rejected, retrying without it`);
190
+ text = await tryStream(false);
191
+ }
192
+ return text;
193
+ };
194
+ }
195
+
196
+ // backfillMissingEmbeddings 已随语义装配整体搬至 src/semantic.js(纯搬移);
197
+ // 保留 barrel 再出口 —— test/reindex-backfill.test.js 仍从本模块 import,调用方零改动。
198
+ export { backfillMissingEmbeddings } from "./semantic.js";
199
+
200
+ export const apply = (ctx, config) => {
201
+ const rawCfg = Config(config);
202
+
203
+ // Resolve memoryDir: expand leading "~"
204
+ const memoryDir = rawCfg.memoryDir.startsWith("~")
205
+ ? join(homedir(), rawCfg.memoryDir.slice(1))
206
+ : rawCfg.memoryDir;
207
+ mkdirSync(memoryDir, { recursive: true });
208
+
209
+ // #218 末楼的实际事故:profile 被清理,落在其中的记忆库连带丢失。命中
210
+ // profile 作用域时给一条可操作的告警(只提示、不搬库、不加配置键),启发
211
+ // 式的边界与取舍见 src/placement.js 文件头。
212
+ const profileScope = findProfileScopeSegment(memoryDir);
213
+ if (profileScope) {
214
+ ctx.logger?.warn?.(
215
+ `[dsh-mneme] memoryDir is inside a profile scope ("${profileScope}"): ${memoryDir} — the library may be lost when the host cleans or migrates profiles. Prefer a location outside profiles/ (default: ~/.dsh/memory; config key: memoryDir).`
216
+ );
217
+ }
218
+
219
+ const store = createStore(join(memoryDir, "memory.db"));
220
+
221
+ // User-configurable settings (profile, rules, panel mode, standalone API
222
+ // token) share the same SQLite file in dedicated tables, isolated from
223
+ // memories. Created before the config is finalized: the persisted
224
+ // panel_mode participates in light-mode resolution below.
225
+ const settings = createSettings(store.db);
226
+
227
+ // Light mode (v0.7.12): the bundle config flag OR a persisted panel_mode of
228
+ // "light" (the panel switch wins over the bundle config so it survives
229
+ // config redeploys). applyLightModePreset turns every heavy background /
230
+ // semantic feature off and keeps the core loop (autoInject, autoSummarize,
231
+ // hot memory, quality filter).
232
+ const lightMode = rawCfg.lightMode === true || settings.getPanelMode() === "light";
233
+ // 功能开关合并顺序即优先级:用户显式开关(feature_flags kv,面板写入)>
234
+ // 轻量预设(applyLightModePreset 批量置关的重型能力)> bundle 配置。预设必须
235
+ // 先应用、用户开关后展开,否则 LIGHT_MODE_OFF 会把用户显式打开的开关再次
236
+ // 压掉。合并结果只作用于本次启动:面板改开关后与 panel_mode 一样在下次
237
+ // 启动生效。
238
+ // 嵌套对象开关按首个点号拆开(kv 里平铺存的 "memoryQualityFilter.enabled" →
239
+ // cfg.memoryQualityFilter.enabled),点号键不原样留在 cfg 顶层属性里。
240
+ const flags = settings.getFeatureFlags();
241
+ const flatFlags = {};
242
+ const nestedFlags = {};
243
+ for (const [key, value] of Object.entries(flags)) {
244
+ const dot = key.indexOf(".");
245
+ if (dot > 0) {
246
+ const objKey = key.slice(0, dot);
247
+ const subKey = key.slice(dot + 1);
248
+ nestedFlags[objKey] = { ...(nestedFlags[objKey] ?? {}), [subKey]: value };
249
+ } else {
250
+ flatFlags[key] = value;
251
+ }
252
+ }
253
+ const cfg = { ...applyLightModePreset({ ...rawCfg, lightMode }), ...flatFlags };
254
+ for (const [objKey, sub] of Object.entries(nestedFlags)) {
255
+ cfg[objKey] = { ...(cfg[objKey] ?? {}), ...sub };
256
+ }
257
+ // #622:错峰时段串(summarizePeakHours / dreamPeakHours)解析失败 = 静默未配置
258
+ // (parsePeakSpec 的契约:排程是省钱手段,绝不该因写错格式停掉蒸馏/巩固)。但
259
+ // 「不救错」不等于「不吭声」——配置写错的唯一症状是功能悄悄没生效(#424 的排查
260
+ // 只能靠「高峰期还在巩固」倒推)。这里对**非空**且解析为 null 的键各出一条 warn:
261
+ // 空串是合法的未配置,不告警(否则每个没配错峰的用户开机都被警告一遍)。helper
262
+ // 只转述 parsePeakSpec:等价类归一(PR #621,评审中)落地后,救得了的写法自然
263
+ // 不再走到这。行为零变化:只 log,不改 cfg。
264
+ for (const [key, spec] of Object.entries(invalidPeakSpecs(cfg))) {
265
+ ctx.logger?.warn?.(
266
+ `[dsh-mneme] ${key} "${spec}" 不是合法的高峰时段串,将按未配置处理(错峰不生效、全天候运行)。` +
267
+ `写法:09:00-18:00,多段用逗号分隔,可带星期前缀(mon-fri 08:00-12:00),支持跨零点;24:00 越界请写 00:00`
268
+ );
269
+ }
270
+ const sessionGate = createSessionGate(cfg);
271
+
272
+ // 失败表的启动期清理也放在装配之后(此前它在 createStore 之后、装配之前,写死
273
+ // 90 天)。保留期现在是显式配置键 reflectionFailureRetentionDays;等它进面板
274
+ // 白名单的那天,这里读的就是装配后的值,不用再改第二处。best-effort 语义不变:
275
+ // 清理失败不挡启动,否则一次锁表就能让插件起不来——但「不挡启动」不等于「可以
276
+ // 悄悄失败」:这一族的 recall_runs 清理当年就是静默蒸发,表涨到两万余行无人
277
+ // 察觉,所以本处与 llm_audit / distill_records / recall_runs 三处一样出声。
278
+ try {
279
+ const failureRetentionDays = Number.isInteger(cfg.reflectionFailureRetentionDays)
280
+ ? cfg.reflectionFailureRetentionDays
281
+ : 90;
282
+ store.deleteOldFailures(new Date(Date.now() - failureRetentionDays * 86400000).toISOString());
283
+ } catch (err) {
284
+ ctx.logger?.warn?.(`[dsh-mneme] failure-table retention purge failed: ${String(err)}`);
285
+ }
286
+
287
+ // Bug8 的启动期清理放在装配之后:面板把 llmAudit.* 写进 kv、经 nestedFlags 合进
288
+ // cfg,而写入侧(dream / summarize / 写入准入)读的都是装配后的 cfg——清理若读
289
+ // rawCfg,面板改了保留期它不认,更糟的是「开不开审计」与写入侧可能取到不同的值
290
+ // (raw 说关 → 不清理,cfg 说开 → 照写,审计表就无保留期地长)。保留期默认 90 天、
291
+ // 失败只 warn,与失败表清理同款:账本清理绝不许挡住插件启动。
292
+ try {
293
+ if (cfg.llmAudit?.enabled !== false) {
294
+ const retentionMs = Number.isInteger(cfg.llmAudit?.retentionDays) ? cfg.llmAudit.retentionDays : 90;
295
+ store.deleteOldLlmAudits(new Date(Date.now() - retentionMs * 86400000).toISOString());
296
+ }
297
+ } catch (err) {
298
+ ctx.logger?.warn?.(`[dsh-mneme] llm_audit retention purge failed: ${String(err)}`);
299
+ }
300
+
301
+ // #399 的启动期裁剪:与上面同款(读装配后的 cfg,理由同上——面板写的保留期必须
302
+ // 与写入侧取到同一个值)。不同的一点:蒸馏原文留存默认关,而「默认关」在写入侧
303
+ // 的含义就是零写入,所以这里也只在 enabled === true 时才动手——否则一张空表被
304
+ // 每次启动扫一遍,白付一次全表 DELETE 的代价。只裁 distill_records;链接
305
+ // (memory_provenance)保留,指向空区间即为「来源已过期」。
306
+ try {
307
+ if (cfg.distillRecords?.enabled === true) {
308
+ const retentionMs = Number.isInteger(cfg.distillRecords?.retentionDays)
309
+ ? cfg.distillRecords.retentionDays
310
+ : 90;
311
+ store.clearDistillRecords(new Date(Date.now() - retentionMs * 86400000).toISOString());
312
+ }
313
+ } catch (err) {
314
+ ctx.logger?.warn?.(`[dsh-mneme] distill_records retention purge failed: ${String(err)}`);
315
+ }
316
+
317
+ // recall_runs 的滚动清理(v0.7.0 原设计,2026-10-14 找回):与 llm_audit 同策略,
318
+ // 按 cfg.recallRetentionDays(默认 90 天)启动时清一次。读装配后的 cfg,理由与上面
319
+ // 两处一致 —— 写入侧取的是 cfg,清理侧若取 rawCfg,面板改的保留期就不生效。
320
+ // 这一对曾在 dd0ed78(v0.7.12 发布提交)里被整块弄丢,配置键与文档却一直留着,
321
+ // 实测 2026-10-13 那天从 16,041 涨到 20,510 行(一天四千余行,只增不减;
322
+ // 单给一个静态行数会随表腐坏,故带日期的区间才是可核对的说法)。注入行开始写真实 query 之后,这条从「表膨胀」
323
+ // 升级为「原话无限期留存」,所以必须与那次改动同批交付。
324
+ // 保留期语义是「天」(函数内部乘 86400000),清理失败只 warn,绝不挡插件启动。
325
+ try {
326
+ const recallRetentionDays = Number.isInteger(cfg.recallRetentionDays) ? cfg.recallRetentionDays : 90;
327
+ store.purgeRecallRunsOlderThan(recallRetentionDays);
328
+ } catch (err) {
329
+ // 这里必须真的出声。这一对当初是**静默**蒸发的:函数与调用一起没了、没人发现,
330
+ // 表就这么涨到两万余行。清理再失败一次而仍然不出声,同样的回归照样看不见——
331
+ // 「只 warn」不是修辞,是这族清理唯一的事后线索。
332
+ ctx.logger?.warn?.(`[dsh-mneme] recall_runs retention purge failed: ${String(err)}`);
333
+ }
334
+
335
+ // 记忆语言(memory.language):本实例逐层传入 inject / summarize / dream /
336
+ // sleep / mirror,多实例(如 agent preset 内挂载)互不影响。
337
+ const mirror = createMirror(memoryDir, langOf(cfg));
338
+
339
+ // #296 第二批:managed 文档目录。解析与 memoryDir 同一套(空 = 跟随 memoryDir 的
340
+ // <memoryDir>/documents/,~ 展开,相对路径落在 memoryDir 下)。索引对象总是建:
341
+ // 闸关时它只承担「清掉陈旧 index.md」这一件事,与 documents.md 在闸关时被镜像删
342
+ // 掉同一口径。建目录与写索引失败都只 warn,不阻断插件加载(它们是机器产物)。
343
+ const documentDir = resolveDocumentDir(memoryDir, cfg.documentDir);
344
+ const documentIndex = createDocumentIndex(documentDir, langOf(cfg));
345
+ if (cfg.documentMemoryEnabled === true) {
346
+ const ensured = documentIndex.ensure();
347
+ if (!ensured.ok) {
348
+ ctx.logger?.warn?.(`[dsh-mneme] documentDir is not writable: ${documentDir}: ${ensured.error}`);
349
+ }
350
+ // 启动即渲染一次:索引不该等到第一次业务写才存在(删掉它之后重启也能自愈)。
351
+ const synced = documentIndex.sync(store.list({ type: "document", limit: null }));
352
+ if (!synced.ok) ctx.logger?.warn?.(`[dsh-mneme] document index sync failed: ${synced.error}`);
353
+ } else {
354
+ documentIndex.remove();
355
+ }
356
+
357
+ // 写入准入实例(#254)。两个开关 writeAdmission.enabled / .enforce 都默认关;
358
+ // 关时它的返回与只计量那一阶段逐字段一致,写入路径完全不碰。既有的
359
+ // llmAudit.enabled 关掉时它同样既不判也不写(那个开关连审计行的启动期清理一起
360
+ // 关掉,在无保留期的表里按写入频次增长是不能接受的)。
361
+ //
362
+ // sensitiveScan 是密钥 / PII 那一类判据的注入点。按 #254 验收第 4 条它是 #164 A2
363
+ // 的判据来源,现在由 src/sensitive-scan.js 实现(维护者 09-28 把 A2 的认领转给
364
+ // 本侧)。这里只做接线:判据开不开由它自己的键 sensitiveScanEnabled 决定,工厂在
365
+ // 关时返回 null,闸门的行为就与 #332 合并时逐字段一致(那一版根本没有这个函数);
366
+ // 命中之后是仅告警还是真拦,仍是 writeAdmission.enforce 的事(#164 口径:
367
+ // 默认仅告警、拦截 opt-in),判据不碰决策。
368
+ const writeAdmission = createWriteAdmission({
369
+ store,
370
+ config: cfg,
371
+ logger: ctx.logger,
372
+ sensitiveScan: createSensitiveScan({ config: cfg })
373
+ });
374
+ const service = createService({ store, mirror, config: cfg, logger: ctx.logger, documentIndex, writeAdmission });
375
+
376
+ // F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
377
+ // a safe re-render at boot so a stale mirror converges without needing a
378
+ // business write. Bounded: single attempt; on failure dirty stays for the
379
+ // next boot. Never throws.
380
+ service.recoverMirror();
381
+
382
+ // Recall-layer receipt: when searchMemories runs with recordRecall=true, the
383
+ // retrieval scene (query/mode/topK/threshold + candidates) is persisted to
384
+ // recall_runs for audit/replay — the sibling of the dream_runs judgment trail.
385
+ // Best-effort: a failed recall write must never break the search.
386
+ service.setRecallRecorder((recall) => {
387
+ try {
388
+ store.saveRecallRun({
389
+ query: recall.query,
390
+ mode: recall.mode,
391
+ topK: recall.topK,
392
+ threshold: recall.threshold ?? null,
393
+ candidates: recall.candidates ?? [],
394
+ created_at: recall.createdAt
395
+ });
396
+ } catch { /* non-fatal: recall recording is bookkeeping */ }
397
+ });
398
+
399
+ // Semantic pipeline: a local/ollama embedder when configured, otherwise the
400
+ // legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
401
+ // the store's embedding column and tracks the active model fingerprint. A
402
+ // slow embedder init (model download) never blocks plugin boot — failures
403
+ // degrade to keyword search.
404
+ const vectorIndex = createVectorIndex({ store, logger: ctx.logger });
405
+ service.setVectorIndex(vectorIndex);
406
+
407
+ // Human edits in mirror files win on every sync; merge them back first.
408
+ // TYPE_FILE maps each memory type to its mirror filename. Read every type's
409
+ // edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
410
+ // a per-type read-then-merge loop would overwrite edits in files not yet read
411
+ // (e.g. preferences.md merging would clobber unsynced projects.md edits).
412
+ const humanEdits = new Map();
413
+ for (const type of Object.keys(TYPE_FILE)) {
414
+ humanEdits.set(type, mirror.readHumanEdits(type));
415
+ }
416
+ const applyHumanEdits = () => {
417
+ for (const [type, edits] of humanEdits) {
418
+ if (edits.length) service.mergeHumanEdits(type, edits);
419
+ }
420
+ };
421
+
422
+ // embedder/reranker/boot 回填装配已整体搬至 src/semantic.js(纯搬移,宿主与 daemon
423
+ // 共用):调用时序(applyHumanEdits 在各分支的触发点、#118 重试、autoReindexOnBoot)
424
+ // 原样保留在 createSemantic 内部,这里只拿引用。init 全失败的 embedder 引用仍会进入
425
+ // dream/sleep 的 semantic 面 —— 与搬移前一致:检索侧 setEmbedder(null) 降级关键词。
426
+ const semantic = createSemantic({
427
+ store, service, settings, cfg, logger: ctx.logger, vectorIndex, applyHumanEdits, lightMode
428
+ });
429
+ const { embedder, reranker } = semantic;
430
+
431
+ // Custom commands: register persisted commands into the DSH command registry
432
+ // on boot; add/remove re-register live through the API.
433
+ let commands = null;
434
+ // M.O. 执行记账器:默认 null(旗标关),开了才装配。
435
+ let recorder = null;
436
+ if (ctx.commands) {
437
+ commands = createCommandManager({ ctx, settings, logger: ctx.logger, language: langOf(cfg) });
438
+ commands.sync();
439
+ }
440
+
441
+ // M.O. 执行记账(v0.9.0 第二批):`/mo-run` 显式开一次执行窗口,窗口内的命令执行
442
+ // 才可能被记成一条 run。挂的是 `session/event`——本仓已有三处消费者(continuity /
443
+ // summarize / api),不是新引入的宿主依赖。
444
+ //
445
+ // 为什么必须显式开窗:工作流的 trigger 是人话,拿它去猜「用户这句话是不是要开始走
446
+ // 某条流程」只能猜错,而猜错会把一条从没跑过的流程记成跑过。所以窗口边界由人的
447
+ // 显式动作划,**没开窗时一个字都不记**。
448
+ //
449
+ // 门控:默认关(actionRunEnabled),并且它挂热路径,轻量档也关。
450
+ if (cfg.actionRunEnabled === true) {
451
+ recorder = createExecutionRecorder({ ctx, logger: ctx.logger, sessionGate });
452
+ const disposeMoRun = registerMoRunCommand({
453
+ ctx,
454
+ recorder,
455
+ enabled: true,
456
+ language: () => langOf(cfg)
457
+ });
458
+ if (disposeMoRun) {
459
+ disposers.push(disposeMoRun);
460
+ } else {
461
+ // 注册不上(宿主没有命令注册表)时别留一个只开着窗口却没人能操作的记录器。
462
+ recorder.dispose();
463
+ recorder = null;
464
+ ctx.logger?.warn?.("[dsh-mneme] actionRunEnabled=true 但宿主没有命令注册表,M.O. 记账未启用");
465
+ }
466
+ }
467
+
468
+ // 「Agent 眼中的你」自动事实(2026-11-06,opt-in:profileFactsEnabled):dream 巩固
469
+ // 轮成功后的逐卡维护 + 面板手动轮(api.js)+ 注入事实段(factsProvider 传注入器与
470
+ // 预览路由)。装配与开关无关——闸在消费侧(注入渲染 / 自动轮判空 / 路由 409),
471
+ // 与 profileExtractor 同款:503 只该指装配缺失,不该被读成「用户没开」。
472
+ // 位置必须在 dream 调度器之前:onRun 的 fire-and-forget 收尾要引用它。
473
+ const profileFacts = createProfileFacts({ ctx, service, store, config: cfg, logger: ctx.logger });
474
+
475
+ // Dream scheduler: automatic consolidation + summary runs, triggered by
476
+ // store growth. Writes through the service fire the dream hook, which asks
477
+ // the scheduler to (re)schedule a run once absolute and since-last-run
478
+ // thresholds are both exceeded. onRun is deferred through `dream` so the
479
+ // closure sees the assigned scheduler; the null guard keeps a run safe even
480
+ // if the hook fires before assignment or after dispose.
481
+ let dream = null;
482
+ if (cfg.autoDream) {
483
+ dream = createDreamScheduler({
484
+ thresholdCount: cfg.dreamThresholdCount,
485
+ thresholdChars: cfg.dreamThresholdChars,
486
+ delayMs: cfg.dreamDelayMs,
487
+ minIntervalMs: (cfg.dreamMinIntervalMinutes ?? 0) * 60000,
488
+ // Issue #292:连续失败指数退避(opt-in,默认关 = 行为不变)。
489
+ failureBackoff: cfg.autoDreamFailureBackoff === true,
490
+ logger: ctx.logger,
491
+ semantic: { embedder, vectorIndex },
492
+ lastRunAtSeed: store.lastDreamRunAt("auto"),
493
+ // Issue #89(基线半边):阈值基线同样跨重启恢复——只恢复 lastRunAt 的话,
494
+ // 重启后的第一条写入仍会绕过阈值(库里记忆多于阈值时 overBase 恒成立)开跑
495
+ // 整轮。种子取上次成功轮的库规模,口径与运行期刷新一致;读不到时调度器退回
496
+ // 零基线(= 升级前行为)。
497
+ baselineSeed: store.lastDreamBaseline("auto"),
498
+ // Issue #239(第 4 项)镜像到巩固:高峰期不做梦,顺延到最近的高峰结束时刻。
499
+ peakHours: cfg.dreamPeakHours ?? "",
500
+ peakMaxDeferMinutes: cfg.dreamPeakMaxDeferMinutes ?? 120,
501
+ // 跳过时的审计行在这里落地(调度器只拿到 service,拿不到 config 的
502
+ // llmAudit 开关与巩固模型路由)。口径与 runAuditedLlm 一致:审计关掉就
503
+ // 不写;写失败只 warn,绝不反噬调度(CONTRIBUTING 的 fail-safe 硬约定)。
504
+ auditPeakSkip: ({ count, chars }) => {
505
+ if (cfg?.llmAudit?.enabled === false || typeof service?.saveLlmAudit !== "function") return;
506
+ const modelId = cfg.dreamProvider && cfg.dreamModel ? `${cfg.dreamProvider}:${cfg.dreamModel}` : "";
507
+ service.saveLlmAudit({
508
+ timestamp: new Date().toISOString(),
509
+ trigger_source: "autoDream",
510
+ operation_type: "dream_consolidate",
511
+ model_id: modelId,
512
+ input_tokens: 0,
513
+ output_tokens: 0,
514
+ total_tokens: 0,
515
+ cost_usd: 0,
516
+ duration_ms: 0,
517
+ status: "skipped",
518
+ error_message: "peak-hours",
519
+ related_memory_ids: [],
520
+ // 观测用:跳过时窗口里积了多少(阈值继续累积,不是丢弃)。
521
+ metadata: JSON.stringify({ count, chars })
522
+ });
523
+ },
524
+ // Issue #634:归档/删除/合并把活跃量压到上次成功轮的峰值(= 阈值基线)之下
525
+ // 时,调度器在判定口把基线夹回当前活跃量。这里把「夹回发生过」落进审计——
526
+ // 修复前这类收缩会变成永久静默停摆(overBase 恒为假且无任何账面),这条行
527
+ // 就是「为什么 X 天没巩固」的答案。status 与 peak-hours 同用
528
+ // "skipped":按状态聚合(store.auditSummary)只做分组计数,不会把这行算进
529
+ // 成功率的分母口径之外的新桶;成败口径仍以 dream_runs 为准。门禁与写失败的
530
+ // fail-safe 同 auditPeakSkip(调度器侧还有一层 try/catch 兜底)。
531
+ auditBaselineClamp: ({ previous, count, chars }) => {
532
+ if (cfg?.llmAudit?.enabled === false || typeof service?.saveLlmAudit !== "function") return;
533
+ const modelId = cfg.dreamProvider && cfg.dreamModel ? `${cfg.dreamProvider}:${cfg.dreamModel}` : "";
534
+ service.saveLlmAudit({
535
+ timestamp: new Date().toISOString(),
536
+ trigger_source: "autoDream",
537
+ operation_type: "dream_consolidate",
538
+ model_id: modelId,
539
+ input_tokens: 0,
540
+ output_tokens: 0,
541
+ total_tokens: 0,
542
+ cost_usd: 0,
543
+ duration_ms: 0,
544
+ status: "skipped",
545
+ error_message: "baseline-clamp",
546
+ related_memory_ids: [],
547
+ // 观测用:previous = 夹回前的基线(旧峰值),count/chars = 夹回后的
548
+ // 活跃量(即新基线)——两份放一起,收缩幅度一眼可读。
549
+ metadata: JSON.stringify({ previous, count, chars })
550
+ });
551
+ },
552
+ onRun: () => {
553
+ const run = dream
554
+ ? dream.runDream(ctx, service, cfg)
555
+ : Promise.resolve({ ok: true, skipped: true });
556
+ // 画像事实自动维护:挂在巩固轮之后 fire-and-forget。刻意不并进 onRun 的
557
+ // promise——profileFacts 的失败不能被计入 dream 的失败退避
558
+ // (autoDreamFailureBackoff 的口径是巩固轮自身);逐卡经 service.enqueue
559
+ // 串行,不与 sleep/下一轮巩固抢模型。单卡失败只 warn,账本已有 failed 回执。
560
+ run.then(async (rep) => {
561
+ if (cfg.profileFactsEnabled !== true || (rep && rep.ok === false)) return;
562
+ for (const key of profileFacts.autoPassCandidates()) {
563
+ try {
564
+ await service.enqueue(() => profileFacts.runFor(key, { trigger: "auto" }));
565
+ } catch (error) {
566
+ ctx.logger?.warn?.(`[dsh-mneme] profile-facts auto pass failed for card "${key || "default"}": ${error?.message ?? error}`);
567
+ }
568
+ }
569
+ }).catch(() => { /* dream 自身失败走调度器退避,这里不做第二遍日志 */ });
570
+ return run;
571
+ }
572
+ });
573
+ service.setDreamHook(() => dream.maybeSchedule(service));
574
+ }
575
+
576
+ // Sleep scheduler (v0.4.0): idle-triggered deep maintenance. Fires when the
577
+ // store has been quiet for sleepIdleMinutes and re-arms on every write via
578
+ // noteWrite (hooked to the service's write path). Runs go through
579
+ // service.enqueue so they serialize with autoDream — the two never overlap.
580
+ // Abortable on user activity; audited into dream_runs with run_type='sleep'.
581
+ let sleep = null;
582
+ if (cfg.sleepModeEnabled) {
583
+ sleep = createSleepScheduler({
584
+ service,
585
+ config: cfg,
586
+ logger: ctx.logger,
587
+ lastRunAtSeed: store.lastDreamRunAt("sleep"),
588
+ onRun: (signal) => (sleep ? runSleep(ctx, service, cfg, ctx.logger, { embedder, vectorIndex }, signal) : Promise.resolve({ ok: true, skipped: true }))
589
+ });
590
+ service.setSleepHook(() => sleep.noteWrite());
591
+ }
592
+
593
+ // Entity gene extraction (v0.3.0): wire the extractor into the service as a
594
+ // hook so saveWithDedupe can fire-and-forget an extraction pass on fresh
595
+ // writes. The service never sees ctx.llm — index.js adapts it here into the
596
+ // callLLM(messages, options) => Promise<string> contract the extractor
597
+ // expects, reusing the same ctx.llm.stream consumption pattern as dream.js.
598
+ // Explicit opt-in only (entityExtractionEnabled defaults to false); any LLM
599
+ // failure degrades inside the extractor to { ok:false }, never a write error.
600
+ if (cfg.entityExtractionEnabled) {
601
+ if (!ctx.llm) {
602
+ // Issue #108: an enabled-but-unwired extractor failed silently before —
603
+ // zero entities, zero llm_audit_logs, no log line anywhere. Make the
604
+ // missing dependency visible so a user can tell "extractor not installed"
605
+ // from "extraction failed".
606
+ ctx.logger?.warn?.("[dsh-mneme] entityExtractionEnabled=true but ctx.llm unavailable — entity extractor NOT installed");
607
+ } else {
608
+ const streamEntityText = createEntityStreamAdapter({
609
+ llm: ctx.llm,
610
+ agentDefaultModel: ctx.agentDefaultModel,
611
+ logger: ctx.logger,
612
+ // Issue #250: the adapter needs service to write its llm_audit_logs row
613
+ // (it never had it) and config for the shared llmAudit.enabled gate.
614
+ service,
615
+ config: cfg
616
+ });
617
+ service.setEntityExtractor((memory) =>
618
+ extractEntities(memory, { store, config: cfg, callLLM: streamEntityText, logger: ctx.logger })
619
+ .catch((err) => {
620
+ ctx.logger?.warn?.(`[dsh-mneme] entity extraction failed: ${String(err)}`);
621
+ return { ok: false, error: String(err) };
622
+ })
623
+ );
624
+ }
625
+ }
626
+
627
+ const disposers = [];
628
+
629
+ try {
630
+ const disposeSessionGate = ctx.on?.("session/event", (session) => sessionGate.observe(session));
631
+ if (typeof disposeSessionGate === "function") disposers.push(disposeSessionGate);
632
+ } catch {
633
+ // 宿主没有该事件钩子时,注入、工具与蒸馏消费点仍直接检查当前 Session。
634
+ }
635
+
636
+ // Issue #380:注入前判定闸门。判定 LLM 走 entity adapter 同款装配(ctx.llm +
637
+ // agentDefaultModel 回退),gate 自持 enabled/usable 判定(未启用/llm 缺席/
638
+ // llmAudit 关闭时内部恒 off,注入行为与现状逐字节一致)。
639
+ const preInjectGate = createPreInjectGate({
640
+ llm: ctx.llm,
641
+ agentDefaultModel: ctx.agentDefaultModel,
642
+ service,
643
+ config: cfg,
644
+ logger: ctx.logger
645
+ });
646
+ disposers.push(() => preInjectGate.clear());
647
+
648
+ // #118: never let a pending embedder init retry fire after unload and touch
649
+ // a torn-down context.
650
+ disposers.push(() => semantic.dispose()); // #118 重试计时器 + boot 回填计时器(搬入 semantic.js 后由它自持)
651
+
652
+ // chorography M3:地志提纲折进 memory context(不新开 context——宿主侧同 order 无
653
+ // 兜底排序,多一块就多一份拼装抖动;spec §7 也要求「合并成一次拼装」)。档位由
654
+ // chorographyInject 决定,off / 未匹配到仓库 / 渲染失败一律返回空串,宿主会过滤
655
+ // 掉空文本,整块消失且不占位。
656
+ //
657
+ // 预算是 0:注入块只在 memory context 里多几行,本身不做检索、不 fork git——
658
+ // 真过期判定留给 /choro(P29)。所以这里不需要额外的「热路径」闸,chorographyEnabled
659
+ // 与 chorographyInject 两个开关在 resolveInjectTier 里已经判完。
660
+ const choroInject = createChoroInject({
661
+ choro: createChoroQueries({ store }),
662
+ config: cfg,
663
+ logger: ctx.logger,
664
+ language: () => langOf(cfg)
665
+ });
666
+ disposers.push(() => choroInject.dispose());
667
+
668
+ ctx.inject(["systemPrompt"], (promptCtx) => {
669
+ if (cfg.autoInject) {
670
+ disposers.push(createInjector(promptCtx, service, settings, cfg, preInjectGate, sessionGate, choroInject, profileFacts.factsProvider));
671
+ }
672
+ });
673
+
674
+ // #249 N3:压缩边缘双落点。触发靠宿主自己落的压缩事件(订阅 + pre-step 追加),
675
+ // 不需要 systemPrompt / tools 的任何能力,所以不塞进上面的 inject 回调;父/子
676
+ // 闸门在挂载点判一次,与 inject.js 共用同一个 injectChildEnabled 判据。
677
+ if (cfg.autoInject && injectChildEnabled(cfg, "continuityRescueEnabled")) {
678
+ disposers.push(createContinuityRescue(ctx, store, sessionGate));
679
+ }
680
+
681
+ // 知识库第二层(契约 §3.4):装配与开关无关(createKb 只建对象,读侧零 IO),闸在
682
+ // 路由与工具两层——这样「没开」(409)与「没装配」(503)在对外两面都是可区分的。
683
+ // 位置必须在 tools/api 两处消费点之前:cordis 的 inject 回调可能在任一之后才触发,
684
+ // 在消费点里现建就会造出第二份实例(两份缓存、两套 dispose)。
685
+ const kb = createKb({ store, embedder, cfg });
686
+ disposers.push(() => kb.dispose?.());
687
+
688
+ ctx.inject(["tools"], (toolsCtx) => {
689
+ // choro(M1):只读查询对象,chorographyEnabled 关着时工具压根不注册(闸在
690
+ // createTools 的 hiddenTools,与 organizeEnabled 同款反向语义)。
691
+ disposers.push(createTools(toolsCtx, service, cfg, embedder, sessionGate, createChoroQueries({ store }), kb));
692
+ });
693
+
694
+ // chorography M3:/choro 只读回执。与 /mo-run 同形(ctx.commands.register),
695
+ // 但这里**不持有任何状态**、不写盘、不重建——spec §2 D6「只手动触发,过期只提示」
696
+ // 决定了这条命令只能看,不能改。位置必须在 disposers 声明之后(同 actionNudge 的
697
+ // TDZ 注释),否则插件加载时会直接抛 ReferenceError。
698
+ if (cfg.chorographyEnabled === true) {
699
+ const disposeChoro = registerChoroCommand({
700
+ ctx,
701
+ choro: createChoroQueries({ store }),
702
+ config: cfg,
703
+ enabled: true,
704
+ language: () => langOf(cfg)
705
+ });
706
+ if (disposeChoro) disposers.push(disposeChoro);
707
+ else ctx.logger?.info?.("[dsh-mneme] chorographyEnabled=true 但宿主没有命令注册表,/choro 未注册");
708
+ }
709
+
710
+ const summarizer = createSummarizer(ctx, service, cfg, {}, sessionGate);
711
+ disposers.push(summarizer.dispose);
712
+
713
+ // QQ 日记本(v0.13,diaryEnabled):构造期零副作用(只闭包住依赖,不建目录、不读盘、
714
+ // 不调模型),所以位置可以放在这里——它必须早于下面的 createApi,面板的
715
+ // 「写一篇」按钮要转调同一个 composer;而自动触发侧在 webServer 块之后挂(同
716
+ // actionNudge 的 TDZ 理由)。关闸时 composer 是 null:路由回 409、座位不注册,
717
+ // 与「装配缺失」的 503 严格区分(#497 先例)。
718
+ const diary = cfg.diaryEnabled === true
719
+ ? createDiaryComposer({ ctx, config: cfg, logger: ctx.logger, service, settings, memoryDir })
720
+ : null;
721
+
722
+ // #275 存储生命周期第一批:无损回收的手动入口。刻意不挂启动路径、不接定时器——
723
+ // 这一步是不可逆的内容丢弃,只由人显式触发(面板的维护卡、`dsh-mneme reclaim`)。
724
+ // 实例建在插件进程内,是为了让 CLI 也能走这条路:VACUUM 要排他锁,跟宿主抢锁的
725
+ // 那条路走不通。
726
+ // 位置在 webServer 块之前:面板的 in-process API(createApi)与 standalone 数据面
727
+ // 都要用它,而 webServer 是可选的——headless 宿主不注册面板路由,构造点若留在块后
728
+ // 就永远晚于 createApi(#386 第 4 条)。构造期无副作用(只返回 { reclaim }),前移安全。
729
+ const maintenance = createMaintenance({ store, config: cfg, logger: ctx.logger });
730
+
731
+ // webServer 可选依赖:cordis 4 的 ctx 是 Proxy,直接访问未在 inject 声明的
732
+ // 属性会抛 "cannot get property without inject"(不会返回 undefined),所以
733
+ // 不能用 if (ctx.webServer) 守卫。ctx.reflect.get 是 cordis 提供的免 inject
734
+ // 读取(未提供时返回 undefined);对象字面量 mock ctx(测试)没有 reflect,
735
+ // 退回直接属性访问。headless/无 UI 宿主无 webServer 时跳过 API 注册,其余
736
+ // 功能(工具/注入/dream)照常。
737
+ const webServer = typeof ctx.reflect?.get === "function" ? ctx.reflect.get("webServer") : ctx.webServer;
738
+ if (webServer) {
739
+ // 画像提取草稿(#497):装配与开关无关——闸在路由那一侧(关着回 409 disabled,
740
+ // 本模块一次都不会被调用)。这样「没启用」与「没装配」在 HTTP 面上是两个可区分的
741
+ // 答案(503 unavailable 只该出现在旧宿主/装配缺失上,不该被读成「用户没开」)。
742
+ const extractProfileDraft = createProfileExtractor({ ctx, service, config: cfg, logger: ctx.logger });
743
+ // chorography 读侧(M1):与 service 刻意分开——所有权模型相反(spec §3)。
744
+ // 闸在路由/工具两层(关着 409 / 不注册),queries 本体装配与开关无关。
745
+ // 收尾档:choro 对象加两件 host 侧能力——embedder(向量惰性物化,P37)与
746
+ // runT0Apply(面板重建按钮的写路径,P34;T1 不上 HTTP,CLI 仍是它的入口)。
747
+ // T1 任务账随 choro 装配建立(与开关无关:开关在路由那侧判,没开时这些方法一次
748
+ // 都不会被调到,符合「503=装配缺件 / 409=用户没开」的分叉)。
749
+ const t1Jobs = createT1Jobs({ store, dbPath: join(memoryDir, "memory.db") });
750
+ disposers.push(() => t1Jobs.dispose());
751
+ const choro = {
752
+ ...createChoroQueries({ store }),
753
+ embedder,
754
+ // guardT1Loss 只给「读侧顺手刷新」用:那条路不是用户的明确指令,不能替他
755
+ // 丢掉花模型调用换来的语义段。手动重建(面板按钮 / CLI)不传,行为不变。
756
+ runT0Apply: ({ repoRoot, guardT1Loss = false }) => runChoroT0({ repoRoot, store, apply: true, guardT1Loss }),
757
+ startT1Job: (args) => t1Jobs.start(args),
758
+ t1JobStatus: (jobId) => t1Jobs.status(jobId),
759
+ };
760
+ // The semantic surface handed to the API is createSemantic()'s own return
761
+ // value, plus the vectorIndex it does not carry. Passing the bare literal
762
+ // `{ vectorIndex, reranker }` used to drop embeddingDiagnostics, so the
763
+ // panel's `data.embeddings` was permanently null on a real install — the
764
+ // typeof guard in api.js fell through every time, and no test noticed
765
+ // because none asserted on `data.embeddings`.
766
+ const api = createApi(ctx, service, settings, commands ?? {
767
+ add: () => { throw new Error("commands unavailable"); },
768
+ remove: () => false,
769
+ list: () => []
770
+ }, embedder, { ...semantic, vectorIndex, reranker }, cfg.apiToken, cfg, maintenance, extractProfileDraft, choro, profileFacts, kb, diary);
771
+ disposers.push(api.dispose);
772
+
773
+ // dsh-go 原生面板:只有宿主提供 dshGo 服务(dsh-go 手机 App)时才挂载。
774
+ // 动态 import 是刻意的——静态导入会让「没有桥就整块不加载」这句承诺落空。
775
+ // cfg.apiToken 一并交给面板:createApi 给 token / token-write 路由包了鉴权,
776
+ // 进程内调用不带令牌会被自家路由拦成 401,不为此在服务端开白名单。
777
+ ctx.inject(["dshGo"], (dshCtx) => {
778
+ import("./dsh-go/panel.js")
779
+ .then((panel) => panel.mountDshGoPanel(dshCtx, { apiToken: cfg.apiToken }))
780
+ .catch((error) => ctx.logger?.warn?.(`[dsh-mneme] dsh-go 面板未加载:${error?.message ?? error}`));
781
+ });
782
+ }
783
+
784
+ // M.O. 自动沉淀(v0.9.3):项目里重复走的那套动作,自动变成 memoryDir 里的一条
785
+ // 「提议」,等人确认后才可能进仓库。默认关(actionNudgeEnabled)。
786
+ //
787
+ // 为什么位置在这里而不是和 M.O. 记账同段:`disposers` 声明在 :532,而记账块在它
788
+ // 之前(TDZ)——照抄过去会在插件加载时直接抛 ReferenceError。这里已在其后。
789
+ //
790
+ // 为什么挂 session/event 却又要进轻量档黑名单:它读的是 zstd 会话日志(同步解压),
791
+ // 是本插件里最重的一条后台路径;且它只产「提议」,关掉不影响记忆本身的正确性。
792
+ if (cfg.actionNudgeEnabled === true) {
793
+ disposers.push(createNudgeAutoRun({ ctx, memoryDir, logger: ctx.logger, sessionGate }));
794
+ }
795
+
796
+ // 日记的触发侧(v0.13):turn/end 跨天判定 + 启动补写。
797
+ //
798
+ // 启动补写为什么在这里而不是 webServer 块里:它不依赖面板,headless 宿主也该补。
799
+ // 补哪个项目只能靠状态文件里记过的项目根——宿主刚起来时没有任何会话,
800
+ // 而「上次是哪个项目、哪天写的」就在那儿。补写不 await:启动路径上吊住一次
801
+ // 模型调用,等于让整个插件加载等模型回来(本机 p90 五十秒级)。
802
+ if (diary) {
803
+ // diaryWriteEveryMinutes > 0 时额外起一根定时器(见 auto.js 的 tick):日记不该
804
+ // 取决于「谁先开口」,整天不说话也要有一篇。listProjects 用 knownProjects ——
805
+ // 定时器唤醒时没有会话,只能靠状态文件里记过的项目根。
806
+ const everyMinutes = Number(cfg.diaryWriteEveryMinutes) || 0;
807
+ const diaryAuto = createDiaryAutoRun({
808
+ ctx,
809
+ composer: diary,
810
+ logger: ctx.logger,
811
+ sessionGate,
812
+ everyMs: everyMinutes > 0 ? everyMinutes * 60 * 1000 : 0,
813
+ listProjects: () => diary.knownProjects()
814
+ });
815
+ disposers.push(diaryAuto);
816
+ const projects = diary.knownProjects();
817
+ if (projects.length) {
818
+ Promise.resolve(diaryAuto.catchUp(projects)).catch((error) => {
819
+ ctx.logger?.warn?.("[dsh-mneme] diary startup catch-up failed: " + String(error));
820
+ });
821
+ }
822
+ }
823
+
824
+ // Standalone external API (v0.7.12): plain node:http server for ecosystem
825
+ // integrations outside the DSH host. Persisted external_api settings win
826
+ // over the bundle config (enabled/port); the Bearer token lives in the same
827
+ // kv and is auto-generated on first boot by createStandaloneApi. Binding a
828
+ // non-loopback host is the operator's documented responsibility.
829
+ if ((settings.getExternalApi?.()?.enabled ?? cfg.externalApiEnabled) === true) {
830
+ // embedder 交给 /context 的查询嵌入(issue #370):与 service.setEmbedder 同一
831
+ // 实例(semantic.js 装配产物),lightMode 下为 null,路由侧降级规则档。
832
+ const standalone = createStandaloneApi({ service, store, config: cfg, logger: ctx.logger, settings, maintenance, embedder });
833
+ disposers.push(() => standalone.server.close());
834
+ standalone.ready.catch((error) => {
835
+ ctx.logger?.warn?.(`[dsh-mneme] standalone API failed to start: ${String(error)}`);
836
+ });
837
+ }
838
+
839
+ // Async disposer: cordis awaits the returned promise on unload (runDisposable),
840
+ // so an in-flight dream run is allowed to finish before the SQLite store is
841
+ // closed — dream.dispose() resolves only after its current run settles.
842
+ return async () => {
843
+ for (const dispose of disposers) {
844
+ if (typeof dispose === "function") dispose();
845
+ }
846
+ commands?.dispose();
847
+ recorder?.dispose();
848
+ if (dream) await dream.dispose();
849
+ if (sleep) sleep.dispose();
850
+ store.close();
851
+ };
852
+ };