@modusensus/dsh-mneme 0.7.10 → 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 (154) hide show
  1. package/README.en.md +368 -0
  2. package/README.md +298 -169
  3. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  4. package/{dsh-mneme/lib → lib}/api.js +50 -295
  5. package/{dsh-mneme/lib → lib}/client.js +780 -936
  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/lib → 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/lib → 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 -4
  23. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  24. package/{dsh-mneme/src → src}/api.js +50 -295
  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/src → 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/src → 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 +10 -206
  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 -293
  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 -419
  70. package/dsh-mneme/LICENSE +0 -21
  71. package/dsh-mneme/README.md +0 -501
  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/dream/tag-extractor.js +0 -156
  79. package/dsh-mneme/lib/heat.js +0 -136
  80. package/dsh-mneme/lib/parser/tag.js +0 -59
  81. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  82. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  83. package/dsh-mneme/lib/tools.js +0 -458
  84. package/dsh-mneme/package-lock.json +0 -1936
  85. package/dsh-mneme/package.json +0 -83
  86. package/dsh-mneme/scripts/check-sync.js +0 -42
  87. package/dsh-mneme/src/client.js +0 -2242
  88. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  89. package/dsh-mneme/src/heat.js +0 -136
  90. package/dsh-mneme/src/parser/tag.js +0 -59
  91. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  92. package/dsh-mneme/src/search/tag-boost.js +0 -61
  93. package/dsh-mneme/src/tools.js +0 -458
  94. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  95. package/dsh-mneme/test/directory.test.js +0 -134
  96. package/dsh-mneme/test/heat.test.js +0 -148
  97. package/dsh-mneme/test/inject.test.js +0 -206
  98. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  99. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  100. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  101. package/dsh-mneme/test/provenance.test.js +0 -103
  102. package/dsh-mneme/test/recall-runs.test.js +0 -93
  103. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  104. package/dsh-mneme/test/tag-boost.test.js +0 -125
  105. package/dsh-mneme/test/tag.test.js +0 -426
  106. package/dsh-mneme/test/tools.test.js +0 -674
  107. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  108. package/dsh-mneme/test/wiki-link.test.js +0 -332
  109. package//346/250/252/345/271/205.png +0 -0
  110. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  111. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  112. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  113. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  114. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  118. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  119. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  120. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  121. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  122. /package/{dsh-mneme/src → src}/commands.js +0 -0
  123. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  124. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  125. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  126. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  127. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  128. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  129. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  130. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  131. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  140. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
@@ -0,0 +1,120 @@
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 { createInjector } from "../src/inject.js";
6
+ import { createSettings } from "../src/settings.js";
7
+
8
+ function setup(over = {}) {
9
+ const store = createStore(":memory:");
10
+ const service = createService({ store, mirror: null, config: {} });
11
+ const settings = createSettings(store.db);
12
+ const contexts = [];
13
+ const disposers = [];
14
+ const ctx = {
15
+ systemPrompt: {
16
+ context(def) {
17
+ contexts.push(def);
18
+ const dispose = () => disposers.push(def.name);
19
+ return dispose;
20
+ }
21
+ }
22
+ };
23
+ const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
24
+ const injector = createInjector(ctx, service, settings, config);
25
+ return { store, service, contexts, injector, settings };
26
+ }
27
+
28
+ test("registers memory and user-settings contexts", () => {
29
+ const { contexts } = setup();
30
+ assert.equal(contexts.length, 2);
31
+ assert.equal(contexts[0].name, "memory");
32
+ assert.equal(contexts[1].name, "user-settings");
33
+ });
34
+
35
+ test("context text renders injected memories as markdown block", () => {
36
+ const { contexts, service } = setup();
37
+ service.saveWithDedupe({ type: "preference", title: "语言", content: "用户用中文交流", importance: 5 });
38
+ service.saveWithDedupe({ type: "project", title: "记忆插件", content: "SQLite+Markdown", importance: 4 });
39
+ const text = contexts[0].text({});
40
+ assert.ok(text.includes("[记忆库]"), "has header");
41
+ assert.ok(text.includes("语言"), "includes preference");
42
+ assert.ok(text.includes("记忆插件"), "includes high-importance project");
43
+ });
44
+
45
+ test("returns empty text when nothing qualifies", () => {
46
+ const { contexts } = setup();
47
+ const text = contexts[0].text({});
48
+ assert.equal(text, "");
49
+ });
50
+
51
+ test("renders summary block first when summary candidate exists", () => {
52
+ const { contexts, service } = setup();
53
+ service.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "这是总览摘要", importance: 5 });
54
+ service.saveWithDedupe({ type: "preference", title: "语言", content: "中文", importance: 5 });
55
+ const text = contexts[0].text({});
56
+ const summaryIdx = text.indexOf("这是总览摘要");
57
+ const prefIdx = text.indexOf("语言");
58
+ assert.ok(summaryIdx !== -1, "summary present");
59
+ assert.ok(prefIdx !== -1, "preference present");
60
+ assert.ok(summaryIdx < prefIdx, "summary rendered first");
61
+ });
62
+
63
+ test("user-settings context renders profile and rules, empty when unset", () => {
64
+ const { contexts, settings } = setup();
65
+ const settingsCtx = contexts.find((c) => c.name === "user-settings");
66
+ assert.ok(settingsCtx, "user-settings context registered");
67
+ assert.equal(settingsCtx.text({}), "", "empty when no profile/rules");
68
+ settings.setProfile("我叫小明,是一名前端开发者");
69
+ settings.setRules(["回答时先给结论", "使用简体中文"]);
70
+ const text = settingsCtx.text({});
71
+ assert.ok(text.includes("用户画像"), "has profile header");
72
+ assert.ok(text.includes("前端开发者"), "includes profile");
73
+ assert.ok(text.includes("先给结论"), "includes rule");
74
+ assert.ok(text.includes("简体中文"), "includes second rule");
75
+ });
76
+
77
+ test("user-settings context precedes memory block (order 85 < 90)", () => {
78
+ const { contexts, settings } = setup();
79
+ settings.setProfile("画像");
80
+ const settingsCtx = contexts.find((c) => c.name === "user-settings");
81
+ assert.ok(settingsCtx.order < contexts.find((c) => c.name === "memory").order);
82
+ });
83
+
84
+ test("Bug6: long content is truncated to ~300 chars with an ellipsis", () => {
85
+ const { contexts, service } = setup();
86
+ const longContent = "这是一段非常长的记忆正文".repeat(200); // ~2600 chars
87
+ service.saveWithDedupe({ type: "preference", title: "长记忆", content: longContent, importance: 5 });
88
+ const text = contexts[0].text({});
89
+ assert.ok(text.includes("长记忆"), "memory still rendered");
90
+ assert.ok(text.includes("…"), "ellipsis marks the truncation");
91
+ assert.ok(!text.includes(longContent.slice(300)), "full body not injected verbatim");
92
+ });
93
+
94
+ test("Bug6: injected block stays within the ~1500 char budget, later entries collapse", () => {
95
+ const { contexts, service } = setup({ maxInjectedItems: 8 });
96
+ for (let i = 0; i < 8; i++) {
97
+ service.saveWithDedupe({ type: "preference", title: `长标题记忆${i}`, content: "这是一段".repeat(100), importance: 5 });
98
+ }
99
+ const text = contexts[0].text({});
100
+ assert.ok(text.length <= 1600, `memory block bounded near budget, got ${text.length} chars`);
101
+ // The first entries render full bodies; every entry is present by title.
102
+ for (let i = 0; i < 8; i++) assert.ok(text.includes(`长标题记忆${i}`), `entry ${i} present`);
103
+ });
104
+
105
+ // DSH ≥0.1.2-rc removed Session.events; events are only reachable via
106
+ // snapshotEvents(). Regression for #59: the hot block must still render from
107
+ // a session object that has no .events property.
108
+ test("extracts hot rounds from snapshotEvents() when Session.events is absent", () => {
109
+ const { contexts } = setup();
110
+ const session = {
111
+ snapshotEvents: () => [
112
+ { type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "怎么修图谱面板" }] } },
113
+ { type: "assistant/message", data: { content: [{ type: "text", text: "用 viewBox 单位跑模拟" }] } }
114
+ ]
115
+ };
116
+ const text = contexts[0].text({ agent: { session } });
117
+ assert.ok(text.includes("[短期上下文]"), "hot block rendered");
118
+ assert.ok(text.includes("怎么修图谱面板"), "round query present");
119
+ assert.ok(text.includes("用 viewBox 单位跑模拟"), "round response present");
120
+ });
@@ -50,10 +50,10 @@ test("autoDream writes llm_audit_logs rows for consolidation and summary", async
50
50
  assert.equal(summarize.trigger_source, "autoDream");
51
51
  assert.equal(consolidate.status, "success");
52
52
  assert.equal(summarize.status, "success");
53
- // Issue #25: config route (dreamProvider/dreamModel) wins over the agent
54
- // default from mockCtxthe audit must reflect the actually-used route.
55
- assert.equal(consolidate.model_id, "deepseek:deepseek-chat");
56
- assert.equal(summarize.model_id, "deepseek:deepseek-chat");
53
+ // mockCtx resolves the route from agentDefaultModel (mock:stress-model), not
54
+ // the config fallbackassert the actually-used route.
55
+ assert.equal(consolidate.model_id, "mock:stress-model");
56
+ assert.equal(summarize.model_id, "mock:stress-model");
57
57
  assert.ok(Array.isArray(consolidate.related_memory_ids) && consolidate.related_memory_ids.length === 2,
58
58
  "consolidation audit records the snapshot ids");
59
59
  assert.ok(consolidate.total_tokens >= 0 && summarize.total_tokens >= 0);
@@ -36,11 +36,6 @@ test("issue#9: reasoningEffort config defaults to none and rejects unknown value
36
36
  assert.equal(cfg.sleepReasoningEffort, "none");
37
37
  assert.equal(Config({ dreamReasoningEffort: "high" }).dreamReasoningEffort, "high");
38
38
  assert.equal(Config({ sleepReasoningEffort: "medium" }).sleepReasoningEffort, "medium");
39
- // v0.5.3: 'off' explicitly disables thinking — required for thinking-type
40
- // models (deepseek-v4-flash) whose reasoning otherwise drains the whole
41
- // token budget and returns an empty consolidation body.
42
- assert.equal(Config({ dreamReasoningEffort: "off" }).dreamReasoningEffort, "off");
43
- assert.equal(Config({ sleepReasoningEffort: "off" }).sleepReasoningEffort, "off");
44
39
  assert.throws(() => Config({ dreamReasoningEffort: "bogus" }), "invalid effort rejected");
45
40
  assert.throws(() => Config({ sleepReasoningEffort: "ultra" }), "invalid effort rejected");
46
41
  });
@@ -112,28 +107,6 @@ test("issue#9: dream forwards dreamReasoningEffort on both LLM calls", async ()
112
107
  store.close();
113
108
  });
114
109
 
115
- test("issue#9: dream forwards dreamReasoningEffort:off (thinking disabled) on both LLM calls", async () => {
116
- const store = createStore(":memory:");
117
- const service = createService({ store, mirror: null, config: {} });
118
- const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
119
- const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
120
- const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
121
- const captured = [];
122
- const ctx = dreamCtx({
123
- captured,
124
- onConsolidation: () => JSON.stringify([
125
- { action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
126
- ])
127
- });
128
- const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "off" });
129
- assert.equal(result.ok, true);
130
- assert.equal(captured.length, 2);
131
- for (const options of captured) {
132
- assert.equal(options.reasoningEffort, "off", `reasoningEffort:"off" forwarded on ${options.purpose}`);
133
- }
134
- store.close();
135
- });
136
-
137
110
  // ---------------------------------------------------------------- sleep passthrough
138
111
 
139
112
  function sleepSetup() {
@@ -77,23 +77,14 @@ test("recorded candidates carry id/title/content/score/source and match the retu
77
77
  assert.ok(cands.some((c) => c.id === b.id), "second memory recorded");
78
78
  });
79
79
 
80
- test("recordRecall defaults on (v0.7.0) — recorder is called by default", async () => {
80
+ test("recordRecall defaults to off — recorder is not called", async () => {
81
81
  const { service } = setup();
82
82
  let calls = 0;
83
83
  service.setRecallRecorder(() => calls++);
84
84
  saveMemory(service, null, { title: "量子计算", content: "入门" });
85
85
  await service.searchMemories("量子", { mode: "keyword" });
86
- assert.equal(calls, 1, "recorder called once by default (recall_runs 记录默认开)");
87
- });
88
-
89
- test("recordRecall can be opted out via explicit false or config.recallRecordDefault=false", async () => {
90
- const { service } = setup({ recallRecordDefault: false });
91
- let calls = 0;
92
- service.setRecallRecorder(() => calls++);
93
- saveMemory(service, null, { title: "量子计算", content: "入门" });
94
- await service.searchMemories("量子", { mode: "keyword" });
95
86
  await service.searchMemories("量子", { mode: "keyword", recordRecall: false });
96
- assert.equal(calls, 0, "no recorder call when default disabled or explicit false");
87
+ assert.equal(calls, 0, "no recorder call when recordRecall is unset or false");
97
88
  });
98
89
 
99
90
  test("recordRecall=true with no recorder installed is safe and returns normally", async () => {
@@ -310,7 +301,7 @@ test("e2e: index.js wiring — recordRecall search lands a row listRecallRuns ca
310
301
  assert.equal(store.getRecallRun(run.id).query, "量子", "row readable right away");
311
302
  });
312
303
 
313
- test("e2e: recordRecall defaults on (v0.7.0) but is disabled by config.recallRecordDefault=false", async () => {
304
+ test("e2e: without recordRecall the recall_runs table gains no rows", async () => {
314
305
  const store = createStore(":memory:");
315
306
  const service = createService({ store, mirror: null, config: {} });
316
307
  service.setRecallRecorder((recall) => store.saveRecallRun({
@@ -319,18 +310,6 @@ test("e2e: recordRecall defaults on (v0.7.0) but is disabled by config.recallRec
319
310
  }));
320
311
  service.saveWithDedupe({ type: "preference", title: "量子计算入门", content: "叠加态" });
321
312
  await service.searchMemories("量子", { mode: "keyword" });
322
- assert.equal(store.listRecallRuns().length, 1, "default config records a row (记录默认开)");
323
-
324
- const store2 = createStore(":memory:");
325
- const service2 = createService({ store: store2, mirror: null, config: { recallRecordDefault: false } });
326
- service2.setRecallRecorder((recall) => store2.saveRecallRun({
327
- query: recall.query, mode: recall.mode, topK: recall.topK, threshold: recall.threshold ?? null,
328
- candidates: recall.candidates ?? [], created_at: recall.createdAt
329
- }));
330
- service2.saveWithDedupe({ type: "preference", title: "量子计算入门", content: "叠加态" });
331
- await service2.searchMemories("量子", { mode: "keyword" });
332
- await service2.searchMemories("量子", { mode: "keyword", recordRecall: false });
333
- assert.deepEqual(store2.listRecallRuns(), [], "no rows when recallRecordDefault=false or explicit false");
334
- store.close();
335
- store2.close();
313
+ await service.searchMemories("量子", { mode: "keyword", recordRecall: false });
314
+ assert.deepEqual(store.listRecallRuns(), [], "no rows without recordRecall=true");
336
315
  });
@@ -1,7 +1,5 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { spawnSync } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
5
3
  import { LocalReranker } from "../src/reranker.js";
6
4
 
7
5
  /** Injected scorer: records (query, passage) calls, returns a fixed score. */
@@ -197,44 +195,3 @@ test("dispose releases the loaded pipeline", async () => {
197
195
  assert.equal(r.pipeline, null);
198
196
  assert.equal(extractor.disposed, true);
199
197
  });
200
-
201
- test("default pipeline loader mirrors cache_dir onto env.cacheDir (issue #13)", () => {
202
- // The fix lives in the module's *default* loader — the dynamic-import of
203
- // @huggingface/transformers that the in-process tests bypass by injecting
204
- // engineFactory. So it is exercised in a child node process under the
205
- // --experimental-test-module-mocks flag: the transformers module is mocked
206
- // with an empty env, LocalReranker uses the real default loader, and we
207
- // assert env.cacheDir picks up the constructor's cache_dir. A regression
208
- // (loader no longer mirroring) fails the child and surfaces here as a
209
- // non-zero exit.
210
- const srcUrl = new URL("../src/reranker.js", import.meta.url).href;
211
- const cacheDir = "/tmp/dsh-mneme-cache-mirror-test";
212
- const script = `
213
- import { test } from "node:test";
214
- const cacheDir = ${JSON.stringify(cacheDir)};
215
- test("cache_dir is mirrored onto env.cacheDir", async (t) => {
216
- t.mock.module("@huggingface/transformers", {
217
- namedExports: {
218
- env: {},
219
- pipeline: async () => ({ dispose: () => {} })
220
- }
221
- });
222
- const { LocalReranker } = await import(${JSON.stringify(srcUrl)});
223
- const r = new LocalReranker({ cacheDir, device: "cpu" });
224
- await r.init();
225
- const { env } = await import("@huggingface/transformers");
226
- if (env.cacheDir !== cacheDir) {
227
- throw new Error("env.cacheDir not mirrored from cache_dir: " + env.cacheDir);
228
- }
229
- console.log("CACHE_MIRROR_OK");
230
- });
231
- `;
232
- const res = spawnSync(process.execPath, [
233
- "--experimental-test-module-mocks",
234
- "--input-type=module",
235
- "-e",
236
- script
237
- ], { encoding: "utf8", cwd: fileURLToPath(new URL("..", import.meta.url)) });
238
- assert.equal(res.status, 0, `cache-dir mirror child failed:\n${res.stdout}\n${res.stderr}`);
239
- assert.match(res.stdout, /CACHE_MIRROR_OK/);
240
- });
@@ -85,31 +85,6 @@ test("hybrid blend honors configured vector/keyword weights", async () => {
85
85
  assert.ok(Math.abs(t.score - 0.9) < 1e-9, `tuned blend ${t.score}`);
86
86
  });
87
87
 
88
- test("hybrid fusion clamps blended scores into [0,1]", async () => {
89
- // Weights summing above 1 (1.0 + 1.0) push a same-memory blend over 1.0:
90
- // title hit (importance 5) = 1 * (0.5 + 0.5) = 1.0, vector cosine = 1.0 →
91
- // raw blend 1.0*1 + 1.0*1 = 2.0. The fused score must be clamped so
92
- // consumers never see a score outside [0,1].
93
- const store = createStore(":memory:");
94
- const service = createService({ store, mirror: null, config: {
95
- hybridSearchVectorWeight: 1,
96
- hybridSearchKeywordWeight: 1,
97
- adaptiveThresholdEnabled: false,
98
- searchSemanticDedup: false
99
- } });
100
- const vi = createVectorIndex({ store });
101
- service.setEmbedder(embedder);
102
- service.setVectorIndex(vi);
103
- const m = service.saveWithDedupe({ type: "preference", title: "量子计算", content: "量子计算入门", importance: 5 });
104
- vi.saveEmbedding(m.memory.id, [1, 0, 0]);
105
-
106
- const rows = await service.searchMemories("量子计算", { mode: "hybrid", topK: 10 });
107
- const hit = rows.find((r) => r.id === m.memory.id);
108
- assert.ok(hit, "blended row is recalled");
109
- assert.equal(hit.score, 1, `over-weight blend clamped to 1 (got ${hit.score})`);
110
- assert.ok(hit.score >= 0 && hit.score <= 1);
111
- });
112
-
113
88
  test("auto = keyword leads, vector fills the remaining slots", async () => {
114
89
  const { service, vectorIndex } = setup();
115
90
  const kw = service.saveWithDedupe({ type: "preference", title: "量子计算", content: "量子计算入门" });
@@ -76,10 +76,6 @@ test("toApiList maps store rows to wire DTOs", () => {
76
76
  assert.equal(dto[0].title, "选型");
77
77
  assert.equal(dto[0].content, "node:sqlite");
78
78
  assert.equal(dto[0].importance, 3);
79
- // session_id rides the DTO when present (session lifecycle needs it)
80
- const withSession = service.saveWithDedupe({ type: "decision", title: "带会话", content: "y", session_id: "sess-x" });
81
- const dto2 = service.toApiList([withSession.memory]);
82
- assert.equal(dto2[0].session_id, "sess-x");
83
79
  });
84
80
 
85
81
  test("mergeHumanEdits skips edits without id and keeps applying the rest", () => {
@@ -331,105 +327,3 @@ test("Bug7: injection ranking re-weights by quality_score (degraded memories ran
331
327
  assert.equal(candidates[0].title, "高质量", "100-quality preference leads the degraded one");
332
328
  assert.equal(candidates[1].title, "元记忆");
333
329
  });
334
-
335
- // --- session lifecycle (v0.6.0): dispose/restore by session -----------------
336
- // session_disposed_at is orthogonal to `archived`: dispose hides only entries
337
- // born in the deleted session, and restore never resurrects user-archived ones.
338
-
339
- test("disposeBySession marks only that session's memories", () => {
340
- const store = createStore(":memory:");
341
- const svc = createService({ store, mirror: null, config: {} });
342
- const s1 = svc.saveWithDedupe({ type: "project", title: "会话A记忆", content: "x", session_id: "sess-1" });
343
- svc.saveWithDedupe({ type: "project", title: "会话B记忆", content: "y", session_id: "sess-2" });
344
-
345
- const res = svc.disposeBySession("sess-1");
346
- assert.deepEqual(res, { disposed: 1 });
347
- assert.ok(svc.getById(s1.memory.id).session_disposed_at, "session-1 memory marked disposed");
348
- // disposed memories vanish from default list (like archived)
349
- assert.ok(!svc.list({ type: "project" }).some((m) => m.id === s1.memory.id), "disposed hidden from default list");
350
- const other = svc.list({ type: "project", includeDisposed: true });
351
- assert.ok(other.some((m) => m.title === "会话B记忆" && !m.session_disposed_at), "other session untouched");
352
- });
353
-
354
- test("restoreBySession clears the dispose mark and makes entries visible again", () => {
355
- const store = createStore(":memory:");
356
- const svc = createService({ store, mirror: null, config: {} });
357
- const s1 = svc.saveWithDedupe({ type: "project", title: "存档会话", content: "x", session_id: "sess-9" });
358
- svc.disposeBySession("sess-9");
359
-
360
- assert.ok(!svc.list({ type: "project" }).some((m) => m.id === s1.memory.id), "hidden while disposed");
361
- const res = svc.restoreBySession("sess-9");
362
- assert.deepEqual(res, { restored: 1 });
363
- assert.equal(svc.getById(s1.memory.id).session_disposed_at, undefined, "dispose mark cleared");
364
- assert.ok(svc.list({ type: "project" }).some((m) => m.id === s1.memory.id), "visible again");
365
- });
366
-
367
- test("disposeBySession is idempotent: re-disposing returns 0", () => {
368
- const store = createStore(":memory:");
369
- const svc = createService({ store, mirror: null, config: {} });
370
- svc.saveWithDedupe({ type: "project", title: "重复销毁", content: "x", session_id: "sess-3" });
371
-
372
- assert.deepEqual(svc.disposeBySession("sess-3"), { disposed: 1 });
373
- assert.deepEqual(svc.disposeBySession("sess-3"), { disposed: 0 }, "no rows flipped the second time");
374
- });
375
-
376
- test("disposeBySession on an unknown session is a no-op", () => {
377
- const store = createStore(":memory:");
378
- const svc = createService({ store, mirror: null, config: {} });
379
- assert.deepEqual(svc.disposeBySession("nope"), { disposed: 0 });
380
- assert.deepEqual(svc.restoreBySession("nope"), { restored: 0 });
381
- });
382
-
383
- test("legacy rows without session_id are never touched by session lifecycle", () => {
384
- const store = createStore(":memory:");
385
- const svc = createService({ store, mirror: null, config: {} });
386
- const legacy = svc.saveWithDedupe({ type: "preference", title: "全局记忆", content: "no session", session_id: undefined });
387
-
388
- svc.disposeBySession("sess-x");
389
- assert.equal(svc.getById(legacy.memory.id).session_disposed_at, undefined, "global memory survives session dispose");
390
- });
391
-
392
- test("restoreBySession does NOT resurrect user-archived memories (orthogonal)", () => {
393
- const store = createStore(":memory:");
394
- const svc = createService({ store, mirror: null, config: {} });
395
- // user manually archived this one (archived=true), then the session is disposed
396
- const { memory: userArchived } = svc.saveWithDedupe({ type: "decision", title: "手动归档", content: "x", session_id: "sess-4" });
397
- svc.setArchived(userArchived.id, true);
398
- svc.disposeBySession("sess-4");
399
-
400
- svc.restoreBySession("sess-4");
401
- assert.equal(svc.getById(userArchived.id).archived, true, "user archive survives restore");
402
- assert.equal(svc.getById(userArchived.id).session_disposed_at, undefined, "dispose mark cleared but archive kept");
403
- assert.ok(!svc.list({ type: "decision" }).some((m) => m.id === userArchived.id), "still hidden by user archive");
404
- });
405
-
406
- test("listBySession returns only that session's memories via wire DTOs", () => {
407
- const store = createStore(":memory:");
408
- const svc = createService({ store, mirror: null, config: {} });
409
- const { memory } = svc.saveWithDedupe({ type: "decision", title: "会话决策", content: "d", session_id: "sess-7" });
410
- svc.saveWithDedupe({ type: "decision", title: "别会话", content: "e", session_id: "sess-8" });
411
-
412
- const rows = svc.listBySession("sess-7");
413
- assert.equal(rows.length, 1);
414
- assert.equal(rows[0].id, memory.id);
415
- assert.equal(rows[0].title, "会话决策");
416
- assert.equal(rows[0].session_id, "sess-7");
417
- });
418
-
419
- test("listBySession DTO hides disposed by default and flags it when included", () => {
420
- const store = createStore(":memory:");
421
- const svc = createService({ store, mirror: null, config: {} });
422
- const { memory } = svc.saveWithDedupe({ type: "decision", title: "会话决策", content: "d", session_id: "sess-9" });
423
- svc.disposeBySession("sess-9");
424
-
425
- assert.equal(svc.listBySession("sess-9").length, 0, "disposed hidden by default");
426
- const visible = svc.listBySession("sess-9", { includeDisposed: true });
427
- assert.equal(visible.length, 1);
428
- assert.equal(visible[0].id, memory.id);
429
- assert.equal(visible[0].disposed, true, "DTO carries disposed marker so restore is not blind");
430
-
431
- svc.restoreBySession("sess-9");
432
- const restored = svc.listBySession("sess-9");
433
- assert.equal(restored.length, 1, "restore brings it back to default view");
434
- assert.equal(restored[0].disposed, undefined, "marker cleared after restore");
435
- });
@@ -99,46 +99,3 @@ test("vector config disabled value is stored as false", () => {
99
99
  assert.equal(settings.getVectorConfig().enabled, false);
100
100
  store.close();
101
101
  });
102
-
103
- test("autoTag config defaults to null (unset) and round-trips only explicit keys", () => {
104
- const { store, settings } = setup();
105
- // Issue #31: an unset key is null so runtime consumers fall back to the
106
- // plugin config default — the settings layer must not invent `false`.
107
- assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: null, manualTagEnabled: null });
108
- settings.setAutoTagConfig({ autoTagEnabled: true, manualTagEnabled: true });
109
- assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: true, manualTagEnabled: true });
110
- // A partial update only touches the key it was given: manualTagEnabled was
111
- // stored earlier so it keeps its value (true), it does not revert to null.
112
- settings.setAutoTagConfig({ autoTagEnabled: false });
113
- assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: true });
114
- store.close();
115
- });
116
-
117
- test("autoTag config coerces non-boolean to false", () => {
118
- const { store, settings } = setup();
119
- settings.setAutoTagConfig({ autoTagEnabled: "yes", manualTagEnabled: 1 });
120
- assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: false });
121
- store.close();
122
- });
123
-
124
- test("ui config (sidebar trigger) defaults to null and round-trips only explicit keys", () => {
125
- const { store, settings } = setup();
126
- // Issue #38: same settings-over-config pattern as the tag toggles — an unset
127
- // key is null so consumers fall back to the plugin-config default (visible).
128
- assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: null });
129
- settings.setUiConfig({ showSidebarTrigger: false });
130
- assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: false });
131
- settings.setUiConfig({ showSidebarTrigger: true });
132
- assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: true });
133
- // A partial update with no showSidebarTrigger keeps the stored value.
134
- settings.setUiConfig({});
135
- assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: true });
136
- store.close();
137
- });
138
-
139
- test("ui config coerces non-boolean to false", () => {
140
- const { store, settings } = setup();
141
- settings.setUiConfig({ showSidebarTrigger: "yes" });
142
- assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: false });
143
- store.close();
144
- });