@modusensus/dsh-mneme 0.7.9 → 0.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.en.md +368 -0
  2. package/README.md +298 -167
  3. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  4. package/{dsh-mneme/lib → lib}/api.js +58 -284
  5. package/lib/client.js +2086 -0
  6. package/{dsh-mneme/src → lib}/config.js +6 -100
  7. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  8. package/{dsh-mneme/src → lib}/dream/sleep.js +12 -152
  9. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  10. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  11. package/{dsh-mneme/src → lib}/index.js +6 -34
  12. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  13. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  14. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  15. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  16. package/{dsh-mneme/lib → lib}/service.js +20 -410
  17. package/{dsh-mneme/lib → lib}/settings.js +0 -70
  18. package/{dsh-mneme/lib → lib}/store.js +35 -450
  19. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  20. package/lib/tools.js +274 -0
  21. package/package.json +41 -19
  22. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -3
  23. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  24. package/{dsh-mneme/src → src}/api.js +58 -284
  25. package/{dsh-mneme/lib → src}/config.js +6 -100
  26. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  27. package/{dsh-mneme/lib → src}/dream/sleep.js +12 -152
  28. package/{dsh-mneme/src → src}/dream.js +22 -168
  29. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  30. package/{dsh-mneme/lib → src}/index.js +6 -34
  31. package/{dsh-mneme/src → src}/inject.js +11 -53
  32. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  33. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  34. package/{dsh-mneme/src → src}/reranker.js +1 -4
  35. package/{dsh-mneme/src → src}/service.js +20 -410
  36. package/{dsh-mneme/src → src}/settings.js +0 -70
  37. package/{dsh-mneme/src → src}/store.js +35 -450
  38. package/{dsh-mneme/src → src}/summarize.js +10 -10
  39. package/src/tools.js +274 -0
  40. package/{dsh-mneme/test → test}/api.test.js +1 -156
  41. package/{dsh-mneme/test → test}/client.test.js +22 -207
  42. package/{dsh-mneme/test → test}/config.test.js +0 -21
  43. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  44. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  45. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  46. package/test/inject.test.js +120 -0
  47. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  48. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  49. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  50. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  51. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  52. package/{dsh-mneme/test → test}/service.test.js +0 -106
  53. package/{dsh-mneme/test → test}/settings.test.js +0 -43
  54. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  55. package/{dsh-mneme/test → test}/store.test.js +0 -76
  56. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  57. package/test/tools.test.js +265 -0
  58. package/.github/workflows/publish.yml +0 -58
  59. package/.github/workflows/test.yml +0 -32
  60. package/CHANGELOG.md +0 -91
  61. package/CONTRIBUTING.md +0 -291
  62. package/SECURITY.md +0 -718
  63. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  64. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  65. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  66. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  67. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  68. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  69. package/dsh-mneme/CHANGELOG.md +0 -406
  70. package/dsh-mneme/LICENSE +0 -21
  71. package/dsh-mneme/README.md +0 -498
  72. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  73. package/dsh-mneme/docs/ENTITIES.md +0 -245
  74. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  75. package/dsh-mneme/docs/MIGRATION.md +0 -127
  76. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  77. package/dsh-mneme/docs/SLEEP.md +0 -163
  78. package/dsh-mneme/lib/client.js +0 -2050
  79. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  80. package/dsh-mneme/lib/heat.js +0 -136
  81. package/dsh-mneme/lib/parser/tag.js +0 -59
  82. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  83. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  84. package/dsh-mneme/lib/tools.js +0 -458
  85. package/dsh-mneme/package-lock.json +0 -1936
  86. package/dsh-mneme/package.json +0 -83
  87. package/dsh-mneme/scripts/check-sync.js +0 -42
  88. package/dsh-mneme/src/client.js +0 -2050
  89. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  90. package/dsh-mneme/src/heat.js +0 -136
  91. package/dsh-mneme/src/parser/tag.js +0 -59
  92. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  93. package/dsh-mneme/src/search/tag-boost.js +0 -61
  94. package/dsh-mneme/src/tools.js +0 -458
  95. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  96. package/dsh-mneme/test/directory.test.js +0 -134
  97. package/dsh-mneme/test/heat.test.js +0 -148
  98. package/dsh-mneme/test/inject.test.js +0 -206
  99. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  100. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  101. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  102. package/dsh-mneme/test/provenance.test.js +0 -103
  103. package/dsh-mneme/test/recall-runs.test.js +0 -93
  104. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  105. package/dsh-mneme/test/tag-boost.test.js +0 -125
  106. package/dsh-mneme/test/tag.test.js +0 -426
  107. package/dsh-mneme/test/tools.test.js +0 -674
  108. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  109. package/dsh-mneme/test/wiki-link.test.js +0 -332
  110. package//346/250/252/345/271/205.png +0 -0
  111. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  112. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  113. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  114. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  118. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  119. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  120. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  121. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  122. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  123. /package/{dsh-mneme/src → src}/commands.js +0 -0
  124. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  125. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  126. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  127. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  128. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  129. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  130. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  131. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  141. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  155. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
@@ -1,8 +1,7 @@
1
1
  import { validateDecisions, applyDecisions } from "./dream/decisions.js";
2
2
  import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
3
- import { runAutoTag } from "./dream/tag-extractor.js";
4
3
  import { createHash, randomUUID } from "node:crypto";
5
- export { validateDecisions, applyDecisions, normalizeDecisions };
4
+ export { validateDecisions, applyDecisions };
6
5
 
7
6
 
8
7
  // Extract the first JSON array from LLM output, tolerating markdown fences,
@@ -41,95 +40,6 @@ function extractJsonArray(text) {
41
40
  }
42
41
  return null;
43
42
  }
44
-
45
- /**
46
- * Field-name drift guard (v0.5.3): thinking-type models (deepseek-v4-flash
47
- * etc.) occasionally ignore the prompt's exact decision schema and emit alias
48
- * keys —实测方案 A 输出 "consolidation"/"target_ids"、方案 B 输出
49
- * "action"/"targetIds",都不是插件要求的 "action"/"ids"。normalizeDecisions
50
- * 在 validateDecisions 之前把常见变体重写回规范字段名,让"字段名不听话但
51
- * 语义正确"的输出仍可被应用,而不是整单被拒。覆盖三类漂移:
52
- * 1. 整个 body 是包在对象里的数组({consolidation:[...]} /
53
- * {decisions:[...]} / {actions:[...]});
54
- * 2. 决策对象用了别名键(target_ids→ids、keep_source→keepSource、
55
- * winner_id→winner、targetIds→ids 等);
56
- * 3. action 值用了同义词(archived→archive、consolidation→merge 等)。
57
- * 无归一化必要时原样返回(null→null,调用方"no json array"分支不受影响)。
58
- */
59
- const FIELD_ALIASES = {
60
- // "consolidation" 也可作 action 键(实测 deepseek-v4-flash 这么写过);
61
- // 但 normalizeOne 对 action 键做字符串过滤,{consolidation:[...]} 这种
62
- // wrapper 数组不会被误当成 action(顶层 WRAPPER_KEYS 负责解包)。
63
- action: ["action", "decision", "operation", "op", "action_type", "actionType", "mode", "consolidation"],
64
- // 注意:action 别名不含 "type"——create 决策的 type 是合法的记忆类型字段,
65
- // 不能误当 action。
66
- ids: ["ids", "target_ids", "targetIds", "targets", "memory_ids", "memoryIds", "id_list", "idList", "memories"],
67
- reason: ["reason", "rationale", "why", "comment", "note", "explanation"],
68
- importance: ["importance", "priority", "weight", "level", "score"],
69
- keepSource: ["keepSource", "keep_source", "source_id", "sourceId", "keeper", "keep_id", "keepId"],
70
- winner: ["winner", "winner_id", "winnerId", "win_id", "winId", "preferred", "primary"],
71
- loser: ["loser", "loser_id", "loserId", "lose_id", "loseId", "drop_id", "dropId", "archive_id", "archiveId"],
72
- title: ["title", "new_title", "newTitle", "merged_title", "mergedTitle", "merge_title", "mergeTitle"],
73
- content: ["content", "new_content", "newContent", "merged_content", "mergedContent", "merge_content", "mergeContent"],
74
- evidence: ["evidence", "evidence_ids", "evidenceIds"]
75
- };
76
-
77
- // 保守的 action 值同义词:只收语义无歧义、不可能被误认为记忆类型/其他 action 的映射。
78
- const ACTION_SYNONYMS = {
79
- archived: "archive",
80
- remove: "archive",
81
- delete: "archive",
82
- combine: "merge",
83
- consolidation: "merge",
84
- consolidate: "merge",
85
- modify: "update",
86
- edit: "update"
87
- };
88
-
89
- const WRAPPER_KEYS = ["consolidation", "decisions", "actions", "results", "updates", "list", "data"];
90
-
91
- /** 把单个决策对象重写到规范字段名;非对象原样返回。 */
92
- function normalizeOne(raw) {
93
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
94
- const d = {};
95
- const consumed = new Set();
96
- for (const [canon, aliases] of Object.entries(FIELD_ALIASES)) {
97
- for (const key of aliases) {
98
- if (key in raw && raw[key] !== undefined && raw[key] !== null) {
99
- // action 必须是字符串——{consolidation:[...]} 的 wrapper 数组跳过。
100
- if (canon === "action" && Array.isArray(raw[key])) continue;
101
- d[canon] = raw[key];
102
- consumed.add(key);
103
- break;
104
- }
105
- }
106
- }
107
- // 保留未被别名消费的原始键(create 的 type、未来字段)原样透传。
108
- for (const key of Object.keys(raw)) {
109
- if (!consumed.has(key)) d[key] = raw[key];
110
- }
111
- if (typeof d.action === "string") {
112
- const low = d.action.toLowerCase();
113
- if (ACTION_SYNONYMS[low] !== undefined) d.action = ACTION_SYNONYMS[low];
114
- }
115
- // ids 若被写成单个字符串则包成数组(validateDecisions 要求数组)。
116
- if (d.ids !== undefined && typeof d.ids === "string") d.ids = [d.ids];
117
- return d;
118
- }
119
-
120
- /** 归一化 LLM 决策 body(数组 / 包裹对象 / 单个决策对象),null 原样返回。 */
121
- function normalizeDecisions(raw) {
122
- if (!raw) return raw;
123
- if (Array.isArray(raw)) return raw.map(normalizeOne).filter((d) => d && typeof d === "object");
124
- if (typeof raw === "object") {
125
- for (const key of WRAPPER_KEYS) {
126
- if (Array.isArray(raw[key])) return normalizeDecisions(raw[key]);
127
- }
128
- // 单个决策对象 → 包成单元素数组(validateDecisions 要求数组)。
129
- return [normalizeOne(raw)];
130
- }
131
- return raw;
132
- }
133
43
  const SUMMARY_PROMPT = `你是记忆库摘要助手。根据整理后的记忆,生成一段 150-200 字的记忆库总览,覆盖:用户偏好、活跃项目、关键决策。之后作为会话上下文注入。只输出摘要文本,不要其他内容。`;
134
44
 
135
45
  const CONSOLIDATION_PROMPT = `你是记忆库整理助手。下面是全部记忆条目(id、类型、标题、内容、重要性、更新时间)。
@@ -401,26 +311,20 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
401
311
  }
402
312
 
403
313
  /**
404
- * Resolve the LLM route (Issue #25): an explicit plugin config
405
- * (dreamProvider/dreamModel) is the user's declared override and wins; the
406
- * agent default model (deployment) is only a fallback when no config route is
407
- * set. In a standard DSH install agentDefaultModel always resolves, so without
408
- * this ordering the config route would be dead code and dreamProvider/dreamModel
409
- * could never take effect. Falls through to undefined when no route exists —
410
- * runDream then fails safe. A config→default switch is logged so it is observable.
314
+ * Resolve the LLM route: agent default model (deployment) first, plugin config
315
+ * (dreamProvider/dreamModel) as fallback. Falls through to undefined when no
316
+ * route exists runDream then fails safe. Fallback is logged so a silent
317
+ * route switch is observable.
411
318
  */
412
319
  function resolveRoute(ctx, config, logger) {
413
- if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
414
320
  try {
415
321
  const sel = ctx.agentDefaultModel?.currentSelection?.();
416
- if (sel?.provider && sel?.model) {
417
- logger?.info?.("dsh-mneme dream: no dreamProvider/dreamModel config, falling back to agent default");
418
- return { provider: sel.provider, model: sel.model };
419
- }
420
- logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, no config route either");
322
+ if (sel?.provider && sel?.model) return { provider: sel.provider, model: sel.model };
323
+ logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, falling back to config route");
421
324
  } catch (error) {
422
- logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed: ${String(error)}`);
325
+ logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed, falling back to config route: ${String(error)}`);
423
326
  }
327
+ if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
424
328
  return undefined;
425
329
  }
426
330
 
@@ -502,7 +406,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
502
406
  let inFlight = null;
503
407
 
504
408
  function shouldTrigger(service) {
505
- const memories = service.all().filter((m) => !m.archived && !m.session_disposed_at && m.type !== "summary");
409
+ const memories = service.all().filter((m) => !m.archived && m.type !== "summary");
506
410
  const count = memories.length;
507
411
  const chars = totalChars(memories);
508
412
  const overBase = count >= baseline.count + thresholdCount || chars >= baseline.chars + thresholdChars;
@@ -559,7 +463,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
559
463
  if (inFlight) await inFlight.catch(() => {});
560
464
  }
561
465
 
562
- async function runDream(ctx, service, config, settings) {
466
+ async function runDream(ctx, service, config) {
563
467
  const logger = ctx.logger;
564
468
  let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
565
469
  if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
@@ -712,35 +616,23 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
712
616
  return finish({ ok: false, error: "llm failed", summary: false });
713
617
  }
714
618
 
715
- // v0.5.3 字段名归一化兜底:thinking 模型可能输出别名键/wrapper 对象,
716
- // 在 validateDecisions 之前重写到规范字段名,语义正确但 schema 不听话的
717
- // 输出不再整单被拒。
718
- const decisions = normalizeDecisions(extractJsonArray(decisionText));
619
+ const decisions = extractJsonArray(decisionText);
719
620
  if (!Array.isArray(decisions)) {
720
621
  logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0})`);
721
622
  return finish({ ok: false, error: "no json array in llm output", summary: false });
722
623
  }
723
- const { ok, errors, skipped = [] } = validateDecisions(decisions, snapshot, {
624
+ const { ok, errors } = validateDecisions(decisions, snapshot, {
724
625
  maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
725
626
  minAgeHours: config.reflectionUpdateMinAgeHours,
726
627
  // v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
727
628
  // 真正生效,dreamMinExplicitCoverage 决定隐式 keep 下的覆盖率下限。
728
629
  dreamImplicitKeep: config.dreamImplicitKeep,
729
- dreamMinExplicitCoverage: config.dreamMinExplicitCoverage,
730
- // Issue #26 (P0):跨类型 merge 等"单条非法"决策不再拖垮整批——默认开启
731
- // skipInvalid,非法决策跳过、合法子集照常应用(run 记为 degraded)。
732
- // dreamSkipInvalid:false 可恢复旧的"任意非法即整单拒绝"。
733
- skipInvalid: config.dreamSkipInvalid !== false,
734
- // Issue #26 (P1):显式开启 allowCrossTypeMerge 后放宽跨类型合并检查。
735
- allowCrossTypeMerge: config.allowCrossTypeMerge === true
630
+ dreamMinExplicitCoverage: config.dreamMinExplicitCoverage
736
631
  });
737
632
  if (!ok) {
738
633
  logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
739
634
  return finish({ ok: false, error: `invalid decisions: ${errors.length} errors`, summary: false });
740
635
  }
741
- for (const s of skipped) {
742
- logger?.warn?.(`dsh-mneme dream: skipping invalid decision[${s.index}] (${s.action}): ${s.error}`);
743
- }
744
636
 
745
637
  // Capture pre-update snapshots so the audit records what each update changed.
746
638
  const updateSnapshots = {};
@@ -813,11 +705,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
813
705
  );
814
706
  // Outcome is derived from the ACTUALLY committed sub-steps, never from the
815
707
  // raw LLM decision list — a merge whose archive step rolled back must not
816
- // claim "merge-archived" (item ②). Conflicts/failures/skipped ride along
817
- // so the audit row records why the run diverged (Issue #26: skipped =
818
- // decisions dropped by skipInvalid because they were individually invalid,
819
- // e.g. cross-type merge).
820
- const outcome = { ...buildOutcome(committed), conflicts, failures, skipped };
708
+ // claim "merge-archived" (item ②). Conflicts/failures ride along so the
709
+ // audit row records why the run diverged.
710
+ const outcome = { ...buildOutcome(committed), conflicts, failures };
821
711
  // Frozen conflicts were not adjudicated: mark both sides pending in the
822
712
  // per-id outcome so the audit row shows they were parked, not skipped.
823
713
  if (frozenIds.length) {
@@ -833,30 +723,6 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
833
723
  // that only froze conflicts is not a noop.
834
724
  const noChange = frozenCount === 0 && applied === 0 && committed.every((c) => c.action === "keep");
835
725
 
836
- // v0.6.2 auto-tag: a light LLM pass over the retained (post-consolidation)
837
- // memories. Opt-in via config.autoTagEnabled, bounded by autoTagMaxPerRun,
838
- // and always fail-safe — a tag failure must never change the consolidation
839
- // outcome reported below (it is logged and counted, nothing more).
840
- let autoTagged = 0;
841
- // Effective opt-in (issue #31): the Web panel's settings toggle
842
- // (user_settings) overrides the zod plugin config when explicitly stored;
843
- // unset → fall back to config.autoTagEnabled. settings is null-safe (absent
844
- // in tests / direct calls) so behaviour is unchanged without it.
845
- const autoTagEnabled = settings?.getAutoTagConfig?.().autoTagEnabled ?? config.autoTagEnabled;
846
- if (autoTagEnabled === true) {
847
- try {
848
- const tagResult = await runAutoTag({ ctx, service, config, route });
849
- autoTagged = tagResult?.tagged ?? 0;
850
- if (tagResult?.ok === false && tagResult?.skippedBy && tagResult.skippedBy !== "empty") {
851
- logger?.warn?.(`dsh-mneme dream: auto-tag skipped (${tagResult.skippedBy})`);
852
- } else if (autoTagged > 0) {
853
- logger?.info?.(`[dsh-mneme] auto-tag: ${autoTagged} memory(ies) tagged`);
854
- }
855
- } catch (error) {
856
- logger?.warn?.(`dsh-mneme dream: auto-tag failed: ${String(error)}`);
857
- }
858
- }
859
-
860
726
  // Keep the vector index consistent with the post-dream store state.
861
727
  if (semantic?.embedder && semantic?.vectorIndex) {
862
728
  try {
@@ -886,7 +752,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
886
752
  : {}),
887
753
  messages: [
888
754
  { role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
889
- { role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && !m.session_disposed_at && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
755
+ { role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
890
756
  ]
891
757
  }, reportUsage));
892
758
  } catch (error) {
@@ -919,12 +785,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
919
785
  // run. ok:false keeps the scheduler from moving the baseline.
920
786
  // ok — either real changes landed, or a fresh summary was stored
921
787
  // (all-keep + summary is a substantive summary refresh).
922
- // degraded — real consolidation landed but the run did not produce its
923
- // full output: the summary came back empty/missing, or
924
- // (Issue #26) some individually-invalid decisions were
925
- // skipped by skipInvalid. The valid subset was absorbed (ok
926
- // for the baseline — a future run won't re-fail on the same
927
- // permanently-invalid pairs), but the run is marked, not faked.
788
+ // degraded — real consolidation landed but the summary came back empty/
789
+ // missing: the store was absorbed (ok for the baseline) but
790
+ // the run did not produce its full output (marked, not faked).
928
791
  let status;
929
792
  let okResult;
930
793
  if (partial) {
@@ -933,13 +796,6 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
933
796
  } else if (noChange) {
934
797
  status = summaryStored ? "ok" : "noop";
935
798
  okResult = summaryStored;
936
- } else if (skipped.length > 0) {
937
- // Issue #26: valid subset landed but at least one decision was dropped as
938
- // invalid (e.g. cross-type merge). degraded (not ok) — never a fake ok.
939
- // autoTag still runs on degraded runs, so it is not blocked behind a
940
- // "success" that a skipped-invalid run can never reach.
941
- status = "degraded";
942
- okResult = true;
943
799
  } else {
944
800
  status = summaryStored ? "ok" : "degraded";
945
801
  okResult = true;
@@ -952,10 +808,8 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
952
808
  outcome,
953
809
  conflicts,
954
810
  failures,
955
- skipped,
956
811
  frozen: frozenCount,
957
- summary: summaryStored,
958
- error: skipped.length > 0 ? `skipped ${skipped.length} invalid decision(s)` : undefined
812
+ summary: summaryStored
959
813
  });
960
814
  }
961
815
 
@@ -21,13 +21,6 @@ export function estimateTokens(text) {
21
21
  * rounds(): Array, clear(): void}}
22
22
  */
23
23
  export function createHotMemory({ maxRounds = 5, maxTokens = 2000 } = {}) {
24
- // Entry defense: a non-positive or non-integer maxRounds (0, -1, 1.5, NaN,
25
- // null, "2") would make the eviction while-loop unbounded — the buffer can
26
- // never shrink below `buffer.length > maxRounds`, so `add` would spin forever.
27
- // Fall back to the defaults so a hostile/buggy caller can never wedge the
28
- // hot-memory buffer in an infinite loop.
29
- maxRounds = (Number.isInteger(maxRounds) && maxRounds > 0) ? maxRounds : 5;
30
- maxTokens = (Number.isFinite(maxTokens) && maxTokens > 0) ? maxTokens : 2000;
31
24
  const buffer = [];
32
25
 
33
26
  function totalTokens() {
@@ -52,20 +52,8 @@ export const apply = (ctx, config) => {
52
52
  store.deleteOldLlmAudits(new Date(Date.now() - retentionMs * 86400000).toISOString());
53
53
  }
54
54
  } catch { /* non-fatal */ }
55
- // v0.7.0: recall_runs 滚动清理 —— recordRecall 默认开且注入路径也记账,表随
56
- // 活跃度增长;启动时按 recallRetentionDays(默认 90 天)清一次,防膨胀。
57
- // 与 llm_audit 同策略:纯 bookkeeping,清理失败绝不阻塞插件启动。
58
- try {
59
- const recallRetentionDays = Number.isInteger(cfg.recallRetentionDays) ? cfg.recallRetentionDays : 90;
60
- store.purgeRecallRunsOlderThan(recallRetentionDays);
61
- } catch { /* non-fatal */ }
62
55
  const mirror = createMirror(memoryDir);
63
- // User-configurable settings (profile, rules) and custom commands share the
64
- // same SQLite file but live in dedicated tables, isolated from memories.
65
- // Created before the service so the tag-config resolver (issue #31) can merge
66
- // the Web panel's persisted toggles over the plugin config at runtime.
67
- const settings = createSettings(store.db);
68
- const service = createService({ store, mirror, config: cfg, logger: ctx.logger, settings });
56
+ const service = createService({ store, mirror, config: cfg, logger: ctx.logger });
69
57
 
70
58
  // F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
71
59
  // a safe re-render at boot so a stale mirror converges without needing a
@@ -90,6 +78,10 @@ export const apply = (ctx, config) => {
90
78
  } catch { /* non-fatal: recall recording is bookkeeping */ }
91
79
  });
92
80
 
81
+ // User-configurable settings (profile, rules) and custom commands share the
82
+ // same SQLite file but live in dedicated tables, isolated from memories.
83
+ const settings = createSettings(store.db);
84
+
93
85
  // Semantic pipeline: a local/ollama embedder when configured, otherwise the
94
86
  // legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
95
87
  // the store's embedding column and tracks the active model fingerprint. A
@@ -252,7 +244,7 @@ export const apply = (ctx, config) => {
252
244
  delayMs: cfg.dreamDelayMs,
253
245
  logger: ctx.logger,
254
246
  semantic: { embedder, vectorIndex },
255
- onRun: () => (dream ? dream.runDream(ctx, service, cfg, settings) : Promise.resolve({ ok: true, skipped: true }))
247
+ onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
256
248
  });
257
249
  service.setDreamHook(() => dream.maybeSchedule(service));
258
250
  }
@@ -325,26 +317,6 @@ export const apply = (ctx, config) => {
325
317
  const summarizer = createSummarizer(ctx, service, cfg);
326
318
  disposers.push(summarizer.dispose);
327
319
 
328
- // Session lifecycle (v0.6.0): when a session leaves the store and the toggle
329
- // is enabled, mark every memory born in it as session-disposed (hidden from
330
- // injection/search/dream but never destroyed — recoverable via
331
- // restoreBySession). Default off, so a disposed session leaves its memories
332
- // active (legacy behavior). Every path is guarded: a failure inside the
333
- // callback must never propagate into DSH's session teardown (that would crash
334
- // the plugin on the very delete action it serves).
335
- if (cfg.sessionLifecycleEnabled) {
336
- disposers.push(ctx.on("session/disposed", (session) => {
337
- const sessionId = session?.id;
338
- if (!sessionId) return;
339
- try {
340
- const { disposed } = service.disposeBySession(sessionId);
341
- ctx.logger?.info?.(`[dsh-mneme] session disposed, hid ${disposed} memory(s) for ${sessionId}`);
342
- } catch (error) {
343
- ctx.logger?.warn?.(`[dsh-mneme] session dispose failed for ${sessionId}: ${String(error)}`);
344
- }
345
- }));
346
- }
347
-
348
320
  if (ctx.webServer) {
349
321
  const api = createApi(ctx, service, settings, commands ?? {
350
322
  add: () => { throw new Error("commands unavailable"); },
@@ -8,7 +8,10 @@ import { createHotMemory } from "./hot-memory.js";
8
8
  // falls back to the legacy rule-based pick, never breaking the render.
9
9
  function lastUserQuery(ctx) {
10
10
  try {
11
- const events = ctx?.agent?.session?.snapshotEvents?.() ?? ctx?.agent?.session?.events;
11
+ const session = ctx?.agent?.session;
12
+ // DSH ≥0.1.2-rc only exposes events via snapshotEvents(); older builds
13
+ // still have the .events property, so fall back to it.
14
+ const events = session?.snapshotEvents?.() ?? session?.events;
12
15
  if (!Array.isArray(events) || events.length === 0) return "";
13
16
  for (let i = events.length - 1; i >= 0; i--) {
14
17
  const event = events[i];
@@ -34,7 +37,8 @@ function lastUserQuery(ctx) {
34
37
  // returns [] on any failure, and the hot block simply does not render.
35
38
  function extractRounds(ctx, maxRounds) {
36
39
  try {
37
- const events = ctx?.agent?.session?.snapshotEvents?.() ?? ctx?.agent?.session?.events;
40
+ const session = ctx?.agent?.session;
41
+ const events = session?.snapshotEvents?.() ?? session?.events;
38
42
  if (!Array.isArray(events) || events.length === 0) return [];
39
43
  const rounds = [];
40
44
  let pendingQuery = null;
@@ -75,49 +79,6 @@ export function createInjector(ctx, service, settings, config) {
75
79
  const maxItems = config.maxInjectedItems ?? 5;
76
80
  const threshold = config.importanceThreshold ?? 3;
77
81
 
78
- // Prompt-variable escaping (issue #40): DSH's interpolate() treats `{{name}}`
79
- // as a prompt variable and strictly validates the name against
80
- // /^[a-z][a-z0-9_]*$/ — memory/profile content carrying legal template syntax
81
- // (Obsidian-style `{{hl|}}`, `{{挖空}}`, `{{关键词}}`) would hit an illegal
82
- // variable name and throw, crashing the whole turn. At the injection boundary
83
- // we escape every run of 2+ consecutive braces, inserting a `\` between each
84
- // pair, so no `{{`/`}}` substring survives into the prompt: `{{a}}` → `{\{a\}\}`,
85
- // and odd runs like `{{{a}}}` (which pair-wise escaping would leave with a
86
- // literal `{{`) are handled too. The text keeps its readable template form,
87
- // the transform is idempotent (escaped braces are single + `\`, never two
88
- // adjacent), and single braces pass through untouched — interpolate only
89
- // scans `{{`. Off via config.escapePromptVariables=false (default true).
90
- function escapePromptVars(text) {
91
- if (config.escapePromptVariables === false) return String(text);
92
- return String(text).replace(/[{}]{2,}/g, (run) => run.split("").join("\\"));
93
- }
94
-
95
- // Time-prefix injection (issue #34): opt-in, off by default. When enabled the
96
- // current date/time is injected once per conversation — at the first prompt
97
- // assembly of a new session — so the model can sense what time/day it is.
98
- // A per-session latch means later assemblies in the same session never
99
- // re-inject; a bare render ctx (no session id) falls back to once per
100
- // injector lifetime.
101
- const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
102
- let timePrefixSession = null;
103
-
104
- function renderTimePrefix(ctx) {
105
- if (config.injectTimePrefix !== true) return "";
106
- const sessionId = ctx?.agent?.session?.id;
107
- if (sessionId === undefined) {
108
- if (timePrefixSession !== null) return "";
109
- timePrefixSession = true;
110
- } else {
111
- if (sessionId === timePrefixSession) return "";
112
- timePrefixSession = sessionId;
113
- }
114
- const now = new Date();
115
- const pad = (n) => String(n).padStart(2, "0");
116
- const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
117
- const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
118
- return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
119
- }
120
-
121
82
  // Bug6: bound the injected memory block. Each entry's content is truncated to
122
83
  // MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
123
84
  // and an entry that would exceed it collapses to its title only, so a long
@@ -151,7 +112,7 @@ export function createInjector(ctx, service, settings, config) {
151
112
  for (const r of rounds) hot.add(r);
152
113
  const body = hot.getContext();
153
114
  if (!body) return "";
154
- return escapePromptVars(`[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}`);
115
+ return `[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}`;
155
116
  }
156
117
 
157
118
  function render(candidates) {
@@ -175,7 +136,7 @@ export function createInjector(ctx, service, settings, config) {
175
136
  lines.push(`- [${m.type}] ${verified}${title}`);
176
137
  }
177
138
  }
178
- return escapePromptVars(lines.join("\n"));
139
+ return lines.join("\n");
179
140
  }
180
141
 
181
142
  // Bug4: the system-prompt render is synchronous, so the semantic query vector
@@ -209,7 +170,7 @@ export function createInjector(ctx, service, settings, config) {
209
170
  const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
210
171
  if (profile) lines.push(`- 用户画像:${profile}`);
211
172
  for (const rule of rules) lines.push(`- 规则:${rule}`);
212
- return escapePromptVars(lines.join("\n"));
173
+ return lines.join("\n");
213
174
  }
214
175
 
215
176
  const disposers = [
@@ -231,11 +192,8 @@ export function createInjector(ctx, service, settings, config) {
231
192
  // separate context) keeps the prompt assembly stable at two blocks.
232
193
  const hotText = renderHotContext(ctx);
233
194
  const body = render(candidates);
234
- let out = !hotText ? body : (body ? `${hotText}\n\n${body}` : hotText);
235
- // Time prefix (issue #34): leads the injection block once per session.
236
- const timePrefix = renderTimePrefix(ctx);
237
- if (!timePrefix) return out;
238
- return out ? `${timePrefix}\n\n${out}` : timePrefix;
195
+ if (!hotText) return body;
196
+ return body ? `${hotText}\n\n${body}` : hotText;
239
197
  }
240
198
  }),
241
199
  ctx.systemPrompt.context({
@@ -7,9 +7,7 @@ export const TYPE_FILE = {
7
7
  project: "projects.md",
8
8
  decision: "decisions.md",
9
9
  history: "history.md",
10
- summary: "summary.md",
11
- user: "user.md",
12
- fact: "facts.md"
10
+ summary: "summary.md"
13
11
  };
14
12
 
15
13
  const ESCAPE = /([\\`*_[\]{}()#+.!|>~-])/g;
@@ -33,15 +31,6 @@ function renderMemory(m) {
33
31
  .digest("hex");
34
32
  const lines = [];
35
33
  lines.push(`## ${esc(m.title)}`);
36
- // v0.6.2 tag line: entity_attrs-backed tags (attached by the service as
37
- // `entityTags`) rendered as `#tag` space-separated under the title. No tags
38
- // → no line. Legacy `- **标签**:` metadata below keeps the memories.tags
39
- // column (still written by save/update) readable.
40
- const entityTags = Array.isArray(m.entityTags) ? m.entityTags : [];
41
- if (entityTags.length) {
42
- lines.push("");
43
- lines.push(entityTags.map((t) => `#${esc(t)}`).join(" "));
44
- }
45
34
  lines.push("");
46
35
  lines.push(`- **ID**: \`${m.id}\``);
47
36
  lines.push(`- **类型**: ${m.type}`);
@@ -34,9 +34,7 @@ const TYPE_LABELS = {
34
34
  decision: ["decision", "决策", "决定"],
35
35
  history: ["history", "历史", "事件"],
36
36
  summary: ["summary", "总结", "摘要", "总览"],
37
- pattern: ["pattern", "模式", "规律"],
38
- user: ["user", "用户"],
39
- fact: ["fact", "事实", "原子事实"]
37
+ pattern: ["pattern", "模式", "规律"]
40
38
  };
41
39
 
42
40
  /** Normalized bigram-overlap similarity in [0,1]; 0 for tiny/empty inputs. */
@@ -21,10 +21,7 @@ function modelHash(model) {
21
21
 
22
22
  /** Lazy default pipeline factory: dynamic import keeps module load cheap. */
23
23
  async function defaultPipelineLoader(task, model, options) {
24
- const { env, pipeline } = await import("@huggingface/transformers");
25
- // issue #13: mirror cache_dir onto env.cacheDir so the tokenizer pre-check
26
- // resolves locally too (same fix as local-embedder.js).
27
- if (options?.cache_dir) env.cacheDir = options.cache_dir;
24
+ const { pipeline } = await import("@huggingface/transformers");
28
25
  return pipeline(task, model, options);
29
26
  }
30
27