@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
@@ -1,610 +1,610 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { createStore } from "../src/store.js";
4
- import { createService } from "../src/service.js";
5
- import { createTools } from "../src/tools.js";
6
- import { runSleep } from "../src/dream/sleep.js";
7
- import { createDreamScheduler } from "../src/dream.js";
8
-
9
- // issue #471:作用域级可逆闸门(政策轴)。
10
- //
11
- // 与 #17 / #170 那条事实轴(scope 标注与可见性)是两条不同的轴:
12
- // - 事实轴回答「这条记忆归谁」,答案写在记忆行上,改了它就是在改事实;
13
- // - 政策轴回答「这一路现在通不通」,答案写在 scope_state 表里,可逆、
14
- // 不改任何记忆行、不改作用域标注、不动向量与归档位。
15
- //
16
- // 三道验收线(对应 issue 正文的三处落点):
17
- // 1. 默认关:gateByScope 的输出与改动前**逐字段一致**,且是**同一个数组引用**
18
- // (引用相等才钉得住「零行为变化」,内容相等会让「悄悄复制一份」蒙过去);
19
- // 2. 按下与松开:同一作用域的候选进不来,松开后原样回来——两个方向都要断言,
20
- // 只锁「进不来」的话,一个「永远返回空」的实现也能过;
21
- // 3. 观察档:enabled 开、enforce 关 → 留一条 llm_audit_logs 但注入照常,
22
- // 这条是两级开关的意义所在(先在真实流量里看「如果按下会少掉多少」)。
23
-
24
- function setup(config) {
25
- const store = createStore(":memory:");
26
- const service = createService({ store, mirror: null, config });
27
- return { store, service };
28
- }
29
-
30
- const PAUSED_CONFIG = { scopeState: { enabled: true, enforce: true } };
31
- const OBSERVE_CONFIG = { scopeState: { enabled: true, enforce: false } };
32
-
33
- /** 造一批带 workspace 标注的记忆,一半属于 `proj-a`,一半无标注。 */
34
- function seed(store) {
35
- store.save({ type: "decision", title: "a-1", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
36
- store.save({ type: "decision", title: "a-2", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
37
- store.save({ type: "decision", title: "b-1", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-b", workspace_scope_source: "explicit" });
38
- store.save({ type: "decision", title: "g-1", content: "alpha beta gamma", importance: 4 });
39
- }
40
-
41
- const titles = (rows) => rows.map((m) => m.title).sort();
42
-
43
- // --- 验收线 1:默认关时逐字段一致,且引用不变 -----------------------------
44
-
45
- test("默认关:gateByScope 原样返回同一个数组(引用相等,不是等值副本)", () => {
46
- const { store, service } = setup({});
47
- seed(store);
48
- const rows = store.list({});
49
- const out = service.filterPaused(rows);
50
- // 引用相等:内容相等的副本会让「复制一份再返回」蒙过去,那样后续每次
51
- // 检索都白复制一遍,而「零行为变化」这条验收就退化成「看起来一样」。
52
- assert.equal(out, rows);
53
- });
54
-
55
- test("默认关:按下过状态也不生效——开关不打开时状态表根本不被读", () => {
56
- const { store, service } = setup({});
57
- seed(store);
58
- // 即便有人在开关关着的时候写进了状态(比如另一台机器同步过来),也不生效。
59
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
60
- const rows = store.list({});
61
- assert.equal(service.filterPaused(rows), rows);
62
- });
63
-
64
- test("默认关:注入候选集与无闸门时逐条相同", () => {
65
- const off = setup({});
66
- seed(off.store);
67
- const baseline = titles(off.service.injectCandidates({ maxItems: 10, threshold: 1 }));
68
-
69
- const pressed = setup({});
70
- seed(pressed.store);
71
- pressed.service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
72
- const after = titles(pressed.service.injectCandidates({ maxItems: 10, threshold: 1 }));
73
-
74
- assert.deepEqual(after, baseline);
75
- });
76
-
77
- // --- 验收线 2:按下与松开(两个方向都锁) ---------------------------------
78
-
79
- test("按下后:被按下作用域的候选进不来注入池,别的照常进", () => {
80
- const { store, service } = setup(PAUSED_CONFIG);
81
- seed(store);
82
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
83
-
84
- const got = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
85
- assert.deepEqual(got, ["b-1", "g-1"]);
86
- // 反向断言:不是「全都进不来」。
87
- assert.ok(got.includes("b-1") && got.includes("g-1"));
88
- });
89
-
90
- test("松开后:原样回来(同一个 service 实例,不重开库)", () => {
91
- const { store, service } = setup(PAUSED_CONFIG);
92
- seed(store);
93
- const before = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
94
-
95
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
96
- assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["b-1", "g-1"]);
97
-
98
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active" });
99
- // 逐条相同(不只「数量相同」):松开要真的恢复原样,不是恢复个大概。
100
- assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), before);
101
- });
102
-
103
- test("按下不动事实:记忆行、作用域标注、归档位一字段都不变", () => {
104
- const { store, service } = setup(PAUSED_CONFIG);
105
- seed(store);
106
- const snapshot = () => store.list({}).map((m) => ({
107
- id: m.id, title: m.title, workspace_scope: m.workspace_scope,
108
- workspace_scope_source: m.workspace_scope_source, archived: m.archived, forgotten: m.forgotten
109
- })).sort((x, y) => x.title.localeCompare(y.title));
110
-
111
- const before = snapshot();
112
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
113
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active" });
114
- assert.deepEqual(snapshot(), before);
115
- });
116
-
117
- test("检索侧同款:被按下的作用域在 searchMemories 里也出局", async () => {
118
- const { store, service } = setup(PAUSED_CONFIG);
119
- seed(store);
120
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
121
-
122
- const hits = await service.searchMemories("alpha", { mode: "keyword", limit: 10 });
123
- const got = titles(hits);
124
- assert.ok(!got.includes("a-1") && !got.includes("a-2"), `unexpected: ${got.join(",")}`);
125
- assert.ok(got.includes("b-1"));
126
- });
127
-
128
- test("全局按下('*')拦下该维度所有有标注的行,但不波及未标注行", () => {
129
- const { store, service } = setup(PAUSED_CONFIG);
130
- seed(store);
131
- service.setScopeState({ dimension: "workspace", scope_value: "*", state: "paused" });
132
-
133
- // g-1 无 workspace 标注 ⇒ 在 workspace 这一维上不参与比较,不受影响。
134
- assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["g-1"]);
135
- });
136
-
137
- test("维度独立:按下 agent 维不影响 workspace 维的行", () => {
138
- const { store, service } = setup(PAUSED_CONFIG);
139
- seed(store);
140
- service.setScopeState({ dimension: "agent", scope_value: "proj-a", state: "paused" });
141
-
142
- // 这批行没有 agent 标注,agent 维的按下一条都命中不了。
143
- const got = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
144
- assert.deepEqual(got, ["a-1", "a-2", "b-1", "g-1"]);
145
- });
146
-
147
- // --- 验收线 3:观察档留审计但不拦 -----------------------------------------
148
-
149
- test("观察档:注入照常,但留一条 scopeState 审计行", () => {
150
- const { store, service } = setup(OBSERVE_CONFIG);
151
- seed(store);
152
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
153
-
154
- const got = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
155
- // 观察档不拦:四条全在。
156
- assert.deepEqual(got, ["a-1", "a-2", "b-1", "g-1"]);
157
-
158
- const audits = store.listLlmAudits({ source: "scopeState" });
159
- assert.equal(audits.length, 1);
160
- assert.equal(audits[0].operation_type, "scope_state_gate");
161
- assert.equal(audits[0].model_id, "-");
162
- assert.equal(audits[0].status, "skipped");
163
- assert.equal(audits[0].metadata.enforced, false);
164
- // gated 记的是「本应拦下几条」,不是「拦了几条」——观察档下后者恒 0,
165
- // 记它就等于没记。
166
- assert.equal(audits[0].metadata.gated, 2);
167
- assert.deepEqual(audits[0].metadata.paused, [{ dimension: "workspace", scope_value: "proj-a" }]);
168
- });
169
-
170
- test("拦截档:同一条审计行但 enforced=true,且真拦下了", () => {
171
- const { store, service } = setup(PAUSED_CONFIG);
172
- seed(store);
173
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
174
-
175
- assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["b-1", "g-1"]);
176
-
177
- const audits = store.listLlmAudits({ source: "scopeState" });
178
- assert.equal(audits.length, 1);
179
- assert.equal(audits[0].metadata.enforced, true);
180
- assert.equal(audits[0].metadata.gated, 2);
181
- });
182
-
183
- test("没有候选被拦时不写审计行(门没经过人,不该留痕)", () => {
184
- const { store, service } = setup(OBSERVE_CONFIG);
185
- seed(store);
186
- service.setScopeState({ dimension: "workspace", scope_value: "proj-zzz", state: "paused" });
187
- service.injectCandidates({ maxItems: 10, threshold: 1 });
188
- assert.equal(store.listLlmAudits({ source: "scopeState" }).length, 0);
189
- });
190
-
191
- test("enforce 开、enabled 关:无效果(enforce 不单独生效,同 preInjectGate)", () => {
192
- const { store, service } = setup({ scopeState: { enabled: false, enforce: true } });
193
- seed(store);
194
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
195
- assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["a-1", "a-2", "b-1", "g-1"]);
196
- assert.equal(store.listLlmAudits({ source: "scopeState" }).length, 0);
197
- });
198
-
199
- // --- 状态表本身的语义 -----------------------------------------------------
200
-
201
- test("同一状态重复按下不刷新 decided_at(按两次不等于换了决定)", () => {
202
- const { service } = setup(PAUSED_CONFIG);
203
- const first = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", decided_at: "2026-01-01T00:00:00.000Z" });
204
- const again = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", decided_at: "2026-01-02T00:00:00.000Z" });
205
- assert.equal(first.decided_at, "2026-01-01T00:00:00.000Z");
206
- assert.equal(again.decided_at, "2026-01-01T00:00:00.000Z");
207
-
208
- const flipped = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active", decided_at: "2026-01-03T00:00:00.000Z" });
209
- assert.equal(flipped.decided_at, "2026-01-03T00:00:00.000Z");
210
- assert.equal(flipped.state, "active");
211
- });
212
-
213
- test("不带理由的重按不抹掉已有理由(理由被一次手滑抹掉是净损失)", () => {
214
- const { service } = setup(PAUSED_CONFIG);
215
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", note: "等这轮重构完再开" });
216
- const again = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active" });
217
- assert.equal(again.note, "等这轮重构完再开");
218
-
219
- const replaced = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", note: "换了个理由" });
220
- assert.equal(replaced.note, "换了个理由");
221
- });
222
-
223
- test("非法维度/值/状态返回 undefined,不静默落一行永不生效的状态", () => {
224
- const { service, store } = setup(PAUSED_CONFIG);
225
- assert.equal(service.setScopeState({ dimension: "agent", scope_value: "ok", state: "maybe" }), undefined);
226
- assert.equal(service.setScopeState({ dimension: "nope", scope_value: "ok", state: "paused" }), undefined);
227
- assert.equal(service.setScopeState({ dimension: "agent", scope_value: " ", state: "paused" }), undefined);
228
- assert.equal(store.listScopeStates().length, 0);
229
- });
230
-
231
- test("一次现读:另一个实例改了状态,本实例下一次判定立刻看得见(没有缓存要失效)", () => {
232
- const store = createStore(":memory:");
233
- const a = createService({ store, mirror: null, config: PAUSED_CONFIG });
234
- const b = createService({ store, mirror: null, config: PAUSED_CONFIG });
235
- store.save({ type: "decision", title: "a-1", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
236
-
237
- assert.deepEqual(titles(a.injectCandidates({ maxItems: 10, threshold: 1 })), ["a-1"]);
238
- b.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
239
- assert.deepEqual(titles(a.injectCandidates({ maxItems: 10, threshold: 1 })), []);
240
- });
241
-
242
- // --- 工具面 ---------------------------------------------------------------
243
-
244
- function makeTools(config) {
245
- const store = createStore(":memory:");
246
- const service = createService({ store, mirror: null, config });
247
- const ctx = {
248
- tools: { register() {} },
249
- logger: { warn() {}, info() {}, error() {} }
250
- };
251
- const tools = createTools(ctx, service, config, null);
252
- const byName = new Map(tools.map((t) => [t.name, t]));
253
- return { store, service, tool: byName.get("set_scope_state") };
254
- }
255
-
256
- test("set_scope_state 回执回显库内当前值,不是回显入参", async () => {
257
- const { tool } = makeTools(PAUSED_CONFIG);
258
- const first = await tool.execute({ dimension: "workspace", scope_value: "proj-a", state: "paused", note: "先停一下" });
259
- assert.deepEqual(first.scope, {
260
- dimension: "workspace",
261
- scope_value: "proj-a",
262
- state: "paused",
263
- note: "先停一下",
264
- decided_at: first.scope.decided_at
265
- });
266
-
267
- // 同一状态重按:入参没带 note,但回执里 note 还在,decided_at 也没变。
268
- const again = await tool.execute({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
269
- assert.equal(again.scope.decided_at, first.scope.decided_at);
270
- assert.equal(again.scope.note, "先停一下");
271
- });
272
-
273
- test("set_scope_state 对非法参数抛可行动的错,不是静默返回空", async () => {
274
- const { tool } = makeTools(PAUSED_CONFIG);
275
- await assert.rejects(() => tool.execute({ dimension: "nope", scope_value: "x", state: "paused" }), /dimension/);
276
- await assert.rejects(() => tool.execute({ dimension: "agent", scope_value: "x", state: "maybe" }), /state/);
277
- await assert.rejects(() => tool.execute({ dimension: "agent", scope_value: "", state: "paused" }), /scope_value/);
278
- });
279
-
280
- test("检视面刻意不过闸:按下之后 memory_list / memory_get 仍看得见那一行", async () => {
281
- const config = { ...PAUSED_CONFIG, strictScope: false };
282
- const store = createStore(":memory:");
283
- const service = createService({ store, mirror: null, config });
284
- const ctx = { tools: { register() {} }, logger: { warn() {}, info() {}, error() {} } };
285
- const tools = new Map(createTools(ctx, service, config, null).map((t) => [t.name, t]));
286
-
287
- const memory = store.save({ type: "project", title: "hidden", content: "body", importance: 3, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
288
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
289
-
290
- // 供料面:被按下 → 不进注入池。
291
- const injected = service.injectCandidates({ maxItems: 5, threshold: 0, scope: { agent_scope: null, workspace_scope: "proj-a" } });
292
- assert.ok(!injected.some((m) => m.id === memory.id), "供料面必须已挡住");
293
-
294
- // 检视面:仍看得见。这是刻意的——按下的是政策不是事实,人还要能查
295
- // 「我按下了什么、里面有什么」,否则就没有入口确认该不该松开。
296
- // 若将来有人「顺手」把闸门也接到 list/get 上,这条会红。
297
- const listed = await tools.get("memory_list").execute({});
298
- assert.ok(listed.items.some((m) => m.id === memory.id), "memory_list 不该被政策轴关掉(检视面)");
299
- const got = await tools.get("memory_get").execute({ id: memory.id });
300
- assert.equal(got.memory.id, memory.id, "memory_get 不该被政策轴关掉(检视面)");
301
- store.close();
302
- });
303
-
304
- // --- 睡眠侧:政策轴不只管「看得见」,还要管「改得到」 -----------------------
305
- //
306
- // 上面所有用例都停在读侧(检索出局、注入不进)。但 sleep 的四个相位里有三个会
307
- // **写**:demotion 会 setArchived / demoteToSummary(真改归档面),patterns 会
308
- // mint 出 type=pattern 的新行。只挡读侧的话,闸门开着而每 6 小时一次的睡眠照样
309
- // 在被按下的作用域上动手——「按下」就只等于让它在白天看不见,夜里照旧。
310
- //
311
- // 每条都配一个同轮的反向断言(未被按下的同类记忆**确实**被处理了):只断言
312
- // 「暂停的没动」的话,一个「整个相位都没跑」的实现也能全绿。
313
-
314
- /** demotion 要用时间窗 + 热联合判定,故把 project 的 λ 调快到冷态可降级。 */
315
- function sleepConfig(overrides = {}) {
316
- return {
317
- ...PAUSED_CONFIG,
318
- sleepModeEnabled: true,
319
- heatTypeDecay: { project: 0.02 },
320
- sleepArchiveDays: 30,
321
- sleepCompressDays: 90,
322
- sleepPatternMinMemories: 100,
323
- sleepMaxPatternPerRun: 3,
324
- ...overrides
325
- };
326
- }
327
-
328
- function sleepCtx() {
329
- return {
330
- logger: { warn() {}, info() {} },
331
- agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "sleep-model" }) },
332
- llm: {
333
- async *stream() {
334
- yield { type: "text-delta", index: 0, text: "[]" };
335
- yield { type: "finish", reason: { kind: "stop" } };
336
- }
337
- }
338
- };
339
- }
340
-
341
- // 标注走 store.save 的入参——那是存储层唯一的落标注路径,saveWithDedupe 不收
342
- // 这两个字段(标注是 scope.js 在工具层盖的)。这一点本身也是被测的性质:
343
- // 政策轴按**行上已有的标注**匹配,与标注从哪条路写进来无关。
344
-
345
- test("睡眠侧:被按下作用域的冷记忆不被 demotion 降级,同轮的未按下记忆照常降级", async () => {
346
- const { store, service } = setup(sleepConfig());
347
- const paused = store.save({
348
- type: "project", title: "paused-cold", content: "paused-cold 的正文".repeat(20), importance: 3,
349
- workspace_scope: "proj-a", workspace_scope_source: "explicit"
350
- });
351
- const open = store.save({
352
- type: "project", title: "open-cold", content: "open-cold 的正文".repeat(20), importance: 3
353
- });
354
- for (const m of [paused, open]) {
355
- service.touchLastAccess(m.id, new Date(Date.now() - 40 * 86400000).toISOString());
356
- }
357
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
358
-
359
- const ctx = sleepCtx();
360
- await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
361
-
362
- const afterPaused = service.getById(paused.id);
363
- const afterOpen = service.getById(open.id);
364
- assert.equal(afterPaused._full_content ?? null, null, "被按下的那条一个字都不该被改写");
365
- assert.ok(afterOpen._full_content, "同轮里未按下的冷记忆必须真被降级——否则本用例只是证明了相位没跑");
366
- store.close();
367
- });
368
-
369
- test("睡眠侧:被按下作用域的冷记忆不被 demotion 归档,同轮的未按下记忆照常归档", async () => {
370
- const { store, service } = setup(sleepConfig());
371
- const paused = store.save({
372
- type: "project", title: "paused-ancient", content: "paused-ancient 的正文", importance: 3,
373
- agent_scope: "agent-x", agent_scope_source: "explicit"
374
- });
375
- const open = store.save({
376
- type: "project", title: "open-ancient", content: "open-ancient 的正文", importance: 3
377
- });
378
- // 推到 compressCut 之外(>= 90 天)→ 直接归档而不是压缩成摘要。
379
- const ancient = new Date(Date.now() - 100 * 86400000).toISOString();
380
- for (const m of [paused, open]) service.touchLastAccess(m.id, ancient);
381
- service.setScopeState({ dimension: "agent", scope_value: "agent-x", state: "paused" });
382
-
383
- const ctx = sleepCtx();
384
- await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
385
-
386
- assert.equal(service.getById(paused.id).archived, false, "被按下的作用域不该被睡眠归档");
387
- assert.equal(service.getById(open.id).archived, true, "未按下的同类必须真被归档");
388
- store.close();
389
- });
390
-
391
- test("睡眠侧:被按下作用域的行不进 pattern 扫描池,也就不该占掉名额", async () => {
392
- const { store, service } = setup(sleepConfig());
393
- // 顺序是这条用例的关键:池子按 updated_at 降序取前 sleepPatternMinMemories 条,
394
- // 所以**被按下的必须最后创建**(= 最新),才能在「政策轴排在截断之后」的实现里
395
- // 抢到名额。第一版把被按下的写在前头,未修复代码取到的恰好是两条 open,
396
- // 用例照样全绿——那是「没测到东西」,不是「测过了」。
397
- store.save({ type: "project", title: "open-1", content: "open-1 正文", importance: 3 });
398
- store.save({ type: "project", title: "open-2", content: "open-2 正文", importance: 3 });
399
- for (const t of ["p1", "p2"]) {
400
- store.save({ type: "project", title: t, content: `${t} 正文`, importance: 3, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
401
- }
402
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
403
-
404
- const seen = [];
405
- const ctx = {
406
- ...sleepCtx(),
407
- llm: {
408
- async *stream(options) {
409
- const text = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
410
- seen.push(text);
411
- yield { type: "text-delta", index: 0, text: "[]" };
412
- yield { type: "finish", reason: { kind: "stop" } };
413
- }
414
- }
415
- };
416
- await runSleep(ctx, service, sleepConfig({ sleepPatternMinMemories: 2 }), ctx.logger, null, null);
417
-
418
- const scanned = seen.join("\n");
419
- assert.ok(scanned.includes("open-1"), "未按下的记忆必须进扫描池——被按下的不该把它们挤出名额");
420
- assert.ok(!scanned.includes("p1"), "被按下的记忆不该出现在模式扫描的提示里");
421
- store.close();
422
- });
423
-
424
- // --- issue #515:闸门把可扫行挡在 LIMIT 窗口之外 ---------------------------
425
- //
426
- // 上面那条用例锁的是「政策轴排在截断之前」,但它的库只有 4 行,根本碰不到
427
- // store.list 的 `LIMIT 200`,所以它测不到 #515 真正的症状:被按下的行**多到占满窗口**
428
- // 时,过滤前它们吃掉全部名额、过滤后自家行一条都不剩,相位回 `no memories to scan`
429
- // ——读起来像「库里没东西可扫」,实际是「闸门把可扫的行挡在了窗口里」。
430
- // 修复方向是不动判定、只放宽取数:窗口过滤后不够本次要扫的名额时,不带窗口重取一次。
431
- //
432
- // 造「自家行在窗口之外」用的是 importance 而不是 updated_at:默认档位是
433
- // `importance DESC, updated_at DESC, id`,而 store.save 的时间戳由实现内部取
434
- // nowIso()(调用方传不进去),同毫秒落库的行会退到 id 平局键——靠时间戳排序会依赖
435
- // 插入速度,快机器上自家行可能反而排进窗口,用例就成了偶发绿。importance 差(5 vs 1)
436
- // 让被按下的那批恒占窗口前排,与毫秒无关。
437
-
438
- /** 填满 200 窗口的按下行 + 两条落在窗口之外的自家行。 */
439
- function seedStarvedPatternPool(store, service) {
440
- const own = [];
441
- for (const t of ["own-1", "own-2"]) {
442
- own.push(store.save({ type: "project", title: t, content: `${t} 正文`, importance: 1 }));
443
- }
444
- for (let i = 0; i < 200; i++) {
445
- store.save({
446
- type: "project", title: `paused-${i}`, content: `paused-${i} 正文`, importance: 5,
447
- workspace_scope: "proj-a", workspace_scope_source: "explicit"
448
- });
449
- }
450
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
451
- return own;
452
- }
453
-
454
- /** 记录 service.list 的入参(只包 list,其余方法原样透出),用于「付了几次查询」。 */
455
- function countingList(service, calls) {
456
- return new Proxy(service, {
457
- get(target, prop) {
458
- if (prop === "list") {
459
- return (query) => {
460
- calls.push(query);
461
- return target.list(query);
462
- };
463
- }
464
- return Reflect.get(target, prop, target);
465
- }
466
- });
467
- }
468
-
469
- test("#515:被按下的行占满 LIMIT 200 窗口时,模式扫描不报「没东西可扫」", async () => {
470
- const { store, service } = setup(sleepConfig());
471
- seedStarvedPatternPool(store, service);
472
-
473
- const seen = [];
474
- const ctx = {
475
- ...sleepCtx(),
476
- llm: {
477
- async *stream(options) {
478
- const text = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
479
- // 认清单**形状**(行里带 `| importance=`),不认首行——#501/#493 那批围栏让 user 段以角色声明开头了
480
- if (text.includes("| importance=")) seen.push(text);
481
- yield { type: "text-delta", index: 0, text: "[]" };
482
- yield { type: "finish", reason: { kind: "stop" } };
483
- }
484
- }
485
- };
486
- const result = await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
487
- const patterns = result.phases.patterns;
488
-
489
- assert.notEqual(patterns?.reason, "no memories to scan",
490
- `窗口被按下行占满时不该静默跳过,实得 ${JSON.stringify(patterns)}`);
491
- // 反向断言:这条才是「真的扫了」的证据——只断上面那句的话,一个「相位整个没跑」
492
- // 的实现(连 reason 都不产出)也能过。
493
- assert.equal(seen.length, 1, "模式扫描必须真发起了这次 LLM 调用");
494
- assert.ok(seen[0].includes("own-1") && seen[0].includes("own-2"), "窗口外的自家行要被重取回来");
495
- // 重取只放宽窗口,不放宽判定:被按下的行一条都不许混进提示。
496
- assert.ok(!seen[0].includes("paused-"), "被按下的行不该因为重取而混进扫描池");
497
- store.close();
498
- });
499
-
500
- test("#515:重取按需——闸门没按下且窗口够用时,仍只付一次有界查询", async () => {
501
- const { store, service } = setup(sleepConfig());
502
- // 120 条自家行(无闸门):过滤后的池子 ≥ sleepPatternMinMemories(100),窗口没被吃掉,
503
- // 第二趟不该发生。这一条锁的是「常态仍只付一次有界查询」那半句承诺。
504
- for (let i = 0; i < 120; i++) {
505
- store.save({ type: "project", title: `m-${i}`, content: `m-${i} 正文`, importance: 3 });
506
- }
507
- const calls = [];
508
- const ctx = sleepCtx();
509
- await runSleep(ctx, countingList(service, calls), sleepConfig(), ctx.logger, null, null);
510
-
511
- assert.equal(calls.length, 1, `常态只该取一次,实得 ${calls.length} 次`);
512
- assert.equal(calls[0].limit, 200, "常态走的是有界窗口");
513
- store.close();
514
- });
515
-
516
- test("#515:窗口跟着配额走——把 sleepPatternMinMemories 配到 200 之上时,常态仍不触发无界重取", async () => {
517
- // 这条锁的是我自己差点留下的坑:合法上限是 1000(src/config.js:612),若窗口仍是死的
518
- // 200,凡把这个值配到 200 以上的人**每一轮**都会白付一次全表重取,跟闸门按没按下无关。
519
- const config = sleepConfig({ sleepPatternMinMemories: 300 });
520
- const { store, service } = setup(config);
521
- for (let i = 0; i < 400; i++) {
522
- store.save({ type: "project", title: `m-${i}`, content: `m-${i} 正文`, importance: 3 });
523
- }
524
- const calls = [];
525
- const ctx = sleepCtx();
526
- await runSleep(ctx, countingList(service, calls), config, ctx.logger, null, null);
527
-
528
- assert.equal(calls.length, 1, `窗口够用时不该重取,实得 ${calls.length} 次`);
529
- assert.equal(calls[0].limit, 300, "窗口至少等于本次要扫的名额");
530
- store.close();
531
- });
532
-
533
- test("#515:窗口饿死时重取一次,且只放宽 limit——三条过滤轴一条不许动", async () => {
534
- const { store, service } = setup(sleepConfig());
535
- seedStarvedPatternPool(store, service);
536
-
537
- const calls = [];
538
- const ctx = sleepCtx();
539
- await runSleep(ctx, countingList(service, calls), sleepConfig(), ctx.logger, null, null);
540
-
541
- assert.equal(calls.length, 2, `饿死时补一趟无界重取,实得 ${calls.length} 次`);
542
- assert.equal(calls[0].limit, 200);
543
- assert.equal(calls[1].limit, null, "重取是不带窗口(store.list 的 limit == null 即无界档)");
544
- // 除 limit 之外的口径必须逐字段相同:重取放宽的是窗口,不是判定。
545
- assert.deepEqual(
546
- { ...calls[0], limit: null },
547
- calls[1],
548
- "第二趟只能比第一趟少一个窗口,excludeTypes/excludeImported/includeForgotten 都得在"
549
- );
550
- store.close();
551
- });
552
-
553
- test("睡眠侧:被按下作用域的行不进关系补全的候选池", async () => {
554
- const { store, service } = setup(sleepConfig());
555
- const alpha = service.createEntity({ name: "Alpha", type: "project" });
556
- service.createEntity({ name: "Beta", type: "project" });
557
- store.save({
558
- type: "project", title: "t", content: "Alpha 与 Beta 一起出现", importance: 3,
559
- workspace_scope: "proj-a", workspace_scope_source: "explicit"
560
- });
561
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
562
-
563
- const ctx = sleepCtx();
564
- await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
565
- assert.equal((service.getRelations(alpha.id) ?? []).length, 0, "被按下的记忆不该被拿去连关系");
566
- store.close();
567
- });
568
-
569
- // ---------------------------------------------------------------- dream 侧
570
-
571
- test("巩固侧:被按下作用域的行不进总览输入——否则会被洗成无作用域行", async () => {
572
- const { store, service } = setup(PAUSED_CONFIG);
573
- service.saveWithDedupe({ type: "project", title: "open-title", content: "open 正文", importance: 3 });
574
- // 标注走 store.save 的入参——saveWithDedupe 不收这两个字段(标注是 scope.js 在工具层盖的)。
575
- store.save({ type: "project", title: "paused-title", content: "paused 正文", importance: 3, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
576
- service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
577
-
578
- const summaryPrompts = [];
579
- const ctx = {
580
- logger: { warn() {} },
581
- agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
582
- llm: {
583
- async *stream(options) {
584
- const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
585
- if (userText.includes("| type=")) {
586
- yield { type: "text-delta", index: 0, text: "[]" };
587
- yield { type: "finish", reason: { kind: "stop" } };
588
- return;
589
- }
590
- summaryPrompts.push(userText);
591
- yield { type: "text-delta", index: 0, text: "总览正文" };
592
- yield { type: "finish", reason: { kind: "stop" } };
593
- }
594
- }
595
- };
596
- const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true }), thresholdCount: 1 });
597
- await dream.runDream(ctx, service, {});
598
-
599
- const fed = summaryPrompts.join("\n");
600
- assert.ok(fed.includes("open-title"), "未按下的记忆必须进总览输入——否则本用例只是证明了总览没跑");
601
- assert.ok(!fed.includes("paused-title"), "被按下的记忆不该进总览输入");
602
-
603
- // 这是这条用例真正的要点:总览落库时是**无作用域**的 summary 行,
604
- // 所以一旦被按下的内容混进去,注入侧按作用域过滤就再也拦不住它——
605
- // 内容已经被洗成全局行了。别的入口漏了只是「漏一条」,这里漏了是「一直漏」。
606
- const overview = service.all().find((m) => m.type === "summary" && m.source === "dream");
607
- assert.ok(overview, "总览仍应正常落库(闸门不该把总览整个关掉)");
608
- assert.equal(overview.workspace_scope ?? null, null, "总览本身没有作用域标注——正因如此,输入必须在源头过滤");
609
- store.close();
610
- });
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createStore } from "../src/store.js";
4
+ import { createService } from "../src/service.js";
5
+ import { createTools } from "../src/tools.js";
6
+ import { runSleep } from "../src/dream/sleep.js";
7
+ import { createDreamScheduler } from "../src/dream.js";
8
+
9
+ // issue #471:作用域级可逆闸门(政策轴)。
10
+ //
11
+ // 与 #17 / #170 那条事实轴(scope 标注与可见性)是两条不同的轴:
12
+ // - 事实轴回答「这条记忆归谁」,答案写在记忆行上,改了它就是在改事实;
13
+ // - 政策轴回答「这一路现在通不通」,答案写在 scope_state 表里,可逆、
14
+ // 不改任何记忆行、不改作用域标注、不动向量与归档位。
15
+ //
16
+ // 三道验收线(对应 issue 正文的三处落点):
17
+ // 1. 默认关:gateByScope 的输出与改动前**逐字段一致**,且是**同一个数组引用**
18
+ // (引用相等才钉得住「零行为变化」,内容相等会让「悄悄复制一份」蒙过去);
19
+ // 2. 按下与松开:同一作用域的候选进不来,松开后原样回来——两个方向都要断言,
20
+ // 只锁「进不来」的话,一个「永远返回空」的实现也能过;
21
+ // 3. 观察档:enabled 开、enforce 关 → 留一条 llm_audit_logs 但注入照常,
22
+ // 这条是两级开关的意义所在(先在真实流量里看「如果按下会少掉多少」)。
23
+
24
+ function setup(config) {
25
+ const store = createStore(":memory:");
26
+ const service = createService({ store, mirror: null, config });
27
+ return { store, service };
28
+ }
29
+
30
+ const PAUSED_CONFIG = { scopeState: { enabled: true, enforce: true } };
31
+ const OBSERVE_CONFIG = { scopeState: { enabled: true, enforce: false } };
32
+
33
+ /** 造一批带 workspace 标注的记忆,一半属于 `proj-a`,一半无标注。 */
34
+ function seed(store) {
35
+ store.save({ type: "decision", title: "a-1", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
36
+ store.save({ type: "decision", title: "a-2", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
37
+ store.save({ type: "decision", title: "b-1", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-b", workspace_scope_source: "explicit" });
38
+ store.save({ type: "decision", title: "g-1", content: "alpha beta gamma", importance: 4 });
39
+ }
40
+
41
+ const titles = (rows) => rows.map((m) => m.title).sort();
42
+
43
+ // --- 验收线 1:默认关时逐字段一致,且引用不变 -----------------------------
44
+
45
+ test("默认关:gateByScope 原样返回同一个数组(引用相等,不是等值副本)", () => {
46
+ const { store, service } = setup({});
47
+ seed(store);
48
+ const rows = store.list({});
49
+ const out = service.filterPaused(rows);
50
+ // 引用相等:内容相等的副本会让「复制一份再返回」蒙过去,那样后续每次
51
+ // 检索都白复制一遍,而「零行为变化」这条验收就退化成「看起来一样」。
52
+ assert.equal(out, rows);
53
+ });
54
+
55
+ test("默认关:按下过状态也不生效——开关不打开时状态表根本不被读", () => {
56
+ const { store, service } = setup({});
57
+ seed(store);
58
+ // 即便有人在开关关着的时候写进了状态(比如另一台机器同步过来),也不生效。
59
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
60
+ const rows = store.list({});
61
+ assert.equal(service.filterPaused(rows), rows);
62
+ });
63
+
64
+ test("默认关:注入候选集与无闸门时逐条相同", () => {
65
+ const off = setup({});
66
+ seed(off.store);
67
+ const baseline = titles(off.service.injectCandidates({ maxItems: 10, threshold: 1 }));
68
+
69
+ const pressed = setup({});
70
+ seed(pressed.store);
71
+ pressed.service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
72
+ const after = titles(pressed.service.injectCandidates({ maxItems: 10, threshold: 1 }));
73
+
74
+ assert.deepEqual(after, baseline);
75
+ });
76
+
77
+ // --- 验收线 2:按下与松开(两个方向都锁) ---------------------------------
78
+
79
+ test("按下后:被按下作用域的候选进不来注入池,别的照常进", () => {
80
+ const { store, service } = setup(PAUSED_CONFIG);
81
+ seed(store);
82
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
83
+
84
+ const got = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
85
+ assert.deepEqual(got, ["b-1", "g-1"]);
86
+ // 反向断言:不是「全都进不来」。
87
+ assert.ok(got.includes("b-1") && got.includes("g-1"));
88
+ });
89
+
90
+ test("松开后:原样回来(同一个 service 实例,不重开库)", () => {
91
+ const { store, service } = setup(PAUSED_CONFIG);
92
+ seed(store);
93
+ const before = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
94
+
95
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
96
+ assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["b-1", "g-1"]);
97
+
98
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active" });
99
+ // 逐条相同(不只「数量相同」):松开要真的恢复原样,不是恢复个大概。
100
+ assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), before);
101
+ });
102
+
103
+ test("按下不动事实:记忆行、作用域标注、归档位一字段都不变", () => {
104
+ const { store, service } = setup(PAUSED_CONFIG);
105
+ seed(store);
106
+ const snapshot = () => store.list({}).map((m) => ({
107
+ id: m.id, title: m.title, workspace_scope: m.workspace_scope,
108
+ workspace_scope_source: m.workspace_scope_source, archived: m.archived, forgotten: m.forgotten
109
+ })).sort((x, y) => x.title.localeCompare(y.title));
110
+
111
+ const before = snapshot();
112
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
113
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active" });
114
+ assert.deepEqual(snapshot(), before);
115
+ });
116
+
117
+ test("检索侧同款:被按下的作用域在 searchMemories 里也出局", async () => {
118
+ const { store, service } = setup(PAUSED_CONFIG);
119
+ seed(store);
120
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
121
+
122
+ const hits = await service.searchMemories("alpha", { mode: "keyword", limit: 10 });
123
+ const got = titles(hits);
124
+ assert.ok(!got.includes("a-1") && !got.includes("a-2"), `unexpected: ${got.join(",")}`);
125
+ assert.ok(got.includes("b-1"));
126
+ });
127
+
128
+ test("全局按下('*')拦下该维度所有有标注的行,但不波及未标注行", () => {
129
+ const { store, service } = setup(PAUSED_CONFIG);
130
+ seed(store);
131
+ service.setScopeState({ dimension: "workspace", scope_value: "*", state: "paused" });
132
+
133
+ // g-1 无 workspace 标注 ⇒ 在 workspace 这一维上不参与比较,不受影响。
134
+ assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["g-1"]);
135
+ });
136
+
137
+ test("维度独立:按下 agent 维不影响 workspace 维的行", () => {
138
+ const { store, service } = setup(PAUSED_CONFIG);
139
+ seed(store);
140
+ service.setScopeState({ dimension: "agent", scope_value: "proj-a", state: "paused" });
141
+
142
+ // 这批行没有 agent 标注,agent 维的按下一条都命中不了。
143
+ const got = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
144
+ assert.deepEqual(got, ["a-1", "a-2", "b-1", "g-1"]);
145
+ });
146
+
147
+ // --- 验收线 3:观察档留审计但不拦 -----------------------------------------
148
+
149
+ test("观察档:注入照常,但留一条 scopeState 审计行", () => {
150
+ const { store, service } = setup(OBSERVE_CONFIG);
151
+ seed(store);
152
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
153
+
154
+ const got = titles(service.injectCandidates({ maxItems: 10, threshold: 1 }));
155
+ // 观察档不拦:四条全在。
156
+ assert.deepEqual(got, ["a-1", "a-2", "b-1", "g-1"]);
157
+
158
+ const audits = store.listLlmAudits({ source: "scopeState" });
159
+ assert.equal(audits.length, 1);
160
+ assert.equal(audits[0].operation_type, "scope_state_gate");
161
+ assert.equal(audits[0].model_id, "-");
162
+ assert.equal(audits[0].status, "skipped");
163
+ assert.equal(audits[0].metadata.enforced, false);
164
+ // gated 记的是「本应拦下几条」,不是「拦了几条」——观察档下后者恒 0,
165
+ // 记它就等于没记。
166
+ assert.equal(audits[0].metadata.gated, 2);
167
+ assert.deepEqual(audits[0].metadata.paused, [{ dimension: "workspace", scope_value: "proj-a" }]);
168
+ });
169
+
170
+ test("拦截档:同一条审计行但 enforced=true,且真拦下了", () => {
171
+ const { store, service } = setup(PAUSED_CONFIG);
172
+ seed(store);
173
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
174
+
175
+ assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["b-1", "g-1"]);
176
+
177
+ const audits = store.listLlmAudits({ source: "scopeState" });
178
+ assert.equal(audits.length, 1);
179
+ assert.equal(audits[0].metadata.enforced, true);
180
+ assert.equal(audits[0].metadata.gated, 2);
181
+ });
182
+
183
+ test("没有候选被拦时不写审计行(门没经过人,不该留痕)", () => {
184
+ const { store, service } = setup(OBSERVE_CONFIG);
185
+ seed(store);
186
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-zzz", state: "paused" });
187
+ service.injectCandidates({ maxItems: 10, threshold: 1 });
188
+ assert.equal(store.listLlmAudits({ source: "scopeState" }).length, 0);
189
+ });
190
+
191
+ test("enforce 开、enabled 关:无效果(enforce 不单独生效,同 preInjectGate)", () => {
192
+ const { store, service } = setup({ scopeState: { enabled: false, enforce: true } });
193
+ seed(store);
194
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
195
+ assert.deepEqual(titles(service.injectCandidates({ maxItems: 10, threshold: 1 })), ["a-1", "a-2", "b-1", "g-1"]);
196
+ assert.equal(store.listLlmAudits({ source: "scopeState" }).length, 0);
197
+ });
198
+
199
+ // --- 状态表本身的语义 -----------------------------------------------------
200
+
201
+ test("同一状态重复按下不刷新 decided_at(按两次不等于换了决定)", () => {
202
+ const { service } = setup(PAUSED_CONFIG);
203
+ const first = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", decided_at: "2026-01-01T00:00:00.000Z" });
204
+ const again = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", decided_at: "2026-01-02T00:00:00.000Z" });
205
+ assert.equal(first.decided_at, "2026-01-01T00:00:00.000Z");
206
+ assert.equal(again.decided_at, "2026-01-01T00:00:00.000Z");
207
+
208
+ const flipped = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active", decided_at: "2026-01-03T00:00:00.000Z" });
209
+ assert.equal(flipped.decided_at, "2026-01-03T00:00:00.000Z");
210
+ assert.equal(flipped.state, "active");
211
+ });
212
+
213
+ test("不带理由的重按不抹掉已有理由(理由被一次手滑抹掉是净损失)", () => {
214
+ const { service } = setup(PAUSED_CONFIG);
215
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", note: "等这轮重构完再开" });
216
+ const again = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "active" });
217
+ assert.equal(again.note, "等这轮重构完再开");
218
+
219
+ const replaced = service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused", note: "换了个理由" });
220
+ assert.equal(replaced.note, "换了个理由");
221
+ });
222
+
223
+ test("非法维度/值/状态返回 undefined,不静默落一行永不生效的状态", () => {
224
+ const { service, store } = setup(PAUSED_CONFIG);
225
+ assert.equal(service.setScopeState({ dimension: "agent", scope_value: "ok", state: "maybe" }), undefined);
226
+ assert.equal(service.setScopeState({ dimension: "nope", scope_value: "ok", state: "paused" }), undefined);
227
+ assert.equal(service.setScopeState({ dimension: "agent", scope_value: " ", state: "paused" }), undefined);
228
+ assert.equal(store.listScopeStates().length, 0);
229
+ });
230
+
231
+ test("一次现读:另一个实例改了状态,本实例下一次判定立刻看得见(没有缓存要失效)", () => {
232
+ const store = createStore(":memory:");
233
+ const a = createService({ store, mirror: null, config: PAUSED_CONFIG });
234
+ const b = createService({ store, mirror: null, config: PAUSED_CONFIG });
235
+ store.save({ type: "decision", title: "a-1", content: "alpha beta gamma", importance: 4, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
236
+
237
+ assert.deepEqual(titles(a.injectCandidates({ maxItems: 10, threshold: 1 })), ["a-1"]);
238
+ b.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
239
+ assert.deepEqual(titles(a.injectCandidates({ maxItems: 10, threshold: 1 })), []);
240
+ });
241
+
242
+ // --- 工具面 ---------------------------------------------------------------
243
+
244
+ function makeTools(config) {
245
+ const store = createStore(":memory:");
246
+ const service = createService({ store, mirror: null, config });
247
+ const ctx = {
248
+ tools: { register() {} },
249
+ logger: { warn() {}, info() {}, error() {} }
250
+ };
251
+ const tools = createTools(ctx, service, config, null);
252
+ const byName = new Map(tools.map((t) => [t.name, t]));
253
+ return { store, service, tool: byName.get("set_scope_state") };
254
+ }
255
+
256
+ test("set_scope_state 回执回显库内当前值,不是回显入参", async () => {
257
+ const { tool } = makeTools(PAUSED_CONFIG);
258
+ const first = await tool.execute({ dimension: "workspace", scope_value: "proj-a", state: "paused", note: "先停一下" });
259
+ assert.deepEqual(first.scope, {
260
+ dimension: "workspace",
261
+ scope_value: "proj-a",
262
+ state: "paused",
263
+ note: "先停一下",
264
+ decided_at: first.scope.decided_at
265
+ });
266
+
267
+ // 同一状态重按:入参没带 note,但回执里 note 还在,decided_at 也没变。
268
+ const again = await tool.execute({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
269
+ assert.equal(again.scope.decided_at, first.scope.decided_at);
270
+ assert.equal(again.scope.note, "先停一下");
271
+ });
272
+
273
+ test("set_scope_state 对非法参数抛可行动的错,不是静默返回空", async () => {
274
+ const { tool } = makeTools(PAUSED_CONFIG);
275
+ await assert.rejects(() => tool.execute({ dimension: "nope", scope_value: "x", state: "paused" }), /dimension/);
276
+ await assert.rejects(() => tool.execute({ dimension: "agent", scope_value: "x", state: "maybe" }), /state/);
277
+ await assert.rejects(() => tool.execute({ dimension: "agent", scope_value: "", state: "paused" }), /scope_value/);
278
+ });
279
+
280
+ test("检视面刻意不过闸:按下之后 memory_list / memory_get 仍看得见那一行", async () => {
281
+ const config = { ...PAUSED_CONFIG, strictScope: false };
282
+ const store = createStore(":memory:");
283
+ const service = createService({ store, mirror: null, config });
284
+ const ctx = { tools: { register() {} }, logger: { warn() {}, info() {}, error() {} } };
285
+ const tools = new Map(createTools(ctx, service, config, null).map((t) => [t.name, t]));
286
+
287
+ const memory = store.save({ type: "project", title: "hidden", content: "body", importance: 3, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
288
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
289
+
290
+ // 供料面:被按下 → 不进注入池。
291
+ const injected = service.injectCandidates({ maxItems: 5, threshold: 0, scope: { agent_scope: null, workspace_scope: "proj-a" } });
292
+ assert.ok(!injected.some((m) => m.id === memory.id), "供料面必须已挡住");
293
+
294
+ // 检视面:仍看得见。这是刻意的——按下的是政策不是事实,人还要能查
295
+ // 「我按下了什么、里面有什么」,否则就没有入口确认该不该松开。
296
+ // 若将来有人「顺手」把闸门也接到 list/get 上,这条会红。
297
+ const listed = await tools.get("memory_list").execute({});
298
+ assert.ok(listed.items.some((m) => m.id === memory.id), "memory_list 不该被政策轴关掉(检视面)");
299
+ const got = await tools.get("memory_get").execute({ id: memory.id });
300
+ assert.equal(got.memory.id, memory.id, "memory_get 不该被政策轴关掉(检视面)");
301
+ store.close();
302
+ });
303
+
304
+ // --- 睡眠侧:政策轴不只管「看得见」,还要管「改得到」 -----------------------
305
+ //
306
+ // 上面所有用例都停在读侧(检索出局、注入不进)。但 sleep 的四个相位里有三个会
307
+ // **写**:demotion 会 setArchived / demoteToSummary(真改归档面),patterns 会
308
+ // mint 出 type=pattern 的新行。只挡读侧的话,闸门开着而每 6 小时一次的睡眠照样
309
+ // 在被按下的作用域上动手——「按下」就只等于让它在白天看不见,夜里照旧。
310
+ //
311
+ // 每条都配一个同轮的反向断言(未被按下的同类记忆**确实**被处理了):只断言
312
+ // 「暂停的没动」的话,一个「整个相位都没跑」的实现也能全绿。
313
+
314
+ /** demotion 要用时间窗 + 热联合判定,故把 project 的 λ 调快到冷态可降级。 */
315
+ function sleepConfig(overrides = {}) {
316
+ return {
317
+ ...PAUSED_CONFIG,
318
+ sleepModeEnabled: true,
319
+ heatTypeDecay: { project: 0.02 },
320
+ sleepArchiveDays: 30,
321
+ sleepCompressDays: 90,
322
+ sleepPatternMinMemories: 100,
323
+ sleepMaxPatternPerRun: 3,
324
+ ...overrides
325
+ };
326
+ }
327
+
328
+ function sleepCtx() {
329
+ return {
330
+ logger: { warn() {}, info() {} },
331
+ agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "sleep-model" }) },
332
+ llm: {
333
+ async *stream() {
334
+ yield { type: "text-delta", index: 0, text: "[]" };
335
+ yield { type: "finish", reason: { kind: "stop" } };
336
+ }
337
+ }
338
+ };
339
+ }
340
+
341
+ // 标注走 store.save 的入参——那是存储层唯一的落标注路径,saveWithDedupe 不收
342
+ // 这两个字段(标注是 scope.js 在工具层盖的)。这一点本身也是被测的性质:
343
+ // 政策轴按**行上已有的标注**匹配,与标注从哪条路写进来无关。
344
+
345
+ test("睡眠侧:被按下作用域的冷记忆不被 demotion 降级,同轮的未按下记忆照常降级", async () => {
346
+ const { store, service } = setup(sleepConfig());
347
+ const paused = store.save({
348
+ type: "project", title: "paused-cold", content: "paused-cold 的正文".repeat(20), importance: 3,
349
+ workspace_scope: "proj-a", workspace_scope_source: "explicit"
350
+ });
351
+ const open = store.save({
352
+ type: "project", title: "open-cold", content: "open-cold 的正文".repeat(20), importance: 3
353
+ });
354
+ for (const m of [paused, open]) {
355
+ service.touchLastAccess(m.id, new Date(Date.now() - 40 * 86400000).toISOString());
356
+ }
357
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
358
+
359
+ const ctx = sleepCtx();
360
+ await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
361
+
362
+ const afterPaused = service.getById(paused.id);
363
+ const afterOpen = service.getById(open.id);
364
+ assert.equal(afterPaused._full_content ?? null, null, "被按下的那条一个字都不该被改写");
365
+ assert.ok(afterOpen._full_content, "同轮里未按下的冷记忆必须真被降级——否则本用例只是证明了相位没跑");
366
+ store.close();
367
+ });
368
+
369
+ test("睡眠侧:被按下作用域的冷记忆不被 demotion 归档,同轮的未按下记忆照常归档", async () => {
370
+ const { store, service } = setup(sleepConfig());
371
+ const paused = store.save({
372
+ type: "project", title: "paused-ancient", content: "paused-ancient 的正文", importance: 3,
373
+ agent_scope: "agent-x", agent_scope_source: "explicit"
374
+ });
375
+ const open = store.save({
376
+ type: "project", title: "open-ancient", content: "open-ancient 的正文", importance: 3
377
+ });
378
+ // 推到 compressCut 之外(>= 90 天)→ 直接归档而不是压缩成摘要。
379
+ const ancient = new Date(Date.now() - 100 * 86400000).toISOString();
380
+ for (const m of [paused, open]) service.touchLastAccess(m.id, ancient);
381
+ service.setScopeState({ dimension: "agent", scope_value: "agent-x", state: "paused" });
382
+
383
+ const ctx = sleepCtx();
384
+ await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
385
+
386
+ assert.equal(service.getById(paused.id).archived, false, "被按下的作用域不该被睡眠归档");
387
+ assert.equal(service.getById(open.id).archived, true, "未按下的同类必须真被归档");
388
+ store.close();
389
+ });
390
+
391
+ test("睡眠侧:被按下作用域的行不进 pattern 扫描池,也就不该占掉名额", async () => {
392
+ const { store, service } = setup(sleepConfig());
393
+ // 顺序是这条用例的关键:池子按 updated_at 降序取前 sleepPatternMinMemories 条,
394
+ // 所以**被按下的必须最后创建**(= 最新),才能在「政策轴排在截断之后」的实现里
395
+ // 抢到名额。第一版把被按下的写在前头,未修复代码取到的恰好是两条 open,
396
+ // 用例照样全绿——那是「没测到东西」,不是「测过了」。
397
+ store.save({ type: "project", title: "open-1", content: "open-1 正文", importance: 3 });
398
+ store.save({ type: "project", title: "open-2", content: "open-2 正文", importance: 3 });
399
+ for (const t of ["p1", "p2"]) {
400
+ store.save({ type: "project", title: t, content: `${t} 正文`, importance: 3, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
401
+ }
402
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
403
+
404
+ const seen = [];
405
+ const ctx = {
406
+ ...sleepCtx(),
407
+ llm: {
408
+ async *stream(options) {
409
+ const text = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
410
+ seen.push(text);
411
+ yield { type: "text-delta", index: 0, text: "[]" };
412
+ yield { type: "finish", reason: { kind: "stop" } };
413
+ }
414
+ }
415
+ };
416
+ await runSleep(ctx, service, sleepConfig({ sleepPatternMinMemories: 2 }), ctx.logger, null, null);
417
+
418
+ const scanned = seen.join("\n");
419
+ assert.ok(scanned.includes("open-1"), "未按下的记忆必须进扫描池——被按下的不该把它们挤出名额");
420
+ assert.ok(!scanned.includes("p1"), "被按下的记忆不该出现在模式扫描的提示里");
421
+ store.close();
422
+ });
423
+
424
+ // --- issue #515:闸门把可扫行挡在 LIMIT 窗口之外 ---------------------------
425
+ //
426
+ // 上面那条用例锁的是「政策轴排在截断之前」,但它的库只有 4 行,根本碰不到
427
+ // store.list 的 `LIMIT 200`,所以它测不到 #515 真正的症状:被按下的行**多到占满窗口**
428
+ // 时,过滤前它们吃掉全部名额、过滤后自家行一条都不剩,相位回 `no memories to scan`
429
+ // ——读起来像「库里没东西可扫」,实际是「闸门把可扫的行挡在了窗口里」。
430
+ // 修复方向是不动判定、只放宽取数:窗口过滤后不够本次要扫的名额时,不带窗口重取一次。
431
+ //
432
+ // 造「自家行在窗口之外」用的是 importance 而不是 updated_at:默认档位是
433
+ // `importance DESC, updated_at DESC, id`,而 store.save 的时间戳由实现内部取
434
+ // nowIso()(调用方传不进去),同毫秒落库的行会退到 id 平局键——靠时间戳排序会依赖
435
+ // 插入速度,快机器上自家行可能反而排进窗口,用例就成了偶发绿。importance 差(5 vs 1)
436
+ // 让被按下的那批恒占窗口前排,与毫秒无关。
437
+
438
+ /** 填满 200 窗口的按下行 + 两条落在窗口之外的自家行。 */
439
+ function seedStarvedPatternPool(store, service) {
440
+ const own = [];
441
+ for (const t of ["own-1", "own-2"]) {
442
+ own.push(store.save({ type: "project", title: t, content: `${t} 正文`, importance: 1 }));
443
+ }
444
+ for (let i = 0; i < 200; i++) {
445
+ store.save({
446
+ type: "project", title: `paused-${i}`, content: `paused-${i} 正文`, importance: 5,
447
+ workspace_scope: "proj-a", workspace_scope_source: "explicit"
448
+ });
449
+ }
450
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
451
+ return own;
452
+ }
453
+
454
+ /** 记录 service.list 的入参(只包 list,其余方法原样透出),用于「付了几次查询」。 */
455
+ function countingList(service, calls) {
456
+ return new Proxy(service, {
457
+ get(target, prop) {
458
+ if (prop === "list") {
459
+ return (query) => {
460
+ calls.push(query);
461
+ return target.list(query);
462
+ };
463
+ }
464
+ return Reflect.get(target, prop, target);
465
+ }
466
+ });
467
+ }
468
+
469
+ test("#515:被按下的行占满 LIMIT 200 窗口时,模式扫描不报「没东西可扫」", async () => {
470
+ const { store, service } = setup(sleepConfig());
471
+ seedStarvedPatternPool(store, service);
472
+
473
+ const seen = [];
474
+ const ctx = {
475
+ ...sleepCtx(),
476
+ llm: {
477
+ async *stream(options) {
478
+ const text = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
479
+ // 认清单**形状**(行里带 `| importance=`),不认首行——#501/#493 那批围栏让 user 段以角色声明开头了
480
+ if (text.includes("| importance=")) seen.push(text);
481
+ yield { type: "text-delta", index: 0, text: "[]" };
482
+ yield { type: "finish", reason: { kind: "stop" } };
483
+ }
484
+ }
485
+ };
486
+ const result = await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
487
+ const patterns = result.phases.patterns;
488
+
489
+ assert.notEqual(patterns?.reason, "no memories to scan",
490
+ `窗口被按下行占满时不该静默跳过,实得 ${JSON.stringify(patterns)}`);
491
+ // 反向断言:这条才是「真的扫了」的证据——只断上面那句的话,一个「相位整个没跑」
492
+ // 的实现(连 reason 都不产出)也能过。
493
+ assert.equal(seen.length, 1, "模式扫描必须真发起了这次 LLM 调用");
494
+ assert.ok(seen[0].includes("own-1") && seen[0].includes("own-2"), "窗口外的自家行要被重取回来");
495
+ // 重取只放宽窗口,不放宽判定:被按下的行一条都不许混进提示。
496
+ assert.ok(!seen[0].includes("paused-"), "被按下的行不该因为重取而混进扫描池");
497
+ store.close();
498
+ });
499
+
500
+ test("#515:重取按需——闸门没按下且窗口够用时,仍只付一次有界查询", async () => {
501
+ const { store, service } = setup(sleepConfig());
502
+ // 120 条自家行(无闸门):过滤后的池子 ≥ sleepPatternMinMemories(100),窗口没被吃掉,
503
+ // 第二趟不该发生。这一条锁的是「常态仍只付一次有界查询」那半句承诺。
504
+ for (let i = 0; i < 120; i++) {
505
+ store.save({ type: "project", title: `m-${i}`, content: `m-${i} 正文`, importance: 3 });
506
+ }
507
+ const calls = [];
508
+ const ctx = sleepCtx();
509
+ await runSleep(ctx, countingList(service, calls), sleepConfig(), ctx.logger, null, null);
510
+
511
+ assert.equal(calls.length, 1, `常态只该取一次,实得 ${calls.length} 次`);
512
+ assert.equal(calls[0].limit, 200, "常态走的是有界窗口");
513
+ store.close();
514
+ });
515
+
516
+ test("#515:窗口跟着配额走——把 sleepPatternMinMemories 配到 200 之上时,常态仍不触发无界重取", async () => {
517
+ // 这条锁的是我自己差点留下的坑:合法上限是 1000(src/config.js:612),若窗口仍是死的
518
+ // 200,凡把这个值配到 200 以上的人**每一轮**都会白付一次全表重取,跟闸门按没按下无关。
519
+ const config = sleepConfig({ sleepPatternMinMemories: 300 });
520
+ const { store, service } = setup(config);
521
+ for (let i = 0; i < 400; i++) {
522
+ store.save({ type: "project", title: `m-${i}`, content: `m-${i} 正文`, importance: 3 });
523
+ }
524
+ const calls = [];
525
+ const ctx = sleepCtx();
526
+ await runSleep(ctx, countingList(service, calls), config, ctx.logger, null, null);
527
+
528
+ assert.equal(calls.length, 1, `窗口够用时不该重取,实得 ${calls.length} 次`);
529
+ assert.equal(calls[0].limit, 300, "窗口至少等于本次要扫的名额");
530
+ store.close();
531
+ });
532
+
533
+ test("#515:窗口饿死时重取一次,且只放宽 limit——三条过滤轴一条不许动", async () => {
534
+ const { store, service } = setup(sleepConfig());
535
+ seedStarvedPatternPool(store, service);
536
+
537
+ const calls = [];
538
+ const ctx = sleepCtx();
539
+ await runSleep(ctx, countingList(service, calls), sleepConfig(), ctx.logger, null, null);
540
+
541
+ assert.equal(calls.length, 2, `饿死时补一趟无界重取,实得 ${calls.length} 次`);
542
+ assert.equal(calls[0].limit, 200);
543
+ assert.equal(calls[1].limit, null, "重取是不带窗口(store.list 的 limit == null 即无界档)");
544
+ // 除 limit 之外的口径必须逐字段相同:重取放宽的是窗口,不是判定。
545
+ assert.deepEqual(
546
+ { ...calls[0], limit: null },
547
+ calls[1],
548
+ "第二趟只能比第一趟少一个窗口,excludeTypes/excludeImported/includeForgotten 都得在"
549
+ );
550
+ store.close();
551
+ });
552
+
553
+ test("睡眠侧:被按下作用域的行不进关系补全的候选池", async () => {
554
+ const { store, service } = setup(sleepConfig());
555
+ const alpha = service.createEntity({ name: "Alpha", type: "project" });
556
+ service.createEntity({ name: "Beta", type: "project" });
557
+ store.save({
558
+ type: "project", title: "t", content: "Alpha 与 Beta 一起出现", importance: 3,
559
+ workspace_scope: "proj-a", workspace_scope_source: "explicit"
560
+ });
561
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
562
+
563
+ const ctx = sleepCtx();
564
+ await runSleep(ctx, service, sleepConfig(), ctx.logger, null, null);
565
+ assert.equal((service.getRelations(alpha.id) ?? []).length, 0, "被按下的记忆不该被拿去连关系");
566
+ store.close();
567
+ });
568
+
569
+ // ---------------------------------------------------------------- dream 侧
570
+
571
+ test("巩固侧:被按下作用域的行不进总览输入——否则会被洗成无作用域行", async () => {
572
+ const { store, service } = setup(PAUSED_CONFIG);
573
+ service.saveWithDedupe({ type: "project", title: "open-title", content: "open 正文", importance: 3 });
574
+ // 标注走 store.save 的入参——saveWithDedupe 不收这两个字段(标注是 scope.js 在工具层盖的)。
575
+ store.save({ type: "project", title: "paused-title", content: "paused 正文", importance: 3, workspace_scope: "proj-a", workspace_scope_source: "explicit" });
576
+ service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
577
+
578
+ const summaryPrompts = [];
579
+ const ctx = {
580
+ logger: { warn() {} },
581
+ agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
582
+ llm: {
583
+ async *stream(options) {
584
+ const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
585
+ if (userText.includes("| type=")) {
586
+ yield { type: "text-delta", index: 0, text: "[]" };
587
+ yield { type: "finish", reason: { kind: "stop" } };
588
+ return;
589
+ }
590
+ summaryPrompts.push(userText);
591
+ yield { type: "text-delta", index: 0, text: "总览正文" };
592
+ yield { type: "finish", reason: { kind: "stop" } };
593
+ }
594
+ }
595
+ };
596
+ const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true }), thresholdCount: 1 });
597
+ await dream.runDream(ctx, service, {});
598
+
599
+ const fed = summaryPrompts.join("\n");
600
+ assert.ok(fed.includes("open-title"), "未按下的记忆必须进总览输入——否则本用例只是证明了总览没跑");
601
+ assert.ok(!fed.includes("paused-title"), "被按下的记忆不该进总览输入");
602
+
603
+ // 这是这条用例真正的要点:总览落库时是**无作用域**的 summary 行,
604
+ // 所以一旦被按下的内容混进去,注入侧按作用域过滤就再也拦不住它——
605
+ // 内容已经被洗成全局行了。别的入口漏了只是「漏一条」,这里漏了是「一直漏」。
606
+ const overview = service.all().find((m) => m.type === "summary" && m.source === "dream");
607
+ assert.ok(overview, "总览仍应正常落库(闸门不该把总览整个关掉)");
608
+ assert.equal(overview.workspace_scope ?? null, null, "总览本身没有作用域标注——正因如此,输入必须在源头过滤");
609
+ store.close();
610
+ });