@modusensus/dsh-mneme 0.7.0 → 0.7.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 (145) hide show
  1. package/README.md +415 -149
  2. package/{dsh-mneme/src → lib}/api.js +6 -3
  3. package/{dsh-mneme/lib → lib}/client.js +55 -8
  4. package/{dsh-mneme/src → lib}/config.js +5 -0
  5. package/{dsh-mneme/src → lib}/dream.js +7 -2
  6. package/{dsh-mneme/lib → lib}/index.js +7 -6
  7. package/{dsh-mneme/lib → lib}/inject.js +31 -2
  8. package/{dsh-mneme/src → lib}/service.js +37 -3
  9. package/{dsh-mneme/src → lib}/settings.js +15 -6
  10. package/{dsh-mneme/src → lib}/store.js +5 -0
  11. package/package.json +40 -18
  12. package/{dsh-mneme/lib → src}/api.js +6 -3
  13. package/{dsh-mneme/lib → src}/config.js +5 -0
  14. package/{dsh-mneme/lib → src}/dream.js +7 -2
  15. package/{dsh-mneme/src → src}/index.js +7 -6
  16. package/{dsh-mneme/src → src}/inject.js +31 -2
  17. package/{dsh-mneme/lib → src}/service.js +37 -3
  18. package/{dsh-mneme/lib → src}/settings.js +15 -6
  19. package/{dsh-mneme/lib → src}/store.js +5 -0
  20. package/{dsh-mneme/test → test}/api.test.js +10 -3
  21. package/{dsh-mneme/test → test}/client.test.js +37 -0
  22. package/{dsh-mneme/test → test}/config.test.js +7 -0
  23. package/{dsh-mneme/test → test}/inject.test.js +28 -0
  24. package/{dsh-mneme/test → test}/settings.test.js +6 -2
  25. package/{dsh-mneme/test → test}/tag.test.js +114 -0
  26. package/.github/workflows/test.yml +0 -32
  27. package/CHANGELOG.md +0 -89
  28. package/CONTRIBUTING.md +0 -245
  29. package/SECURITY.md +0 -544
  30. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  31. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  32. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  33. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  34. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  35. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  36. package/dsh-mneme/CHANGELOG.md +0 -286
  37. package/dsh-mneme/LICENSE +0 -21
  38. package/dsh-mneme/README.md +0 -482
  39. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  40. package/dsh-mneme/docs/ENTITIES.md +0 -245
  41. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  42. package/dsh-mneme/docs/MIGRATION.md +0 -127
  43. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  44. package/dsh-mneme/docs/SLEEP.md +0 -163
  45. package/dsh-mneme/package-lock.json +0 -1936
  46. package/dsh-mneme/package.json +0 -80
  47. package/v0.7-plan-2026-08-21.md +0 -75
  48. package//346/250/252/345/271/205.png +0 -0
  49. /package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +0 -0
  50. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  51. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  52. /package/{dsh-mneme/lib → lib}/dream/decisions.js +0 -0
  53. /package/{dsh-mneme/lib → lib}/dream/sleep.js +0 -0
  54. /package/{dsh-mneme/lib → lib}/dream/tag-extractor.js +0 -0
  55. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  56. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  57. /package/{dsh-mneme/lib → lib}/heat.js +0 -0
  58. /package/{dsh-mneme/lib → lib}/hot-memory.js +0 -0
  59. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  60. /package/{dsh-mneme/lib → lib}/mirror.js +0 -0
  61. /package/{dsh-mneme/lib → lib}/parser/tag.js +0 -0
  62. /package/{dsh-mneme/lib → lib}/parser/wiki-link.js +0 -0
  63. /package/{dsh-mneme/lib → lib}/quality-filter.js +0 -0
  64. /package/{dsh-mneme/lib → lib}/reranker.js +0 -0
  65. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  66. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  67. /package/{dsh-mneme/lib → lib}/search/tag-boost.js +0 -0
  68. /package/{dsh-mneme/lib → lib}/summarize.js +0 -0
  69. /package/{dsh-mneme/lib → lib}/tools.js +0 -0
  70. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  71. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  72. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  73. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  74. /package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +0 -0
  75. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  76. /package/{dsh-mneme/scripts → scripts}/sync-lib.js +0 -0
  77. /package/{dsh-mneme/src → src}/commands.js +0 -0
  78. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  79. /package/{dsh-mneme/src → src}/dream/decisions.js +0 -0
  80. /package/{dsh-mneme/src → src}/dream/sleep.js +0 -0
  81. /package/{dsh-mneme/src → src}/dream/tag-extractor.js +0 -0
  82. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  83. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  84. /package/{dsh-mneme/src → src}/heat.js +0 -0
  85. /package/{dsh-mneme/src → src}/hot-memory.js +0 -0
  86. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  87. /package/{dsh-mneme/src → src}/mirror.js +0 -0
  88. /package/{dsh-mneme/src → src}/parser/tag.js +0 -0
  89. /package/{dsh-mneme/src → src}/parser/wiki-link.js +0 -0
  90. /package/{dsh-mneme/src → src}/quality-filter.js +0 -0
  91. /package/{dsh-mneme/src → src}/reranker.js +0 -0
  92. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  93. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  94. /package/{dsh-mneme/src → src}/search/tag-boost.js +0 -0
  95. /package/{dsh-mneme/src → src}/summarize.js +0 -0
  96. /package/{dsh-mneme/src → src}/tools.js +0 -0
  97. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  98. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  99. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  100. /package/{dsh-mneme/test → test}/boundary-v0625.test.js +0 -0
  101. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  102. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  103. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  104. /package/{dsh-mneme/test → test}/directory.test.js +0 -0
  105. /package/{dsh-mneme/test → test}/dream.test.js +0 -0
  106. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  107. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  108. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  109. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  110. /package/{dsh-mneme/test → test}/graph-api.test.js +0 -0
  111. /package/{dsh-mneme/test → test}/heat.test.js +0 -0
  112. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  113. /package/{dsh-mneme/test → test}/hot-memory.test.js +0 -0
  114. /package/{dsh-mneme/test → test}/llm-audit.test.js +0 -0
  115. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  116. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  117. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  118. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  119. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  120. /package/{dsh-mneme/test → test}/normalize-decisions.test.js +0 -0
  121. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  122. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  123. /package/{dsh-mneme/test → test}/provenance.test.js +0 -0
  124. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  125. /package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -0
  126. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  127. /package/{dsh-mneme/test → test}/recall-layer.test.js +0 -0
  128. /package/{dsh-mneme/test → test}/recall-runs.test.js +0 -0
  129. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  130. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  131. /package/{dsh-mneme/test → test}/reranker.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/service-search.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/service.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/sleep-heat.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/sleep.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/store.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/summarize.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/tag-boost.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/tools.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/updated-at-semantics.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/wiki-link.test.js +0 -0
@@ -65,7 +65,7 @@ export function computeRetrievalMetrics(actualIds, expectedIds) {
65
65
  };
66
66
  }
67
67
 
68
- export function createService({ store, mirror, config, onWrite, logger }) {
68
+ export function createService({ store, mirror, config, onWrite, logger, settings }) {
69
69
  // Optional dream scheduler hook, installed via setDreamHook after creation
70
70
  // (the scheduler holds a reference back to the service, so it cannot be
71
71
  // passed in the constructor). Fired on the same write events as onWrite.
@@ -91,6 +91,20 @@ export function createService({ store, mirror, config, onWrite, logger }) {
91
91
  let vectorIndex = null;
92
92
  let reranker = null;
93
93
 
94
+ // Effective tag toggles (issue #31): the settings layer (Web panel switches,
95
+ // persisted in user_settings) overrides the plugin config when explicitly
96
+ // stored; unset keys fall back to the zod config default. This is the single
97
+ // runtime source of truth for setMemoryTags' manual gate and the Web panel's
98
+ // getTagConfig display — the panel toggle and the dream autoTag gate were
99
+ // previously writing/reading two disconnected stores.
100
+ const tagConfig = () => {
101
+ const stored = settings?.getAutoTagConfig?.() ?? {};
102
+ return {
103
+ autoTagEnabled: typeof stored.autoTagEnabled === "boolean" ? stored.autoTagEnabled : (config.autoTagEnabled ?? false),
104
+ manualTagEnabled: typeof stored.manualTagEnabled === "boolean" ? stored.manualTagEnabled : (config.manualTagEnabled ?? true)
105
+ };
106
+ };
107
+
94
108
  // Optional recall recorder, installed via setRecallRecorder after creation.
95
109
  // When searchMemories is called with recordRecall=true it receives the
96
110
  // actual merged recall scene (candidates + scores + source + threshold) so
@@ -933,6 +947,13 @@ export function createService({ store, mirror, config, onWrite, logger }) {
933
947
  });
934
948
  // Bug7: a degraded/archived result is applied on top of the merged row.
935
949
  const result = applyQualityDisposition(merged, quality, qf);
950
+ // Bridge tool-passed tags (plus any quality signal tags) into the
951
+ // entity_attrs tag store so directory / tag: recall / tagBoost see them
952
+ // (issue #31). Must run before afterSync so the mirror re-render reads
953
+ // the bridged row. Unconditional on the array (empty included): a merge
954
+ // that clears tags must also invalidate any stale entity_attrs row from
955
+ // an earlier save, or the directory keeps showing the old tag.
956
+ if (Array.isArray(result.tags)) store.setMemoryTags(result.id, result.tags);
936
957
  afterSync("write");
937
958
  notifyWrite();
938
959
  scheduleEmbed(result);
@@ -952,6 +973,11 @@ export function createService({ store, mirror, config, onWrite, logger }) {
952
973
  ...(quality ? { quality_score: quality.score } : {})
953
974
  });
954
975
  const result = applyQualityDisposition(created, quality, qf);
976
+ // Bridge tool-passed tags (plus any quality signal tags) into the
977
+ // entity_attrs tag store so directory / tag: recall / tagBoost see them
978
+ // (issue #31). Must run before afterSync so the mirror re-render reads
979
+ // the bridged row.
980
+ if (Array.isArray(result.tags) && result.tags.length) store.setMemoryTags(result.id, result.tags);
955
981
  afterSync("write");
956
982
  notifyWrite();
957
983
  scheduleEmbed(result);
@@ -1598,6 +1624,11 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1598
1624
  update: (id, p, ctx = {}) => {
1599
1625
  const old = store.getById(id);
1600
1626
  const updated = store.update(id, p);
1627
+ // Bridge tag edits made through update (memory_update tool / panel) into
1628
+ // the entity_attrs tag store too (issue #31) — same convergence as
1629
+ // saveWithDedupe. An explicit empty array clears the live tag row, moving
1630
+ // the memory back to `untagged`.
1631
+ if (p?.tags !== undefined) store.setMemoryTags(updated.id, updated.tags);
1601
1632
  // Record a user correction when any meaningful field changed and the
1602
1633
  // reflection failure tracker is enabled. expected = what it became,
1603
1634
  // actual = what it was before; query (when provided) captures the
@@ -1757,7 +1788,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1757
1788
  // tag pass (gated by autoTagEnabled, wrapped in a transaction by the
1758
1789
  // extractor so the mirror re-renders exactly once).
1759
1790
  setMemoryTags: (memoryId, tags) => {
1760
- if (config.manualTagEnabled === false) {
1791
+ if (tagConfig().manualTagEnabled === false) {
1761
1792
  return { ok: false, error: "manualTagEnabled is off" };
1762
1793
  }
1763
1794
  const stored = store.setMemoryTags(memoryId, tags);
@@ -1768,7 +1799,10 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1768
1799
  getMemoryTags: (memoryId) => store.getMemoryTags(memoryId),
1769
1800
  // Read-only gate flag so the Web panel can hide tag editing when the
1770
1801
  // manual path is disabled (default: manual tagging is on).
1771
- manualTagEnabled: () => config.manualTagEnabled !== false,
1802
+ manualTagEnabled: () => tagConfig().manualTagEnabled !== false,
1803
+ // Effective tag toggle pair (settings-over-config merge) for the panel
1804
+ // (/api/dsh-mneme/config GET/PUT).
1805
+ getTagConfig: tagConfig,
1772
1806
  applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
1773
1807
  saveAttr: (r) => store.saveAttr(r),
1774
1808
  createEntity: (r) => store.createEntity(r),
@@ -148,9 +148,13 @@ export function createSettings(db) {
148
148
  if (j && typeof j === "object") stored = j;
149
149
  } catch { /* fall through to defaults */ }
150
150
  }
151
+ // null = never explicitly stored → callers fall back to the plugin
152
+ // config default, so the settings layer can't override a toggle the
153
+ // user never touched (issue #31). This also unifies the manualTagEnabled
154
+ // default (settings no longer invents false where plugin config says true).
151
155
  return {
152
- autoTagEnabled: stored.autoTagEnabled === true,
153
- manualTagEnabled: stored.manualTagEnabled === true
156
+ autoTagEnabled: typeof stored.autoTagEnabled === "boolean" ? stored.autoTagEnabled : null,
157
+ manualTagEnabled: typeof stored.manualTagEnabled === "boolean" ? stored.manualTagEnabled : null
154
158
  };
155
159
  },
156
160
  setAutoTagConfig(partial) {
@@ -161,10 +165,15 @@ export function createSettings(db) {
161
165
  return j && typeof j === "object" ? j : {};
162
166
  } catch { return {}; }
163
167
  })();
164
- const cfg = {
165
- autoTagEnabled: partial.autoTagEnabled === undefined ? cur.autoTagEnabled === true : partial.autoTagEnabled === true,
166
- manualTagEnabled: partial.manualTagEnabled === undefined ? cur.manualTagEnabled === true : partial.manualTagEnabled === true
167
- };
168
+ const cfg = {};
169
+ // Only persist keys the caller explicitly set (or already stored). A
170
+ // partial update must never invent `false` for a key the user left
171
+ // alone — that would silently disable manual tagging on a toggle of
172
+ // autoTag alone (issue #31).
173
+ if (partial.autoTagEnabled !== undefined) cfg.autoTagEnabled = partial.autoTagEnabled === true;
174
+ else if (cur.autoTagEnabled !== undefined) cfg.autoTagEnabled = cur.autoTagEnabled === true;
175
+ if (partial.manualTagEnabled !== undefined) cfg.manualTagEnabled = partial.manualTagEnabled === true;
176
+ else if (cur.manualTagEnabled !== undefined) cfg.manualTagEnabled = cur.manualTagEnabled === true;
168
177
  setSetting("autoTag", JSON.stringify(cfg));
169
178
  return cfg;
170
179
  }
@@ -1743,6 +1743,11 @@ export function createStore(path) {
1743
1743
  VALUES (?, ?, 'tags', ?, ?, ?, NULL, 1.0, 'manual')`
1744
1744
  ).run(id, memoryId, JSON.stringify(arr), memoryId, now);
1745
1745
  }
1746
+ // Keep memories.tags (the display/search column) as a mirror of the
1747
+ // entity_attrs source of truth, so API rows and keyword search never
1748
+ // drift from the directory/tag store (issue #31).
1749
+ db.prepare("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?")
1750
+ .run(JSON.stringify(arr), now, memoryId);
1746
1751
  db.exec("RELEASE set_memory_tags");
1747
1752
  } catch (e) {
1748
1753
  db.exec("ROLLBACK TO set_memory_tags");
@@ -29,8 +29,10 @@ function req(path, method = "GET", body = null) {
29
29
 
30
30
  function setup(embedder, apiToken = "") {
31
31
  const store = createStore(":memory:");
32
- const service = createService({ store, mirror: null, config: {} });
33
32
  const settings = createSettings(store.db);
33
+ // settings is threaded into the service so the /config endpoint reports the
34
+ // effective settings-over-config tag toggles (issue #31).
35
+ const service = createService({ store, mirror: null, config: {}, settings });
34
36
  const commands = {
35
37
  add: (def) => settings.addCommand(def),
36
38
  remove: (id) => settings.removeCommand(id),
@@ -554,7 +556,9 @@ test("GET/PUT /api/dsh-mneme/config round-trips autoTag switches", async () => {
554
556
  const res1 = new FakeRes();
555
557
  await route.handler(req("/api/dsh-mneme/config"), res1);
556
558
  assert.equal(res1.statusCode, 200);
557
- assert.deepEqual(JSON.parse(res1.body).config, { autoTagEnabled: false, manualTagEnabled: false });
559
+ // unset → effective config falls back to plugin-config defaults:
560
+ // autoTagEnabled=false, manualTagEnabled=true (issue #31 default unification)
561
+ assert.deepEqual(JSON.parse(res1.body).config, { autoTagEnabled: false, manualTagEnabled: true });
558
562
  const res2 = new FakeRes();
559
563
  await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true, manualTagEnabled: false }), res2);
560
564
  assert.equal(res2.statusCode, 200);
@@ -590,5 +594,8 @@ test("PUT /api/dsh-mneme/config supports partial update", async () => {
590
594
  const res = new FakeRes();
591
595
  await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true }), res);
592
596
  assert.equal(res.statusCode, 200);
593
- assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: true, manualTagEnabled: false });
597
+ // Partial PUT only stores autoTagEnabled; manualTagEnabled stays unset and
598
+ // falls back to the plugin-config default (true) — a lone autoTag toggle must
599
+ // never silently disable manual tagging (issue #31).
600
+ assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: true, manualTagEnabled: true });
594
601
  });
@@ -366,3 +366,40 @@ test("directory ships localized labels in both dictionaries", () => {
366
366
  );
367
367
  }
368
368
  });
369
+
370
+ // issue #35: the delete confirm step must not depend on the host window.confirm —
371
+ // in the DSH web host the native dialog is unreliable/blocked, so clicks
372
+ // appeared to do nothing. Deletion is now an inline two-step confirm
373
+ // (✕ → 确认? → DELETE), gated per row by confirmingId.
374
+ test("delete flow drops window.confirm for an inline two-step confirm", () => {
375
+ assert.equal(
376
+ /window\.confirm\(/.test(clientSource),
377
+ false,
378
+ "the delete path must not call the host window.confirm"
379
+ );
380
+ assert.ok(
381
+ clientSource.includes("confirmingId"),
382
+ "a per-row confirm state must gate the second click"
383
+ );
384
+ assert.ok(
385
+ clientSource.includes('"directory.deleteConfirmShort"'),
386
+ "the short inline confirm label must be localized"
387
+ );
388
+ });
389
+
390
+ // issue #35: a failed DELETE (network / 401 / 500 / deleted!==true) must be
391
+ // visible — the old code swallowed it with a bare return and .catch(() => {}).
392
+ test("delete failure surfaces visible error feedback", () => {
393
+ assert.ok(
394
+ clientSource.includes("delErrorId"),
395
+ "a per-row error state must track failed deletes"
396
+ );
397
+ assert.ok(
398
+ clientSource.includes('"directory.deleteError"'),
399
+ "the delete error message must be localized"
400
+ );
401
+ assert.ok(
402
+ clientSource.includes(".mneme-dir-error"),
403
+ "a failed row must get a visible error style"
404
+ );
405
+ });
@@ -27,6 +27,13 @@ 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
+
30
37
  test("dream explicit coverage threshold config defaults and bounds (v0.4.4 fix)", () => {
31
38
  const cfg = Config({});
32
39
  assert.equal(cfg.dreamMinExplicitCoverage, 0.5, "coverage threshold defaults to 0.5");
@@ -101,3 +101,31 @@ test("Bug6: injected block stays within the ~1500 char budget, later entries col
101
101
  // The first entries render full bodies; every entry is present by title.
102
102
  for (let i = 0; i < 8; i++) assert.ok(text.includes(`长标题记忆${i}`), `entry ${i} present`);
103
103
  });
104
+
105
+ test("injectTimePrefix: off by default, no time prefix injected", () => {
106
+ const { contexts } = setup();
107
+ const text = contexts[0].text({ agent: { session: { id: "s1" } } });
108
+ assert.ok(!text.includes("[当前时间:"), "no time prefix when disabled");
109
+ });
110
+
111
+ test("injectTimePrefix: enabled injects once at conversation start with correct format", () => {
112
+ const { contexts, service } = setup({ injectTimePrefix: true });
113
+ service.saveWithDedupe({ type: "preference", title: "语言", content: "用户用中文交流", importance: 5 });
114
+ const text = contexts[0].text({ agent: { session: { id: "s1" } } });
115
+ assert.match(
116
+ text,
117
+ /^\[当前时间: \d{4}-\d{2}-\d{2} 周[一二三四五六日] \d{2}:\d{2}\]\n\n/,
118
+ "prefix leads the injection with the documented format"
119
+ );
120
+ assert.ok(text.includes("语言"), "memory body still rendered after the prefix");
121
+ });
122
+
123
+ test("injectTimePrefix: same session never re-injects, a new session injects again", () => {
124
+ const { contexts } = setup({ injectTimePrefix: true });
125
+ const first = contexts[0].text({ agent: { session: { id: "s1" } } });
126
+ assert.match(first, /^\[当前时间:/, "injects on the first render of s1");
127
+ const second = contexts[0].text({ agent: { session: { id: "s1" } } });
128
+ assert.ok(!second.includes("[当前时间:"), "no re-inject within the same session");
129
+ const third = contexts[0].text({ agent: { session: { id: "s2" } } });
130
+ assert.match(third, /^\[当前时间:/, "a new session injects the prefix again");
131
+ });
@@ -100,11 +100,15 @@ test("vector config disabled value is stored as false", () => {
100
100
  store.close();
101
101
  });
102
102
 
103
- test("autoTag config defaults to disabled and round-trips", () => {
103
+ test("autoTag config defaults to null (unset) and round-trips only explicit keys", () => {
104
104
  const { store, settings } = setup();
105
- assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: false });
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 });
106
108
  settings.setAutoTagConfig({ autoTagEnabled: true, manualTagEnabled: true });
107
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.
108
112
  settings.setAutoTagConfig({ autoTagEnabled: false });
109
113
  assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: true });
110
114
  store.close();
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { createStore } from "../src/store.js";
13
13
  import { createService } from "../src/service.js";
14
14
  import { createMirror } from "../src/mirror.js";
15
+ import { createSettings } from "../src/settings.js";
15
16
  import { parseTags, sanitizeTags, MAX_TAG_LENGTH } from "../src/parser/tag.js";
16
17
  import { runAutoTag } from "../src/dream/tag-extractor.js";
17
18
  import { createDreamScheduler } from "../src/dream.js";
@@ -310,3 +311,116 @@ test("service.setMemoryTags respects manualTagEnabled=false gate", () => {
310
311
  assert.equal(r.ok, false);
311
312
  assert.match(r.error, /manualTagEnabled/);
312
313
  });
314
+
315
+ // ============================================================ issue #31 fixes
316
+
317
+ test("memory_save with tags bridges into the entity_attrs tag store (issue #31)", () => {
318
+ const { store, service } = makeService();
319
+ const { memory } = service.saveWithDedupe({
320
+ type: "project", title: "桥接", content: "c", tags: ["linux", "考研"]
321
+ });
322
+ assert.deepEqual(store.getMemoryTags(memory.id), ["linux", "考研"], "tool-passed tags land in entity_attrs");
323
+ assert.deepEqual(store.getMemoryTagsMap([memory.id]).get(memory.id), ["linux", "考研"], "tag map sees them");
324
+ const dir = store.getDirectory();
325
+ assert.deepEqual(dir.groups.map((g) => g.tag).sort(), ["linux", "考研"], "directory groups by tool-passed tags");
326
+ assert.equal(dir.untagged.length, 0, "not dumped in the untagged bucket");
327
+ assert.ok(store.findMemoriesByTags(["linux"]).some((m) => m.id === memory.id), "tag: recall finds it");
328
+ store.close();
329
+ });
330
+
331
+ test("saveWithDedupe merge with tags bridges into the tag store too (issue #31)", () => {
332
+ const { store, service } = makeService();
333
+ const { memory } = service.saveWithDedupe({
334
+ type: "project", title: "同题", content: "第一段", tags: ["linux"]
335
+ });
336
+ assert.deepEqual(store.getMemoryTags(memory.id), ["linux"]);
337
+ const { action, memory: merged } = service.saveWithDedupe({
338
+ type: "project", title: "同题", content: "第二段", tags: ["bash"]
339
+ });
340
+ assert.equal(action, "merged");
341
+ assert.deepEqual(store.getMemoryTags(merged.id), ["bash"], "merge overwrites tags and bridges them");
342
+ store.close();
343
+ });
344
+
345
+ test("saveWithDedupe merge with empty tags clears stale entity_attrs tags (issue #31)", () => {
346
+ const { store, service } = makeService();
347
+ const { memory } = service.saveWithDedupe({ type: "project", title: "清空", content: "第一段", tags: ["old"] });
348
+ assert.deepEqual(store.getMemoryTags(memory.id), ["old"]);
349
+ const { action, memory: merged } = service.saveWithDedupe({
350
+ type: "project", title: "清空", content: "第二段", tags: []
351
+ });
352
+ assert.equal(action, "merged");
353
+ assert.deepEqual(store.getMemoryTags(merged.id), [], "empty tags on merge invalidate the stale live row");
354
+ const dir = store.getDirectory();
355
+ assert.equal(dir.groups.length, 0, "directory no longer shows the stale tag");
356
+ assert.equal(dir.untagged.length, 1, "memory back to untagged");
357
+ store.close();
358
+ });
359
+
360
+ test("memory_update (service.update) with tags bridges and clearing moves to untagged (issue #31)", () => {
361
+ const { store, service } = makeService();
362
+ const { memory } = service.saveWithDedupe({ type: "project", title: "更新", content: "c", tags: ["old"] });
363
+ const updated = service.update(memory.id, { tags: ["new", "考研"] });
364
+ assert.deepEqual(store.getMemoryTags(updated.id), ["new", "考研"], "update bridges new tags");
365
+ assert.ok(store.getDirectory().groups.some((g) => g.tag === "new"), "directory reflects update tags");
366
+ service.update(updated.id, { tags: [] });
367
+ assert.deepEqual(store.getMemoryTags(updated.id), [], "clearing removes the live row");
368
+ const dir = store.getDirectory();
369
+ assert.equal(dir.groups.length, 0, "no tag groups after clearing");
370
+ assert.equal(dir.untagged.length, 1, "memory back to untagged");
371
+ store.close();
372
+ });
373
+
374
+ test("setMemoryTags keeps the memories.tags column in sync with entity_attrs (issue #31)", () => {
375
+ const { store, service } = makeService();
376
+ const { memory } = service.saveWithDedupe({ type: "project", title: "列同步", content: "c" });
377
+ service.setMemoryTags(memory.id, ["linux"]);
378
+ assert.deepEqual(store.getById(memory.id).tags, ["linux"], "column mirrors the live tag row");
379
+ service.setMemoryTags(memory.id, []);
380
+ assert.deepEqual(store.getById(memory.id).tags, [], "clearing also clears the column");
381
+ store.close();
382
+ });
383
+
384
+ test("runDream auto-tag respects the settings toggle over plugin config (issue #31)", async () => {
385
+ const store = createStore(":memory:");
386
+ const settings = createSettings(store.db);
387
+ settings.setAutoTagConfig({ autoTagEnabled: true });
388
+ // plugin config says auto-tagging is OFF; the settings toggle flips it on.
389
+ const service = createService({ store, mirror: null, config: { autoTagEnabled: false }, settings });
390
+ const a = service.saveWithDedupe({ type: "preference", title: "旧1", content: "第一段" }).memory;
391
+ const b = service.saveWithDedupe({ type: "preference", title: "旧2", content: "第二段" }).memory;
392
+ const keep = (id) => JSON.stringify([{ action: "keep", ids: [id] }]);
393
+ let calls = 0;
394
+ const ctx = {
395
+ llm: {
396
+ stream: async function* () {
397
+ calls++;
398
+ if (calls === 1) yield { type: "text-delta", text: keep(a.id) }; // consolidation
399
+ else if (calls === 2) yield { type: "text-delta", text: JSON.stringify([{ id: a.id, tags: ["内核"] }]) }; // auto-tag
400
+ else yield { type: "text-delta", text: "总览" }; // summary
401
+ yield { type: "finish", reason: { kind: "ok" } };
402
+ }
403
+ },
404
+ logger: { warn: () => {}, info: () => {} }
405
+ };
406
+ const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
407
+ const result = await dream.runDream(ctx, service, {
408
+ dreamProvider: "deepseek", dreamModel: "deepseek-chat", autoTagEnabled: false
409
+ }, settings);
410
+ assert.equal(result.ok, true);
411
+ assert.deepEqual(store.getMemoryTags(a.id), ["内核"], "settings toggle enabled auto-tag despite config.autoTagEnabled=false");
412
+ store.close();
413
+ });
414
+
415
+ test("service.setMemoryTags manual gate follows the settings toggle (issue #31)", () => {
416
+ const store = createStore(":memory:");
417
+ const settings = createSettings(store.db);
418
+ settings.setAutoTagConfig({ manualTagEnabled: false });
419
+ const service = createService({ store, mirror: null, config: {}, settings });
420
+ const a = service.saveWithDedupe({ type: "preference", title: "Alpha", content: "正文" }).memory;
421
+ const r = service.setMemoryTags(a.id, ["linux"]);
422
+ assert.equal(r.ok, false, "settings gate blocks manual tagging");
423
+ assert.match(r.error, /manualTagEnabled/);
424
+ assert.equal(service.manualTagEnabled(), false, "gate flag reflects the settings override");
425
+ store.close();
426
+ });
@@ -1,32 +0,0 @@
1
- name: Test
2
-
3
- on:
4
- push:
5
- branches: [master, main]
6
- pull_request:
7
- branches: [master, main]
8
- workflow_dispatch:
9
-
10
- jobs:
11
- test:
12
- runs-on: ubuntu-latest
13
- strategy:
14
- matrix:
15
- node-version: [24]
16
- steps:
17
- - uses: actions/checkout@v4
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: ${{ matrix.node-version }}
21
- - name: Install dependencies
22
- working-directory: dsh-mneme
23
- run: npm ci
24
- - name: Run tests
25
- working-directory: dsh-mneme
26
- run: npm run test:coverage
27
- - name: Upload coverage to Codecov
28
- uses: codecov/codecov-action@v4
29
- with:
30
- token: ${{ secrets.CODECOV_TOKEN }}
31
- files: dsh-mneme/coverage/lcov.info
32
- fail_ci_if_error: false
package/CHANGELOG.md DELETED
@@ -1,89 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to dsh-mneme are documented here.
4
-
5
- ## [Unreleased]
6
-
7
- - **诚实审计修复(v0.2.7,F-03)**
8
- - autoDream 决策无变更且 summary 为空时,不再误报 ok:true——新增 `noop` 状态(ok:false,baseline 不刷新),避免空跑循环
9
- - 有变更但 summary 缺失 → `degraded`(ok:true 但如实标记 summary 缺失)
10
- - parseReceipt 支持 ok/noop/degraded/reconcile/failed
11
- - 测试 259 → **263**
12
-
13
- - **社区贡献 · 嵌入式面板 UI 改进(PR #2,@Liuxin4950)**
14
- - 记忆面板与设置面板的嵌入模式(settings.section 插槽)不再复用弹窗面板样式(去掉 boxShadow / background / borderRadius / padding),改为全宽平铺渲染,消除 DSH 设置页内多余的悬浮卡片与阴影
15
- - 弹窗(portal)模式保持原有面板样式不变;新增回归测试锁定 embedded 分支不携带 modal chrome
16
-
17
- - **安全审计修复(v0.2.5)**
18
- - 并发安全:CAS 冲突守卫——过期快照不再覆盖并发写入(防丢更新)
19
- - 事务化决策应用:merge/archive 原子提交,receipt 反映已提交子步骤,部分提交 = reconcile(绝不虚报 ok)
20
- - 压测硬断言:lost-update 与多步原子性失败即非零退出(`npm run stress`)
21
- - 运行时人工编辑三方合并(three-way),不再静默覆盖
22
- - 新增 `memory_archive` 工具(第 7 个模型工具)+ `memory_list include_archived`,归档可恢复
23
- - reranker 改为 opt-in:`rerankEnabled=false`、`rerankProvider=none` 默认(裸装不加载 onnxruntime)
24
- - npm `files` 纳入 scripts/test,tarball 内可直接跑压测
25
- - 测试 236 → **258**(+22)
26
-
27
- - **安全加固(v0.2.4)**:API 鉴权 `apiToken`(写操作与密钥端点要求 Bearer 校验)+ `apiKey` 掩码回传 + timing-safe token 比对
28
-
29
- - **审查修复补丁(v0.2.3)**
30
- - failure 记录增加 `before` JSON 快照(title/content/importance 变更可追溯,不只 content)
31
- - vector-index `rebuildIndex` guard 改为检查 `embedSingle`(修复 embed/embedSingle 不一致导致的静默跳过)
32
-
33
- - **流水线补全(v0.2.2)**
34
- - reflection 修复:failure 记录检查 title/importance 变化(不只 content);支持 query 上下文(memory_update 加 `reason` 参数);update 校验失败不污染 claimed;`failure_memories` 清理(`deleteOldFailures` + 启动自动清理 90 天前)
35
- - 专项测试补全:`vector-index.test.js`(modelHash 漂移/重建/增量)+ `service-search.test.js`(hybrid/auto/rerank 端到端)
36
- - api.js `/search` 的 mode 参数文档化
37
- - 测试 212 → **233**
38
-
39
- - **反思更新(v0.2.1)**:`update` 决策 + 失败追踪
40
- - autoDream 新增 `update` 决策类型:修正单条记忆的过时/错误内容(单 id、必须实际变化、非 summary、24h 保护、每次 ≤2)
41
- - 审计记录 update 的 `_before` 快照;update 后向量索引同步
42
- - `failure_memories` 表:记录用户纠正(user_correction)+ 查询/统计接口
43
- - 配置:`reflectionUpdateEnabled` / `reflectionFailureTracking` / `reflectionUpdateMaxPerRun` / `reflectionUpdateMinAgeHours`
44
-
45
- - **Semantic 升级(v0.2.0-semantic)**:完全离线语义记忆引擎
46
- - 本地 Embedding:`embedProvider: local`(ONNX bge-small-zh-v1.5,transformers.js/onnxruntime)或 `ollama`;原 `openai` 外部 API 保留为默认,向后兼容
47
- - 向量索引层:`vector_meta` 模型指纹追踪 + 索引统计(`/api/dsh-mneme/semantic`)
48
- - Rerank 精排:`rerankEnabled`(bge-reranker-base cross-encoder),召回后精排 Top-K,失败自动跳过
49
- - 混合搜索:`memory_search` 新增 `mode: hybrid`(向量优先 + 关键词补位);`auto` 保持关键词优先 + 向量补位
50
- - autoDream 语义增强:K-Means 聚类预分组(k-means++)、`[潜在冲突]` 向量相似度标记、整理后向量索引重建
51
- - 模型下载:断点续传 + 缓存(transformers.js 内置);`.npmrc` 跳过 onnxruntime CUDA 下载避免安装失败
52
- - 文档:`docs/SEMANTIC.md` / `docs/LOCAL_MODEL.md` / `docs/MIGRATION.md` + 基准脚本 `benchmark-embed` / `benchmark-rerank`
53
- - autoDream 裁决审计:每次运行写入 `dream_runs`(输入快照 sha256 digest + 完整输入快照 + LLM 决策清单 + 逐 id 去向 + receipt `dsh-mneme:run:<id>:<status>:<hash>:<count>:<applied>`),可离线回放、定位静默错误
54
- - 幂等决策应用:merge / conflict 重复应用无累积副作用(来源注释不重复追加),防并发/重放下的重复合并
55
- - 三轴线压测 `npm run stress`:长会话检索(Recall@k、陈旧残留率)/ 冲突裁决(可重放仲裁集)/ 多 Agent 并发(丢更新、重复合并、事务/崩溃恢复)
56
-
57
- ## [0.1.6] - 2026-08-14
58
-
59
- - Vector (semantic) search via OpenAI-compatible embeddings endpoint, with automatic fallback to LIKE keyword search on failure
60
- - Web GUI memory panel: browse by type, full-text + semantic search
61
-
62
- ## [0.1.5] - 2026-08-14
63
-
64
- - autoDream consolidation refinements: conflict resolution with source tracking, fail-safe decision validation
65
-
66
- ## [0.1.4] - 2026-08-14
67
-
68
- - User settings: profile + behavior rules injected every turn
69
- - Custom slash commands (register, route to agent)
70
-
71
- ## [0.1.3] - 2026-08-14
72
-
73
- - autoDream background consolidation: dedup / merge / archive / conflict resolution
74
-
75
- ## [0.1.2] - 2026-08-14
76
-
77
- - Session summarization: auto-distill preferences / decisions / lessons at session end
78
- - Web panel improvements
79
-
80
- ## [0.1.1] - 2026-08-13
81
-
82
- - Memory tools: memory_save / memory_search / memory_list / memory_update / memory_delete / memory_forget
83
- - Markdown mirror sync (human-editable, manual edits take priority)
84
-
85
- ## [0.1.0] - 2026-08-13
86
-
87
- - Initial release: cross-session memory for DeepSeek Harness
88
- - SQLite store (node:sqlite, zero native deps) + human-editable Markdown mirror
89
- - Auto-injection of relevant memories at session start