@modusensus/dsh-mneme 0.7.9 → 0.7.11

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 (155) hide show
  1. package/README.en.md +368 -0
  2. package/README.md +298 -167
  3. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  4. package/{dsh-mneme/lib → lib}/api.js +58 -284
  5. package/lib/client.js +2086 -0
  6. package/{dsh-mneme/src → lib}/config.js +6 -100
  7. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  8. package/{dsh-mneme/src → lib}/dream/sleep.js +12 -152
  9. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  10. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  11. package/{dsh-mneme/src → lib}/index.js +6 -34
  12. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  13. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  14. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  15. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  16. package/{dsh-mneme/lib → lib}/service.js +20 -410
  17. package/{dsh-mneme/lib → lib}/settings.js +0 -70
  18. package/{dsh-mneme/lib → lib}/store.js +35 -450
  19. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  20. package/lib/tools.js +274 -0
  21. package/package.json +41 -19
  22. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -3
  23. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  24. package/{dsh-mneme/src → src}/api.js +58 -284
  25. package/{dsh-mneme/lib → src}/config.js +6 -100
  26. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  27. package/{dsh-mneme/lib → src}/dream/sleep.js +12 -152
  28. package/{dsh-mneme/src → src}/dream.js +22 -168
  29. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  30. package/{dsh-mneme/lib → src}/index.js +6 -34
  31. package/{dsh-mneme/src → src}/inject.js +11 -53
  32. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  33. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  34. package/{dsh-mneme/src → src}/reranker.js +1 -4
  35. package/{dsh-mneme/src → src}/service.js +20 -410
  36. package/{dsh-mneme/src → src}/settings.js +0 -70
  37. package/{dsh-mneme/src → src}/store.js +35 -450
  38. package/{dsh-mneme/src → src}/summarize.js +10 -10
  39. package/src/tools.js +274 -0
  40. package/{dsh-mneme/test → test}/api.test.js +1 -156
  41. package/{dsh-mneme/test → test}/client.test.js +22 -207
  42. package/{dsh-mneme/test → test}/config.test.js +0 -21
  43. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  44. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  45. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  46. package/test/inject.test.js +120 -0
  47. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  48. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  49. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  50. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  51. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  52. package/{dsh-mneme/test → test}/service.test.js +0 -106
  53. package/{dsh-mneme/test → test}/settings.test.js +0 -43
  54. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  55. package/{dsh-mneme/test → test}/store.test.js +0 -76
  56. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  57. package/test/tools.test.js +265 -0
  58. package/.github/workflows/publish.yml +0 -58
  59. package/.github/workflows/test.yml +0 -32
  60. package/CHANGELOG.md +0 -91
  61. package/CONTRIBUTING.md +0 -291
  62. package/SECURITY.md +0 -718
  63. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  64. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  65. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  66. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  67. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  68. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  69. package/dsh-mneme/CHANGELOG.md +0 -406
  70. package/dsh-mneme/LICENSE +0 -21
  71. package/dsh-mneme/README.md +0 -498
  72. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  73. package/dsh-mneme/docs/ENTITIES.md +0 -245
  74. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  75. package/dsh-mneme/docs/MIGRATION.md +0 -127
  76. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  77. package/dsh-mneme/docs/SLEEP.md +0 -163
  78. package/dsh-mneme/lib/client.js +0 -2050
  79. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  80. package/dsh-mneme/lib/heat.js +0 -136
  81. package/dsh-mneme/lib/parser/tag.js +0 -59
  82. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  83. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  84. package/dsh-mneme/lib/tools.js +0 -458
  85. package/dsh-mneme/package-lock.json +0 -1936
  86. package/dsh-mneme/package.json +0 -83
  87. package/dsh-mneme/scripts/check-sync.js +0 -42
  88. package/dsh-mneme/src/client.js +0 -2050
  89. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  90. package/dsh-mneme/src/heat.js +0 -136
  91. package/dsh-mneme/src/parser/tag.js +0 -59
  92. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  93. package/dsh-mneme/src/search/tag-boost.js +0 -61
  94. package/dsh-mneme/src/tools.js +0 -458
  95. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  96. package/dsh-mneme/test/directory.test.js +0 -134
  97. package/dsh-mneme/test/heat.test.js +0 -148
  98. package/dsh-mneme/test/inject.test.js +0 -206
  99. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  100. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  101. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  102. package/dsh-mneme/test/provenance.test.js +0 -103
  103. package/dsh-mneme/test/recall-runs.test.js +0 -93
  104. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  105. package/dsh-mneme/test/tag-boost.test.js +0 -125
  106. package/dsh-mneme/test/tag.test.js +0 -426
  107. package/dsh-mneme/test/tools.test.js +0 -674
  108. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  109. package/dsh-mneme/test/wiki-link.test.js +0 -332
  110. package//346/250/252/345/271/205.png +0 -0
  111. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  112. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  113. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  114. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  118. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  119. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  120. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  121. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  122. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  123. /package/{dsh-mneme/src → src}/commands.js +0 -0
  124. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  125. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  126. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  127. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  128. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  129. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  130. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  131. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  141. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  155. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
@@ -5,9 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
7
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
8
- // src/ is the single source of truth for every module (including the Web
9
- // client bundle); lib/ is build output produced by `npm run sync`.
10
- const clientSource = readFileSync(join(root, "src/client.js"), "utf8");
8
+ const clientSource = readFileSync(join(root, "lib/client.js"), "utf8");
11
9
  const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
12
10
 
13
11
  // The Web client bundle registers itself via __ModuleLoader__.load. DSH
@@ -20,14 +18,11 @@ test("client bundle registers under the package name", () => {
20
18
  assert.equal(match[1], pkg.name, "registered id must equal package.json name");
21
19
  });
22
20
 
23
- // client.js is authored under src/ like every other module; lib/client.js is
24
- // build output generated by `npm run sync` (prepack), so it must stay a
25
- // byte-identical copy drift here means the sync wasn't run.
26
- test("client bundle is src-authored and synced into lib", () => {
27
- assert.equal(existsSync(join(root, "src/client.js")), true, "src/client.js must exist");
21
+ // client.js is hand-authored under lib/ only (no src/ counterpart), so the
22
+ // src->lib sync must never prune it.
23
+ test("client bundle is lib-only with no src counterpart", () => {
24
+ assert.equal(existsSync(join(root, "src/client.js")), false, "src/ must not contain client.js");
28
25
  assert.equal(existsSync(join(root, "lib/client.js")), true, "lib/client.js must exist");
29
- const libClient = readFileSync(join(root, "lib/client.js"), "utf8");
30
- assert.equal(libClient, clientSource, "lib/client.js must equal src/client.js (run `npm run sync`)");
31
26
  });
32
27
 
33
28
  // The memory entry lives at the sidebar foot, not in the settings modal: the
@@ -67,13 +62,24 @@ test("tab-first activation with hero-screen overlay fallback", () => {
67
62
  "a failed tab activation must open the fallback overlay"
68
63
  );
69
64
  assert.ok(
70
- /if \(!tab\) \{ resolve\(false\); return; \}/.test(clientSource),
65
+ /if \(candidates\.length === 0\) \{ resolve\(false\); return; \}/.test(clientSource),
71
66
  "activation must resolve false when the tab ring is absent (hero screen)"
72
67
  );
73
68
  assert.ok(
74
69
  /aria-selected.*true/.test(clientSource),
75
70
  "activation is only confirmed once the host marks the tab selected"
76
71
  );
72
+ // Same-labelled tabs from other plugins must never be activated by mistake:
73
+ // the click is only trusted once OUR explorer view actually rendered, and
74
+ // hidden tab panes are excluded before any click happens.
75
+ assert.ok(
76
+ /querySelector\("\.mneme-x"\) !== null/.test(clientSource),
77
+ "activation must verify the memory explorer rendered, not just the tab state"
78
+ );
79
+ assert.ok(
80
+ /offsetParent === null/.test(clientSource),
81
+ "hidden tab panes (settings dialogs etc.) must be excluded from activation"
82
+ );
77
83
  });
78
84
 
79
85
  // The fallback overlay reuses the full MemoryExplorer (three columns, graph,
@@ -144,18 +150,18 @@ test("graph toggle uses a node-graph glyph, not the share icon", () => {
144
150
  });
145
151
 
146
152
  // Every memory feature lives in the main-area library now: the explorer
147
- // hosts three sub-views (browse / graph / settings) switched by tabs whose
153
+ // hosts three sub-views (browse / entities / settings) switched by tabs whose
148
154
  // labels come from dedicated dictionary keys.
149
- test("explorer hosts browse, graph and settings sub-views", () => {
150
- for (const key of ["tabMemory", "tabGraph", "tabSettings"]) {
155
+ test("explorer hosts browse, entities and settings sub-views", () => {
156
+ for (const key of ["tabMemory", "tabEntities", "tabSettings"]) {
151
157
  assert.ok(
152
158
  clientSource.includes(`"memory.explorer.${key}"`),
153
159
  `sub-view labels must come from memory.explorer.${key}`
154
160
  );
155
161
  }
156
162
  assert.ok(
157
- clientSource.includes('h(GraphPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory })'),
158
- "the graph panel must be embedded as a sub-view"
163
+ clientSource.includes("h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory })"),
164
+ "the entity panel must be embedded as a sub-view"
159
165
  );
160
166
  assert.ok(
161
167
  clientSource.includes('h(SettingsContent, { t })'),
@@ -237,194 +243,3 @@ test("explorer chrome aligns with the host design system", () => {
237
243
  "pill chips belong to the drawer era and must stay gone"
238
244
  );
239
245
  });
240
-
241
- // The v0.6.1 wiki-link feature: the detail pane mounts a BacklinksPanel that
242
- // fetches the two read-only link endpoints by the selected memory id and
243
- // renders back/forward rows, each jumping back into the browser.
244
- test("detail pane mounts a BacklinksPanel backed by the two link endpoints", () => {
245
- assert.ok(
246
- clientSource.includes("h(BacklinksPanel, { memory: selected, t, onJump: jumpToMemory })"),
247
- "the detail pane must mount BacklinksPanel after the actions row"
248
- );
249
- assert.ok(
250
- clientSource.includes("/api/dsh-mneme/wikilinks/backlinks?id="),
251
- "BacklinksPanel must fetch the backlinks endpoint by memory id"
252
- );
253
- assert.ok(
254
- clientSource.includes("/api/dsh-mneme/wikilinks/forward?id="),
255
- "BacklinksPanel must fetch the forward-links endpoint by memory id"
256
- );
257
- });
258
-
259
- // Detail content turns [[target]] / [[display|target]] into clickable links:
260
- // the display text survives, the target title is kept for hover, and a click
261
- // resolves the title through the resolve endpoint before jumping.
262
- test("detail content renders wiki-links and resolves them on click", () => {
263
- assert.ok(
264
- clientSource.includes('className: "mneme-wikilink"'),
265
- "inline [[target]] links must use the .mneme-wikilink style"
266
- );
267
- assert.ok(
268
- clientSource.includes("/api/dsh-mneme/wikilinks/resolve?title="),
269
- "clicking a wiki-link must resolve the title via the resolve endpoint"
270
- );
271
- assert.ok(
272
- clientSource.includes('className: "mneme-backlinks"'),
273
- "the panel must render inside a .mneme-backlinks block"
274
- );
275
- });
276
-
277
- // v0.6.2 tag system: the detail pane renders tags as clickable chips wired to
278
- // the memory/tags endpoints, and a tag: query always goes server-side.
279
- test("detail pane renders editable tag chips backed by the tags endpoint", () => {
280
- assert.ok(
281
- clientSource.includes("/api/dsh-mneme/memory/tags?id="),
282
- "the panel must fetch the selected memory's tags via the GET endpoint"
283
- );
284
- assert.ok(
285
- clientSource.includes('method: "POST"'),
286
- "adding/removing a tag must POST to the tags endpoint"
287
- );
288
- assert.ok(
289
- clientSource.includes("onClick: () => onTagClick(tag)"),
290
- "a tag chip click must trigger a tag: search"
291
- );
292
- assert.ok(
293
- clientSource.includes("tagManual"),
294
- "the panel must read the manualTagEnabled gate to hide editing"
295
- );
296
- });
297
-
298
- test("tag: queries run server-side regardless of the semantic toggle", () => {
299
- assert.ok(
300
- clientSource.includes('(!semantic && !q.startsWith("tag:"))'),
301
- "tag: must bypass the semantic toggle and use the search endpoint"
302
- );
303
- assert.ok(
304
- clientSource.includes('setQuery(`tag:${tag}`)'),
305
- "a tag chip click must set the query to tag:<tag>"
306
- );
307
- });
308
-
309
- test("tag UI ships localized labels in both dictionaries", () => {
310
- for (const key of ["tagAdd", "tagRemove", "tagPlaceholder", "tagsEmpty"]) {
311
- assert.ok(
312
- clientSource.includes(`"memory.explorer.${key}"`),
313
- `memory.explorer.${key} key must exist`
314
- );
315
- }
316
- });
317
-
318
- // v0.6.3 directory view: a tag-folder tree backed by the directory endpoint.
319
- // Folders accordion via data-expanded controlled by MemoryExplorer-lifted
320
- // state; a memory entry click jumps back into the browser via onJump.
321
- test("directory sub-view mounts DirectoryPanel backed by the directory endpoint", () => {
322
- assert.ok(
323
- clientSource.includes("h(DirectoryPanel, { t, onJump: jumpToMemory, collapsed: dirCollapsed, setCollapsed: setDirCollapsed })"),
324
- "the directory panel must be embedded as a sub-view with lifted collapse state"
325
- );
326
- assert.ok(
327
- clientSource.includes("/api/dsh-mneme/directory"),
328
- "DirectoryPanel must fetch the directory endpoint"
329
- );
330
- assert.ok(
331
- clientSource.includes('className: "mneme-directory"'),
332
- "the directory panel must render inside the .mneme-directory block"
333
- );
334
- });
335
-
336
- test("directory folders accordion via data-expanded driven by MemoryExplorer state", () => {
337
- assert.ok(
338
- clientSource.includes('"data-expanded": String(open)'),
339
- "folder expansion must be driven by the data-expanded attribute"
340
- );
341
- assert.ok(
342
- clientSource.includes("const [dirCollapsed, setDirCollapsed] = useState({})"),
343
- "collapse state must live in MemoryExplorer so tab switches keep it"
344
- );
345
- assert.ok(
346
- clientSource.includes('"aria-expanded": String(open)'),
347
- "folder headers must announce their expanded state"
348
- );
349
- });
350
-
351
- test("directory entry click jumps to the memory detail via onJump", () => {
352
- assert.ok(
353
- clientSource.includes("onClick: () => onJump(m)"),
354
- "clicking a memory entry must call onJump with the memory"
355
- );
356
- assert.ok(
357
- clientSource.includes("formatDate(m.updated_at || m.created_at)"),
358
- "entries must render a timestamp via formatDate"
359
- );
360
- assert.ok(
361
- clientSource.includes("memory.directory.untagged"),
362
- "untagged memories must be bucketed under a labeled fallback folder"
363
- );
364
- });
365
-
366
- test("directory ships localized labels in both dictionaries", () => {
367
- for (const key of ["memory.explorer.tabDirectory", "memory.directory.untagged", "memory.directory.loading", "memory.directory.empty"]) {
368
- assert.ok(
369
- clientSource.includes(`"${key}"`),
370
- `${key} key must exist`
371
- );
372
- }
373
- });
374
-
375
- // issue #35: the delete confirm step must not depend on the host window.confirm —
376
- // in the DSH web host the native dialog is unreliable/blocked, so clicks
377
- // appeared to do nothing. Deletion is now an inline two-step confirm
378
- // (✕ → 确认? → DELETE), gated per row by confirmingId.
379
- test("delete flow drops window.confirm for an inline two-step confirm", () => {
380
- assert.equal(
381
- /window\.confirm\(/.test(clientSource),
382
- false,
383
- "the delete path must not call the host window.confirm"
384
- );
385
- assert.ok(
386
- clientSource.includes("confirmingId"),
387
- "a per-row confirm state must gate the second click"
388
- );
389
- assert.ok(
390
- clientSource.includes('"directory.deleteConfirmShort"'),
391
- "the short inline confirm label must be localized"
392
- );
393
- });
394
-
395
- // issue #35: a failed DELETE (network / 401 / 500 / deleted!==true) must be
396
- // visible — the old code swallowed it with a bare return and .catch(() => {}).
397
- test("delete failure surfaces visible error feedback", () => {
398
- assert.ok(
399
- clientSource.includes("delErrorId"),
400
- "a per-row error state must track failed deletes"
401
- );
402
- assert.ok(
403
- clientSource.includes('"directory.deleteError"'),
404
- "the delete error message must be localized"
405
- );
406
- assert.ok(
407
- clientSource.includes(".mneme-dir-error"),
408
- "a failed row must get a visible error style"
409
- );
410
- });
411
-
412
- // issue #38: the sidebar foot trigger must be disableable so it stops colliding
413
- // with other plugins that also claim the bottom-left footer slot (dsh-cost-meter
414
- // et al). The client reads the settings-over-config toggle from the /config
415
- // endpoint and renders no button when it is off; the 记忆库 conversation tab is
416
- // unaffected, so nothing is lost by hiding the entry.
417
- test("sidebar trigger is hidden when showSidebarTrigger is false (issue #38)", () => {
418
- assert.ok(
419
- clientSource.includes('apiFetch("/api/dsh-mneme/config")'),
420
- "trigger must read the effective config from /api/dsh-mneme/config"
421
- );
422
- assert.ok(
423
- /d\.config\?\.showSidebarTrigger !== false/.test(clientSource),
424
- "trigger must hide when showSidebarTrigger is explicitly false and stay visible by default"
425
- );
426
- assert.ok(
427
- clientSource.includes('"memory.settings.sidebarTriggerTitle"'),
428
- "settings panel must expose a sidebar-trigger toggle"
429
- );
430
- });
@@ -27,13 +27,6 @@ test("dream sliding window + implicit keep config defaults and bounds (v0.4.4)",
27
27
  assert.equal(off.dreamImplicitKeep, false, "implicit keep can be disabled");
28
28
  });
29
29
 
30
- test("injectTimePrefix is opt-in: default off, enabled explicitly (issue #34)", () => {
31
- const cfg = Config({});
32
- assert.equal(cfg.injectTimePrefix, false, "time prefix defaults to false (behavior unchanged)");
33
- const on = Config({ injectTimePrefix: true });
34
- assert.equal(on.injectTimePrefix, true, "explicit opt-in enables it");
35
- });
36
-
37
30
  test("dream explicit coverage threshold config defaults and bounds (v0.4.4 fix)", () => {
38
31
  const cfg = Config({});
39
32
  assert.equal(cfg.dreamMinExplicitCoverage, 0.5, "coverage threshold defaults to 0.5");
@@ -55,17 +48,3 @@ test("startup probe: the reranker module never statically imports transformers/o
55
48
  );
56
49
  assert.match(src, /await import\("@huggingface\/transformers"\)/, "transformers.js loads lazily via dynamic import");
57
50
  });
58
-
59
- test("showSidebarTrigger is on by default, disabled explicitly (issue #38)", () => {
60
- const cfg = Config({});
61
- assert.equal(cfg.showSidebarTrigger, true, "sidebar trigger defaults to visible (behavior unchanged)");
62
- const off = Config({ showSidebarTrigger: false });
63
- assert.equal(off.showSidebarTrigger, false, "explicit false hides the trigger");
64
- });
65
-
66
- test("escapePromptVariables is on by default, disabled explicitly (issue #40)", () => {
67
- const cfg = Config({});
68
- assert.equal(cfg.escapePromptVariables, true, "{{...}} escaping defaults to on (protects interpolate)");
69
- const off = Config({ escapePromptVariables: false });
70
- assert.equal(off.escapePromptVariables, false, "explicit false passes {{...}} through verbatim");
71
- });
@@ -137,100 +137,6 @@ test("merge across types rejects", () => {
137
137
  assert.ok(errors.some((e) => e.includes("multiple types")), "multi-type error present");
138
138
  });
139
139
 
140
- // --- Issue #26 (P0): skipInvalid — 单条非法决策跳过,合法子集照常应用 ---------
141
-
142
- test("validateDecisions skipInvalid: a single invalid decision is skipped, valid subset survives", () => {
143
- const snap = new Map([
144
- ["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
145
- ["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }],
146
- ["a", { id: "a", type: "project", title: "旧A", content: "过时A", importance: 3, archived: false, forgotten: false }],
147
- ["b", { id: "b", type: "project", title: "旧B", content: "过时B", importance: 3, archived: false, forgotten: false }]
148
- ]);
149
- const decisions = [
150
- { action: "merge", ids: ["p", "j"], keepSource: "p", title: "跨类型", content: "不应合并", importance: 4 },
151
- { action: "archive", ids: ["a"], reason: "stale" },
152
- { action: "archive", ids: ["b"], reason: "stale" }
153
- ];
154
- const { ok, errors, skipped } = validateDecisions(decisions, snap, { skipInvalid: true });
155
- assert.equal(ok, true, `valid subset should survive, got: ${errors.join("; ")}`);
156
- assert.equal(skipped.length, 1, "cross-type merge recorded as skipped");
157
- assert.equal(skipped[0].index, 0, "the skipped one is the cross-type merge");
158
- assert.match(skipped[0].error, /multiple types/, "skip reason mentions types");
159
- // invalid merge spliced out of the caller's array; valid archives + implicit
160
- // keeps for p/j survive (p/j were left unclaimed by the skipped merge)
161
- assert.deepEqual(decisions.map((d) => d.action), ["archive", "archive", "keep", "keep"]);
162
- assert.deepEqual(decisions[0].ids, ["a"]);
163
- assert.ok(decisions.some((d) => d.action === "keep" && d.ids.includes("p")), "p auto-kept");
164
- assert.ok(decisions.some((d) => d.action === "keep" && d.ids.includes("j")), "j auto-kept");
165
- });
166
-
167
- test("validateDecisions skipInvalid: an all-invalid batch still rejects (coverage floor guards truncation)", () => {
168
- const snap = new Map([
169
- ["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
170
- ["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }],
171
- ["d1", { id: "d1", type: "decision", title: "决定", content: "内容D", importance: 3, archived: false, forgotten: false }],
172
- ["b", { id: "b", type: "project", title: "旧B", content: "过时B", importance: 3, archived: false, forgotten: false }]
173
- ]);
174
- // both decisions are cross-type merges → both skipped → valid claims = 0
175
- const decisions = [
176
- { action: "merge", ids: ["p", "j"], keepSource: "p", title: "跨类型", content: "不应合并", importance: 4 },
177
- { action: "merge", ids: ["d1", "b"], keepSource: "d1", title: "跨类型2", content: "不应合并", importance: 4 }
178
- ];
179
- const { ok, errors, skipped } = validateDecisions(decisions, snap, { skipInvalid: true });
180
- assert.equal(ok, false, "no valid decisions left → whole batch rejected");
181
- assert.equal(skipped.length, 2);
182
- assert.ok(errors.some((e) => e.includes("coverage")), "coverage error present");
183
- assert.equal(decisions.length, 2, "rejected batch left untouched (splice only on the success path)");
184
- });
185
-
186
- test("validateDecisions skipInvalid: runaway update count still rejects the whole batch (global cap)", () => {
187
- const snap = new Map([
188
- ["a", { id: "a", type: "project", title: "A", content: "旧A", importance: 3, archived: false, forgotten: false, created_at: "2020-01-01T00:00:00.000Z" }],
189
- ["b", { id: "b", type: "project", title: "B", content: "旧B", importance: 3, archived: false, forgotten: false, created_at: "2020-01-01T00:00:00.000Z" }],
190
- ["c", { id: "c", type: "project", title: "C", content: "旧C", importance: 3, archived: false, forgotten: false, created_at: "2020-01-01T00:00:00.000Z" }]
191
- ]);
192
- const decisions = [
193
- { action: "update", ids: ["a"], content: "新A" },
194
- { action: "update", ids: ["b"], content: "新B" },
195
- { action: "update", ids: ["c"], content: "新C" }
196
- ];
197
- const { ok, errors } = validateDecisions(decisions, snap, { skipInvalid: true });
198
- assert.equal(ok, false, "3 updates > default cap 2 → still rejects");
199
- assert.ok(errors.some((e) => e.includes("too many update decisions")), "global cap error present");
200
- });
201
-
202
- // --- Issue #26 (P1): allowCrossTypeMerge — 显式放宽跨类型合并 -----------------
203
-
204
- test("validateDecisions allowCrossTypeMerge: cross-type merge is allowed when the flag is on", () => {
205
- const snap = new Map([
206
- ["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
207
- ["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }]
208
- ]);
209
- const decisions = [
210
- { action: "merge", ids: ["p", "j"], keepSource: "p", title: "合并", content: "合并内容", importance: 4 }
211
- ];
212
- const { ok, errors } = validateDecisions(decisions, snap, { allowCrossTypeMerge: true });
213
- assert.equal(ok, true, `cross-type merge allowed with the flag, got: ${errors.join("; ")}`);
214
- assert.equal(decisions.length, 1, "no keep appended (both snapshot ids claimed)");
215
- });
216
-
217
- test("validateDecisions allowCrossTypeMerge off + skipInvalid: cross-type merge is skipped, not applied", () => {
218
- const snap = new Map([
219
- ["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
220
- ["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }],
221
- ["a", { id: "a", type: "project", title: "旧A", content: "过时A", importance: 3, archived: false, forgotten: false }],
222
- ["b", { id: "b", type: "project", title: "旧B", content: "过时B", importance: 3, archived: false, forgotten: false }]
223
- ]);
224
- const decisions = [
225
- { action: "merge", ids: ["p", "j"], keepSource: "p", title: "跨类型", content: "不应合并", importance: 4 },
226
- { action: "archive", ids: ["a"], reason: "stale" },
227
- { action: "archive", ids: ["b"], reason: "stale" }
228
- ];
229
- const { ok, skipped } = validateDecisions(decisions, snap, { skipInvalid: true, allowCrossTypeMerge: false });
230
- assert.equal(ok, true);
231
- assert.equal(skipped.length, 1, "flag off → cross-type merge still skipped");
232
- });
233
-
234
140
  function dreamSetup() {
235
141
  const store = createStore(":memory:");
236
142
  const service = createService({ store, mirror: null, config: {} });
@@ -577,69 +483,6 @@ test("runDream fails safe on invalid decisions", async () => {
577
483
  store.close();
578
484
  });
579
485
 
580
- // --- Issue #26: 部分非法决策 → 跳过非法、应用合法子集、run 记为 degraded ----
581
-
582
- test("runDream applies the valid subset and records degraded when some decisions are invalid (Issue #26)", async () => {
583
- const { store, service } = dreamSetup();
584
- const a = service.saveWithDedupe({ type: "project", title: "项目A", content: "内容A", importance: 3 });
585
- const b = service.saveWithDedupe({ type: "project", title: "项目B", content: "内容B", importance: 3 });
586
- const c = service.saveWithDedupe({ type: "preference", title: "偏好C", content: "内容C", importance: 3 });
587
- const d = service.saveWithDedupe({ type: "preference", title: "偏好D", content: "内容D", importance: 3 });
588
- // 2 个合法同类型 merge + 1 个非法跨类型 merge(a×c,且 a/c 已被合法决策占用)
589
- const decisions = [
590
- { action: "merge", ids: [a.memory.id, b.memory.id], title: "项目总览", content: "AB 合并", importance: 4, keepSource: a.memory.id },
591
- { action: "merge", ids: [c.memory.id, d.memory.id], title: "偏好总览", content: "CD 合并", importance: 4, keepSource: c.memory.id },
592
- { action: "merge", ids: [a.memory.id, c.memory.id], title: "非法跨类型", content: "不应合并", importance: 4, keepSource: a.memory.id }
593
- ];
594
- const warnings = [];
595
- const ctx = mockCtx({ onConsolidation: () => JSON.stringify(decisions) });
596
- ctx.logger.warn = (m) => warnings.push(m);
597
- const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
598
- const result = await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat" });
599
- assert.equal(result.status, "degraded", "partial-invalid run recorded as degraded");
600
- assert.equal(result.ok, true, "degraded is ok for the scheduler baseline");
601
- assert.equal(result.applied, 2, "2 valid merges applied");
602
- assert.equal(result.skipped.length, 1, "cross-type merge skipped");
603
- // valid merges landed
604
- assert.equal(store.getById(a.memory.id).title, "项目总览", "keeper A updated");
605
- assert.equal(store.getById(b.memory.id).archived, true, "B archived");
606
- assert.equal(store.getById(c.memory.id).title, "偏好总览", "keeper C updated");
607
- assert.equal(store.getById(d.memory.id).archived, true, "D archived");
608
- // audit row records degraded + the skipped decision, never a fake ok
609
- const run = store.listDreamRuns()[0];
610
- assert.equal(run.status, "degraded", "audit row records degraded");
611
- assert.equal(run.applied, 2, "audit row applied count");
612
- assert.equal(run.outcome.skipped.length, 1, "audit outcome records the skipped decision");
613
- assert.match(run.error, /skipped 1 invalid decision/, "audit error explains the skip");
614
- assert.ok(warnings.some((m) => m.includes("skipping invalid decision")), "skip logged");
615
- store.close();
616
- });
617
-
618
- test("runDream with dreamSkipInvalid=false keeps the old whole-batch rejection (Issue #26 opt-out)", async () => {
619
- const { store, service } = dreamSetup();
620
- const a = service.saveWithDedupe({ type: "project", title: "项目A", content: "内容A", importance: 3 });
621
- const b = service.saveWithDedupe({ type: "project", title: "项目B", content: "内容B", importance: 3 });
622
- const c = service.saveWithDedupe({ type: "preference", title: "偏好C", content: "内容C", importance: 3 });
623
- const d = service.saveWithDedupe({ type: "preference", title: "偏好D", content: "内容D", importance: 3 });
624
- const decisions = [
625
- { action: "merge", ids: [a.memory.id, c.memory.id], title: "非法跨类型", content: "不应合并", importance: 4, keepSource: a.memory.id },
626
- { action: "merge", ids: [b.memory.id, d.memory.id], title: "项目B", content: "合并", importance: 4, keepSource: b.memory.id }
627
- ];
628
- const warnings = [];
629
- const ctx = mockCtx({ onConsolidation: () => JSON.stringify(decisions) });
630
- ctx.logger.warn = (m) => warnings.push(m);
631
- const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
632
- const result = await dream.runDream(ctx, service, {
633
- dreamProvider: "deepseek", dreamModel: "deepseek-chat",
634
- dreamSkipInvalid: false
635
- });
636
- assert.equal(result.ok, false, "opt-out rejects the whole batch");
637
- assert.equal(store.listDreamRuns()[0].status, "failed", "audited as failed");
638
- assert.equal(store.getById(a.memory.id).content, "内容A", "nothing changed");
639
- assert.equal(store.getById(b.memory.id).archived, false, "valid-looking decision also not applied");
640
- store.close();
641
- });
642
-
643
486
  // --- item ①: CAS guard against concurrent edits ----------------------------
644
487
 
645
488
  test("applyDecisions CAS: merge onto a concurrently-edited target is skipped as a conflict, not applied", () => {
@@ -1027,9 +870,7 @@ test("Bug8: runDream records llm_audit_logs rows for consolidation and summary",
1027
870
  assert.deepEqual(summarize.related_memory_ids, [], "summary audit has no related ids");
1028
871
  for (const row of rows) {
1029
872
  assert.equal(row.status, "success");
1030
- // Issue #25: config route (dreamProvider/dreamModel) wins over the agent
1031
- // default — the audit must reflect the actually-used config route.
1032
- assert.equal(row.model_id, "deepseek:deepseek-chat");
873
+ assert.equal(row.model_id, "mock:stress-model");
1033
874
  assert.ok(Number.isInteger(row.duration_ms) && row.duration_ms >= 0, "duration recorded");
1034
875
  assert.equal(row.input_tokens, 0);
1035
876
  assert.equal(row.output_tokens, 0);
@@ -173,37 +173,3 @@ test("ego 2-hop over 100+ nodes stays under 50ms (spec §7)", async () => {
173
173
  assert.ok(data.edges.length > 0);
174
174
  assert.ok(ms < 50, `2-hop query took ${ms.toFixed(1)}ms, spec budget is 50ms`);
175
175
  });
176
-
177
- test("ego node heat projects from linked memory heat (v0.7.0)", async () => {
178
- const { routes, service } = setup();
179
- const mem = service.saveWithDedupe({ type: "history", title: "test", content: "test entity heat", importance: 3 });
180
- const entity = service.createEntity({ name: "E", type: "project" });
181
- service.saveRelation({ from_entity: entity.id, to_entity: entity.id, relation_type: "evidence", memory_id: mem.memory.id });
182
- const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/graph/ego");
183
- assert.ok(route, "ego route exists");
184
- const res = new FakeRes();
185
- await route.handler(req("/api/dsh-mneme/semantic/graph/ego?entity=E"), res);
186
- const data = JSON.parse(res.body);
187
- const node = data.nodes.find((n) => n.name === "E");
188
- assert.ok(typeof node.heat === "number" && node.heat >= 0 && node.heat <= 1, "entity node has a heat value");
189
- assert.ok(Math.abs(node.heat - 1) < 0.01, "fresh memory -> heat ~1.0 -> entity heat ~1.0");
190
- });
191
-
192
- test("ego node heat is null when heatEnabled=false (v0.7.0)", async () => {
193
- const store = createStore(":memory:");
194
- const service = createService({ store, mirror: null, config: { heatEnabled: false } });
195
- const settings = createSettings(store.db);
196
- const commands = { add: () => {}, remove: () => {}, list: () => [] };
197
- const routes = [];
198
- const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
199
- createApi(ctx, service, settings, commands, null, undefined, "");
200
- const mem = service.saveWithDedupe({ type: "history", title: "关联记忆", content: "实体关联", importance: 3 });
201
- const entity = service.createEntity({ name: "F", type: "project" });
202
- service.saveRelation({ from_entity: entity.id, to_entity: entity.id, relation_type: "evidence", memory_id: mem.memory.id });
203
- const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/graph/ego");
204
- const res = new FakeRes();
205
- await route.handler(req("/api/dsh-mneme/semantic/graph/ego?entity=F"), res);
206
- const data = JSON.parse(res.body);
207
- assert.equal(data.nodes.find((n) => n.name === "F").heat, null, "heat disabled → entity heat is null");
208
- store.close();
209
- });
@@ -49,35 +49,6 @@ test("estimateTokens counts CJK heavier than ASCII", () => {
49
49
  assert.ok(estimateTokens("中文内容") > estimateTokens("abcd"));
50
50
  });
51
51
 
52
- // --- entry defense: non-positive / non-integer bounds fall back to defaults ---
53
- // Bug: createHotMemory({ maxRounds: -1 }) made the eviction while-loop
54
- // `while (buffer.length > maxRounds)` unbounded — after the buffer emptied,
55
- // `0 > -1` stayed true and buffer.shift() on an empty array is a no-op, so
56
- // every add() spun forever. Non-integer values (1.5, NaN, null) were also
57
- // silently wrong. The fix clamps them to the 5/2000 defaults at the door.
58
-
59
- test("hot memory falls back to maxRounds=5 for non-positive/invalid values", () => {
60
- for (const bad of [0, -1, 1.5, NaN, null]) {
61
- const hot = createHotMemory({ maxRounds: bad, maxTokens: 10000 });
62
- for (let i = 0; i < 8; i++) hot.add({ query: `第${i}轮`, response: "x" });
63
- assert.equal(hot.rounds().length, 5, `maxRounds=${bad} must fall back to 5, no infinite loop`);
64
- assert.ok(hot.getContext().includes("第7轮"), `maxRounds=${bad}: newest round survives`);
65
- assert.ok(!hot.getContext().includes("第0轮"), `maxRounds=${bad}: oldest round evicted`);
66
- }
67
- });
68
-
69
- test("hot memory falls back to maxTokens=2000 for non-positive/infinite values", () => {
70
- for (const bad of [0, -1, Infinity]) {
71
- const hot = createHotMemory({ maxRounds: 50, maxTokens: bad });
72
- // 50 rounds at ~74 tokens each blow a 2000-token budget; the fallback must
73
- // evict into (1, 50). A broken budget of 0/-1 would squeeze to 1 round and
74
- // Infinity would keep all 50 — both are the pre-fix behavior.
75
- for (let i = 0; i < 50; i++) hot.add({ query: `第${i}轮`, response: "长回答".repeat(40) });
76
- const n = hot.rounds().length;
77
- assert.ok(n > 1 && n < 50, `maxTokens=${bad} falls back to 2000 (kept ${n} rounds)`);
78
- }
79
- });
80
-
81
52
  // --- service-level: BM25 fusion + semantic dedup + selective injection ---
82
53
 
83
54
  function toyVec(text) {