@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
@@ -1,15 +1,9 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { TYPE_DECAY_DEFAULTS } from "./heat.js";
3
2
 
4
3
  export const Config = z.object({
5
4
  memoryDir: z.string().default("~/.dsh/memory"),
6
5
  autoInject: z.boolean().default(true),
7
6
  autoSummarize: z.boolean().default(true),
8
- // Session lifecycle (v0.6.0): when enabled, deleting/disposing a session also
9
- // archives every memory that was born in it (treating the session as a save
10
- // point — entries stay recoverable via memory_archive/restoreBySession).
11
- // Default OFF: legacy behavior, a disposed session leaves its memories active.
12
- sessionLifecycleEnabled: z.boolean().default(false),
13
7
  // Optional model override for summarization. When both are non-empty, they
14
8
  // take priority over the session's current model. Empty = use the session's
15
9
  // active provider/model (same as before).
@@ -17,18 +11,6 @@ export const Config = z.object({
17
11
  summarizeModel: z.string().default(""),
18
12
  maxInjectedItems: z.natural().min(1).max(20).default(5),
19
13
  importanceThreshold: z.natural().min(1).max(5).default(3),
20
- // 时间前缀注入(issue #34,opt-in,默认关):开启时,新对话开始时把当前
21
- // 日期时间以 [当前时间: 2026-08-26 周二 19:30] 前缀注入一次,让模型感知
22
- // "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
23
- // 关闭时行为与之前完全一致。
24
- injectTimePrefix: z.boolean().default(false),
25
- // 花括号转义(issue #40,默认开):DSH 核心 interpolate() 会把 `{{name}}`
26
- // 当 prompt 变量做严格校验(变量名须匹配 /^[a-z][a-z0-9_]*$/),记忆正文里
27
- // 合法的模板语法(如 `{{hl|}}`、`{{挖空}}`、`{{关键词}}`)会因非法变量名
28
- // 直接 throw、让整轮对话崩溃。开启后注入边界把 `{{`→`\{\{`、`}}`→`\}\}`,
29
- // 文本不再含 `{{`/`}}` 子串,interpolate 不再扫描到,内容保持可读且幂等
30
- // (不会二次转义)。关闭后按原样透传。
31
- escapePromptVariables: z.boolean().default(true),
32
14
  autoDream: z.boolean().default(true),
33
15
  dreamThresholdCount: z.natural().min(1).max(1000).default(10),
34
16
  dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
@@ -37,13 +19,11 @@ export const Config = z.object({
37
19
  dreamModel: z.string(),
38
20
  dreamMaxTokens: z.natural().min(256).max(131072).default(8192),
39
21
  // Pass-through reasoning effort for dream's LLM calls. 'none' (default)
40
- // omits the field so the provider's own default applies; 'off' explicitly
41
- // disables thinking REQUIRED for thinking-type models (deepseek-v4-flash
42
- // etc.) that would otherwise drain the whole token budget into reasoning and
43
- // return an empty body ("no json array in llm output"); low/medium/high are
44
- // forwarded verbatim to cap reasoning spend.
22
+ // omits the field so the provider's own default applies; low/medium/high
23
+ // are forwarded verbatim. Useful to cap reasoning spend on thinking-type
24
+ // models that would otherwise drain the whole token budget and return an
25
+ // empty body ("no json array in llm output").
45
26
  dreamReasoningEffort: z.union([
46
- z.const("off"),
47
27
  z.const("low"),
48
28
  z.const("medium"),
49
29
  z.const("high"),
@@ -62,16 +42,6 @@ export const Config = z.object({
62
42
  // → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
63
43
  // 默认 0.5(至少显式覆盖一半 snapshot)。
64
44
  dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
65
- // 跳过非法决策(Issue #26 P0,默认开):跨类型 merge 等"单条非法"决策不再
66
- // 让整批校验失败 → 跳过该决策、应用合法子集,run 记为 degraded(applied>0)。
67
- // 关闭后恢复旧的"任意非法即整单拒绝"(applied=0)。防洗白语义不受影响——
68
- // 显式覆盖率不足/update 超量等全局错误仍整单拒绝。
69
- dreamSkipInvalid: z.boolean().default(true),
70
- // 允许跨类型合并(Issue #26 P1,默认关):类型有语义边界(preference 注入
71
- // 权重更高、decision/project 注入上下文不同),跨类型合并会丢类型信息,故
72
- // 默认禁止并在 skipInvalid 下被跳过;显式开启后放宽该检查,类型边界由用户
73
- // 自行承担(需与 dreamSkipInvalid 配合:开启后跨类型 merge 视为合法、可应用)。
74
- allowCrossTypeMerge: z.boolean().default(false),
75
45
  // Rule version for dream adjudication: when this bumps, older dream_runs
76
46
  // degrade to historical evidence (their receipts no longer drive live
77
47
  // decisions). Default 0 = no versioning in use yet.
@@ -189,43 +159,6 @@ export const Config = z.object({
189
159
  // Prefix/semantic search over entity names (used by recall).
190
160
  entitySearchEnabled: z.boolean().default(true),
191
161
 
192
- // --- wiki-link: explicit cross-memory [[links]] (v0.6.1) ----------------
193
- // Opt-in, off by default. When enabled, saveWithDedupe/update fire-and-forget
194
- // a wiki-link resolution pass: [[target]] / [[显示|target]] markers in a
195
- // memory's content become links_to relations in entity_relations (idempotent,
196
- // deduped by the unique relation index). The storage layer + read APIs
197
- // (getBacklinks/getForwardLinks/resolveWikiLink) are always available
198
- // regardless of this flag.
199
- wikiLinkEnabled: z.boolean().default(false),
200
-
201
- // --- tag system (v0.6.2) ---------------------------------------------------
202
- // Opt-in: when autoTagEnabled is true, a light LLM pass runs after each
203
- // autoDream consolidation and extracts 1-3 tags per retained memory
204
- // (autoTagMaxPerRun bounds how many memories are tagged per run). The tag
205
- // storage layer (store.setMemoryTags/getMemoryTags + tag: search + mirror
206
- // `#tag` line) is always available regardless of this flag.
207
- autoTagEnabled: z.boolean().default(false),
208
- autoTagMaxPerRun: z.natural().min(1).max(100).default(10),
209
- // Manual tagging (service.setMemoryTags / memory tools) is on by default;
210
- // set false to disable the manual write path too.
211
- manualTagEnabled: z.boolean().default(true),
212
-
213
- // --- sidebar trigger (issue #38) ---------------------------------------
214
- // The memory library is reachable from two UI surfaces: a "记忆库" tab in the
215
- // conversation header AND an entrance button at the sidebar footer (bottom
216
- // left). Some sidebars get crowded there — dsh-cost-meter and friends also
217
- // claim the footer slot — so the entrance button is now optional. Off just
218
- // hides the button; the conversation tab stays, so no functionality is lost.
219
- // The Web panel exposes this as a toggle (settings-over-config, see
220
- // settings.getUiConfig); the value here is the plugin-config default.
221
- showSidebarTrigger: z.boolean().default(true),
222
-
223
- // --- tag-weighted re-rank (v0.6.4) -------------------------------------
224
- // Opt-in: boost candidates whose tags overlap the query/session tags.
225
- tagBoostEnabled: z.boolean().default(false),
226
- tagBoostFactor: z.number().min(1).max(2).default(1.15),
227
- sessionTagBoostFactor: z.number().min(1).max(2).default(1.08),
228
-
229
162
  // --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
230
163
  // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
231
164
  // sleep fires when the store has been quiet for sleepIdleMinutes and deep-
@@ -264,25 +197,14 @@ export const Config = z.object({
264
197
  sleepProvider: z.string().default(""),
265
198
  sleepModel: z.string().default(""),
266
199
  // Pass-through reasoning effort for sleep's LLM passes, same semantics as
267
- // dreamReasoningEffort: 'none' (default) omits the field; 'off' explicitly
268
- // disables thinking (thinking-type models would burn the whole budget on
269
- // reasoning); low/medium/high are forwarded verbatim.
200
+ // dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
201
+ // are forwarded verbatim.
270
202
  sleepReasoningEffort: z.union([
271
- z.const("off"),
272
203
  z.const("low"),
273
204
  z.const("medium"),
274
205
  z.const("high"),
275
206
  z.const("none")
276
207
  ]).default("none"),
277
- // Batch entity extraction during sleep (issue #23). The write-path extractor
278
- // only fires when entityExtractionEnabled is on (an LLM call per write);
279
- // this additive phase backfills entities/attrs/relations for memories that
280
- // never went through it, so stores that leave the write-path extractor off
281
- // still accumulate an ego graph as long as sleep runs. On by default (it is
282
- // a no-op until a sleep cycle fires), capped per run to bound token spend.
283
- sleepEntityExtractionEnabled: z.boolean().default(true),
284
- // Max memories entity-extracted per sleep run (oldest un-extracted first).
285
- sleepEntityExtractionMaxPerRun: z.natural().min(1).max(100).default(20),
286
208
 
287
209
  // --- epistemic trust: memory source credibility (v0.4.5) -----------------
288
210
  // Distinguish memories by source: observation (measured / witnessed),
@@ -328,19 +250,52 @@ export const Config = z.object({
328
250
  // flag (production isolation is unconditional).
329
251
  evalPersistTestResults: z.boolean().default(false),
330
252
 
331
- // --- heat: v0.7.0 self-evolution (heat + interest drift) ----------------
332
- // 总开关,默认开但保守:不改变召回排序,只提供热度字段 / sleep 降级
333
- // 联合判定保护 / 前端热度投影。关闭后跳过所有 heat 计算与热度触达。
334
- heatEnabled: z.boolean().default(true),
335
- // 幂律形状参数 α(heat = 1/(1+λΔt)^α),越大衰减越快。
336
- heatGlobalAlpha: z.number().min(0.1).max(5).default(1.2),
337
- // per-type 衰减因子 λ;λ=0 的类型免疫(热度恒 1.0,sleep 永不降级)。
338
- // 未知类型走默认 0.002。z.dict 的键为 type 字符串、值为数字 λ。
339
- heatTypeDecay: z.dict(z.number(), z.string()).default({ ...TYPE_DECAY_DEFAULTS }),
340
- // sleep 降级联合判定的热度下限:heat < 该值 且 importance<5 才允许降级。
341
- sleepHeatThreshold: z.number().min(0).max(1).default(0.05),
342
- // recordRecall 默认值(recall_runs 记录默认开;显式传 false 的调用方不受影响)。
343
- recallRecordDefault: z.boolean().default(true),
344
- // recall_runs 滚动清理保留天数。
345
- recallRetentionDays: z.natural().min(1).max(3650).default(90),
253
+ // --- standalone external API (v0.7.12) ------------------------------------
254
+ // A plain node:http server for ecosystem integrations that cannot reach the
255
+ // DSH-internal webServer. Disabled by default; when enabled the Bearer token
256
+ // is persisted in the settings kv ("external_api"), auto-generated on first
257
+ // boot. Bind host: keep the loopback default — moving it to a non-loopback
258
+ // address exposes the whole memory store to the network and is the
259
+ // operator's responsibility.
260
+ externalApiEnabled: z.boolean().default(false),
261
+ externalApiPort: z.natural().default(8790),
262
+ externalApiHost: z.string().default("127.0.0.1"),
263
+
264
+ // --- light mode preset (v0.7.12) -------------------------------------------
265
+ // One switch for low-resource setups: turns off every background/semantic
266
+ // heavy path (dream consolidation, entity extraction, vector pipeline,
267
+ // reranker, BM25, semantic dedup / selective inject, sleep mode) while
268
+ // keeping the core loop (autoInject, autoSummarize, hot memory, quality
269
+ // filter, keyword search). Applied by applyLightModePreset before the config
270
+ // reaches any service; a persisted panel_mode="light" (settings kv) counts
271
+ // as lightMode=true too and wins over the bundle config.
272
+ lightMode: z.boolean().default(false),
346
273
  });
274
+
275
+ // Fields forced to false by the light-mode preset. Everything not listed here
276
+ // (autoInject, autoSummarize, hotMemory*, memoryQualityFilter, dream
277
+ // thresholds/delays, ...) is left untouched — those are the core loop.
278
+ const LIGHT_MODE_OFF = [
279
+ "entityExtractionEnabled",
280
+ "autoDream",
281
+ "sleepModeEnabled",
282
+ "rerankEnabled",
283
+ "autoReindexOnBoot",
284
+ "hybridInject",
285
+ "searchSemanticDedup",
286
+ "selectiveInjectEnabled",
287
+ "bm25SearchEnabled"
288
+ ];
289
+
290
+ /**
291
+ * Apply the light-mode preset to a resolved config object (pure function,
292
+ * exported for tests). When cfg.lightMode is not exactly true the config is
293
+ * returned unchanged; otherwise a shallow copy carries false for every heavy
294
+ * feature. Idempotent and side-effect free.
295
+ */
296
+ export function applyLightModePreset(cfg) {
297
+ if (cfg?.lightMode !== true) return cfg;
298
+ const preset = { ...cfg, lightMode: true };
299
+ for (const key of LIGHT_MODE_OFF) preset[key] = false;
300
+ return preset;
301
+ }
@@ -7,163 +7,120 @@ const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
7
7
 
8
8
  /**
9
9
  * Validate a dream decision list against a snapshot of eligible memories.
10
- *
11
- * Issue #26 (skipInvalid): 模型几乎必然为了语义相关性而产出跨类型 merge
12
- * (type 不同,硬性禁止),这类"单条非法"此前让整批校验 ok:false、整单拒绝
13
- * (applied=0、白耗一次 LLM 调用)。`options.skipInvalid` 开启后,逐条非法的
14
- * 决策被跳过(从 decisions 中移除、不 claim 任何 id、记录到 `skipped`),
15
- * 只有全局性错误(update/create 超量、显式覆盖率不足——
16
- * 这些是"模型失控/输出被截断"信号)仍整单拒绝,防洗白语义不变。
17
- * `options.allowCrossTypeMerge`(Issue #26 P1)显式放宽跨类型合并检查——
18
- * 默认 false 保持现有类型边界,true 时跨类型 merge 被允许(类型边界由
19
- * 用户自行承担)。
20
- *
21
- * @param decisions - LLM-produced decision list. In skipInvalid mode, invalid
22
- * entries are spliced out in place so the caller's downstream apply/audit
23
- * operates on the surviving subset only.
10
+ * @param decisions - LLM-produced decision list.
24
11
  * @param snapshot - Map<id, memory> of eligible (non-archived, non-summary) entries.
25
- * @returns {{ok: boolean, errors: string[], skipped?: Array<{index, action, ids, error}>}}
12
+ * @returns {{ok: boolean, errors: string[]}}
26
13
  */
27
14
  export function validateDecisions(decisions, snapshot, options = {}) {
28
15
  const errors = [];
29
- const skipped = [];
30
16
  const maxUpdatePerRun = options.maxUpdatePerRun ?? 2;
31
17
  const minAgeHours = options.minAgeHours ?? 24;
32
- const skipInvalid = options.skipInvalid === true;
33
18
  if (!Array.isArray(decisions) || decisions.length === 0) {
34
19
  return { ok: false, errors: ["decision list must be a non-empty array"] };
35
20
  }
36
21
  const claimed = new Set();
37
- const survivors = [];
38
22
  for (const [index, d] of decisions.entries()) {
39
23
  const at = `decision[${index}]`;
40
- const local = [];
41
- const ids = d && d.action === "conflict" ? [d.winner, d.loser] : (d?.ids ?? []);
42
24
  if (!d || typeof d !== "object" || !ACTIONS.has(d.action)) {
43
- local.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
44
- } else {
45
- if (d.action === "conflict") {
46
- if (!d.winner || !d.loser || d.winner === d.loser) {
47
- local.push(`${at}: conflict needs distinct winner and loser`);
48
- }
49
- } else if (d.action === "create") {
50
- // Mint a fresh memory (sleep pattern discovery). Claims no existing id,
51
- // so it skips the claiming loop below; evidence is optional provenance
52
- // (already filtered to real ids by the caller) and is stored in content.
53
- if (typeof d.title !== "string" || !d.title.trim()) {
54
- local.push(`${at}: create needs non-empty title`);
55
- }
56
- if (typeof d.content !== "string" || !d.content.trim()) {
57
- local.push(`${at}: create needs non-empty content`);
58
- }
59
- if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
60
- local.push(`${at}: create importance must be an integer 1-5 when provided`);
61
- }
62
- if (typeof d.type !== "string" || !d.type.trim()) {
63
- local.push(`${at}: create needs non-empty type`);
64
- }
65
- } else if (!Array.isArray(d.ids) || d.ids.length === 0) {
66
- local.push(`${at}: ${d.action} needs non-empty ids`);
25
+ errors.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
26
+ continue;
27
+ }
28
+ const ids = d.action === "conflict" ? [d.winner, d.loser] : (d.ids ?? []);
29
+ if (d.action === "conflict") {
30
+ if (!d.winner || !d.loser || d.winner === d.loser) {
31
+ errors.push(`${at}: conflict needs distinct winner and loser`);
32
+ continue;
67
33
  }
68
- // update-specific field validation runs BEFORE claiming ids, so a failing
69
- // update never pollutes the claimed set (which drives the "every id must
70
- // appear in a decision" check below).
71
- if (d.action === "update") {
72
- // 只能更新单条
73
- if (!Array.isArray(d.ids) || d.ids.length !== 1) {
74
- local.push(`${at}: update must target exactly one id`);
75
- } else {
76
- // 必须产生实际变化
77
- const mem = snapshot.get(d.ids[0]);
78
- const hasChange = (d.title !== undefined && d.title !== mem?.title)
79
- || (d.content !== undefined && d.content !== mem?.content)
80
- || (d.importance !== undefined && d.importance !== mem?.importance);
81
- if (!hasChange) {
82
- local.push(`${at}: update must change at least one field`);
83
- }
84
- // 不能更新 summary
85
- if (mem?.type === "summary") {
86
- local.push(`${at}: cannot update summary via update action`);
87
- }
88
- // 保护期:新建记忆不可立即被 update(可配置)
89
- const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
90
- if (ageHours < minAgeHours) {
91
- local.push(`${at}: memory too young (< ${minAgeHours}h)`);
92
- }
93
- }
34
+ } else if (d.action === "create") {
35
+ // Mint a fresh memory (sleep pattern discovery). Claims no existing id,
36
+ // so it skips the claiming loop below; evidence is optional provenance
37
+ // (already filtered to real ids by the caller) and is stored in content.
38
+ if (typeof d.title !== "string" || !d.title.trim()) {
39
+ errors.push(`${at}: create needs non-empty title`);
40
+ continue;
94
41
  }
95
- if (d.action !== "create") {
96
- const seen = new Set();
97
- for (const id of ids) {
98
- const mem = snapshot.get(id);
99
- if (!mem) {
100
- local.push(`${at}: unknown id ${JSON.stringify(id)}`);
101
- } else if (mem.archived || mem.type === "summary") {
102
- local.push(`${at}: id ${JSON.stringify(id)} is archived or summary (not eligible)`);
103
- }
104
- // 单决策内重复 id(同一决策引用同一记忆两次)→ 非法;跨决策重复由
105
- // 全局 claimed 检测(只含已通过校验的幸存决策的 claim)。
106
- if (seen.has(id)) {
107
- local.push(`${at}: duplicate id ${JSON.stringify(id)} within one decision`);
108
- }
109
- if (claimed.has(id)) {
110
- local.push(`${at}: id ${JSON.stringify(id)} claimed by multiple decisions`);
111
- }
112
- seen.add(id);
113
- }
114
- if (d.action === "merge") {
115
- if (!d.keepSource || !d.ids.includes(d.keepSource)) {
116
- local.push(`${at}: merge keepSource must be one of ids`);
117
- }
118
- if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
119
- local.push(`${at}: merge needs non-empty title and content`);
120
- }
121
- if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
122
- local.push(`${at}: merge importance must be an integer 1-5 when provided`);
123
- }
124
- // Merging across types would blur preference/project/decision boundaries
125
- // in the injected context; the snapshot carries each entry's type.
126
- // Issue #26 (P1): 默认禁止跨类型合并(类型有语义作用——preference 注入
127
- // 权重更高、decision/project 注入上下文不同,合并会丢类型边界)。用户显式
128
- // 开启 allowCrossTypeMerge 后放宽该检查,类型边界由用户自行承担。
129
- const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
130
- if (mergeTypes.size > 1 && options.allowCrossTypeMerge !== true) {
131
- local.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
132
- }
133
- }
42
+ if (typeof d.content !== "string" || !d.content.trim()) {
43
+ errors.push(`${at}: create needs non-empty content`);
44
+ continue;
134
45
  }
135
- }
136
- if (local.length > 0) {
137
- if (skipInvalid) {
138
- // 单条非法 跳过该决策,不 claim id(其目标记忆留给其它合法决策/
139
- // 隐式 keep),并记录到 skipped 供调用方日志/审计。信息性跳过绝不
140
- // 写入全局 errors,否则会误触发下方的整单拒绝。
141
- skipped.push({
142
- index,
143
- action: d?.action,
144
- ids,
145
- error: local.join("; ")
146
- });
147
- } else {
148
- errors.push(...local);
46
+ if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
47
+ errors.push(`${at}: create importance must be an integer 1-5 when provided`);
48
+ }
49
+ if (typeof d.type !== "string" || !d.type.trim()) {
50
+ errors.push(`${at}: create needs non-empty type`);
149
51
  }
150
52
  continue;
53
+ } else if (!Array.isArray(d.ids) || d.ids.length === 0) {
54
+ errors.push(`${at}: ${d.action} needs non-empty ids`);
55
+ continue;
56
+ }
57
+ // update-specific field validation runs BEFORE claiming ids, so a failing
58
+ // update never pollutes the claimed set (which drives the "every id must
59
+ // appear in a decision" check below).
60
+ if (d.action === "update") {
61
+ // 只能更新单条
62
+ if (!Array.isArray(d.ids) || d.ids.length !== 1) {
63
+ errors.push(`${at}: update must target exactly one id`);
64
+ continue;
65
+ }
66
+ // 必须产生实际变化
67
+ const mem = snapshot.get(d.ids[0]);
68
+ const hasChange = (d.title !== undefined && d.title !== mem?.title)
69
+ || (d.content !== undefined && d.content !== mem?.content)
70
+ || (d.importance !== undefined && d.importance !== mem?.importance);
71
+ if (!hasChange) {
72
+ errors.push(`${at}: update must change at least one field`);
73
+ continue;
74
+ }
75
+ // 不能更新 summary
76
+ if (mem?.type === "summary") {
77
+ errors.push(`${at}: cannot update summary via update action`);
78
+ continue;
79
+ }
80
+ // 保护期:新建记忆不可立即被 update(可配置)
81
+ const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
82
+ if (ageHours < minAgeHours) {
83
+ errors.push(`${at}: memory too young (< ${minAgeHours}h)`);
84
+ continue;
85
+ }
151
86
  }
152
- if (d.action !== "create") {
153
- for (const id of ids) claimed.add(id);
87
+ for (const id of ids) {
88
+ const mem = snapshot.get(id);
89
+ if (!mem) {
90
+ errors.push(`${at}: unknown id ${JSON.stringify(id)}`);
91
+ } else if (mem.archived || mem.type === "summary") {
92
+ errors.push(`${at}: id ${JSON.stringify(id)} is archived or summary (not eligible)`);
93
+ }
94
+ if (claimed.has(id)) {
95
+ errors.push(`${at}: id ${JSON.stringify(id)} claimed by multiple decisions`);
96
+ }
97
+ claimed.add(id);
98
+ }
99
+ if (d.action === "merge") {
100
+ if (!d.keepSource || !d.ids.includes(d.keepSource)) {
101
+ errors.push(`${at}: merge keepSource must be one of ids`);
102
+ }
103
+ if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
104
+ errors.push(`${at}: merge needs non-empty title and content`);
105
+ }
106
+ if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
107
+ errors.push(`${at}: merge importance must be an integer 1-5 when provided`);
108
+ }
109
+ // Merging across types would blur preference/project/decision boundaries
110
+ // in the injected context; the snapshot carries each entry's type.
111
+ const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
112
+ if (mergeTypes.size > 1) {
113
+ errors.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
114
+ }
154
115
  }
155
- survivors.push(d);
156
116
  }
157
- // Cap update churn: too many edits in one cycle signals a runaway model.
158
- // These are GLOBAL signals in skipInvalid mode they still reject the whole
159
- // batch, because a model that floods updates/creates is broken, not just
160
- // slightly schema-drifting.
161
- const updateCount = survivors.filter((d) => d.action === "update").length;
117
+ // Cap update churn: too many edits in one cycle signals a runaway model
118
+ const updateCount = decisions.filter((d) => d.action === "update").length;
162
119
  if (updateCount > maxUpdatePerRun) {
163
120
  errors.push(`too many update decisions: ${updateCount} > ${maxUpdatePerRun}`);
164
121
  }
165
122
  // Cap pattern minting per run (sleepMaxPatternPerRun passes through here).
166
- const createCount = survivors.filter((d) => d.action === "create").length;
123
+ const createCount = decisions.filter((d) => d.action === "create").length;
167
124
  const maxCreatePerRun = options.maxCreatePerRun ?? 5;
168
125
  if (createCount > maxCreatePerRun) {
169
126
  errors.push(`too many create decisions: ${createCount} > ${maxCreatePerRun}`);
@@ -180,31 +137,25 @@ export function validateDecisions(decisions, snapshot, options = {}) {
180
137
  // snapshot(claimed.size / snapshot.size < dreamMinExplicitCoverage)时整单拒绝,
181
138
  // 而不是用 keep 把绝大部分 snapshot 全部"通过"。
182
139
  if (errors.length > 0) {
183
- return { ok: false, errors, skipped };
140
+ return { ok: false, errors };
184
141
  }
185
142
  const minCoverage = options.dreamMinExplicitCoverage ?? 0.5;
186
143
  if (options.dreamImplicitKeep !== false) {
187
144
  const coverage = snapshot.size > 0 ? claimed.size / snapshot.size : 1;
188
145
  if (coverage < minCoverage) {
189
146
  errors.push(`explicit decision coverage ${Math.round(coverage * 100)}% < minimum ${Math.round(minCoverage * 100)}%`);
190
- return { ok: false, errors, skipped };
147
+ return { ok: false, errors };
191
148
  }
192
149
  for (const id of snapshot.keys()) {
193
- if (!claimed.has(id)) survivors.push({ action: "keep", ids: [id] });
150
+ if (!claimed.has(id)) decisions.push({ action: "keep", ids: [id] });
194
151
  }
195
152
  } else {
196
153
  for (const id of snapshot.keys()) {
197
154
  if (!claimed.has(id)) errors.push(`memory ${JSON.stringify(id)} missing from decisions`);
198
155
  }
199
- if (errors.length > 0) return { ok: false, errors, skipped };
200
- }
201
- // 调用方下游(apply/audit)复用同一 decisions 引用:就地同步为 survivors——
202
- // 在 skipInvalid 模式下去掉被跳过的非法决策;在隐式 keep 下追加补齐的 keep。
203
- // 内容一致时(无跳过、无补齐)为 no-op。
204
- if (survivors.length !== decisions.length) {
205
- decisions.splice(0, decisions.length, ...survivors);
156
+ if (errors.length > 0) return { ok: false, errors };
206
157
  }
207
- return { ok: true, errors, skipped };
158
+ return { ok: true, errors };
208
159
  }
209
160
 
210
161
  /** Marker thrown when a decision target changed since the run snapshot. */