@modusensus/dsh-mneme 0.7.10 → 0.7.12

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 (158) hide show
  1. package/README.en.md +437 -0
  2. package/README.md +364 -166
  3. package/bin/cli.mjs +603 -0
  4. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  5. package/lib/api-standalone.js +264 -0
  6. package/{dsh-mneme/lib → lib}/api.js +133 -289
  7. package/{dsh-mneme/lib → lib}/client.js +1008 -922
  8. package/{dsh-mneme/lib → lib}/config.js +54 -99
  9. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  10. package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
  11. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  12. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  13. package/{dsh-mneme/lib → lib}/index.js +45 -42
  14. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  15. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  16. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  17. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  18. package/{dsh-mneme/lib → lib}/service.js +20 -410
  19. package/{dsh-mneme/src → lib}/settings.js +34 -64
  20. package/{dsh-mneme/lib → lib}/store.js +38 -450
  21. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  22. package/lib/tools.js +274 -0
  23. package/package.json +45 -19
  24. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
  25. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  26. package/src/api-standalone.js +264 -0
  27. package/{dsh-mneme/src → src}/api.js +133 -289
  28. package/{dsh-mneme/src → src}/config.js +54 -99
  29. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  30. package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
  31. package/{dsh-mneme/src → src}/dream.js +22 -168
  32. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  33. package/{dsh-mneme/src → src}/index.js +45 -42
  34. package/{dsh-mneme/src → src}/inject.js +11 -53
  35. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  36. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  37. package/{dsh-mneme/src → src}/reranker.js +1 -4
  38. package/{dsh-mneme/src → src}/service.js +20 -410
  39. package/{dsh-mneme/lib → src}/settings.js +34 -64
  40. package/{dsh-mneme/src → src}/store.js +38 -450
  41. package/{dsh-mneme/src → src}/summarize.js +10 -10
  42. package/src/tools.js +274 -0
  43. package/{dsh-mneme/test → test}/api.test.js +45 -156
  44. package/{dsh-mneme/test → test}/client.test.js +10 -206
  45. package/{dsh-mneme/test → test}/config.test.js +0 -21
  46. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  47. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  48. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  49. package/test/inject.test.js +120 -0
  50. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  51. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  52. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  53. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  54. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  55. package/{dsh-mneme/test → test}/service.test.js +0 -106
  56. package/{dsh-mneme/test → test}/settings.test.js +16 -35
  57. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  58. package/test/standalone-api.test.js +326 -0
  59. package/{dsh-mneme/test → test}/store.test.js +0 -76
  60. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  61. package/test/tools.test.js +265 -0
  62. package/.github/workflows/publish.yml +0 -58
  63. package/.github/workflows/test.yml +0 -32
  64. package/CHANGELOG.md +0 -91
  65. package/CONTRIBUTING.md +0 -293
  66. package/SECURITY.md +0 -718
  67. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  68. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  69. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  70. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  71. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  72. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  73. package/dsh-mneme/CHANGELOG.md +0 -419
  74. package/dsh-mneme/LICENSE +0 -21
  75. package/dsh-mneme/README.md +0 -501
  76. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  77. package/dsh-mneme/docs/ENTITIES.md +0 -245
  78. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  79. package/dsh-mneme/docs/MIGRATION.md +0 -127
  80. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  81. package/dsh-mneme/docs/SLEEP.md +0 -163
  82. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  83. package/dsh-mneme/lib/heat.js +0 -136
  84. package/dsh-mneme/lib/parser/tag.js +0 -59
  85. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  86. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  87. package/dsh-mneme/lib/tools.js +0 -458
  88. package/dsh-mneme/package-lock.json +0 -1936
  89. package/dsh-mneme/package.json +0 -83
  90. package/dsh-mneme/scripts/check-sync.js +0 -42
  91. package/dsh-mneme/src/client.js +0 -2242
  92. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  93. package/dsh-mneme/src/heat.js +0 -136
  94. package/dsh-mneme/src/parser/tag.js +0 -59
  95. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  96. package/dsh-mneme/src/search/tag-boost.js +0 -61
  97. package/dsh-mneme/src/tools.js +0 -458
  98. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  99. package/dsh-mneme/test/directory.test.js +0 -134
  100. package/dsh-mneme/test/heat.test.js +0 -148
  101. package/dsh-mneme/test/inject.test.js +0 -206
  102. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  103. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  104. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  105. package/dsh-mneme/test/provenance.test.js +0 -103
  106. package/dsh-mneme/test/recall-runs.test.js +0 -93
  107. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  108. package/dsh-mneme/test/tag-boost.test.js +0 -125
  109. package/dsh-mneme/test/tag.test.js +0 -426
  110. package/dsh-mneme/test/tools.test.js +0 -674
  111. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  112. package/dsh-mneme/test/wiki-link.test.js +0 -332
  113. package//346/250/252/345/271/205.png +0 -0
  114. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  118. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  119. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  120. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  121. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  122. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  123. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  124. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  125. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  126. /package/{dsh-mneme/src → src}/commands.js +0 -0
  127. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  128. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  129. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  130. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  131. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  132. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  133. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  134. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  144. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  155. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  156. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  157. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  158. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
@@ -3,11 +3,8 @@ import { TYPE_FILE } from "./mirror.js";
3
3
  import { evaluateMemoryQuality } from "./quality-filter.js";
4
4
  import { createBM25Index } from "./search/bm25.js";
5
5
  import { adaptiveThreshold } from "./search/adaptive.js";
6
- import { parseWikiLinks } from "./parser/wiki-link.js";
7
- import { extractQueryTags, applyTagBoost } from "./search/tag-boost.js";
8
- import { computeHeat } from "./heat.js";
9
6
 
10
- const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "user", "fact"]);
7
+ const INJECT_TYPES = new Set(["preference", "project", "decision", "summary"]);
11
8
 
12
9
  // Epistemic trust weights (v0.4.5): when config.trustEpistemicWeighting is on,
13
10
  // each recall candidate's existing score is multiplied by the weight of its
@@ -65,7 +62,7 @@ export function computeRetrievalMetrics(actualIds, expectedIds) {
65
62
  };
66
63
  }
67
64
 
68
- export function createService({ store, mirror, config, onWrite, logger, settings }) {
65
+ export function createService({ store, mirror, config, onWrite, logger }) {
69
66
  // Optional dream scheduler hook, installed via setDreamHook after creation
70
67
  // (the scheduler holds a reference back to the service, so it cannot be
71
68
  // passed in the constructor). Fired on the same write events as onWrite.
@@ -91,33 +88,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
91
88
  let vectorIndex = null;
92
89
  let reranker = null;
93
90
 
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
-
108
- // App-wide effective config for the Web panel (/api/dsh-mneme/config): the
109
- // tag toggles plus UI preferences (issue #38). Settings overrides plugin
110
- // config when explicitly stored; unset keys fall back to the zod default.
111
- const appConfig = () => {
112
- const stored = settings?.getUiConfig?.() ?? {};
113
- return {
114
- ...tagConfig(),
115
- showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean"
116
- ? stored.showSidebarTrigger
117
- : (config.showSidebarTrigger ?? true)
118
- };
119
- };
120
-
121
91
  // Optional recall recorder, installed via setRecallRecorder after creation.
122
92
  // When searchMemories is called with recordRecall=true it receives the
123
93
  // actual merged recall scene (candidates + scores + source + threshold) so
@@ -258,36 +228,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
258
228
  }
259
229
  }
260
230
 
261
- /**
262
- * Fire-and-forget wiki-link resolution for a freshly saved/updated memory
263
- * (v0.6.1). Opt-in via config.wikiLinkEnabled. Parses [[target]] /
264
- * [[显示|target]] markers out of the memory content and writes links_to
265
- * relations (idempotent via the unique relation index). Runs through
266
- * service.enqueue so it serializes with autoDream/sleep and never overlaps
267
- * another background pass. Fully fail-safe: parse/store errors are swallowed
268
- * and logged, never a write failure.
269
- */
270
- function scheduleWikiLinkResolve(memory) {
271
- if (txDepth > 0) return; // deferred to the transaction's commit
272
- if (!config.wikiLinkEnabled || !memory?.id) return;
273
- try {
274
- const links = parseWikiLinks(memory?.content ?? "");
275
- if (!links.length) return;
276
- const targets = [...new Set(links.map((l) => l.target).filter(Boolean))];
277
- enqueue(() => {
278
- try {
279
- store.saveWikiLinks({ memoryId: memory.id, title: memory.title, targets });
280
- } catch (err) {
281
- logger?.warn?.("wiki link resolve failed:", err);
282
- }
283
- }).catch((err) => {
284
- logger?.warn?.("wiki link resolve failed:", err);
285
- });
286
- } catch (err) {
287
- logger?.warn?.("wiki link resolve failed:", err);
288
- }
289
- }
290
-
291
231
  /**
292
232
  * Cross-encoder rerank over a candidate list (best effort). Reranker
293
233
  * failures degrade to the original candidate order — reranking is an
@@ -356,57 +296,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
356
296
  return hits;
357
297
  }
358
298
 
359
- /**
360
- * Search for memories carrying a given tag set (v0.6.2). Multiple tag:
361
- * tokens in one query use AND semantics — a memory must carry every tag.
362
- * Combinable with the leftover query: an entity:/attr: prefix is intersected
363
- * with the tag-matched set, and plain keyword text ranks it (only rows whose
364
- * keyword score > 0 survive). Tags are first-class recall, not entity-gated:
365
- * the tag match itself does not depend on config.entitySearchEnabled.
366
- * @param {string[]} tagTokens
367
- * @param {string} q — leftover query after tag: tokens were stripped
368
- * @param {object} [options]
369
- * @param {number} [options.topK=20]
370
- * @returns {any[]}
371
- */
372
- function searchByTags(tagTokens, q, options) {
373
- const { topK = 20 } = options;
374
- const tags = (Array.isArray(tagTokens) ? tagTokens : [])
375
- .map((t) => String(t).trim()).filter(Boolean);
376
- if (!tags.length) return [];
377
- let hits = store.findMemoriesByTags(tags);
378
- if (!hits.length) return [];
379
- // Intersect with entity:/attr: prefixes present in the leftover query.
380
- if (config?.entitySearchEnabled) {
381
- if (q.startsWith("entity:")) {
382
- const ids = new Set(searchByEntity(q.slice(7).trim(), { topK: 10000 }).map((m) => m.id));
383
- hits = hits.filter((m) => ids.has(m.id));
384
- }
385
- if (q.startsWith("attr:")) {
386
- const [key, value] = q.slice(5).split("=");
387
- const ids = new Set(searchByAttr(key, value, { topK: 10000 }).map((m) => m.id));
388
- hits = hits.filter((m) => ids.has(m.id));
389
- }
390
- }
391
- // Leftover plain text ranks the tag-matched set; only rows whose title or
392
- // content actually mentions the keyword survive (scoreKeyword's 0.3 base is
393
- // a non-match, so the containment check is the real gate); otherwise
394
- // preserve insertion order.
395
- const keywordOnly = q && !q.startsWith("entity:") && !q.startsWith("attr:");
396
- hits = keywordOnly
397
- ? hits
398
- .map((m) => ({ ...m, score: scoreKeyword(m, q), source: "tag" }))
399
- .filter((m) => (m.title ?? "").toLowerCase().includes(q.toLowerCase())
400
- || (m.content ?? "").toLowerCase().includes(q.toLowerCase()))
401
- .sort((a, b) => b.score - a.score)
402
- : hits.map((m) => ({ ...m, source: "tag" }));
403
- const result = hits.slice(0, topK);
404
- // Only count toward recall stats/forgetting when the caller asked for
405
- // recording — the panel's `tag:` search must not pollute the curve.
406
- if (options.recordRecall) touchRecalled(result);
407
- return result;
408
- }
409
-
410
299
  /**
411
300
  * Semantic-aware memory search: keyword recall (store.search) plus optional
412
301
  * vector recall + rerank. mode:
@@ -442,7 +331,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
442
331
  function bm25Recall(q, limit) {
443
332
  if (config?.bm25SearchEnabled === false) return [];
444
333
  try {
445
- const docs = store.list({ limit: 500, includeForgotten: false }).filter((m) => !m.archived && !m.session_disposed_at);
334
+ const docs = store.list({ limit: 500, includeForgotten: false }).filter((m) => !m.archived);
446
335
  if (!docs.length) return [];
447
336
  return createBM25Index(docs).search(q, { limit });
448
337
  } catch {
@@ -495,15 +384,14 @@ export function createService({ store, mirror, config, onWrite, logger, settings
495
384
  }
496
385
 
497
386
  /**
498
- * Recall touch (v0.7.0 数据前提): any memory surfaced by recall or
499
- * auto-injection gets its last_accessed_at bumped so the heat model has a
500
- * truthful access clock. Previously gated on config.sleepModeEnabled now
501
- * gated on config.heatEnabled (default on) so heat data is collected
502
- * independently of sleep mode; sleep consumes it when it runs. Best-effort:
503
- * a touch failure must never break search/inject.
387
+ * Sleep touch (v0.4.0): when sleep is enabled, any memory surfaced by recall
388
+ * or auto-injection gets its last_accessed_at bumped, so the "unrecalled N
389
+ * days demote/archive" tiering counts real access. Best-effort and gated on
390
+ * config.sleepModeEnabled when sleep is off this is a complete no-op (no
391
+ * writes on the hot recall path). A touch failure must never break search/inject.
504
392
  */
505
393
  function touchRecalled(memories) {
506
- if (config?.heatEnabled === false || !Array.isArray(memories) || memories.length === 0) return;
394
+ if (config?.sleepModeEnabled !== true || !Array.isArray(memories) || memories.length === 0) return;
507
395
  for (const m of memories) {
508
396
  if (!m?.id) continue;
509
397
  try {
@@ -513,27 +401,9 @@ export function createService({ store, mirror, config, onWrite, logger, settings
513
401
  }
514
402
 
515
403
  async function searchMemories(query, options = {}) {
516
- // v0.7.0: recall_runs 记录默认开(config.recallRecordDefault,默认 true);
517
- // 显式传 recordRecall:false 或配置 recallRecordDefault:false 可关。
518
- const {
519
- mode = "auto",
520
- topK = 20,
521
- threshold,
522
- useRerank = true,
523
- recordRecall = options.recordRecall ?? (config?.recallRecordDefault ?? true)
524
- } = options;
525
- const raw = String(query ?? "").trim();
526
- if (!raw) return [];
527
-
528
- // tag: 前缀(v0.6.2)。先把所有 tag: 令牌从 query 里剥出来,剩余的 q 仍可带
529
- // entity:/attr: 前缀或普通关键词 —— searchByTags 负责交集/排序。没有 tag:
530
- // 令牌则走下面的 entity:/attr:/文本原逻辑(完全向后兼容)。
531
- const tagTokens = [];
532
- const q = raw.replace(/\btag:([^\s]+)/g, (_, tok) => {
533
- if (tok) tagTokens.push(tok);
534
- return "";
535
- }).trim();
536
- if (tagTokens.length) return searchByTags(tagTokens, q, options);
404
+ const { mode = "auto", topK = 20, threshold, useRerank = true, recordRecall = false } = options;
405
+ const q = String(query ?? "").trim();
406
+ if (!q) return [];
537
407
 
538
408
  // entity:/attr: 前缀路由(v0.3.0 Phase 3)。entitySearchEnabled 关闭时走原逻辑。
539
409
  if (config?.entitySearchEnabled) {
@@ -635,13 +505,8 @@ export function createService({ store, mirror, config, onWrite, logger, settings
635
505
  byId.set(m.id, { ...m, score: wb * (m.score ?? 0) });
636
506
  }
637
507
  }
638
- const ranked = [...byId.values()]
639
- .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
640
- .map((r) => ({ ...r, score: Math.max(0, Math.min(1, r.score ?? 0)) }));
641
- // Tag boost (v0.6.4) needs the full ranked pool, not just the top-lim
642
- // slice, so a tagged candidate just below the line can be re-admitted
643
- // after the boost. Without boost this is the legacy lim truncation.
644
- merged = config.tagBoostEnabled === true ? ranked : ranked.slice(0, lim);
508
+ const ranked = [...byId.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
509
+ merged = ranked.slice(0, lim);
645
510
  if (merged.length < lim && !merged.length) {
646
511
  // Vector unavailable entirely: fall back to plain keyword.
647
512
  merged = keyword.slice(0, lim);
@@ -667,31 +532,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
667
532
  // it is the documented text-only path and must not be altered by
668
533
  // embedding state.
669
534
  merged = mode === "keyword" ? merged : semanticDeduplicate(merged);
670
-
671
- // Tag-weighted re-rank (v0.6.4): boost candidates whose tags overlap the
672
- // query tags or the current session's hot-memory tags — applied BEFORE the
673
- // final top-K cut so tagged candidates just below the line can be
674
- // re-admitted. Opt-in, and skipped entirely on the keyword-only path. When
675
- // a reranker is configured (opt-in), it remains the final authority on
676
- // order; the boost still shapes which candidates reach it.
677
- if (config.tagBoostEnabled === true && mode !== "keyword" && merged.length) {
678
- const ids = merged.map((m) => m.id);
679
- const tagsMap = store.getMemoryTagsMap(ids);
680
- const enriched = merged.map((m) => ({ ...m, tags: tagsMap.get(m.id) ?? [] }));
681
- const knownTags = [...tagsMap.values()].flat();
682
- const queryTags = extractQueryTags(q, knownTags);
683
- const sessionTags = Array.isArray(options?.sessionTags) ? options.sessionTags : [];
684
- if (queryTags.length || sessionTags.length) {
685
- merged = applyTagBoost(enriched, {
686
- queryTags,
687
- sessionTags,
688
- factor: config.tagBoostFactor,
689
- sessionFactor: config.sessionTagBoostFactor,
690
- }).map(({ tags, ...rest }) => rest);
691
- }
692
- }
693
535
  merged = merged.slice(0, lim);
694
-
695
536
  let result = useRerank && reranker && merged.length
696
537
  ? await rerankCandidates(q, merged, lim)
697
538
  : merged;
@@ -720,16 +561,12 @@ export function createService({ store, mirror, config, onWrite, logger, settings
720
561
  mode,
721
562
  topK: lim,
722
563
  threshold: threshold ?? null,
723
- // v0.7.0 recall-layer 强度标记:搜索帧一律 injected:false(被召回 =
724
- // 检索直接命中);注入上下文帧由 injectCandidates 以 injected:true
725
- // 单独记账(被注入 = 替对话搭建上下文),两档强度可区分。
726
564
  candidates: result.map((m) => ({
727
565
  id: m.id,
728
566
  title: m.title,
729
567
  content: m.content,
730
568
  score: m.score ?? null,
731
- source: m.source ?? "keyword",
732
- injected: false
569
+ source: m.source ?? "keyword"
733
570
  })),
734
571
  createdAt: new Date().toISOString()
735
572
  });
@@ -960,17 +797,9 @@ export function createService({ store, mirror, config, onWrite, logger, settings
960
797
  });
961
798
  // Bug7: a degraded/archived result is applied on top of the merged row.
962
799
  const result = applyQualityDisposition(merged, quality, qf);
963
- // Bridge tool-passed tags (plus any quality signal tags) into the
964
- // entity_attrs tag store so directory / tag: recall / tagBoost see them
965
- // (issue #31). Must run before afterSync so the mirror re-render reads
966
- // the bridged row. Unconditional on the array (empty included): a merge
967
- // that clears tags must also invalidate any stale entity_attrs row from
968
- // an earlier save, or the directory keeps showing the old tag.
969
- if (Array.isArray(result.tags)) store.setMemoryTags(result.id, result.tags);
970
800
  afterSync("write");
971
801
  notifyWrite();
972
802
  scheduleEmbed(result);
973
- scheduleWikiLinkResolve(result);
974
803
  return { action: "merged", memory: result };
975
804
  }
976
805
  const created = store.save({
@@ -980,22 +809,13 @@ export function createService({ store, mirror, config, onWrite, logger, settings
980
809
  tags: memory.tags ?? [],
981
810
  importance: memory.importance ?? 3,
982
811
  source: memory.source ?? "manual",
983
- // Provenance (v0.5.x): birth session rides through the create path; the
984
- // merge path above preserves the original row's session_id untouched.
985
- session_id: memory.session_id ?? undefined,
986
812
  ...(quality ? { quality_score: quality.score } : {})
987
813
  });
988
814
  const result = applyQualityDisposition(created, quality, qf);
989
- // Bridge tool-passed tags (plus any quality signal tags) into the
990
- // entity_attrs tag store so directory / tag: recall / tagBoost see them
991
- // (issue #31). Must run before afterSync so the mirror re-render reads
992
- // the bridged row.
993
- if (Array.isArray(result.tags) && result.tags.length) store.setMemoryTags(result.id, result.tags);
994
815
  afterSync("write");
995
816
  notifyWrite();
996
817
  scheduleEmbed(result);
997
818
  scheduleEntityExtraction(result);
998
- scheduleWikiLinkResolve(result);
999
819
  return { action: "created", memory: result };
1000
820
  }
1001
821
 
@@ -1047,15 +867,12 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1047
867
  // (quality_score null) count as 100 (weight 1), so legacy stores keep their
1048
868
  // exact summary>preference>importance ordering.
1049
869
  const qualityWeight = (m) => (m.quality_score != null ? m.quality_score / 100 : 1);
1050
- // summary/user/preference are injected regardless of importance (context
1051
- // layers); every other INJECT_TYPES type (e.g. fact) only when
1052
- // importance >= threshold — the frontend "常注入" badge maps to this split.
1053
870
  const items = store.list({ limit: 200, includeForgotten: false })
1054
871
  .filter((m) => !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
1055
- (m.type === "summary" || m.type === "preference" || m.type === "user" || m.importance >= threshold))
872
+ (m.type === "summary" || m.type === "preference" || m.importance >= threshold))
1056
873
  .sort((a, b) => {
1057
- const pa = a.type === "summary" ? 0 : (a.type === "preference" || a.type === "user") ? 1 : 2;
1058
- const pb = b.type === "summary" ? 0 : (b.type === "preference" || b.type === "user") ? 1 : 2;
874
+ const pa = a.type === "summary" ? 0 : a.type === "preference" ? 1 : 2;
875
+ const pb = b.type === "summary" ? 0 : b.type === "preference" ? 1 : 2;
1059
876
  return pa - pb || (b.importance * qualityWeight(b)) - (a.importance * qualityWeight(a));
1060
877
  });
1061
878
  let candidates = items;
@@ -1071,7 +888,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1071
888
  const hits = vectorIndex.search(queryVector, { limit: maxItems * 2, threshold: 0 });
1072
889
  for (const m of hits) {
1073
890
  if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
1074
- (m.type === "summary" || m.type === "preference" || m.type === "user" || m.importance >= threshold)) {
891
+ (m.type === "summary" || m.type === "preference" || m.importance >= threshold)) {
1075
892
  semanticItems.push(m);
1076
893
  }
1077
894
  }
@@ -1116,32 +933,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1116
933
  } catch { /* topic re-rank unavailable: keep rule-based order */ }
1117
934
  }
1118
935
  const selected = candidates.slice(0, maxItems);
1119
- // v0.7.0 recall-layer 强度标记:注入场景也走 recall_runs 记账(mode="inject"、
1120
- // injected:true),与 searchMemories 的搜索帧(injected:false)互补,区分
1121
- // "被召回"与"被注入上下文"两个消耗强度。与搜索侧同受 recallRecordDefault
1122
- // 门控;记账失败绝不阻断注入本身。
1123
- if (recallRecorder && config.recallRecordDefault !== false) {
1124
- try {
1125
- recallRecorder({
1126
- query: q,
1127
- mode: "inject",
1128
- topK: selected.length,
1129
- threshold: null,
1130
- candidates: selected.map((m) => ({
1131
- id: m.id,
1132
- title: m.title,
1133
- content: m.content,
1134
- score: m.score ?? null,
1135
- // 注入帧的 source 固定为帧级标记 "inject",与搜索帧的
1136
- // keyword/vector/rerank 对齐(记忆行自带的 source 是来源列,
1137
- // 与检索层级无关,不混入)。
1138
- source: "inject",
1139
- injected: true
1140
- })),
1141
- createdAt: new Date().toISOString()
1142
- });
1143
- } catch { /* recall receipt is best effort */ }
1144
- }
1145
936
  touchRecalled(selected);
1146
937
  return selected;
1147
938
  }
@@ -1203,32 +994,11 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1203
994
  tags: m.tags,
1204
995
  importance: m.importance,
1205
996
  source: m.source,
1206
- // session_id is optional on the wire: only carry it when present, so the
1207
- // DTO stays a lossless JSON object (undefined would vanish on serialize).
1208
- ...(m.session_id != null ? { session_id: m.session_id } : {}),
1209
- // Disposed state rides along when set, so a restore flow is not a blind
1210
- // op — the caller can see which entries are hidden before restoreBySession.
1211
- ...(m.session_disposed_at != null ? { disposed: true } : {}),
1212
997
  created_at: m.created_at,
1213
998
  updated_at: m.updated_at
1214
999
  }));
1215
1000
  }
1216
1001
 
1217
- /**
1218
- * Directory view (v0.6.3): group live memories by tag. Delegates to
1219
- * store.getDirectory (tag-sorted groups, importance/updated DESC members,
1220
- * live-only filtering) and maps every memory to the wire DTO so the result
1221
- * is JSON-safe for the API endpoint.
1222
- * @returns {{groups: {tag: string, memories: object[]}[], untagged: object[]}}
1223
- */
1224
- function getDirectory() {
1225
- const { groups, untagged } = store.getDirectory();
1226
- return {
1227
- groups: groups.map((g) => ({ tag: g.tag, memories: toApiList(g.memories) })),
1228
- untagged: toApiList(untagged)
1229
- };
1230
- }
1231
-
1232
1002
  /**
1233
1003
  * Three-way merge of in-flight human mirror edits before a re-render.
1234
1004
  * Runs on every syncMirror, so a human edit made between two store writes is
@@ -1336,14 +1106,8 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1336
1106
  // leaving committed files mislabeled as failed and masking partial state.
1337
1107
  // Absent entries (a type with no memories) count as success: sync prunes
1338
1108
  // the stale file, which is itself a completed physical state.
1339
- // v0.6.2: attach entity_attrs-backed tags (entityTags) after the human-edit
1340
- // merge so renderMemory can draw the `#tag` line under each title. The
1341
- // bulk map is a single query, and a missing row simply renders no line.
1342
- const reconciled = reconcileHumanEdits(list);
1343
- const tagsMap = store.getMemoryTagsMap(reconciled.map((m) => m.id));
1344
- const tagged = reconciled.map((m) => ({ ...m, entityTags: tagsMap.get(m.id) ?? [] }));
1345
1109
  let allOk = true;
1346
- const results = mirror.sync(tagged) ?? {};
1110
+ const results = mirror.sync(reconcileHumanEdits(list)) ?? {};
1347
1111
  for (const type of Object.keys(TYPE_FILE)) {
1348
1112
  const r = results[type];
1349
1113
  const ok = !r || r.ok === true;
@@ -1511,59 +1275,14 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1511
1275
  }
1512
1276
  }
1513
1277
 
1514
- /**
1515
- * Forward links (v0.6.1): memories the given memory explicitly links to via
1516
- * [[wiki-links]] in its content. Reads the links_to relations whose
1517
- * from_entity is this memory's title and resolves each to_entity back to a
1518
- * memory row (case-insensitive title match). Returns [{ target, relation }];
1519
- * a target title with no matching memory surfaces as { target: null }.
1520
- */
1521
- function getForwardLinks(memoryId) {
1522
- const memory = store.getById(memoryId);
1523
- if (!memory) return [];
1524
- const out = [];
1525
- for (const rel of store.getRelations(memory.title) ?? []) {
1526
- if (rel.relation_type !== "links_to" || rel.from_entity !== memory.title) continue;
1527
- out.push({ target: store.findByTitle?.(rel.to_entity) ?? null, relation: rel });
1528
- }
1529
- return out;
1530
- }
1531
-
1532
- /**
1533
- * Back links (v0.6.1): memories that explicitly link TO the given memory
1534
- * (their content carries a wiki-link whose target resolves to this memory's
1535
- * title). Reads the links_to relations whose to_entity is this memory's
1536
- * title; the linking memory is rel.memory_id (the source that wrote the
1537
- * relation). Deduped per source memory; missing/self links are dropped.
1538
- * Returns [{ source, relation }].
1539
- */
1540
- function getBacklinks(memoryId) {
1541
- const memory = store.getById(memoryId);
1542
- if (!memory) return [];
1543
- const out = [];
1544
- const seen = new Set();
1545
- for (const rel of store.getRelations(memory.title) ?? []) {
1546
- if (rel.relation_type !== "links_to" || rel.to_entity !== memory.title) continue;
1547
- const source = rel.memory_id ? store.getById(rel.memory_id) : undefined;
1548
- if (!source || source.id === memory.id || seen.has(source.id)) continue;
1549
- seen.add(source.id);
1550
- out.push({ source, relation: rel });
1551
- }
1552
- return out;
1553
- }
1554
-
1555
1278
  return {
1556
1279
  saveWithDedupe,
1557
- getBacklinks,
1558
- getForwardLinks,
1559
- resolveWikiLink: (title) => store.findByTitle?.(title),
1560
1280
  recoverMirror,
1561
1281
  getMirrorHealth,
1562
1282
  getMirrorState: () => store.getMirrorState(),
1563
1283
  injectCandidates,
1564
1284
  mergeHumanEdits,
1565
1285
  toApiList,
1566
- getDirectory,
1567
1286
  transaction,
1568
1287
  enqueue,
1569
1288
  setDreamHook(fn) { dreamHook = fn; },
@@ -1608,83 +1327,16 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1608
1327
  embeddedCount: () => store.embeddedCount(),
1609
1328
  list: (o) => store.list(o),
1610
1329
  all: () => store.all(),
1611
- listForEntityExtraction: (opts) => store.listForEntityExtraction(opts),
1612
1330
  count: (type, opts) => store.count(type, opts),
1613
- stats: (opts) => store.stats(opts),
1614
1331
  getById: (id) => store.getById(id),
1615
- // issue #48: resolve a possibly-truncated id to its canonical full id.
1616
- // Exact hit wins; otherwise the input is treated as a prefix of the id
1617
- // PRIMARY KEY. Never guesses on ambiguity — returns the candidates and the
1618
- // caller must pass a full id. Outcome is {ok:true,id} or {ok:false,reason,
1619
- // message} with reason ∈ invalid | not-found | ambiguous. warnMiss logs the
1620
- // silent-miss case the delete tool previously swallowed (no return channel
1621
- // for it, so observability has to live here in the service layer).
1622
- resolveMemoryId: (input, { warnMiss = false } = {}) => {
1623
- const bad = (reason, message) => ({ ok: false, reason, message });
1624
- if (typeof input !== "string") return bad("invalid", "memory id is required");
1625
- // 手抄/上下文压缩来的 id 可能带首尾空白,统一 trim 后再做精确与前缀解析。
1626
- const id = input.trim();
1627
- if (!id) return bad("invalid", "memory id is required");
1628
- const exact = store.getById(id);
1629
- if (exact) return { ok: true, id: exact.id };
1630
- const matches = store.listByIdPrefix(id);
1631
- if (matches.length === 0) {
1632
- if (warnMiss) {
1633
- logger?.warn?.(
1634
- `[dsh-mneme] memory id "${id}" matched nothing (exact or prefix) — ` +
1635
- "no entry was deleted; ids are full-length, pass one from memory_list/memory_search output or delete by query=…"
1636
- );
1637
- }
1638
- return bad(
1639
- "not-found",
1640
- `memory not found: ${id} (checked exact id and prefix; use a full id from memory_list/memory_search output)`
1641
- );
1642
- }
1643
- if (matches.length > 1) {
1644
- const sample = matches.slice(0, 5).map((m) => m.id);
1645
- const tail = matches.length > sample.length ? ` …(+${matches.length - sample.length})` : "";
1646
- return bad(
1647
- "ambiguous",
1648
- `memory id prefix "${id}" matches ${matches.length} entries (${sample.join(", ")}${tail}); refusing to guess — pass a full id`
1649
- );
1650
- }
1651
- return { ok: true, id: matches[0].id };
1652
- },
1653
1332
  remove: (id) => {
1654
1333
  store.remove(id);
1655
1334
  afterSync("write");
1656
1335
  notifyWrite();
1657
1336
  },
1658
- // Session lifecycle (v0.6.0): mark/clear the session-disposed state on every
1659
- // memory born in a given session. Uses the dedicated `session_disposed_at`
1660
- // column, orthogonal to `archived` — restoring a session never resurrects
1661
- // memories the user archived on purpose. Nothing is destroyed; a session
1662
- // treated as a save point is fully recoverable via restoreBySession.
1663
- disposeBySession: (sessionId) => {
1664
- const disposed = store.setDisposedBySession(sessionId, true);
1665
- if (disposed > 0) {
1666
- afterSync("write");
1667
- notifyWrite();
1668
- }
1669
- return { disposed };
1670
- },
1671
- restoreBySession: (sessionId) => {
1672
- const restored = store.setDisposedBySession(sessionId, false);
1673
- if (restored > 0) {
1674
- afterSync("write");
1675
- notifyWrite();
1676
- }
1677
- return { restored };
1678
- },
1679
- listBySession: (sessionId, opts = {}) => toApiList(store.listBySession(sessionId, opts)),
1680
1337
  update: (id, p, ctx = {}) => {
1681
1338
  const old = store.getById(id);
1682
1339
  const updated = store.update(id, p);
1683
- // Bridge tag edits made through update (memory_update tool / panel) into
1684
- // the entity_attrs tag store too (issue #31) — same convergence as
1685
- // saveWithDedupe. An explicit empty array clears the live tag row, moving
1686
- // the memory back to `untagged`.
1687
- if (p?.tags !== undefined) store.setMemoryTags(updated.id, updated.tags);
1688
1340
  // Record a user correction when any meaningful field changed and the
1689
1341
  // reflection failure tracker is enabled. expected = what it became,
1690
1342
  // actual = what it was before; query (when provided) captures the
@@ -1708,7 +1360,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1708
1360
  const sync = afterSync("write");
1709
1361
  notifyWrite();
1710
1362
  scheduleEmbed(updated);
1711
- scheduleWikiLinkResolve(updated);
1712
1363
  // Audit peer B: when the mirror sync failed, the store write landed but
1713
1364
  // the mirror did not converge — return an explicit degraded receipt rather
1714
1365
  // than a plain success. Non-enumerable so existing deepEqual assertions on
@@ -1820,54 +1471,13 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1820
1471
  // migrates entity_attrs on merge. Bookkeeping writes like the audit
1821
1472
  // passthroughs above — never write-hook-triggering memory mutations.
1822
1473
  saveRelation: (r) => store.saveRelation(r),
1823
- saveWikiLinks: (r) => store.saveWikiLinks(r),
1824
1474
  listEntities: (o) => store.listEntities(o),
1825
1475
  getRelations: (id) => store.getRelations(id),
1826
- // v0.7.0 实体热投影:实体热 = 关联记忆 heat 聚合(取 max)。无关联记忆
1827
- // 或 heatEnabled=false 时返回 null;前端据此决定图谱节点大小/明暗。
1828
- entityHeat: (entityId) => {
1829
- if (config.heatEnabled === false) return null;
1830
- const rels = store.getRelations(entityId) ?? [];
1831
- let max = -Infinity;
1832
- for (const rel of rels) {
1833
- if (!rel.memory_id) continue;
1834
- const mem = store.getById(rel.memory_id);
1835
- if (!mem) continue;
1836
- const h = computeHeat(mem, Date.now(), config);
1837
- if (h > max) max = h;
1838
- }
1839
- return max === -Infinity ? null : max;
1840
- },
1841
- // Tag system (v0.6.2). setMemoryTags is the manual/user path: gated by
1842
- // config.manualTagEnabled (default true), re-renders the mirror and fires
1843
- // the write hook. applyMemoryTags is the raw write used by the autoDream
1844
- // tag pass (gated by autoTagEnabled, wrapped in a transaction by the
1845
- // extractor so the mirror re-renders exactly once).
1846
- setMemoryTags: (memoryId, tags) => {
1847
- if (tagConfig().manualTagEnabled === false) {
1848
- return { ok: false, error: "manualTagEnabled is off" };
1849
- }
1850
- const stored = store.setMemoryTags(memoryId, tags);
1851
- afterSync("write");
1852
- notifyWrite();
1853
- return { ok: true, tags: stored };
1854
- },
1855
- getMemoryTags: (memoryId) => store.getMemoryTags(memoryId),
1856
- // Read-only gate flag so the Web panel can hide tag editing when the
1857
- // manual path is disabled (default: manual tagging is on).
1858
- manualTagEnabled: () => tagConfig().manualTagEnabled !== false,
1859
- // Effective tag toggle pair (settings-over-config merge) for the panel
1860
- // (/api/dsh-mneme/config GET/PUT).
1861
- getTagConfig: tagConfig,
1862
- getAppConfig: appConfig,
1863
- applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
1864
1476
  saveAttr: (r) => store.saveAttr(r),
1865
1477
  createEntity: (r) => store.createEntity(r),
1866
- updateEntity: (id, patch) => store.updateEntity(id, patch),
1867
1478
  findEntityByName: (n) => store.findEntityByName(n),
1868
1479
  findEntityById: (id) => store.findEntityById(id),
1869
1480
  getAttrsByMemory: (id) => store.getAttrsByMemory(id),
1870
- setMemoryMetadata: (id, metadata) => store.setMemoryMetadata(id, metadata),
1871
1481
  getCurrentAttrs: (id) => store.getCurrentAttrs(id),
1872
1482
  migrateAttrsToMemory: (fromId, toId, now) => store.migrateAttrsToMemory(fromId, toId, now)
1873
1483
  };