@modusensus/dsh-mneme 0.8.14 → 0.8.15

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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +655 -654
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +689 -689
  5. package/bin/dsh-mneme-mcp.mjs +510 -510
  6. package/bin/dsh-mneme-serve.mjs +151 -151
  7. package/cordis.patch.yml +21 -21
  8. package/docs/CONFIGURATION.md +263 -0
  9. package/docs/CONTINUITY.md +124 -0
  10. package/docs/DAEMON.md +46 -0
  11. package/docs/ENTITIES.md +245 -0
  12. package/docs/LOCAL_MODEL.md +222 -0
  13. package/docs/SEMANTIC.md +256 -0
  14. package/docs/SLEEP.md +164 -0
  15. package/docs/STORAGE.md +89 -0
  16. package/lib/api-standalone.js +673 -673
  17. package/lib/api.js +1437 -1433
  18. package/lib/bootstrap.js +211 -211
  19. package/lib/client.js +5444 -5295
  20. package/lib/commands.js +95 -95
  21. package/lib/config.js +880 -843
  22. package/lib/content-hash.js +54 -54
  23. package/lib/continuity.js +220 -220
  24. package/lib/document-index.js +94 -94
  25. package/lib/document.js +303 -303
  26. package/lib/dream/clustering.js +118 -118
  27. package/lib/dream/decisions.js +674 -674
  28. package/lib/dream/narratives.js +44 -44
  29. package/lib/dream/sleep.js +819 -819
  30. package/lib/dream.js +1501 -1501
  31. package/lib/embedding.js +177 -177
  32. package/lib/entities/extractor.js +252 -252
  33. package/lib/graph/anchoring.js +179 -179
  34. package/lib/guide.js +74 -74
  35. package/lib/heat.js +144 -144
  36. package/lib/hot-memory.js +46 -46
  37. package/lib/index.js +535 -503
  38. package/lib/inject.js +464 -450
  39. package/lib/lang.js +248 -229
  40. package/lib/local-embedder.js +358 -358
  41. package/lib/maintenance.js +142 -142
  42. package/lib/mirror.js +330 -330
  43. package/lib/organize.js +450 -450
  44. package/lib/peak-hours.js +117 -117
  45. package/lib/placement.js +13 -0
  46. package/lib/pre-inject-gate.js +340 -0
  47. package/lib/quality-filter.js +126 -126
  48. package/lib/recall-stats.js +175 -175
  49. package/lib/reranker.js +269 -269
  50. package/lib/runtime/adopt.js +176 -176
  51. package/lib/runtime/closure.js +172 -172
  52. package/lib/runtime/download.js +247 -247
  53. package/lib/runtime/layout.js +265 -265
  54. package/lib/runtime/loader.js +252 -252
  55. package/lib/runtime/model-fetch.js +361 -361
  56. package/lib/runtime/provision.js +237 -237
  57. package/lib/runtime/tarball.js +130 -130
  58. package/lib/runtime/verify.js +223 -204
  59. package/lib/scope.js +146 -146
  60. package/lib/search/adaptive.js +60 -60
  61. package/lib/search/bm25.js +96 -96
  62. package/lib/semantic.js +229 -229
  63. package/lib/sensitive-scan.js +209 -209
  64. package/lib/serve.js +215 -215
  65. package/lib/service.js +2663 -2605
  66. package/lib/settings.js +517 -511
  67. package/lib/store.js +3007 -3007
  68. package/lib/summarize.js +891 -856
  69. package/lib/tools.js +743 -737
  70. package/lib/vector-index.js +110 -110
  71. package/lib/version-check.js +122 -122
  72. package/lib/write-admission.js +402 -402
  73. package/locale/en.json +6 -6
  74. package/locale/zh.json +6 -6
  75. package/package.json +118 -118
  76. package/runtime-manifest.json +526 -526
  77. package/scripts/benchmark-embed.js +201 -201
  78. package/scripts/benchmark-recall.js +249 -249
  79. package/scripts/benchmark-rerank.js +166 -166
  80. package/scripts/build-runtime-manifest.mjs +224 -224
  81. package/scripts/changelog-prep.mjs +36 -36
  82. package/scripts/check-sync.js +42 -42
  83. package/scripts/e2e-dsh.js +220 -220
  84. package/scripts/mneme-runtime.mjs +277 -277
  85. package/scripts/stress-dsh.js +255 -255
  86. package/scripts/sync-lib.js +47 -47
  87. package/scripts/sync-test-badge.mjs +69 -69
  88. package/scripts/test-count-sync.mjs +44 -44
  89. package/src/api-standalone.js +673 -673
  90. package/src/api.js +1437 -1433
  91. package/src/bootstrap.js +211 -211
  92. package/src/commands.js +95 -95
  93. package/src/config.js +880 -843
  94. package/src/content-hash.js +54 -54
  95. package/src/continuity.js +220 -220
  96. package/src/document-index.js +94 -94
  97. package/src/document.js +303 -303
  98. package/src/dream/clustering.js +118 -118
  99. package/src/dream/decisions.js +674 -674
  100. package/src/dream/narratives.js +44 -44
  101. package/src/dream/sleep.js +819 -819
  102. package/src/dream.js +1501 -1501
  103. package/src/embedding.js +177 -177
  104. package/src/entities/extractor.js +252 -252
  105. package/src/graph/anchoring.js +179 -179
  106. package/src/guide.js +74 -74
  107. package/src/heat.js +144 -144
  108. package/src/hot-memory.js +46 -46
  109. package/src/index.js +535 -503
  110. package/src/inject.js +464 -450
  111. package/src/lang.js +248 -229
  112. package/src/local-embedder.js +358 -358
  113. package/src/maintenance.js +142 -142
  114. package/src/mirror.js +330 -330
  115. package/src/organize.js +450 -450
  116. package/src/peak-hours.js +117 -117
  117. package/src/placement.js +13 -0
  118. package/src/pre-inject-gate.js +340 -0
  119. package/src/quality-filter.js +126 -126
  120. package/src/recall-stats.js +175 -175
  121. package/src/reranker.js +269 -269
  122. package/src/runtime/adopt.js +176 -176
  123. package/src/runtime/closure.js +172 -172
  124. package/src/runtime/download.js +247 -247
  125. package/src/runtime/layout.js +265 -265
  126. package/src/runtime/loader.js +252 -252
  127. package/src/runtime/model-fetch.js +361 -361
  128. package/src/runtime/provision.js +237 -237
  129. package/src/runtime/tarball.js +130 -130
  130. package/src/runtime/verify.js +223 -204
  131. package/src/scope.js +146 -146
  132. package/src/search/adaptive.js +60 -60
  133. package/src/search/bm25.js +96 -96
  134. package/src/semantic.js +229 -229
  135. package/src/sensitive-scan.js +209 -209
  136. package/src/serve.js +215 -215
  137. package/src/service.js +2663 -2605
  138. package/src/settings.js +517 -511
  139. package/src/store.js +3007 -3007
  140. package/src/summarize.js +891 -856
  141. package/src/tools.js +743 -737
  142. package/src/vector-index.js +110 -110
  143. package/src/version-check.js +122 -122
  144. package/src/write-admission.js +402 -402
  145. package/test/adaptive-inject-budget.test.js +85 -85
  146. package/test/api-routes-gaps.test.js +218 -218
  147. package/test/api.test.js +1702 -1690
  148. package/test/audit.test.js +448 -448
  149. package/test/baseline-persist.test.js +248 -248
  150. package/test/benchmark.test.js +94 -94
  151. package/test/bootstrap.test.js +220 -220
  152. package/test/changelog-prep.test.js +60 -60
  153. package/test/client.test.js +1230 -1169
  154. package/test/clustering.test.js +100 -100
  155. package/test/commands.test.js +106 -106
  156. package/test/config.test.js +66 -66
  157. package/test/conflict-freeze.test.js +300 -300
  158. package/test/content-hash.test.js +204 -204
  159. package/test/continuity.test.js +333 -333
  160. package/test/document-dir.test.js +233 -233
  161. package/test/document.test.js +671 -671
  162. package/test/dream-archive-guardrail.test.js +116 -116
  163. package/test/dream-candidate-hybrid.test.js +281 -281
  164. package/test/dream-coverage-degrade.test.js +86 -86
  165. package/test/dream-failure-backoff.test.js +173 -173
  166. package/test/dream-peak-hours.test.js +194 -194
  167. package/test/dream.test.js +1385 -1383
  168. package/test/embedding-ready.test.js +196 -196
  169. package/test/entities.test.js +627 -547
  170. package/test/entity-recall.test.js +179 -179
  171. package/test/epistemic.test.js +325 -325
  172. package/test/fnew-0112.test.js +311 -311
  173. package/test/fnew-03.test.js +422 -422
  174. package/test/graph-anchoring.test.js +184 -184
  175. package/test/graph-api.test.js +175 -175
  176. package/test/graph-inject-budget.test.js +103 -103
  177. package/test/graph-passive-confirm.test.js +91 -91
  178. package/test/graph-weight-evolution.test.js +104 -104
  179. package/test/heat.test.js +175 -175
  180. package/test/helpers/dream-mock.js +82 -82
  181. package/test/helpers/peer-worker.mjs +25 -25
  182. package/test/helpers/runtime-source.js +60 -60
  183. package/test/helpers/tar-builder.js +80 -80
  184. package/test/helpers/write-admission-samples.js +308 -308
  185. package/test/hot-memory.test.js +145 -145
  186. package/test/inject-content-cap.test.js +91 -91
  187. package/test/inject-first-round-bm25.test.js +89 -89
  188. package/test/inject-heat.test.js +77 -77
  189. package/test/inject-parent-gate.test.js +106 -106
  190. package/test/inject-pools.test.js +194 -194
  191. package/test/inject-recall.test.js +105 -105
  192. package/test/inject-rotation.test.js +161 -161
  193. package/test/inject.test.js +420 -420
  194. package/test/lang.test.js +87 -87
  195. package/test/lastrunat-persist.test.js +101 -101
  196. package/test/lib-smoke.test.js +96 -96
  197. package/test/llm-audit.test.js +598 -598
  198. package/test/llm-message-source.test.js +207 -207
  199. package/test/local-embedder.test.js +292 -292
  200. package/test/maintenance.test.js +391 -391
  201. package/test/mcp-stdio.test.js +385 -385
  202. package/test/mirror-dirty.test.js +424 -424
  203. package/test/mirror-edit-digest.test.js +187 -187
  204. package/test/mirror-generation.test.js +499 -499
  205. package/test/mirror-types.test.js +208 -208
  206. package/test/mirror.test.js +249 -249
  207. package/test/narrative-bars.test.js +162 -162
  208. package/test/organize.test.js +418 -418
  209. package/test/peer-blockers.test.js +193 -193
  210. package/test/placement.test.js +36 -0
  211. package/test/policy-epoch.test.js +259 -259
  212. package/test/pre-inject-gate.test.js +434 -0
  213. package/test/quality-filter.test.js +186 -186
  214. package/test/reasoning-effort.test.js +752 -752
  215. package/test/recall-anti-update.test.js +82 -82
  216. package/test/recall-evals.test.js +253 -253
  217. package/test/recall-layer.test.js +326 -326
  218. package/test/recall-stats.test.js +189 -189
  219. package/test/receipt-chain.test.js +451 -451
  220. package/test/reflection.test.js +226 -226
  221. package/test/reindex-backfill.test.js +62 -62
  222. package/test/reranker-single-logit.test.js +73 -73
  223. package/test/reranker.test.js +197 -197
  224. package/test/resident-status.test.js +83 -83
  225. package/test/runtime-adopt.test.js +206 -206
  226. package/test/runtime-cli.test.js +89 -89
  227. package/test/runtime-closure.test.js +193 -193
  228. package/test/runtime-download.test.js +302 -302
  229. package/test/runtime-layout.test.js +163 -163
  230. package/test/runtime-loader.test.js +255 -255
  231. package/test/runtime-manifest.test.js +159 -159
  232. package/test/runtime-model-fetch.test.js +348 -348
  233. package/test/runtime-provision.test.js +101 -101
  234. package/test/runtime-tarball.test.js +81 -81
  235. package/test/runtime-verify.test.js +319 -279
  236. package/test/scope-candidates.test.js +239 -239
  237. package/test/scope-explicit.test.js +452 -452
  238. package/test/scope-inject-soft.test.js +106 -106
  239. package/test/scope-retrieval.test.js +197 -197
  240. package/test/scope-storage.test.js +255 -255
  241. package/test/scope-strict.test.js +367 -367
  242. package/test/search-fusion.test.js +90 -90
  243. package/test/semantic.test.js +124 -124
  244. package/test/sensitive-scan.test.js +207 -207
  245. package/test/serve-bin.test.js +121 -121
  246. package/test/serve.test.js +196 -196
  247. package/test/service-search.test.js +174 -174
  248. package/test/service.test.js +405 -405
  249. package/test/settings.test.js +261 -261
  250. package/test/skip-invalid-restore.test.js +262 -262
  251. package/test/sleep-action-set.test.js +422 -422
  252. package/test/sleep-heat.test.js +126 -126
  253. package/test/sleep-scheduler-rearm.test.js +89 -89
  254. package/test/sleep.test.js +527 -527
  255. package/test/standalone-api.test.js +751 -751
  256. package/test/store-vector-cache.test.js +98 -98
  257. package/test/store.test.js +396 -396
  258. package/test/stress.test.js +209 -209
  259. package/test/summarize-occurred-at.test.js +127 -0
  260. package/test/summarize-reasoning-effort.test.js +189 -189
  261. package/test/summarize.test.js +1465 -1465
  262. package/test/supersede-state-tracking.test.js +394 -0
  263. package/test/test-count-sync.test.js +60 -60
  264. package/test/tools-dto-schema-matrix.test.js +325 -325
  265. package/test/tools-memory-get-schema.test.js +65 -65
  266. package/test/tools.test.js +428 -374
  267. package/test/updated-at-semantics.test.js +113 -113
  268. package/test/vector-index.test.js +221 -221
  269. package/test/version-check.test.js +152 -152
  270. package/test/webServer-optional.test.js +82 -82
  271. package/test/write-admission.test.js +668 -668
package/src/config.js CHANGED
@@ -1,843 +1,880 @@
1
- import z from "@deepseek-ai/schemastery";
2
- import { TYPE_DECAY_DEFAULTS } from "./heat.js";
3
-
4
- export const Config = z.object({
5
- // 记忆语言:生成记忆、注入标题与后台 LLM 提示词所用语言;'zh'(默认,
6
- // 行为不变)/ 'en'。启动时读入,切换后重启生效。
7
- language: z.union([z.const("zh"), z.const("en")]).default("zh")
8
- .description("记忆语言:生成的记忆条目、注入标题与后台 LLM 提示词所用语言(zh=默认中文,en=英文)。"),
9
- memoryDir: z.string().default("~/.dsh/memory"),
10
- autoInject: z.boolean().default(true),
11
- autoSummarize: z.boolean().default(true),
12
- // Optional model override for summarization. When both are non-empty, they
13
- // take priority over the session's current model. Empty = use the session's
14
- // active provider/model (same as before).
15
- summarizeProvider: z.string().default(""),
16
- summarizeModel: z.string().default(""),
17
- // 蒸馏转录上限(字符)。借鉴 Codex「保留原始、替代压缩摘要」的思路:
18
- // 蒸馏把完整对话上下文交给 LLM 提炼,不硬裁到 8000 字就截断语义;默认
19
- // 24000 字符(约覆盖一整轮中等对话),需要更完整可调大。
20
- distillMaxChars: z.natural().min(1000).max(200000).default(24000),
21
- // autoSummarize 节流、产出上限与写入端同会话去重(Issue #127,默认全 0/off =
22
- // 零行为变化)。此前每个 turn/end 必跑、产出条数由模型输出决定、写入端只按
23
- // 「同类型 + 标题 trim 全等」去重——本机实测单日 84 次、单会话一天 198 条、
24
- // 同一事实 6 小时铸出 28+ 条。阀门交给用户按需拧,升级本身不改行为。
25
- summarizeMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
26
- summarizeMaxEntriesPerRun: z.natural().min(0).max(50).default(0),
27
- // Issue #239(成本感知级联第一级):蒸馏前的零 LLM 预判——窗口内可蒸馏文本
28
- // 不足此字符数时直接跳过 LLM 调用(0 = 关闭,行为与现状逐字节一致)。判定
29
- // 纯规则、无模型参与;被挡下的窗口照常消费游标(否则每个 turn/end 都会重新
30
- // 评估同一段短文本),并留一行 status='skipped' 审计,error_message 写明原因
31
- // ——「这一窗为什么没蒸馏」必须可观测,否则阀门等于黑盒。
32
- summarizeMinWindowChars: z.natural().min(0).max(100000).default(0),
33
- // Issue #239(有界检查点):同一会话最多发起多少次蒸馏(0 = 不限,等同现状)。
34
- // 只统计真正发起过 LLM 调用的 run——被预判挡下的窗口不占预算,否则阀门会把
35
- // 额度浪费在零成本窗口上。计数是进程内 per-session(与最小间隔闸门同生命
36
- // 周期),宿主重启即清零,与 #229 的游标持久化是两件事。
37
- summarizeMaxRunsPerSession: z.natural().min(0).max(1000).default(0),
38
- // Issue #239(第 4 项,错峰队列):高峰期蒸馏顺延。空串 = 关闭,行为与现状逐
39
- // 字节一致。取值是本地时间的时段列表(逗号分隔,支持跨零点),可带**星期前缀**
40
- // (可省;ISO 1=周一…7=周日,也认 mon..sun;省略 = 每天):
41
- // "09:00-18:00" 每天 09:00-18:00
42
- // "mon-fri 08:00-12:00,14:00-18:00" 工作日两段(按高峰计费的供应商即此形态)
43
- // "sat,sun 23:00-06:00" 周末跨零点段
44
- // 命中高峰时:不调 LLM、不消费游标(窗口继续累积,留到非高峰一次性蒸馏——批量
45
- // 比逐轮碎蒸更省),登记一行 status='skipped' / error_message='peak-hours' 审计,
46
- // 并按下面的上限择时补跑。任一写法非法则整串按「未配置」处理:排程是省钱
47
- // 手段,绝不该因为写错格式把蒸馏停掉。
48
- summarizePeakHours: z.string().default(""),
49
- // 高峰顺延上限(分钟,0 = 不设上限):到点仍处高峰就照常跑,避免整天高峰把蒸馏
50
- // 饿死。默认 120 分钟。仅在上面的时段串非空时生效。
51
- summarizePeakMaxDeferMinutes: z.natural().min(0).max(1440).default(120),
52
- // 落库前去重档位:off(默认,等同现状)/ title(零成本,仅拦完全同名)/
53
- // vector(复用已有 embedding 列做同会话语义近邻,无 LLM 调用)。
54
- summarizeDedupeMode: z.union([z.const("off"), z.const("title"), z.const("vector")]).default("off"),
55
- summarizeDedupeMinSim: z.number().min(0.5).max(0.99).default(0.92),
56
- summarizeDedupeWindowHours: z.natural().min(0).max(168).default(24),
57
- // 智能调速器(429 保护,默认开):蒸馏 LLM 调用全局串行排队,相邻请求
58
- // 间隔 distillRateLimitIntervalMs(默认 1s 一次);命中 429 限流时按
59
- // distillRateLimitBaseDelayMs 指数退避(1s→2s→4s…)自动重试
60
- // distillRateLimitRetries 次,全程对用户透明,不把 429 错误码抛给用户。
61
- distillRateLimitIntervalMs: z.natural().min(0).max(60000).default(1000),
62
- distillRateLimitRetries: z.natural().min(0).max(10).default(3),
63
- distillRateLimitBaseDelayMs: z.natural().min(100).max(60000).default(1000),
64
- maxInjectedItems: z.natural().min(1).max(20).default(5),
65
- // Issue #205:注入位跨轮轮换。同一会话里,最近 N 个不同用户查询轮次注入过
66
- // 的记忆本轮不再优先(新鲜优先、原序回填,槽位数不变);长会话中避免相邻
67
- // 轮次反复注入同一条(实测 15.5h 会话 86 次注入 93% 重复)。0 = 关闭(默认,
68
- // 保持既有行为)。会话边界自动重置:新会话从零开始。
69
- injectRotationTurns: z.natural().min(0).max(20).default(0),
70
- // Issue #164①:注入单条正文截断上限可配(原硬编码 300)。截断时尾部带
71
- // 提示:上限/原长/全文 memory_get 指引(BUDGET_EXCEEDED 原则——绝不静默)。
72
- // 默认 300 = 与既有行为一致。
73
- injectContentMaxChars: z.natural().min(60).max(4000).default(300),
74
- // Issue #34(v0.7.2 引入;v0.7.11 误删后随 #333 恢复):对话开始注入一次当前
75
- // 时间。默认关 = 与既有行为一致;格式与键名不变,存量 feature_flags 恢复即生效。
76
- injectTimePrefix: z.boolean().default(false),
77
- importanceThreshold: z.natural().min(1).max(5).default(3),
78
- // Issue #239(第 5 项):注入条数的查询自适应(默认关)。确定性强的话题收缩注入
79
- // 条数(减半、下限 1),模糊话题(回指/时间线索,或极短查询)维持
80
- // maxInjectedItems 上限——只做**单向收缩**,绝不越过用户配置的上限;判据只看
81
- // 查询本身,不做额外检索(先探针检索等于白付一次 fuseRecall)。
82
- injectUncertaintyAdaptive: z.boolean().default(false),
83
- // #249(第一批;第二批归位为注入子开关):能力说明——「怎么用记忆」的判断
84
- // 指引。落两个零注入成本的位:①`memory_search` / `memory_save` 的工具描述补
85
- // 一句判断指引(工具描述是常驻文本,不进每轮上下文);②一段 order 150 的
86
- // 系统提示段(一次性、同会话内不随轮次变化,因此不作废前缀缓存),只讲总则
87
- // (优先序、何时查、何时写、何时 no-op)。默认开=基础档:#249 §10 的判据是
88
- // 「只修正既有位、不引入新注入时机/新表面/额外 LLM 调用」的子项随父开关
89
- // 生效——「agent 不知道何时该查、何时该写」是已实测的缺口,把它默认关着是反的。
90
- // 父开关 `autoInject` 关闭时它不生效(闸门见 injectChildEnabled);用户显式写进
91
- // feature_flags 的值永远优先于这里的默认值。
92
- injectGuidanceEnabled: z.boolean().default(true),
93
- // #249 N3(压缩边缘双落点):上下文即将大幅精简前抢救「正在做什么」。默认关。
94
- // 触发靠宿主自己的压缩事件(`compaction/start|summary|end`,都在事件白名单里),
95
- // 不自定一套阈值参数——「压缩边缘」由宿主定义,我们再校准一份只会与之漂移。
96
- // 为什么是「新时机 + 新表面」因而默认关:它往对话里**追加消息**(新的注入表面,
97
- // 参照实现里最容易累积成一堆历史的那类),并多写一张提案表。按 §10 判据,引入
98
- // 新时机/新表面/新成本的子项独立成键、默认关;只修正既有块的(基础内容分池、
99
- // 库可见性行)才随父开关默认开。`pinnedInjectBudget` 与它同批,但属前者之外:
100
- // 那是既有块内的预算,不是新表面。
101
- // 双落点是硬要求,不能只留一半:宿主的压缩摘要器**只看对话里的内容**,只落库不
102
- // 注入,等于在摘要重建里什么都没留下;只注入不落库,则压缩一过就随旧消息一起
103
- // 消失。两者都做,且落库先于注入(注入失败不该丢提案)。
104
- // 祖先:`autoInject`(父关则本项不生效,闸门见 injectChildEnabled)。轻量档默认
105
- // 置关(见 LIGHT_MODE_OFF):轻量档多一份注入物是反的,与 injectGuidanceEnabled
106
- // 同一取舍。注意预设只是默认值而非强制——装配时用户显式开关在它之后展开,勾了就赢
107
- // (合并顺序见 index.js 装配处)。
108
- continuityRescueEnabled: z.boolean().default(false),
109
- // #249(第一批):B1 pin 池预算——约束/偏好类注入条目的独立小上限。约束与
110
- // 偏好被静默降级是本议题的立项核心(同类知识与情景日志同池同速率摘要,实测
111
- // 一轮压缩后仅保 53%、五轮 10%),故这两类不进相关性竞争、不参与跨轮轮换、
112
- // 逐字保真(仅受超大条目的硬顶保护,截断仍带提示)并排在块内排序之前。
113
- // 独立预算的意义:pin 不占 maxInjectedItems 名额,不会把当前任务需要的情景
114
- // 候选挤出预算(另一种「批量塞历史」)。0(默认)= 关闭,注入块构成与既有
115
- // 行为逐字节一致;超出预算的条数在块内如实标注未展示条数,绝不静默。
116
- pinnedInjectBudget: z.natural().min(0).max(5).default(0),
117
- // 编码记忆蒸馏(codingRetrospect,opt-in,默认关)。开启时,turn/end 蒸馏
118
- // 额外提取三类编码专属记忆:rejected_solution(被否决方案)/ pitfall(踩坑)/
119
- // constraint(工程约束)。蒸馏上下文为整轮完整对话(用户输入 → 助手思考/回答
120
- // → 工具调用与结果 → 代码执行),不再只看用户消息,便于提炼踩坑根因。
121
- // 关闭时行为与之前完全一致。
122
- codingRetrospect: z.boolean().default(false),
123
- // 编码任务识别词表(读取侧门控用):命中即视为编码类任务,编码记忆才注入。
124
- codingKeywords: z.array(z.string()).default([
125
- "代码", "编码", "写一个", "写个", "实现", "函数", "方法", "类",
126
- "接口", "bug", "调试", "报错", "错误", "异常", "堆栈", "脚本",
127
- "python", "javascript", "typescript", "node", "js", "ts",
128
- "sql", "sqlite", "数据库", "算法", "重构", "优化", "性能",
129
- "测试", "单测", "修复", "补丁", "依赖", "npm", "pip",
130
- "命令行", "shell", "配置", "配置文件", "yaml", "json",
131
- "插件", "开发", "编译", "构建", "部署", "git", "commit",
132
- "review", "前端", "后端", "页面", "组件", "dsh", "memos"
133
- ]),
134
- // 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
135
- // 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
136
- codingBoostFactor: z.number().min(1).max(5).default(2),
137
- // 注入边界花括号转义(issue #162,v0.7.4 曾修后被 v0.7.11 重构误删):默认开启,
138
- // 在 memory / 短期上下文 / 用户设置三处注入出口对连续 2+ 个花括号插 `\`
139
- // (`{{a}}`→`{\{a\}\}`),让 DSH 的 interpolate() 不再扫到非法变量名而 throw。
140
- // 关闭时按原样透传。
141
- escapePromptVariables: z.boolean().default(true),
142
- autoDream: z.boolean().default(true),
143
- dreamThresholdCount: z.natural().min(1).max(1000).default(10),
144
- dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
145
- dreamDelayMs: z.natural().min(0).max(60000).default(2000),
146
- // autoDream 触发最小间隔(分钟,0 = 不限制,Issue #89 请求 2):高频写入
147
- // 场景下防止巩固调用(含失败重试)连发刷爆配额。间隔从每次实际开跑时刻
148
- // 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次
149
- // 写入事件会重新评估。
150
- dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
151
- // Issue #292(#135 派生):autoDream 连续失败退避(opt-in,默认关 = 行为与
152
- // 现状逐字节一致)。开启后调度器对连续失败做指数退避:有效最小间隔 =
153
- // dreamMinIntervalMinutes × 2^连续失败数(成功一次清零恢复),封顶 30 分钟。
154
- // #89 的最小间隔闸失败 run 也占用,但间隔恒定——恒定失败的模型(#135 空体
155
- // 面)会按固定节奏连发刷爆配额;退避把下次重试按失败次数指数推远。基数取
156
- // dreamMinIntervalMinutes:基数为 0 时无闸可翻倍,本键不自己产生间隔(先配
157
- // dreamMinIntervalMinutes 再开本键)。与 dreamPeakHours / dreamMinIntervalMinutes
158
- // 同族(节流阀,不新增任何 LLM 调用),故不进 LIGHT_MODE_OFF。
159
- autoDreamFailureBackoff: z.boolean().default(false),
160
- // Issue #239(第 4 项,错峰队列)镜像到巩固:高峰期不做梦。与
161
- // summarizePeakHours 同一份时段语法(复用 src/summarize.js 的 parsePeakSpec /
162
- // isInPeakWindow / nextOffPeakAt,不另写解析器):逗号分隔、可带星期前缀、支持
163
- // 跨零点。空串 = 关闭,行为与现状逐字节一致。
164
- // "09:00-18:00" 每天 09:00-18:00
165
- // "mon-fri 08:00-12:00,14:00-18:00" 工作日两段(按高峰计费的供应商即此形态)
166
- // 为什么巩固比蒸馏更该有这道闸:单次巩固的输入是整窗快照(dreamMaxSnapshotSize
167
- // 条),实测一次 run 的 LLM 时长可达数分钟量级,撞上高峰时既贵又慢;而它由写入
168
- // 事件触发、没有天然的「等到空闲再跑」路径。命中高峰时:不调 LLM、不刷新
169
- // baseline(阈值继续累积,留到非高峰一次性巩固),登记一行 status='skipped' /
170
- // error_message='peak-hours' 审计,并按下面的上限择时补跑。任一写法非法则整串
171
- // 按「未配置」处理——排程是省钱手段,绝不该因为写错格式把巩固停掉。
172
- dreamPeakHours: z.string().default(""),
173
- // 高峰顺延上限(分钟,0 = 不设上限):到点仍处高峰就照常跑,避免整天高峰把巩固
174
- // 饿死。默认 120,与 summarizePeakMaxDeferMinutes 对齐。仅在时段串非空时生效。
175
- dreamPeakMaxDeferMinutes: z.natural().min(0).max(1440).default(120),
176
- // 巩固模型路由(settings panel「巩固模型」/ dreamProvider+dreamModel):
177
- // dream 的记忆沉淀专用 LLM 路由,显式配置优先于 agent 默认模型(config-first,
178
- // Issue #25)。模型分类声明:
179
- // - 非思考模型(推荐,如 glm-5-2 类):无 reasoning 声明,effort 请求被 harness
180
- // 拒绝后 withEffortFallback 去掉字段重试即成功;空体/no json array 风险最低。
181
- // - 思考模型(如 deepseek-v4-flash-ga 等 v4-flash-ga 系):默认开推理,可能烧光
182
- // token 预算返回空体;且部分(如 v4-flash-ga)在 harness 侧被声明为不接受任何
183
- // reasoning effort —— 即使去掉 effort 重试,harness 的 defaultEffort 也会顶上来
184
- // 再次拒绝(UNSUPPORTED_REASONING_EFFORT),插件 fallback 无法绕开。
185
- // 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
186
- dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
187
- dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
188
- // Issue #135 建议 3:#9 只抬了 max(8192→131072),default 停在 32768。思考型
189
- // 模型的推理与正文共享该预算,effort 未压低时 32768 恰好被推理烧光 → 空体
190
- // ("no json array in llm output",报告实测 146 轮因此失败)。默认抬到上限,
191
- // 让默认配置也留足正文预算;流式计费按实际用量,不按上限。
192
- dreamMaxTokens: z.natural().min(256).max(131072).default(131072),
193
- // Pass-through reasoning effort for dream's LLM calls (Issue #135 建议 4/5).
194
- // Unset (no value) resolves to the LOWEST effort the model declares before
195
- // streaming: background consolidation must produce JSON, and omitting the
196
- // field lets the harness substitute the model's defaultEffort (thinking-type
197
- // models often default to high/max) — the reasoning then drains the whole
198
- // token budget and the run dies with an empty body. Explicit 'none' still
199
- // omits the field (provider default applies); off/low/medium/high are
200
- // forwarded verbatim, unsupported values are remapped (v0.7.26+).
201
- // Caveat: on some thinking models (e.g. v4-flash-ga) the harness declares NO
202
- // supported effort, so even the fallback retry (field stripped) is rejected
203
- // again via its defaultEffort — prefer a non-reasoning dreamProvider/dreamModel.
204
- dreamReasoningEffort: z.union([
205
- z.const("off"),
206
- z.const("low"),
207
- z.const("medium"),
208
- z.const("high"),
209
- z.const("none")
210
- ]).description("巩固模型的推理档位:未配置 = 自动取模型支持的最低档(避免思考模型用自带默认档烧光预算返回空体,#135);显式 'none' = 不发送字段、用服务商自带默认;off/low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
211
- // 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
212
- // 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
213
- // 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
214
- dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
215
- // Issue #104 方向 2:单轮 archive 决策上限。一次性大扫除(12 条互不相关主题
216
- // 被批量归档,其中 8 条疑似误伤长保留类型)是失控信号,与 update 上限同类的
217
- // 全局闸门:skipInvalid 也不豁免,超限整单拒绝。正常清理可调高。
218
- dreamMaxArchivePerRun: z.natural().min(1).max(200).default(8),
219
- // Issue #125:候选集构造方式。"window"(默认,等同现状)只取最近
220
- // dreamMaxSnapshotSize 条;"hybrid" 在此基础上并入**向量翻出的高相似组**——
221
- // 本机实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口(窗口覆盖率 11.7%),
222
- // 纯时间窗口让"该合并的一对"几乎永远碰不到面。候选总量仍由下面的上限封顶、
223
- // 与库总量解耦,这是相对"调大窗口"的核心收益:输入成本不随库增长。
224
- dreamCandidateMode: z.union([
225
- z.const("window"),
226
- z.const("hybrid")
227
- ]).default("window"),
228
- // hybrid 的候选总量上限;0 = 复用 dreamMaxSnapshotSize(不迁移,需要时显式覆盖)。
229
- dreamCandidateMax: z.natural().min(0).max(5000).default(0),
230
- // Issue #258:总览(dream_summarize)独立路由。consolidate 有 dreamMaxSnapshotSize
231
- // 窗口、总览原为全库无界——两者对 ctx 的需求差 4 倍以上却强制共用 dream 路由,
232
- // dreamProvider 指向小 ctx 模型时总览当场 CONTEXT_WINDOW_EXCEEDED(实测 120,969
233
- // tokens > 32,768),指向大 ctx 模型则 consolidate 的卸载收益归零。未配置 = 回落
234
- // dream 路由,行为逐字节不变。
235
- dreamSummaryProvider: z.string().default("").description("记忆总览(dream_summarize)专用模型服务商,留空用巩固模型。总览输入为全库(或 dreamSummaryMaxInputs 上限),ctx 需求远大于 consolidation,建议大 ctx 模型(issue #258)。"),
236
- dreamSummaryModel: z.string().default("").description("记忆总览(dream_summarize)专用模型,留空用巩固模型;总览输入随库增长,建议大 ctx 非思考模型(issue #258)。"),
237
- // Issue #258:总览输入条数硬上限。0 = 不设上限(历史行为,库增长可能撑爆小
238
- // ctx 模型);>0 时按 updated_at 倒序保留最新的 N 条(与 consolidate 窗口同一
239
- // 排序口径)。总览是常驻叙述而非逐条巩固,限输入只影响口径脚注里的条数。
240
- dreamSummaryMaxInputs: z.natural().min(0).max(100000).default(0),
241
- // hybrid 判"高相似"的阈值;0.85 与 sleep normal 档对齐——两个模块对"高相似"
242
- // 保持同一个定义。
243
- dreamCandidateMinSim: z.number().min(0.5).max(0.99).default(0.85),
244
- // 叙述条(#164 对齐,opt-in):dream 期间按共享 tag 主题聚类(dream/
245
- // narratives.js),每簇 LLM 合成一条叙述落库(source=narrative、evidence
246
- // 回链簇内原子记忆、求交防捏造)。按需检索、不常驻注入。默认关=行为与
247
- // 此前一致;lightMode 强制关闭。
248
- dreamNarrativeEnabled: z.boolean().default(false),
249
- // 成簇门槛:共享同一 tag 的记忆 ≥ 此值才合成叙述条。
250
- dreamNarrativeMinCluster: z.natural().min(2).max(20).default(3),
251
- // document 型记忆(#164/#230,opt-in):agent 产长文档的指针行——注册校验
252
- // (文件存在 + 路径合法 + evidence 求交)、摘要 + doc_path 落库、C2 比对
253
- // 去重、supersede 记账。全文归 agent,管线零触碰;默认关=行为与此前一致;
254
- // lightMode 强制关闭。
255
- documentMemoryEnabled: z.boolean().default(false),
256
- // document 摘要行的注入预算(#230 拍板):次优先档内最多注入的 document
257
- // 行数,超预算跳过由后续候选补位。只约束注入,不约束检索。
258
- documentInjectBudget: z.natural().min(1).max(5).default(2),
259
- // document 的 managed 落盘目录(#296 第二批):空串 = 跟随 memoryDir 的
260
- // `<memoryDir>/documents/`。`~` / `~/` / `~\` 展开到 home,绝对路径原样用,相对
261
- // 路径落在 memoryDir 下(这是本键自己的规则:memoryDir 的相对路径是原样留着的
262
- // cwd 语义,两者刻意不同;`~user` 形式两边都不展开)。目录由 mneme 建,里面的
263
- // `index.md`
264
- // 整文件机器所有。这个目录**之外**的文件只登记指针行,正文一个字节都不碰
265
- // (= #230 的「管线对正文零读零写」);与 memoryDir 一样是路径配置,不是
266
- // 行为开关,故不进 settings.js 的 feature flags 白名单、也不上面板。
267
- documentDir: z.string().default(""),
268
- // 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
269
- // 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
270
- // (未覆盖即拒绝整单)。
271
- dreamImplicitKeep: z.boolean().default(true),
272
- // 显式决策覆盖率下限(v0.4.4 fix):dreamImplicitKeep 开启时,LLM 输出被
273
- // 截断只显式 claim 少量 snapshot 记忆(claimed.size / snapshot.size < 该阈值)
274
- // → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
275
- // 默认 0.5(至少显式覆盖一半 snapshot)。
276
- dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
277
- // Issue #89:v0.6.9(Issue #26)的宽容路径回归。默认跳过单条非法决策
278
- // (未知 id / 跨类型合并等)、应用合法子集、run 记 degraded;设 false 恢复
279
- // 整单拒绝的严格模式。全局上限与覆盖率下限不受此开关影响、始终整单拒绝。
280
- dreamSkipInvalid: z.boolean().default(true),
281
- // 显式开启后放宽跨类型合并检查(类型边界由用户自行承担);配合
282
- // dreamSkipInvalid 理解:关闭 skipInvalid 时跨类型 merge 直接整单拒绝。
283
- allowCrossTypeMerge: z.boolean().default(false),
284
- // issue #339 / E8 考卷:merge 护栏(opt-in,默认关 = 现状)。开启后合并对象
285
- // 命中长保留类型(preference/pattern/rejected_solution/constraint/pitfall,
286
- // 与 archive 护栏同一张表)的 merge 决策整条跳过——E8 实测巩固损耗里
287
- // 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉,而 archive 护栏只挡
288
- // archive 不挡 merge,「更精炼的摘要」恰是约束失真的主通道。skipInvalid
289
- // (默认开)时被跳条目进 dream_runs.skipped、run 记 degraded;关闭
290
- // skipInvalid 时整单拒绝。与 allowCrossTypeMerge 同用时长 guard 先判。
291
- dreamMergeGuard: z.boolean().default(false),
292
- // Rule version for dream adjudication: when this bumps, older dream_runs
293
- // degrade to historical evidence (their receipts no longer drive live
294
- // decisions). Default 0 = no versioning in use yet.
295
- policyEpoch: z.natural().min(0).max(1000000).default(0),
296
-
297
- // --- API protection ------------------------------------------------------
298
- // Optional shared token for the plugin's HTTP API. Empty (default) keeps
299
- // the API open (DSH binds to 127.0.0.1 and has no built-in auth); when set,
300
- // sensitive endpoints (vector-config, vector-reindex, and all write ops on
301
- // profile/rules/commands) require `Authorization: Bearer <apiToken>` (or
302
- // `X-DSH-Mneme-Token`). Read-only list/search/semantic stay open so the
303
- // Web panel keeps working without the token.
304
- apiToken: z.string(),
305
-
306
- // --- semantic: local embedding provider (v0.2) --------------------------
307
- // "openai" keeps the legacy external-API path (settings vector config);
308
- // "local" runs an ONNX model in-process; "ollama" calls a local Ollama.
309
- embedProvider: z.union([z.const("openai"), z.const("local"), z.const("ollama")]).default("openai"),
310
-
311
- // Local ONNX embedder (transformers.js / onnxruntime).
312
- localEmbedModel: z.string().default("Xenova/bge-small-zh-v1.5"),
313
- localEmbedDimension: z.natural().default(512),
314
- localEmbedDevice: z.union([z.const("cpu"), z.const("gpu")]).default("cpu"),
315
- localEmbedBatchSize: z.natural().min(1).max(64).default(8),
316
- // 本地嵌入的池化方式(**bug 修复,不是新能力**):BGE 系是按 CLS 池化训练的——
317
- // 模型自带的 `1_Pooling/config.json` 明确写着 `pooling_mode_cls_token: true` /
318
- // `pooling_mode_mean_tokens: false`,官方 README 也是「select the last hidden state of
319
- // the first token」+ L2 normalize;而 transformers.js 的 `feature-extraction` 默认 mean。
320
- // ⇒ 此前本地嵌入对 BGE 系一直用错池化:**不报错、只是向量系统性偏差**,检索质量静默受损。
321
- // 'auto' = 按模型族判定(BGE → cls,其余 → mean,未受影响的模型行为不变);也可显式钉住。
322
- // ⚠️ 池化决定向量空间,改它会改变 modelHash(见 local-embedder.js),既有索引会被判失配并重建。
323
- localEmbedPooling: z.union([
324
- z.const("auto"),
325
- z.const("cls"),
326
- z.const("mean")
327
- ]).default("auto"),
328
-
329
- // Ollama embedder.
330
- ollamaBaseUrl: z.string().default("http://localhost:11434"),
331
- ollamaModel: z.string().default("nomic-embed-text"),
332
-
333
- // Model download/cache. When empty (default), models are cached under the
334
- // user-level path ~/.dsh/mneme/models (resolved in local-embedder/reranker);
335
- // a non-empty value is used verbatim.
336
- embedModelCacheDir: z.string().default(""),
337
- embedModelMirror: z.string().default("https://hf-mirror.com"),
338
-
339
- // issue #194:模型文件下载的断点续传与重试。开启时 transformers.js 的 env.fetch 被
340
- // 替换为带断点续传的实现(已收字节落盘、Range/If-Range 续传、空闲看门狗),大文件
341
- // 中断后不必整份重来;关闭 = env.fetch 保持原样(线上回滚开关)。
342
- resilientModelDownload: z.boolean().default(true),
343
-
344
- // 自管运行时目录(issue #131)。空 = ~/.dsh/mneme/runtime:里面放收编或下载来的
345
- // transformers + onnxruntime 闭包,插件经 src/runtime/loader.js 的三层解析加载它。
346
- // 目的就是让这条重依赖不必留在宿主 profile 的依赖图里——profile 是所有插件共用的
347
- // 依赖图,留在里面会让「装任何插件都要替它重走一遍这条链」。
348
- runtimeDir: z.string().default(""),
349
-
350
- // 运行时取件的两个来源(issue #131 / PR-C)。都是「克制」的默认:留空 = 不额外改变行为。
351
- // - runtimeTarballDir:本地 .tgz 目录。某个包网络下不到时(例如 onnxruntime-node),
352
- // 把 `npm pack <pkg>` 出来的 tgz 丢进这个目录即可离线取件;有它就优先于联网。
353
- // - runtimeMirror:registry 镜像前缀,例如 https://npmmirror.com/mirrors/npm/ 。
354
- // 留空则用 runtime-manifest.json 里写死的官方地址(那是随包发布的固定清单)。
355
- runtimeTarballDir: z.string().default(""),
356
- runtimeMirror: z.string().default(""),
357
-
358
- // Vector search tuning.
359
- vectorSearchTopK: z.natural().min(1).max(100).default(20),
360
- vectorSearchThreshold: z.number().min(0).max(1).default(0.65),
361
- hybridSearchVectorWeight: z.number().min(0).max(1).default(0.6),
362
- hybridSearchKeywordWeight: z.number().min(0).max(1).default(0.4),
363
- // Lazy auto-backfill of missing embeddings on boot (Bug2): when the vector
364
- // API is configured and rows still lack an embedding, the index is rebuilt
365
- // in the background after a short delay, rate-limited in batches. On by
366
- // default; set false to keep the backfill manual only.
367
- autoReindexOnBoot: z.boolean().default(true),
368
- // Semantic-first injection (Bug4): when enabled, injectCandidates with a
369
- // non-empty query recalls via the vector index first and falls back to the
370
- // rule-based pick to fill/dedupe. Empty query / no vector → legacy behavior.
371
- hybridInject: z.boolean().default(true),
372
-
373
- // --- recall optimization (v0.5.0) ----------------------------------------
374
- // BM25 third recall path beside vector + LIKE keyword (1.1): per-token IDF
375
- // scoring recalls rows whose query terms are scattered — identifiers, code
376
- // fragments, mixed CJK/ASCII — where substring LIKE cannot match.
377
- bm25SearchEnabled: z.boolean().default(true),
378
- // Query-aware vector cutoff (1.2) replacing the fixed 0.65: entity:/attr:
379
- // prefixes loosen to 0.5, short queries tighten to 0.7, long queries loosen
380
- // to 0.6, and a decisive top-1/top-5 score gap loosens to 0.5 so the tail
381
- // still reaches the reranker. Off = legacy fixed threshold behavior.
382
- adaptiveThresholdEnabled: z.boolean().default(true),
383
- // Session-scoped hot memory (1.3): the latest N dialogue rounds rendered
384
- // ahead of the long-term recall block — short-term context that never
385
- // enters the memory store.
386
- hotMemoryEnabled: z.boolean().default(true),
387
- hotMemoryRounds: z.natural().min(1).max(50).default(5),
388
- hotMemoryMaxTokens: z.natural().min(200).max(32000).default(2000),
389
- // Topic-ranked injection (2.2): when a query vector is available the whole
390
- // injection candidate list is re-ordered by similarity to the current
391
- // query instead of keeping the rule-based order.
392
- selectiveInjectEnabled: z.boolean().default(true),
393
- // Search-time semantic dedup (2.3): greedy pass over the merged candidate
394
- // list dropping rows whose embedding cosine-similarity to an already-kept
395
- // row exceeds the threshold — duplicates are filtered at recall time
396
- // instead of waiting for a dream consolidation. Opt-in aggressive mode:
397
- // small embedding models can collapse legitimately distinct rows, so the
398
- // default keeps every recalled row.
399
- searchSemanticDedup: z.boolean().default(false),
400
- searchSemanticDedupThreshold: z.number().min(0.5).max(1).default(0.95),
401
-
402
- // Recall fusion recipe (plan #1): how the keyword/vector/BM25 ranked lists
403
- // are combined into the final ranking. `blend` (default) is the legacy
404
- // behavior — weighted sum for vector/hybrid, union backfill for auto —
405
- // unchanged. `rrf` (Reciprocal Rank Fusion) and `minmax` (min-max normalized
406
- // weighted sum) are rank/scale-aware recipes that fix the unit mismatch the
407
- // issue describes (raw cosine vs keyword score vs normalized IDF are added
408
- // directly). Off by default so existing behavior holds exactly.
409
- recallFusion: z.union([z.const("blend"), z.const("rrf"), z.const("minmax")]).default("blend"),
410
- // Attach a `signals` object { keyword, vector, bm25, final } to each search
411
- // result for transparency/debugging (plan #2). Default off; when on it only
412
- // decorates the returned rows, never changes the ranking.
413
- signalTransparency: z.boolean().default(false),
414
-
415
- // --- semantic: rerank layer (v0.2) --------------------------------------
416
- // Opt-in by default (item ⑥): the local cross-encoder pulls in onnxruntime
417
- // (transformers.js) at init, so a bare install must not load it. Only an
418
- // explicit rerankEnabled=true + rerankProvider="local" constructs LocalReranker.
419
- rerankEnabled: z.boolean().default(false),
420
- rerankProvider: z.union([z.const("local"), z.const("none")]).default("none"),
421
- rerankModel: z.string().default("Xenova/bge-reranker-base"),
422
- rerankBatchSize: z.natural().min(1).max(64).default(8),
423
- rerankMaxCandidates: z.natural().min(5).max(100).default(30),
424
- rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
425
- // #188:量化档,与嵌入层的 useDtype 同语义(q8 = model_quantized.onnx,约为
426
- // fp32 体积的 1/4)。此前重排层不传 dtype,即使缓存里已有量化文件也会去下载
427
- // 1GB 级的 model.onnx。非法值由 transformers 在 init 时抛出 → 重排降级并告警。
428
- rerankDtype: z.string().default("q8"),
429
-
430
- // --- reflection: update decision + failure tracking (v0.2.1) ------------
431
- reflectionUpdateEnabled: z.boolean().default(true),
432
- reflectionFailureTracking: z.boolean().default(true),
433
- reflectionUpdateMaxPerRun: z.natural().min(0).max(5).default(2),
434
- reflectionUpdateMinAgeHours: z.natural().min(0).max(168).default(24),
435
-
436
- // --- conflict freeze: manual review for conflicting memories (v0.2.1) ---
437
- // Opt-in by default: when true, conflicting memories are not auto-merged
438
- // and are marked as pending manual review instead.
439
- conflictFreezeEnabled: z.boolean().default(false),
440
- // Maximum number of frozen conflicts to keep pending for manual review.
441
- conflictFreezeMaxPending: z.natural().min(1).max(1000).default(100),
442
-
443
- // --- entity gene (v0.3.0) -----------------------------------------------
444
- // Opt-in: when false (default) nothing in the pipeline extracts entities.
445
- // The storage layer (entities/entity_attrs/entity_relations tables + CRUD)
446
- // is always available regardless of this flag.
447
- entityExtractionEnabled: z.boolean().default(false),
448
- // Optional provider override for entity extraction; empty = use the caller's
449
- // default provider/model. Combined with entityExtractionModel — provider
450
- // without model (or vice versa) falls through to the caller default.
451
- entityExtractionProvider: z.string().default(""),
452
- // Optional model override for entity extraction; empty = use the caller's
453
- // default provider/model.
454
- entityExtractionModel: z.string().default(""),
455
- // Reasoning effort for entity extraction (issue #109), mirrors
456
- // dreamReasoningEffort pass-through: 'none' omits the field / provider
457
- // default; off/low/medium/high passed through. A provider that rejects the
458
- // effort retries once without it, so opting in is safe to experiment with.
459
- entityExtractionReasoning: z.union([
460
- z.const("low"),
461
- z.const("medium"),
462
- z.const("high"),
463
- z.const("none")
464
- ]).default("none"),
465
- // Cap on entities per extraction pass and attributes per entity.
466
- entityExtractionMaxEntities: z.natural().min(1).max(20).default(10),
467
- entityExtractionMaxAttrs: z.natural().min(1).max(50).default(20),
468
- // Prefix/semantic search over entity names (used by recall).
469
- entitySearchEnabled: z.boolean().default(true),
470
- // 图召回轴(issue #219):查询文本命中实体名时,把该实体挂联的记忆并入
471
- // 检索融合池(与 BM25 同级的确认/回填信号)。默认关=检索行为与 #219 前
472
- // 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
473
- entityRecallEnabled: z.boolean().default(false),
474
- // 边权重演化(issue #24 块2):touch 门控是否把被寻回路径的边权重抬升。
475
- // 默认关=边初值纯由建边来源决定(store.RELATION_SOURCE_DEFAULTS),演化
476
- // 不生效;开=被激活的边经 bumpRelationWeight 上涨(只加不减、封顶 1.0)。
477
- graphWeightEnabled: z.boolean().default(false),
478
- // 单次有效触达的抬升幅度。
479
- graphWeightDelta: z.number().min(0).max(1).default(0.1),
480
- // 被动确认(issue #24 块4):heat 触达(记忆被召回/注入=被用)即对该记忆
481
- // 挂联的关系边做被动确认——bumpRelationWeight 抬一格。默认关=零行为变化;
482
- // 开=正常触达演化边权,「关系是用出来的」闭环落地(异常路径复核留后续块)。
483
- graphPassiveConfirm: z.boolean().default(false),
484
-
485
- // --- 图谱锚定层(issue #24 · 块1 Activation Anchoring)-------------------
486
- // entityRecallEnabled 之上再开一层:以查询命中的实体为种子,沿实体关系表
487
- // 级联扩散(默认深度 2),把「邻居实体挂联的记忆」也带进融合池。种子集
488
- // 上限 graphSeedCap、级联深度 graphCascadeDepth 一并下发到 anchorSeeds /
489
- // cascadeDepths(见 src/graph/anchoring.js)。默认关=行为与 #219 单跳轴
490
- // 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
491
- graphAnchoringEnabled: z.boolean().default(false),
492
- graphSeedCap: z.natural().min(1).max(30).default(12),
493
- graphCascadeDepth: z.natural().min(1).max(3).default(2),
494
-
495
- // 关联提示防幻觉(issue #24 块3):图召回的候选(查询命中实体的挂联记忆)
496
- // 进注入时默认只参与排序、不改变注入块构成(graphInjectHint=false 保守档);
497
- // 开启后注入文本里以固定句式「[检索线索] …」标出,引导语说明这是链路信息
498
- // 非事实断言,防 LLM 单次消费把桥接召回当有依据的事实回头扩散。
499
- graphInjectHint: z.boolean().default(false),
500
- // 图召回候选在注入块里的独立预算(条数上限)。与 pin/document 预算同构:
501
- // 不占 maxInjectedItems 名额、只约束「桥接线索」这类低信任信息的常驻占比。
502
- graphInjectBudget: z.natural().min(0).max(5).default(1),
503
-
504
- // --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
505
- // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
506
- // sleep fires when the store has been quiet for sleepIdleMinutes and deep-
507
- // maintains the whole library: conflict resolution, archival demotion,
508
- // pattern discovery and entity relation completion. Abortable on user
509
- // activity, audited into dream_runs (run_type='sleep'), and serialized with
510
- // autoDream so the two never overlap.
511
- sleepModeEnabled: z.boolean().default(false),
512
- // Quiet window before a cycle fires (minutes).
513
- sleepIdleMinutes: z.natural().min(1).max(60).default(5),
514
- // Minimum gap between two sleep runs (hours) — a second idle window within
515
- // this interval does not retrigger.
516
- sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
517
- // Conflict adjudication strictness:
518
- // gentle only high-confidence conflicts (threshold 0.92) are resolved
519
- // normal standard dream-level (threshold 0.85)
520
- // aggressive low-confidence pairs are also adjudicated (threshold 0.75)
521
- sleepConflictStrictness: z.union([
522
- z.const("gentle"),
523
- z.const("normal"),
524
- z.const("aggressive")
525
- ]).default("normal"),
526
- // Issue #126:冲突阶段的动作集。"conflict"(默认 = 现状)只用 conflict/keep——
527
- // 后台自动流程不静默扩张行为;"full" 开放六分支(merge / update / supersede /
528
- // differentiate / conflict / keep),互补型与演进型重复因此有了正确出口,不再
529
- // 被迫"输赢化"(实测抽样 10 对里 2 对属演进型/互补型,用 conflict 处理会丢信息)。
530
- sleepActionSet: z.union([
531
- z.const("conflict"),
532
- z.const("full")
533
- ]).default("conflict"),
534
- // Archival demotion tiering (days since last access):
535
- // >= sleepArchiveDays → shrink to summary, full body kept in _full_content
536
- // >= sleepCompressDays → archived outright (entity relations preserved)
537
- sleepArchiveDays: z.natural().min(7).max(365).default(30),
538
- sleepCompressDays: z.natural().min(7).max(365).default(90),
539
- // Pattern discovery scan window (most recent memories to scan).
540
- sleepPatternMinMemories: z.natural().min(10).max(1000).default(100),
541
- // How far back pattern discovery considers entity attr changes (days).
542
- sleepPatternLookbackDays: z.natural().min(1).max(90).default(30),
543
- // Max pattern memories minted per run (0 = disabled).
544
- sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
545
- // Optional LLM route override for sleep's bulk passes (empty = use dream
546
- // route / agent default model).
547
- sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
548
- sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
549
- // Pass-through reasoning effort for sleep's LLM passes, same semantics as
550
- // dreamReasoningEffort (Issue #135 建议 4/5): unset resolves to the lowest
551
- // effort the model declares; explicit 'none' omits the field; off/low/
552
- // medium/high are forwarded verbatim.
553
- sleepReasoningEffort: z.union([
554
- z.const("off"),
555
- z.const("low"),
556
- z.const("medium"),
557
- z.const("high"),
558
- z.const("none")
559
- ]).description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,未配置 = 自动取模型支持的最低档;显式 'none' = 不发送字段、用服务商自带默认。"),
560
- // Pass-through reasoning effort for the distill (summarize) LLM call
561
- // (issue #315). Mirrors entityExtractionReasoning rather than dream: the
562
- // default 'none' omits the field entirely (provider default), so current
563
- // behavior is unchanged until the user opts in — no auto-lowest resolution
564
- // here, distill failures are retried at the window level anyway. off/low/
565
- // medium/high are forwarded verbatim; a provider that rejects the effort
566
- // retries once without it (withEffortFallback), so opting in is safe to
567
- // experiment with. The failure being addressed is the #9 shape on the
568
- // distill path: a thinking model drains the output budget on reasoning and
569
- // the summary comes back empty/truncated.
570
- summarizeReasoningEffort: z.union([
571
- z.const("off"),
572
- z.const("low"),
573
- z.const("medium"),
574
- z.const("high"),
575
- z.const("none")
576
- ]).default("none").description("蒸馏(会话总结提炼)LLM 的推理档位:默认 'none' = 不发送字段、用服务商自带默认;off/low/medium/high 原样传递,被模型拒收时自动去掉字段重试一次(与巩固/睡眠同款降级,#315)。思考型模型建议 off/low,避免推理烧光输出预算。"),
577
- // Issue #257:sleep 冲突/模式两阶段的输出预算(原硬编码 2048)。实测默认档
578
- // 每对裁决约 90 token、24 对 2097——2048 恰好压在边界(53 次运行 48 败);
579
- // full 档六分支实测约 290 token/对、24 对 6967,2048 必然截断。默认 8192
580
- // 覆盖实测峰值(候选对按「每记忆至多一对」去重,饱和在 ~25 对、不随库无限
581
- // 增长);流式计费按实际用量,不按上限。
582
- sleepMaxTokens: z.natural().min(256).max(131072).default(8192).description("sleep 冲突消解与模式发现阶段的 LLM 输出预算上限(token)。原为硬编码 2048,sleepActionSet=full 实测需约 7000 导致裁决被截断而整轮失败;流式计费按实际用量,调大不增加成本。"),
583
-
584
- // --- epistemic trust: memory source credibility (v0.4.5) -----------------
585
- // Distinguish memories by source: observation (measured / witnessed),
586
- // subjective (opinion / guess) and inferred (derived from other evidence).
587
- // Opt-in by default: when false (default) retrieval ranking, injection
588
- // marking and dream merge/conflict keepSource are untouched and
589
- // epistemic_status stays inert data (still written + inferred on save, just
590
- // never used to influence behavior).
591
- trustEpistemicWeighting: z.boolean().default(false),
592
-
593
- // --- memory quality filter (Bug7) ------------------------------------------
594
- // Heuristic gate on what deserves the injection/recall surface. When enabled,
595
- // saveWithDedupe scores each new memory after dedupe and before write:
596
- // score >= degradeThreshold (60) → stored normally
597
- // archiveThreshold (30) <= score < 60 → quality_score persisted and the
598
- // injection sort re-ranks by importance * quality_score/100 (degraded)
599
- // score < 30 → archived + tagged low_quality (still explicitly searchable)
600
- // Meta-memory markers, near-duplicates and repetitive filler lose points.
601
- memoryQualityFilter: z.object({
602
- enabled: z.boolean().default(true),
603
- archiveThreshold: z.natural().min(1).max(100).default(30),
604
- degradeThreshold: z.natural().min(1).max(100).default(60),
605
- minContentLength: z.natural().min(1).max(1000).default(10),
606
- // Issue #135 附属发现 1:importance ≥ 该值的记忆不参与静默自动归档——评分
607
- // 与信号标签照常写入(可观测)、注入排序照常降权,但 setArchived 跳过。
608
- // 报告实测 150 条低分归档里 128 条 importance ≥ 4(51 条 = 5)。设 1 = 全部
609
- // 豁免(等效关闭自动归档),设 5 = 仅最重要的豁免。
610
- exemptImportance: z.natural().min(1).max(5).default(4)
611
- }).default({}),
612
-
613
- // --- LLM audit trail (Bug8) ------------------------------------------------
614
- // Records every background LLM call into llm_audit_logs: tokens, duration,
615
- // status and which trigger produced it. Failures are recorded as
616
- // status='error' and never block the feature. retentionDays bounds the table:
617
- // older rows are purged on boot.
618
- //
619
- // Covered trigger_source values: autoDream (consolidation + summary),
620
- // autoSummarize (compression), sleep (conflict + pattern) and entityExtract
621
- // (issue #250 — the last two were structurally missing the hook, not a
622
- // deliberate narrowing). entityExtract runs on every memory write, so row
623
- // growth scales with write volume; retentionDays caps the ceiling but not the
624
- // rate. If that turns out too fast, sample by operation_type or give it a
625
- // separate retention — do not add a second gate: this flag is the single one.
626
- llmAudit: z.object({
627
- enabled: z.boolean().default(true),
628
- retentionDays: z.natural().min(1).max(3650).default(90)
629
- }).default({}),
630
-
631
- // --- write admission (#254) ------------------------------------------------
632
- // 写入准入:把「这条该不该进库」前移到 LLM 之前。与 memoryQualityFilter 的分工
633
- // 是时点不是判据——那个是**写入后**打分归档(扣分可以错,只影响排序与归档),
634
- // 这个是**写入前**判定(硬拒不能错)。
635
- //
636
- // 两个开关都默认关(验收第 1 条「默认关零行为变化」):
637
- // enabled — 跑第 1 级确定性判据。关时 evaluate 的返回与只计量那一阶段逐字段
638
- // 一致,写入路径完全不碰。
639
- // enforce — 命中时真的拒绝。关时判据照跑、审计照留,决策回落 allow(仅告警)。
640
- // 读审计时「metadata.deny 非空且 metadata.decision=allow」就是这一档的指纹。
641
- // 分层是为了能把「先观察」和「开始拦」分成两次上线:enforce 打开前先在真实流量
642
- // 里看误杀面,而不是靠回滚开关当保险。
643
- //
644
- // 第 1 级的判据面刻意收窄到两类:密钥/PII(#164 A2 是判据来源,经
645
- // createWriteAdmission 的 sensitiveScan 注入;本批只定义它怎么被消费)与空白/
646
- // 纯噪声文本。去重键命中**不**进第 1 级——按维护者 09-22 的判定归 write-update
647
- // 放行(2606.24535 的失效模式:同步近重复门跑在异步矛盾检测之前,把本该被裁决
648
- // 的矛盾直接拒掉);G1/G2 的阈值继续只计量,等遥测有分布再单独提。
649
- writeAdmission: z.object({
650
- enabled: z.boolean().default(false),
651
- enforce: z.boolean().default(false)
652
- }).default({}),
653
-
654
- // --- #164 A2: secret / PII scan at the write boundary ----------------------
655
- // 写入边界的密钥 / PII 判据(src/sensitive-scan.js)自身的闸。与 writeAdmission
656
- // 的 enabled 分开是有意的:那一个管 #254 第 1 级的空白 / 噪声判据,本键管 A2 这
657
- // 一类判据,两批的误杀面差一个量级(空白 / 噪声没有解释空间,邮箱 / 手机号有),
658
- // 绑在同一个开关上就没法单独观察 A2 的命中分布。
659
- //
660
- // 分层必须写清:判据的唯一调用点在 #254 第 1 级的闸门里(write-admission.js 的
661
- // firstLevelHit),闸门不走第 1 级就没人来调这个扫描器。所以本键是「闸门内这一批
662
- // 判据参不参与」,不是一条能独立跑的链路——单开本键就是零行为变化:
663
- // writeAdmission.enabled 关 → 第 1 级整个不跑,本键开也没用
664
- // enabled 开 + 本键关 → 只跑空白 / 噪声那一批
665
- // enabled 开 + 本键开 + enforce 关 → 命中留审计,写入照常(观察档)
666
- // enabled 开 + 本键开 + enforce 开 → 命中即拒绝
667
- // 默认关 = 只计量那一阶段的行为逐字节保留(#332 合并时 sensitiveScan 就是 null)。
668
- // #164 的「默认仅告警、拦截 opt-in」落在 enforce 上:命中落一条审计
669
- // (metadata.deny.kind)但照常写入,真要拦得 enabled 与 enforce 同时开。
670
- // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
671
- sensitiveScanEnabled: z.boolean().default(false),
672
-
673
- // --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
674
- // Separate retrieval evaluation snapshots from the production recall audit.
675
- // When false (default) evaluateRetrieval still computes precision/recall/mrr
676
- // and returns them to the caller, but writes nothing to recall_evals — the
677
- // eval table only grows when the operator opts in. Production searchMemories
678
- // audits to recall_runs and NEVER touches recall_evals, regardless of this
679
- // flag (production isolation is unconditional).
680
- evalPersistTestResults: z.boolean().default(false),
681
-
682
- // --- standalone external API (v0.7.12) ------------------------------------
683
- // A plain node:http server for ecosystem integrations that cannot reach the
684
- // DSH-internal webServer. Disabled by default; when enabled the Bearer token
685
- // is persisted in the settings kv ("external_api"), auto-generated on first
686
- // boot. Bind host: keep the loopback default — moving it to a non-loopback
687
- // address exposes the whole memory store to the network and is the
688
- // operator's responsibility.
689
- externalApiEnabled: z.boolean().default(false),
690
- externalApiPort: z.natural().default(8790),
691
- externalApiHost: z.string().default("127.0.0.1"),
692
-
693
- // --- light mode preset (v0.7.12) -------------------------------------------
694
- // One switch for low-resource setups: turns off every background/semantic
695
- // heavy path (dream consolidation, entity extraction, vector pipeline,
696
- // reranker, BM25, semantic dedup / selective inject, sleep mode) while
697
- // keeping the core loop (autoInject, autoSummarize, hot memory, quality
698
- // filter, keyword search). Applied by applyLightModePreset before the config
699
- // reaches any service; a persisted panel_mode="light" (settings kv) counts
700
- // as lightMode=true too and wins over the bundle config.
701
- lightMode: z.boolean().default(false),
702
-
703
- // --- heat: v0.7.0 self-evolution (heat + interest drift) ----------------
704
- // 总开关,默认关(v0.7.12+ 用户已习惯无 heat 行为,默认开=全员行为变更)。
705
- // 开启后:提供热度字段 / sleep 降级联合判定保护 / 前端热度投影,并只在
706
- // 注入排序的优先级层内乘 heat(#218 v1;store 的 order=chrono 分页序与
707
- // 召回融合序不动)。关闭则跳过所有 heat 计算与热度触达,注入排序乘数
708
- // 恒 1,sleep 降级退回纯时间分层。
709
- // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
710
- heatEnabled: z.boolean().default(false),
711
- // issue #218 / E5 效用考卷:heat 乘进注入排序在真实年龄混合下饿死老约束
712
- // (现行量级 ≡ 拟合参数,注入集逐条相同;importance-only 遵从 +12.7pp),
713
- // 故注入侧 heat 改为独立 opt-in:默认关 = heatEnabled 开启后热度字段/sleep
714
- // 保护/前端投影照常,但注入排序乘数恒 1(与 heat 关闭逐字节一致);
715
- // 显式开启才恢复「层内乘 heat」的旧序。召回侧时钟(touchLastAccess)与
716
- // sleep 降级联判不受本键影响——它们不参与注入排序。
717
- injectHeatEnabled: z.boolean().default(false),
718
- // 广义指数形状参数 β(heat = exp(-λ·Δt^β),issue #218 拍板):β=1 纯指数。
719
- // 快慢以 Δt>1 小时为准——β<1 衰减更慢(亚线性长尾)、β>1 更快(超线性);
720
- // 0<Δt<1 的首小时内方向相反(Δt^β 随 β 增大而变小)。专家调优项,不进面板白名单。
721
- heatGlobalBeta: z.number().min(0.5).max(2).default(1.0),
722
- // per-type 衰减因子 λ;λ=0 的类型免疫(热度恒 1.0,sleep 永不降级)。
723
- // 未知类型走默认 0.002。dict 的键为 type 字符串、值为数字 λ。
724
- heatTypeDecay: z.dict(z.number(), z.string()).default({ ...TYPE_DECAY_DEFAULTS }),
725
- // sleep 降级联合判定的热度下限:heat < 该值 且 importance<5 才允许降级。
726
- sleepHeatThreshold: z.number().min(0).max(1).default(0.05),
727
- // recordRecall 默认值(recall_runs 记录默认开;显式传 false 的调用方不受影响)。
728
- recallRecordDefault: z.boolean().default(true),
729
- // recall_runs 滚动清理保留天数。
730
- recallRetentionDays: z.natural().min(1).max(3650).default(90),
731
-
732
- // --- tool exposure: 慢模型/轻量模型的工具往返节流(v0.8.5)----------------
733
- // 跨会话记忆已由 inject.js 每轮自动注入系统提示词,memory_search 只用于
734
- // 「注入里没有、需要深挖」的补充检索;memory_archive 是隐藏/恢复条目的整理
735
- // 操作,正常会话里很少需要。轻量模型对「何时该用工具」判断弱,容易每轮
736
- // 顺手调一遍——每次工具调用都是一次串行往返(生成参数→执行→回填→再生成),
737
- // 在慢模型上会被放大成明显卡顿。这两个开关允许直接隐藏对应工具(默认全
738
- // 开=行为不变),隐藏后模型根本看不到它,也就不会调。也走 feature_flags
739
- // 白名单,面板可启停=线上回滚开关。
740
- disableMemorySearch: z.boolean().default(false),
741
- disableMemoryArchive: z.boolean().default(false),
742
-
743
- // --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
744
- // 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
745
- // 逐字节不变。开启后 memory_save 写入 agent_scope(session header 的
746
- // agentPreset)与 workspace_scope(registry 反查 canonical path,header.cwd
747
- // 兜底,取不到 NULL——解析绝不阻塞写入),去重键扩展为 (type, title,
748
- // agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
749
- // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
750
- scopeEnabled: z.boolean().default(false),
751
- // strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见,
752
- // 检索与注入排序同权重,issue #339 补齐注入通道);
753
- // 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
754
- // 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
755
- // 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
756
- // 打开才有意义(没有写入标注就无可过滤维度),但独立成键:可先开标注积累
757
- // 数据、观察 A2 加权质量后再切硬过滤。
758
- strictScope: z.boolean().default(false),
759
- });
760
-
761
- // Fields forced to false by the light-mode preset. Everything not listed here
762
- // (autoInject, autoSummarize, hotMemory*, memoryQualityFilter, dream
763
- // thresholds/delays, ...) is left untouched — those are the core loop.
764
- const LIGHT_MODE_OFF = [
765
- "entityExtractionEnabled",
766
- "autoDream",
767
- "sleepModeEnabled",
768
- "rerankEnabled",
769
- "autoReindexOnBoot",
770
- "hybridInject",
771
- // #249 第二批:能力说明在轻量档保持关闭——它是工具描述与提示段上的额外常驻
772
- // 文本,轻量档(小模型 / 小上下文)不该因默认值翻转而多付这份提示成本。
773
- "injectGuidanceEnabled",
774
- "searchSemanticDedup",
775
- "selectiveInjectEnabled",
776
- "bm25SearchEnabled",
777
- // 轻量模式不开图召回轴(#219,依赖实体抽取产出;抽取本身已被关掉)。
778
- "entityRecallEnabled",
779
- // 轻量模式不开图谱锚定层级联(#24 块1,依赖实体抽取 + 关系表,抽取已关则无边可扩)。
780
- "graphAnchoringEnabled",
781
- // 轻量模式不开边权重演化(#24 块2,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
782
- "graphWeightEnabled",
783
- // 轻量模式不开关联提示标注(#24 块3,常驻注入文本多一段提示成本)。
784
- "graphInjectHint",
785
- // 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
786
- "graphPassiveConfirm",
787
- // 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
788
- "dreamNarrativeEnabled",
789
- // 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
790
- "documentMemoryEnabled",
791
- // 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
792
- "heatEnabled",
793
- // #249 N3:轻量档默认不开压缩边缘双落点——它往对话里追加消息(新的注入表面),
794
- // 轻量档(小模型 / 小上下文)最不该再多一份注入物。这是预设给的默认值、不是强制:
795
- // 用户显式勾选仍然赢(合并顺序「用户开关 > 轻量预设 > bundle 配置」,同
796
- // injectGuidanceEnabled)。
797
- "continuityRescueEnabled"
798
- ];
799
-
800
- /**
801
- * Apply the light-mode preset to a resolved config object (pure function,
802
- * exported for tests). When cfg.lightMode is not exactly true the config is
803
- * returned unchanged; otherwise a shallow copy carries false for every heavy
804
- * feature. Idempotent and side-effect free.
805
- */
806
- export function applyLightModePreset(cfg) {
807
- if (cfg?.lightMode !== true) return cfg;
808
- const preset = { ...cfg, lightMode: true };
809
- for (const key of LIGHT_MODE_OFF) preset[key] = false;
810
- return preset;
811
- }
812
-
813
- /**
814
- * #249 第二批:注入形态的父/子开关。父 = `autoInject`(既有的挂载总闸,默认
815
- * 开)。子项按 #249 §10 的判据分档——看它是否引入**新的注入时机/新的注入
816
- * 表面/额外成本**:
817
- *
818
- * - 只修正既有每轮块内的位(能力说明)→ 随父开关生效,键的默认值给开;
819
- * - 需要新时机或新表面(N2 回合结束提醒、N3 压缩边缘)→ 独立开关、默认关,
820
- * 实现时挂到这张表下(今天还没有这两个键,故表里只有能力说明一项)。
821
- *
822
- * 语义:**父关 = 子项一律不生效,但子项的持久值原样保留**(父关是「不生效」,
823
- * 不是「重置用户配置」)。用户显式写进 kv 的值也不因父关被改写。
824
- *
825
- * 闸门放在消费点而不是合并后的 cfg 上:压 cfg 会让 `/features` 的 effective
826
- * 失去「子项自己勾着、父关时当前不生效」这个状态,面板就显示不出来。跨文件
827
- * 契约见 test/inject-parent-gate.test.js(含面板侧关系的漂移检查)。
828
- *
829
- * 表里只放**顶层扁平键**:点号键(如 `memoryQualityFilter.enabled`)在合并后的
830
- * cfg 里是嵌套对象,`cfg?.["a.b"]` 取不到,挂进来会恒判不生效且不报错。N2/N3
831
- * 是扁平键,不受这条限制。
832
- */
833
- export const INJECT_CHILD_FLAGS = Object.freeze({
834
- autoInject: Object.freeze(["injectGuidanceEnabled", "continuityRescueEnabled"])
835
- });
836
-
837
- /** 子开关的运行时生效值:父开关显式关(false)时恒不生效。 */
838
- export function injectChildEnabled(cfg, key) {
839
- for (const [parent, children] of Object.entries(INJECT_CHILD_FLAGS)) {
840
- if (children.includes(key) && cfg?.[parent] === false) return false;
841
- }
842
- return cfg?.[key] === true;
843
- }
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { TYPE_DECAY_DEFAULTS } from "./heat.js";
3
+
4
+ export const Config = z.object({
5
+ // 记忆语言:生成记忆、注入标题与后台 LLM 提示词所用语言;'zh'(默认,
6
+ // 行为不变)/ 'en'。启动时读入,切换后重启生效。
7
+ language: z.union([z.const("zh"), z.const("en")]).default("zh")
8
+ .description("记忆语言:生成的记忆条目、注入标题与后台 LLM 提示词所用语言(zh=默认中文,en=英文)。"),
9
+ memoryDir: z.string().default("~/.dsh/memory"),
10
+ autoInject: z.boolean().default(true),
11
+ autoSummarize: z.boolean().default(true),
12
+ // Optional model override for summarization. When both are non-empty, they
13
+ // take priority over the session's current model. Empty = use the session's
14
+ // active provider/model (same as before).
15
+ summarizeProvider: z.string().default(""),
16
+ summarizeModel: z.string().default(""),
17
+ // 蒸馏转录上限(字符)。借鉴 Codex「保留原始、替代压缩摘要」的思路:
18
+ // 蒸馏把完整对话上下文交给 LLM 提炼,不硬裁到 8000 字就截断语义;默认
19
+ // 24000 字符(约覆盖一整轮中等对话),需要更完整可调大。
20
+ distillMaxChars: z.natural().min(1000).max(200000).default(24000),
21
+ // autoSummarize 节流、产出上限与写入端同会话去重(Issue #127,默认全 0/off =
22
+ // 零行为变化)。此前每个 turn/end 必跑、产出条数由模型输出决定、写入端只按
23
+ // 「同类型 + 标题 trim 全等」去重——本机实测单日 84 次、单会话一天 198 条、
24
+ // 同一事实 6 小时铸出 28+ 条。阀门交给用户按需拧,升级本身不改行为。
25
+ summarizeMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
26
+ summarizeMaxEntriesPerRun: z.natural().min(0).max(50).default(0),
27
+ // Issue #239(成本感知级联第一级):蒸馏前的零 LLM 预判——窗口内可蒸馏文本
28
+ // 不足此字符数时直接跳过 LLM 调用(0 = 关闭,行为与现状逐字节一致)。判定
29
+ // 纯规则、无模型参与;被挡下的窗口照常消费游标(否则每个 turn/end 都会重新
30
+ // 评估同一段短文本),并留一行 status='skipped' 审计,error_message 写明原因
31
+ // ——「这一窗为什么没蒸馏」必须可观测,否则阀门等于黑盒。
32
+ summarizeMinWindowChars: z.natural().min(0).max(100000).default(0),
33
+ // Issue #239(有界检查点):同一会话最多发起多少次蒸馏(0 = 不限,等同现状)。
34
+ // 只统计真正发起过 LLM 调用的 run——被预判挡下的窗口不占预算,否则阀门会把
35
+ // 额度浪费在零成本窗口上。计数是进程内 per-session(与最小间隔闸门同生命
36
+ // 周期),宿主重启即清零,与 #229 的游标持久化是两件事。
37
+ summarizeMaxRunsPerSession: z.natural().min(0).max(1000).default(0),
38
+ // Issue #239(第 4 项,错峰队列):高峰期蒸馏顺延。空串 = 关闭,行为与现状逐
39
+ // 字节一致。取值是本地时间的时段列表(逗号分隔,支持跨零点),可带**星期前缀**
40
+ // (可省;ISO 1=周一…7=周日,也认 mon..sun;省略 = 每天):
41
+ // "09:00-18:00" 每天 09:00-18:00
42
+ // "mon-fri 08:00-12:00,14:00-18:00" 工作日两段(按高峰计费的供应商即此形态)
43
+ // "sat,sun 23:00-06:00" 周末跨零点段
44
+ // 命中高峰时:不调 LLM、不消费游标(窗口继续累积,留到非高峰一次性蒸馏——批量
45
+ // 比逐轮碎蒸更省),登记一行 status='skipped' / error_message='peak-hours' 审计,
46
+ // 并按下面的上限择时补跑。任一写法非法则整串按「未配置」处理:排程是省钱
47
+ // 手段,绝不该因为写错格式把蒸馏停掉。
48
+ summarizePeakHours: z.string().default(""),
49
+ // 高峰顺延上限(分钟,0 = 不设上限):到点仍处高峰就照常跑,避免整天高峰把蒸馏
50
+ // 饿死。默认 120 分钟。仅在上面的时段串非空时生效。
51
+ summarizePeakMaxDeferMinutes: z.natural().min(0).max(1440).default(120),
52
+ // 落库前去重档位:off(默认,等同现状)/ title(零成本,仅拦完全同名)/
53
+ // vector(复用已有 embedding 列做同会话语义近邻,无 LLM 调用)。
54
+ summarizeDedupeMode: z.union([z.const("off"), z.const("title"), z.const("vector")]).default("off"),
55
+ summarizeDedupeMinSim: z.number().min(0.5).max(0.99).default(0.92),
56
+ summarizeDedupeWindowHours: z.natural().min(0).max(168).default(24),
57
+ // 智能调速器(429 保护,默认开):蒸馏 LLM 调用全局串行排队,相邻请求
58
+ // 间隔 distillRateLimitIntervalMs(默认 1s 一次);命中 429 限流时按
59
+ // distillRateLimitBaseDelayMs 指数退避(1s→2s→4s…)自动重试
60
+ // distillRateLimitRetries 次,全程对用户透明,不把 429 错误码抛给用户。
61
+ distillRateLimitIntervalMs: z.natural().min(0).max(60000).default(1000),
62
+ distillRateLimitRetries: z.natural().min(0).max(10).default(3),
63
+ distillRateLimitBaseDelayMs: z.natural().min(100).max(60000).default(1000),
64
+ maxInjectedItems: z.natural().min(1).max(20).default(5),
65
+ // Issue #205:注入位跨轮轮换。同一会话里,最近 N 个不同用户查询轮次注入过
66
+ // 的记忆本轮不再优先(新鲜优先、原序回填,槽位数不变);长会话中避免相邻
67
+ // 轮次反复注入同一条(实测 15.5h 会话 86 次注入 93% 重复)。0 = 关闭(默认,
68
+ // 保持既有行为)。会话边界自动重置:新会话从零开始。
69
+ injectRotationTurns: z.natural().min(0).max(20).default(0),
70
+ // Issue #164①:注入单条正文截断上限可配(原硬编码 300)。截断时尾部带
71
+ // 提示:上限/原长/全文 memory_get 指引(BUDGET_EXCEEDED 原则——绝不静默)。
72
+ // 默认 300 = 与既有行为一致。
73
+ injectContentMaxChars: z.natural().min(60).max(4000).default(300),
74
+ // Issue #34(v0.7.2 引入;v0.7.11 误删后随 #333 恢复):对话开始注入一次当前
75
+ // 时间。默认关 = 与既有行为一致;格式与键名不变,存量 feature_flags 恢复即生效。
76
+ injectTimePrefix: z.boolean().default(false),
77
+ importanceThreshold: z.natural().min(1).max(5).default(3),
78
+ // Issue #239(第 5 项):注入条数的查询自适应(默认关)。确定性强的话题收缩注入
79
+ // 条数(减半、下限 1),模糊话题(回指/时间线索,或极短查询)维持
80
+ // maxInjectedItems 上限——只做**单向收缩**,绝不越过用户配置的上限;判据只看
81
+ // 查询本身,不做额外检索(先探针检索等于白付一次 fuseRecall)。
82
+ injectUncertaintyAdaptive: z.boolean().default(false),
83
+ // #249(第一批;第二批归位为注入子开关):能力说明——「怎么用记忆」的判断
84
+ // 指引。落两个零注入成本的位:①`memory_search` / `memory_save` 的工具描述补
85
+ // 一句判断指引(工具描述是常驻文本,不进每轮上下文);②一段 order 150 的
86
+ // 系统提示段(一次性、同会话内不随轮次变化,因此不作废前缀缓存),只讲总则
87
+ // (优先序、何时查、何时写、何时 no-op)。默认开=基础档:#249 §10 的判据是
88
+ // 「只修正既有位、不引入新注入时机/新表面/额外 LLM 调用」的子项随父开关
89
+ // 生效——「agent 不知道何时该查、何时该写」是已实测的缺口,把它默认关着是反的。
90
+ // 父开关 `autoInject` 关闭时它不生效(闸门见 injectChildEnabled);用户显式写进
91
+ // feature_flags 的值永远优先于这里的默认值。
92
+ injectGuidanceEnabled: z.boolean().default(true),
93
+ // #249 N3(压缩边缘双落点):上下文即将大幅精简前抢救「正在做什么」。默认关。
94
+ // 触发靠宿主自己的压缩事件(`compaction/start|summary|end`,都在事件白名单里),
95
+ // 不自定一套阈值参数——「压缩边缘」由宿主定义,我们再校准一份只会与之漂移。
96
+ // 为什么是「新时机 + 新表面」因而默认关:它往对话里**追加消息**(新的注入表面,
97
+ // 参照实现里最容易累积成一堆历史的那类),并多写一张提案表。按 §10 判据,引入
98
+ // 新时机/新表面/新成本的子项独立成键、默认关;只修正既有块的(基础内容分池、
99
+ // 库可见性行)才随父开关默认开。`pinnedInjectBudget` 与它同批,但属前者之外:
100
+ // 那是既有块内的预算,不是新表面。
101
+ // 双落点是硬要求,不能只留一半:宿主的压缩摘要器**只看对话里的内容**,只落库不
102
+ // 注入,等于在摘要重建里什么都没留下;只注入不落库,则压缩一过就随旧消息一起
103
+ // 消失。两者都做,且落库先于注入(注入失败不该丢提案)。
104
+ // 祖先:`autoInject`(父关则本项不生效,闸门见 injectChildEnabled)。轻量档默认
105
+ // 置关(见 LIGHT_MODE_OFF):轻量档多一份注入物是反的,与 injectGuidanceEnabled
106
+ // 同一取舍。注意预设只是默认值而非强制——装配时用户显式开关在它之后展开,勾了就赢
107
+ // (合并顺序见 index.js 装配处)。
108
+ continuityRescueEnabled: z.boolean().default(false),
109
+ // #249(第一批):B1 pin 池预算——约束/偏好类注入条目的独立小上限。约束与
110
+ // 偏好被静默降级是本议题的立项核心(同类知识与情景日志同池同速率摘要,实测
111
+ // 一轮压缩后仅保 53%、五轮 10%),故这两类不进相关性竞争、不参与跨轮轮换、
112
+ // 逐字保真(仅受超大条目的硬顶保护,截断仍带提示)并排在块内排序之前。
113
+ // 独立预算的意义:pin 不占 maxInjectedItems 名额,不会把当前任务需要的情景
114
+ // 候选挤出预算(另一种「批量塞历史」)。0(默认)= 关闭,注入块构成与既有
115
+ // 行为逐字节一致;超出预算的条数在块内如实标注未展示条数,绝不静默。
116
+ pinnedInjectBudget: z.natural().min(0).max(5).default(0),
117
+ // 编码记忆蒸馏(codingRetrospect,opt-in,默认关)。开启时,turn/end 蒸馏
118
+ // 额外提取三类编码专属记忆:rejected_solution(被否决方案)/ pitfall(踩坑)/
119
+ // constraint(工程约束)。蒸馏上下文为整轮完整对话(用户输入 → 助手思考/回答
120
+ // → 工具调用与结果 → 代码执行),不再只看用户消息,便于提炼踩坑根因。
121
+ // 关闭时行为与之前完全一致。
122
+ codingRetrospect: z.boolean().default(false),
123
+ // 编码任务识别词表(读取侧门控用):命中即视为编码类任务,编码记忆才注入。
124
+ codingKeywords: z.array(z.string()).default([
125
+ "代码", "编码", "写一个", "写个", "实现", "函数", "方法", "类",
126
+ "接口", "bug", "调试", "报错", "错误", "异常", "堆栈", "脚本",
127
+ "python", "javascript", "typescript", "node", "js", "ts",
128
+ "sql", "sqlite", "数据库", "算法", "重构", "优化", "性能",
129
+ "测试", "单测", "修复", "补丁", "依赖", "npm", "pip",
130
+ "命令行", "shell", "配置", "配置文件", "yaml", "json",
131
+ "插件", "开发", "编译", "构建", "部署", "git", "commit",
132
+ "review", "前端", "后端", "页面", "组件", "dsh", "memos"
133
+ ]),
134
+ // 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
135
+ // 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
136
+ codingBoostFactor: z.number().min(1).max(5).default(2),
137
+ // 注入边界花括号转义(issue #162,v0.7.4 曾修后被 v0.7.11 重构误删):默认开启,
138
+ // 在 memory / 短期上下文 / 用户设置三处注入出口对连续 2+ 个花括号插 `\`
139
+ // (`{{a}}`→`{\{a\}\}`),让 DSH 的 interpolate() 不再扫到非法变量名而 throw。
140
+ // 关闭时按原样透传。
141
+ escapePromptVariables: z.boolean().default(true),
142
+ autoDream: z.boolean().default(true),
143
+ dreamThresholdCount: z.natural().min(1).max(1000).default(10),
144
+ dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
145
+ dreamDelayMs: z.natural().min(0).max(60000).default(2000),
146
+ // autoDream 触发最小间隔(分钟,0 = 不限制,Issue #89 请求 2):高频写入
147
+ // 场景下防止巩固调用(含失败重试)连发刷爆配额。间隔从每次实际开跑时刻
148
+ // 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次
149
+ // 写入事件会重新评估。
150
+ dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
151
+ // Issue #292(#135 派生):autoDream 连续失败退避(opt-in,默认关 = 行为与
152
+ // 现状逐字节一致)。开启后调度器对连续失败做指数退避:有效最小间隔 =
153
+ // dreamMinIntervalMinutes × 2^连续失败数(成功一次清零恢复),封顶 30 分钟。
154
+ // #89 的最小间隔闸失败 run 也占用,但间隔恒定——恒定失败的模型(#135 空体
155
+ // 面)会按固定节奏连发刷爆配额;退避把下次重试按失败次数指数推远。基数取
156
+ // dreamMinIntervalMinutes:基数为 0 时无闸可翻倍,本键不自己产生间隔(先配
157
+ // dreamMinIntervalMinutes 再开本键)。与 dreamPeakHours / dreamMinIntervalMinutes
158
+ // 同族(节流阀,不新增任何 LLM 调用),故不进 LIGHT_MODE_OFF。
159
+ autoDreamFailureBackoff: z.boolean().default(false),
160
+ // Issue #239(第 4 项,错峰队列)镜像到巩固:高峰期不做梦。与
161
+ // summarizePeakHours 同一份时段语法(复用 src/summarize.js 的 parsePeakSpec /
162
+ // isInPeakWindow / nextOffPeakAt,不另写解析器):逗号分隔、可带星期前缀、支持
163
+ // 跨零点。空串 = 关闭,行为与现状逐字节一致。
164
+ // "09:00-18:00" 每天 09:00-18:00
165
+ // "mon-fri 08:00-12:00,14:00-18:00" 工作日两段(按高峰计费的供应商即此形态)
166
+ // 为什么巩固比蒸馏更该有这道闸:单次巩固的输入是整窗快照(dreamMaxSnapshotSize
167
+ // 条),实测一次 run 的 LLM 时长可达数分钟量级,撞上高峰时既贵又慢;而它由写入
168
+ // 事件触发、没有天然的「等到空闲再跑」路径。命中高峰时:不调 LLM、不刷新
169
+ // baseline(阈值继续累积,留到非高峰一次性巩固),登记一行 status='skipped' /
170
+ // error_message='peak-hours' 审计,并按下面的上限择时补跑。任一写法非法则整串
171
+ // 按「未配置」处理——排程是省钱手段,绝不该因为写错格式把巩固停掉。
172
+ dreamPeakHours: z.string().default(""),
173
+ // 高峰顺延上限(分钟,0 = 不设上限):到点仍处高峰就照常跑,避免整天高峰把巩固
174
+ // 饿死。默认 120,与 summarizePeakMaxDeferMinutes 对齐。仅在时段串非空时生效。
175
+ dreamPeakMaxDeferMinutes: z.natural().min(0).max(1440).default(120),
176
+ // 巩固模型路由(settings panel「巩固模型」/ dreamProvider+dreamModel):
177
+ // dream 的记忆沉淀专用 LLM 路由,显式配置优先于 agent 默认模型(config-first,
178
+ // Issue #25)。模型分类声明:
179
+ // - 非思考模型(推荐,如 glm-5-2 类):无 reasoning 声明,effort 请求被 harness
180
+ // 拒绝后 withEffortFallback 去掉字段重试即成功;空体/no json array 风险最低。
181
+ // - 思考模型(如 deepseek-v4-flash-ga 等 v4-flash-ga 系):默认开推理,可能烧光
182
+ // token 预算返回空体;且部分(如 v4-flash-ga)在 harness 侧被声明为不接受任何
183
+ // reasoning effort —— 即使去掉 effort 重试,harness 的 defaultEffort 也会顶上来
184
+ // 再次拒绝(UNSUPPORTED_REASONING_EFFORT),插件 fallback 无法绕开。
185
+ // 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
186
+ dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
187
+ dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
188
+ // Issue #135 建议 3:#9 只抬了 max(8192→131072),default 停在 32768。思考型
189
+ // 模型的推理与正文共享该预算,effort 未压低时 32768 恰好被推理烧光 → 空体
190
+ // ("no json array in llm output",报告实测 146 轮因此失败)。默认抬到上限,
191
+ // 让默认配置也留足正文预算;流式计费按实际用量,不按上限。
192
+ dreamMaxTokens: z.natural().min(256).max(131072).default(131072),
193
+ // Pass-through reasoning effort for dream's LLM calls (Issue #135 建议 4/5).
194
+ // Unset (no value) resolves to the LOWEST effort the model declares before
195
+ // streaming: background consolidation must produce JSON, and omitting the
196
+ // field lets the harness substitute the model's defaultEffort (thinking-type
197
+ // models often default to high/max) — the reasoning then drains the whole
198
+ // token budget and the run dies with an empty body. Explicit 'none' still
199
+ // omits the field (provider default applies); off/low/medium/high are
200
+ // forwarded verbatim, unsupported values are remapped (v0.7.26+).
201
+ // Caveat: on some thinking models (e.g. v4-flash-ga) the harness declares NO
202
+ // supported effort, so even the fallback retry (field stripped) is rejected
203
+ // again via its defaultEffort — prefer a non-reasoning dreamProvider/dreamModel.
204
+ dreamReasoningEffort: z.union([
205
+ z.const("off"),
206
+ z.const("low"),
207
+ z.const("medium"),
208
+ z.const("high"),
209
+ z.const("none")
210
+ ]).description("巩固模型的推理档位:未配置 = 自动取模型支持的最低档(避免思考模型用自带默认档烧光预算返回空体,#135);显式 'none' = 不发送字段、用服务商自带默认;off/low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
211
+ // 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
212
+ // 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
213
+ // 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
214
+ dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
215
+ // Issue #104 方向 2:单轮 archive 决策上限。一次性大扫除(12 条互不相关主题
216
+ // 被批量归档,其中 8 条疑似误伤长保留类型)是失控信号,与 update 上限同类的
217
+ // 全局闸门:skipInvalid 也不豁免,超限整单拒绝。正常清理可调高。
218
+ dreamMaxArchivePerRun: z.natural().min(1).max(200).default(8),
219
+ // Issue #125:候选集构造方式。"window"(默认,等同现状)只取最近
220
+ // dreamMaxSnapshotSize 条;"hybrid" 在此基础上并入**向量翻出的高相似组**——
221
+ // 本机实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口(窗口覆盖率 11.7%),
222
+ // 纯时间窗口让"该合并的一对"几乎永远碰不到面。候选总量仍由下面的上限封顶、
223
+ // 与库总量解耦,这是相对"调大窗口"的核心收益:输入成本不随库增长。
224
+ dreamCandidateMode: z.union([
225
+ z.const("window"),
226
+ z.const("hybrid")
227
+ ]).default("window"),
228
+ // hybrid 的候选总量上限;0 = 复用 dreamMaxSnapshotSize(不迁移,需要时显式覆盖)。
229
+ dreamCandidateMax: z.natural().min(0).max(5000).default(0),
230
+ // Issue #258:总览(dream_summarize)独立路由。consolidate 有 dreamMaxSnapshotSize
231
+ // 窗口、总览原为全库无界——两者对 ctx 的需求差 4 倍以上却强制共用 dream 路由,
232
+ // dreamProvider 指向小 ctx 模型时总览当场 CONTEXT_WINDOW_EXCEEDED(实测 120,969
233
+ // tokens > 32,768),指向大 ctx 模型则 consolidate 的卸载收益归零。未配置 = 回落
234
+ // dream 路由,行为逐字节不变。
235
+ dreamSummaryProvider: z.string().default("").description("记忆总览(dream_summarize)专用模型服务商,留空用巩固模型。总览输入为全库(或 dreamSummaryMaxInputs 上限),ctx 需求远大于 consolidation,建议大 ctx 模型(issue #258)。"),
236
+ dreamSummaryModel: z.string().default("").description("记忆总览(dream_summarize)专用模型,留空用巩固模型;总览输入随库增长,建议大 ctx 非思考模型(issue #258)。"),
237
+ // Issue #258:总览输入条数硬上限。0 = 不设上限(历史行为,库增长可能撑爆小
238
+ // ctx 模型);>0 时按 updated_at 倒序保留最新的 N 条(与 consolidate 窗口同一
239
+ // 排序口径)。总览是常驻叙述而非逐条巩固,限输入只影响口径脚注里的条数。
240
+ dreamSummaryMaxInputs: z.natural().min(0).max(100000).default(0),
241
+ // hybrid 判"高相似"的阈值;0.85 与 sleep normal 档对齐——两个模块对"高相似"
242
+ // 保持同一个定义。
243
+ dreamCandidateMinSim: z.number().min(0.5).max(0.99).default(0.85),
244
+ // 叙述条(#164 对齐,opt-in):dream 期间按共享 tag 主题聚类(dream/
245
+ // narratives.js),每簇 LLM 合成一条叙述落库(source=narrative、evidence
246
+ // 回链簇内原子记忆、求交防捏造)。按需检索、不常驻注入。默认关=行为与
247
+ // 此前一致;lightMode 强制关闭。
248
+ dreamNarrativeEnabled: z.boolean().default(false),
249
+ // 成簇门槛:共享同一 tag 的记忆 ≥ 此值才合成叙述条。
250
+ dreamNarrativeMinCluster: z.natural().min(2).max(20).default(3),
251
+ // document 型记忆(#164/#230,opt-in):agent 产长文档的指针行——注册校验
252
+ // (文件存在 + 路径合法 + evidence 求交)、摘要 + doc_path 落库、C2 比对
253
+ // 去重、supersede 记账。全文归 agent,管线零触碰;默认关=行为与此前一致;
254
+ // lightMode 强制关闭。
255
+ documentMemoryEnabled: z.boolean().default(false),
256
+ // document 摘要行的注入预算(#230 拍板):次优先档内最多注入的 document
257
+ // 行数,超预算跳过由后续候选补位。只约束注入,不约束检索。
258
+ documentInjectBudget: z.natural().min(1).max(5).default(2),
259
+ // document 的 managed 落盘目录(#296 第二批):空串 = 跟随 memoryDir 的
260
+ // `<memoryDir>/documents/`。`~` / `~/` / `~\` 展开到 home,绝对路径原样用,相对
261
+ // 路径落在 memoryDir 下(这是本键自己的规则:memoryDir 的相对路径是原样留着的
262
+ // cwd 语义,两者刻意不同;`~user` 形式两边都不展开)。目录由 mneme 建,里面的
263
+ // `index.md`
264
+ // 整文件机器所有。这个目录**之外**的文件只登记指针行,正文一个字节都不碰
265
+ // (= #230 的「管线对正文零读零写」);与 memoryDir 一样是路径配置,不是
266
+ // 行为开关,故不进 settings.js 的 feature flags 白名单、也不上面板。
267
+ documentDir: z.string().default(""),
268
+ // 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
269
+ // 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
270
+ // (未覆盖即拒绝整单)。
271
+ dreamImplicitKeep: z.boolean().default(true),
272
+ // 显式决策覆盖率下限(v0.4.4 fix):dreamImplicitKeep 开启时,LLM 输出被
273
+ // 截断只显式 claim 少量 snapshot 记忆(claimed.size / snapshot.size < 该阈值)
274
+ // → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
275
+ // 默认 0.5(至少显式覆盖一半 snapshot)。
276
+ dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
277
+ // Issue #89:v0.6.9(Issue #26)的宽容路径回归。默认跳过单条非法决策
278
+ // (未知 id / 跨类型合并等)、应用合法子集、run 记 degraded;设 false 恢复
279
+ // 整单拒绝的严格模式。全局上限与覆盖率下限不受此开关影响、始终整单拒绝。
280
+ dreamSkipInvalid: z.boolean().default(true),
281
+ // 显式开启后放宽跨类型合并检查(类型边界由用户自行承担);配合
282
+ // dreamSkipInvalid 理解:关闭 skipInvalid 时跨类型 merge 直接整单拒绝。
283
+ allowCrossTypeMerge: z.boolean().default(false),
284
+ // issue #339 / E8 考卷:merge 护栏(默认开)。开启后合并对象命中长保留类型
285
+ // (preference/pattern/rejected_solution/constraint/pitfall,与 archive 护栏
286
+ // 同一张表)的 merge 决策整条跳过——E8 实测巩固损耗里 10/26 条被丢约束已归位
287
+ // guarded 类型仍被 merge 吃掉,而 archive 护栏只挡 archive 不挡 merge,
288
+ // 「更精炼的摘要」恰是约束失真的主通道(同类压缩管线实测:安全规则被同速压缩
289
+ // 一轮只剩 53%、五轮只剩 10%,且全程无任何报错信号)。skipInvalid(默认开)
290
+ // 时被跳条目进 dream_runs.skipped、run 记 degraded;关闭 skipInvalid 时整单
291
+ // 拒绝。设为 false 才恢复旧行为:guarded 类型的 merge 不再被跳过,约束可被
292
+ // 「更精炼的摘要」吃掉。与 allowCrossTypeMerge 同用时长 guard 先判。
293
+ dreamMergeGuard: z.boolean().default(true),
294
+ // Rule version for dream adjudication: when this bumps, older dream_runs
295
+ // degrade to historical evidence (their receipts no longer drive live
296
+ // decisions). Default 0 = no versioning in use yet.
297
+ policyEpoch: z.natural().min(0).max(1000000).default(0),
298
+
299
+ // --- API protection ------------------------------------------------------
300
+ // Optional shared token for the plugin's HTTP API. Empty (default) keeps
301
+ // the API open (DSH binds to 127.0.0.1 and has no built-in auth); when set,
302
+ // sensitive endpoints (vector-config, vector-reindex, and all write ops on
303
+ // profile/rules/commands) require `Authorization: Bearer <apiToken>` (or
304
+ // `X-DSH-Mneme-Token`). Read-only list/search/semantic stay open so the
305
+ // Web panel keeps working without the token.
306
+ apiToken: z.string(),
307
+
308
+ // --- semantic: local embedding provider (v0.2) --------------------------
309
+ // "openai" keeps the legacy external-API path (settings vector config);
310
+ // "local" runs an ONNX model in-process; "ollama" calls a local Ollama.
311
+ embedProvider: z.union([z.const("openai"), z.const("local"), z.const("ollama")]).default("openai"),
312
+
313
+ // Local ONNX embedder (transformers.js / onnxruntime).
314
+ localEmbedModel: z.string().default("Xenova/bge-small-zh-v1.5"),
315
+ localEmbedDimension: z.natural().default(512),
316
+ localEmbedDevice: z.union([z.const("cpu"), z.const("gpu")]).default("cpu"),
317
+ localEmbedBatchSize: z.natural().min(1).max(64).default(8),
318
+ // 本地嵌入的池化方式(**bug 修复,不是新能力**):BGE 系是按 CLS 池化训练的——
319
+ // 模型自带的 `1_Pooling/config.json` 明确写着 `pooling_mode_cls_token: true` /
320
+ // `pooling_mode_mean_tokens: false`,官方 README 也是「select the last hidden state of
321
+ // the first token」+ L2 normalize;而 transformers.js 的 `feature-extraction` 默认 mean。
322
+ // ⇒ 此前本地嵌入对 BGE 系一直用错池化:**不报错、只是向量系统性偏差**,检索质量静默受损。
323
+ // 'auto' = 按模型族判定(BGE → cls,其余 → mean,未受影响的模型行为不变);也可显式钉住。
324
+ // ⚠️ 池化决定向量空间,改它会改变 modelHash(见 local-embedder.js),既有索引会被判失配并重建。
325
+ localEmbedPooling: z.union([
326
+ z.const("auto"),
327
+ z.const("cls"),
328
+ z.const("mean")
329
+ ]).default("auto"),
330
+
331
+ // Ollama embedder.
332
+ ollamaBaseUrl: z.string().default("http://localhost:11434"),
333
+ ollamaModel: z.string().default("nomic-embed-text"),
334
+
335
+ // Model download/cache. When empty (default), models are cached under the
336
+ // user-level path ~/.dsh/mneme/models (resolved in local-embedder/reranker);
337
+ // a non-empty value is used verbatim.
338
+ embedModelCacheDir: z.string().default(""),
339
+ embedModelMirror: z.string().default("https://hf-mirror.com"),
340
+
341
+ // issue #194:模型文件下载的断点续传与重试。开启时 transformers.js 的 env.fetch 被
342
+ // 替换为带断点续传的实现(已收字节落盘、Range/If-Range 续传、空闲看门狗),大文件
343
+ // 中断后不必整份重来;关闭 = env.fetch 保持原样(线上回滚开关)。
344
+ resilientModelDownload: z.boolean().default(true),
345
+
346
+ // 自管运行时目录(issue #131)。空 = ~/.dsh/mneme/runtime:里面放收编或下载来的
347
+ // transformers + onnxruntime 闭包,插件经 src/runtime/loader.js 的三层解析加载它。
348
+ // 目的就是让这条重依赖不必留在宿主 profile 的依赖图里——profile 是所有插件共用的
349
+ // 依赖图,留在里面会让「装任何插件都要替它重走一遍这条链」。
350
+ runtimeDir: z.string().default(""),
351
+
352
+ // 运行时取件的两个来源(issue #131 / PR-C)。都是「克制」的默认:留空 = 不额外改变行为。
353
+ // - runtimeTarballDir:本地 .tgz 目录。某个包网络下不到时(例如 onnxruntime-node),
354
+ // 把 `npm pack <pkg>` 出来的 tgz 丢进这个目录即可离线取件;有它就优先于联网。
355
+ // - runtimeMirror:registry 镜像前缀,例如 https://npmmirror.com/mirrors/npm/ 。
356
+ // 留空则用 runtime-manifest.json 里写死的官方地址(那是随包发布的固定清单)。
357
+ runtimeTarballDir: z.string().default(""),
358
+ runtimeMirror: z.string().default(""),
359
+
360
+ // Vector search tuning.
361
+ vectorSearchTopK: z.natural().min(1).max(100).default(20),
362
+ vectorSearchThreshold: z.number().min(0).max(1).default(0.65),
363
+ hybridSearchVectorWeight: z.number().min(0).max(1).default(0.6),
364
+ hybridSearchKeywordWeight: z.number().min(0).max(1).default(0.4),
365
+ // Lazy auto-backfill of missing embeddings on boot (Bug2): when the vector
366
+ // API is configured and rows still lack an embedding, the index is rebuilt
367
+ // in the background after a short delay, rate-limited in batches. On by
368
+ // default; set false to keep the backfill manual only.
369
+ autoReindexOnBoot: z.boolean().default(true),
370
+ // Semantic-first injection (Bug4): when enabled, injectCandidates with a
371
+ // non-empty query recalls via the vector index first and falls back to the
372
+ // rule-based pick to fill/dedupe. Empty query / no vector → legacy behavior.
373
+ hybridInject: z.boolean().default(true),
374
+
375
+ // --- recall optimization (v0.5.0) ----------------------------------------
376
+ // BM25 third recall path beside vector + LIKE keyword (1.1): per-token IDF
377
+ // scoring recalls rows whose query terms are scattered — identifiers, code
378
+ // fragments, mixed CJK/ASCII — where substring LIKE cannot match.
379
+ bm25SearchEnabled: z.boolean().default(true),
380
+ // Query-aware vector cutoff (1.2) replacing the fixed 0.65: entity:/attr:
381
+ // prefixes loosen to 0.5, short queries tighten to 0.7, long queries loosen
382
+ // to 0.6, and a decisive top-1/top-5 score gap loosens to 0.5 so the tail
383
+ // still reaches the reranker. Off = legacy fixed threshold behavior.
384
+ adaptiveThresholdEnabled: z.boolean().default(true),
385
+ // Session-scoped hot memory (1.3): the latest N dialogue rounds rendered
386
+ // ahead of the long-term recall block — short-term context that never
387
+ // enters the memory store.
388
+ hotMemoryEnabled: z.boolean().default(true),
389
+ hotMemoryRounds: z.natural().min(1).max(50).default(5),
390
+ hotMemoryMaxTokens: z.natural().min(200).max(32000).default(2000),
391
+ // Topic-ranked injection (2.2): when a query vector is available the whole
392
+ // injection candidate list is re-ordered by similarity to the current
393
+ // query instead of keeping the rule-based order.
394
+ selectiveInjectEnabled: z.boolean().default(true),
395
+ // Search-time semantic dedup (2.3): greedy pass over the merged candidate
396
+ // list dropping rows whose embedding cosine-similarity to an already-kept
397
+ // row exceeds the threshold — duplicates are filtered at recall time
398
+ // instead of waiting for a dream consolidation. Opt-in aggressive mode:
399
+ // small embedding models can collapse legitimately distinct rows, so the
400
+ // default keeps every recalled row.
401
+ searchSemanticDedup: z.boolean().default(false),
402
+ searchSemanticDedupThreshold: z.number().min(0.5).max(1).default(0.95),
403
+
404
+ // Recall fusion recipe (plan #1): how the keyword/vector/BM25 ranked lists
405
+ // are combined into the final ranking. `blend` (default) is the legacy
406
+ // behavior — weighted sum for vector/hybrid, union backfill for auto —
407
+ // unchanged. `rrf` (Reciprocal Rank Fusion) and `minmax` (min-max normalized
408
+ // weighted sum) are rank/scale-aware recipes that fix the unit mismatch the
409
+ // issue describes (raw cosine vs keyword score vs normalized IDF are added
410
+ // directly). Off by default so existing behavior holds exactly.
411
+ recallFusion: z.union([z.const("blend"), z.const("rrf"), z.const("minmax")]).default("blend"),
412
+ // Attach a `signals` object { keyword, vector, bm25, final } to each search
413
+ // result for transparency/debugging (plan #2). Default off; when on it only
414
+ // decorates the returned rows, never changes the ranking.
415
+ signalTransparency: z.boolean().default(false),
416
+
417
+ // --- semantic: rerank layer (v0.2) --------------------------------------
418
+ // Opt-in by default (item ⑥): the local cross-encoder pulls in onnxruntime
419
+ // (transformers.js) at init, so a bare install must not load it. Only an
420
+ // explicit rerankEnabled=true + rerankProvider="local" constructs LocalReranker.
421
+ rerankEnabled: z.boolean().default(false),
422
+ rerankProvider: z.union([z.const("local"), z.const("none")]).default("none"),
423
+ rerankModel: z.string().default("Xenova/bge-reranker-base"),
424
+ rerankBatchSize: z.natural().min(1).max(64).default(8),
425
+ rerankMaxCandidates: z.natural().min(5).max(100).default(30),
426
+ rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
427
+ // #188:量化档,与嵌入层的 useDtype 同语义(q8 = model_quantized.onnx,约为
428
+ // fp32 体积的 1/4)。此前重排层不传 dtype,即使缓存里已有量化文件也会去下载
429
+ // 1GB 级的 model.onnx。非法值由 transformers 在 init 时抛出 → 重排降级并告警。
430
+ rerankDtype: z.string().default("q8"),
431
+
432
+ // --- reflection: update decision + failure tracking (v0.2.1) ------------
433
+ reflectionUpdateEnabled: z.boolean().default(true),
434
+ reflectionFailureTracking: z.boolean().default(true),
435
+ reflectionUpdateMaxPerRun: z.natural().min(0).max(5).default(2),
436
+ reflectionUpdateMinAgeHours: z.natural().min(0).max(168).default(24),
437
+
438
+ // --- conflict freeze: manual review for conflicting memories (v0.2.1) ---
439
+ // Opt-in by default: when true, conflicting memories are not auto-merged
440
+ // and are marked as pending manual review instead.
441
+ conflictFreezeEnabled: z.boolean().default(false),
442
+ // Maximum number of frozen conflicts to keep pending for manual review.
443
+ conflictFreezeMaxPending: z.natural().min(1).max(1000).default(100),
444
+
445
+ // --- entity gene (v0.3.0) -----------------------------------------------
446
+ // Opt-in: when false (default) nothing in the pipeline extracts entities.
447
+ // The storage layer (entities/entity_attrs/entity_relations tables + CRUD)
448
+ // is always available regardless of this flag.
449
+ entityExtractionEnabled: z.boolean().default(false),
450
+ // Optional provider override for entity extraction; empty = use the caller's
451
+ // default provider/model. Combined with entityExtractionModel — provider
452
+ // without model (or vice versa) falls through to the caller default.
453
+ entityExtractionProvider: z.string().default(""),
454
+ // Optional model override for entity extraction; empty = use the caller's
455
+ // default provider/model.
456
+ entityExtractionModel: z.string().default(""),
457
+ // Reasoning effort for entity extraction (issue #109), mirrors
458
+ // dreamReasoningEffort pass-through: 'none' omits the field / provider
459
+ // default; off/low/medium/high passed through. A provider that rejects the
460
+ // effort retries once without it, so opting in is safe to experiment with.
461
+ entityExtractionReasoning: z.union([
462
+ z.const("low"),
463
+ z.const("medium"),
464
+ z.const("high"),
465
+ z.const("none")
466
+ ]).default("none"),
467
+ // Cap on entities per extraction pass and attributes per entity.
468
+ entityExtractionMaxEntities: z.natural().min(1).max(20).default(10),
469
+ entityExtractionMaxAttrs: z.natural().min(1).max(50).default(20),
470
+ // Prefix/semantic search over entity names (used by recall).
471
+ entitySearchEnabled: z.boolean().default(true),
472
+ // 图召回轴(issue #219):查询文本命中实体名时,把该实体挂联的记忆并入
473
+ // 检索融合池(与 BM25 同级的确认/回填信号)。默认关=检索行为与 #219 前
474
+ // 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
475
+ entityRecallEnabled: z.boolean().default(false),
476
+ // 边权重演化(issue #24 块2):touch 门控是否把被寻回路径的边权重抬升。
477
+ // 默认关=边初值纯由建边来源决定(store.RELATION_SOURCE_DEFAULTS),演化
478
+ // 不生效;开=被激活的边经 bumpRelationWeight 上涨(只加不减、封顶 1.0)。
479
+ graphWeightEnabled: z.boolean().default(false),
480
+ // 单次有效触达的抬升幅度。
481
+ graphWeightDelta: z.number().min(0).max(1).default(0.1),
482
+ // 被动确认(issue #24 块4):heat 触达(记忆被召回/注入=被用)即对该记忆
483
+ // 挂联的关系边做被动确认——bumpRelationWeight 抬一格。默认关=零行为变化;
484
+ // 开=正常触达演化边权,「关系是用出来的」闭环落地(异常路径复核留后续块)。
485
+ graphPassiveConfirm: z.boolean().default(false),
486
+
487
+ // --- 图谱锚定层(issue #24 · 块1 Activation Anchoring)-------------------
488
+ // entityRecallEnabled 之上再开一层:以查询命中的实体为种子,沿实体关系表
489
+ // 级联扩散(默认深度 2),把「邻居实体挂联的记忆」也带进融合池。种子集
490
+ // 上限 graphSeedCap、级联深度 graphCascadeDepth 一并下发到 anchorSeeds /
491
+ // cascadeDepths(见 src/graph/anchoring.js)。默认关=行为与 #219 单跳轴
492
+ // 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
493
+ graphAnchoringEnabled: z.boolean().default(false),
494
+ graphSeedCap: z.natural().min(1).max(30).default(12),
495
+ graphCascadeDepth: z.natural().min(1).max(3).default(2),
496
+
497
+ // 关联提示防幻觉(issue #24 块3):图召回的候选(查询命中实体的挂联记忆)
498
+ // 进注入时默认只参与排序、不改变注入块构成(graphInjectHint=false 保守档);
499
+ // 开启后注入文本里以固定句式「[检索线索] …」标出,引导语说明这是链路信息
500
+ // 非事实断言,防 LLM 单次消费把桥接召回当有依据的事实回头扩散。
501
+ graphInjectHint: z.boolean().default(false),
502
+ // 图召回候选在注入块里的独立预算(条数上限)。与 pin/document 预算同构:
503
+ // 不占 maxInjectedItems 名额、只约束「桥接线索」这类低信任信息的常驻占比。
504
+ graphInjectBudget: z.natural().min(0).max(5).default(1),
505
+
506
+ // --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
507
+ // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
508
+ // sleep fires when the store has been quiet for sleepIdleMinutes and deep-
509
+ // maintains the whole library: conflict resolution, archival demotion,
510
+ // pattern discovery and entity relation completion. Abortable on user
511
+ // activity, audited into dream_runs (run_type='sleep'), and serialized with
512
+ // autoDream so the two never overlap.
513
+ sleepModeEnabled: z.boolean().default(false),
514
+ // Quiet window before a cycle fires (minutes).
515
+ sleepIdleMinutes: z.natural().min(1).max(60).default(5),
516
+ // Minimum gap between two sleep runs (hours) — a second idle window within
517
+ // this interval does not retrigger.
518
+ sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
519
+ // Conflict adjudication strictness:
520
+ // gentle only high-confidence conflicts (threshold 0.92) are resolved
521
+ // normal standard dream-level (threshold 0.85)
522
+ // aggressive low-confidence pairs are also adjudicated (threshold 0.75)
523
+ sleepConflictStrictness: z.union([
524
+ z.const("gentle"),
525
+ z.const("normal"),
526
+ z.const("aggressive")
527
+ ]).default("normal"),
528
+ // Issue #126:冲突阶段的动作集。"conflict"(默认 = 现状)只用 conflict/keep——
529
+ // 后台自动流程不静默扩张行为;"full" 开放六分支(merge / update / supersede /
530
+ // differentiate / conflict / keep),互补型与演进型重复因此有了正确出口,不再
531
+ // 被迫"输赢化"(实测抽样 10 对里 2 对属演进型/互补型,用 conflict 处理会丢信息)。
532
+ sleepActionSet: z.union([
533
+ z.const("conflict"),
534
+ z.const("full")
535
+ ]).default("conflict"),
536
+ // Archival demotion tiering (days since last access):
537
+ // >= sleepArchiveDays → shrink to summary, full body kept in _full_content
538
+ // >= sleepCompressDays → archived outright (entity relations preserved)
539
+ sleepArchiveDays: z.natural().min(7).max(365).default(30),
540
+ sleepCompressDays: z.natural().min(7).max(365).default(90),
541
+ // Pattern discovery scan window (most recent memories to scan).
542
+ sleepPatternMinMemories: z.natural().min(10).max(1000).default(100),
543
+ // How far back pattern discovery considers entity attr changes (days).
544
+ sleepPatternLookbackDays: z.natural().min(1).max(90).default(30),
545
+ // Max pattern memories minted per run (0 = disabled).
546
+ sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
547
+ // Optional LLM route override for sleep's bulk passes (empty = use dream
548
+ // route / agent default model).
549
+ sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
550
+ sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
551
+ // Pass-through reasoning effort for sleep's LLM passes, same semantics as
552
+ // dreamReasoningEffort (Issue #135 建议 4/5): unset resolves to the lowest
553
+ // effort the model declares; explicit 'none' omits the field; off/low/
554
+ // medium/high are forwarded verbatim.
555
+ sleepReasoningEffort: z.union([
556
+ z.const("off"),
557
+ z.const("low"),
558
+ z.const("medium"),
559
+ z.const("high"),
560
+ z.const("none")
561
+ ]).description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,未配置 = 自动取模型支持的最低档;显式 'none' = 不发送字段、用服务商自带默认。"),
562
+ // Pass-through reasoning effort for the distill (summarize) LLM call
563
+ // (issue #315). Mirrors entityExtractionReasoning rather than dream: the
564
+ // default 'none' omits the field entirely (provider default), so current
565
+ // behavior is unchanged until the user opts in — no auto-lowest resolution
566
+ // here, distill failures are retried at the window level anyway. off/low/
567
+ // medium/high are forwarded verbatim; a provider that rejects the effort
568
+ // retries once without it (withEffortFallback), so opting in is safe to
569
+ // experiment with. The failure being addressed is the #9 shape on the
570
+ // distill path: a thinking model drains the output budget on reasoning and
571
+ // the summary comes back empty/truncated.
572
+ summarizeReasoningEffort: z.union([
573
+ z.const("off"),
574
+ z.const("low"),
575
+ z.const("medium"),
576
+ z.const("high"),
577
+ z.const("none")
578
+ ]).default("none").description("蒸馏(会话总结提炼)LLM 的推理档位:默认 'none' = 不发送字段、用服务商自带默认;off/low/medium/high 原样传递,被模型拒收时自动去掉字段重试一次(与巩固/睡眠同款降级,#315)。思考型模型建议 off/low,避免推理烧光输出预算。"),
579
+ // Issue #257:sleep 冲突/模式两阶段的输出预算(原硬编码 2048)。实测默认档
580
+ // 每对裁决约 90 token、24 对 2097——2048 恰好压在边界(53 次运行 48 败);
581
+ // full 档六分支实测约 290 token/对、24 对 6967,2048 必然截断。默认 8192
582
+ // 覆盖实测峰值(候选对按「每记忆至多一对」去重,饱和在 ~25 对、不随库无限
583
+ // 增长);流式计费按实际用量,不按上限。
584
+ sleepMaxTokens: z.natural().min(256).max(131072).default(8192).description("sleep 冲突消解与模式发现阶段的 LLM 输出预算上限(token)。原为硬编码 2048,sleepActionSet=full 实测需约 7000 导致裁决被截断而整轮失败;流式计费按实际用量,调大不增加成本。"),
585
+
586
+ // --- epistemic trust: memory source credibility (v0.4.5) -----------------
587
+ // Distinguish memories by source: observation (measured / witnessed),
588
+ // subjective (opinion / guess) and inferred (derived from other evidence).
589
+ // Opt-in by default: when false (default) retrieval ranking, injection
590
+ // marking and dream merge/conflict keepSource are untouched and
591
+ // epistemic_status stays inert data (still written + inferred on save, just
592
+ // never used to influence behavior).
593
+ trustEpistemicWeighting: z.boolean().default(false),
594
+
595
+ // --- memory quality filter (Bug7) ------------------------------------------
596
+ // Heuristic gate on what deserves the injection/recall surface. When enabled,
597
+ // saveWithDedupe scores each new memory after dedupe and before write:
598
+ // score >= degradeThreshold (60) → stored normally
599
+ // archiveThreshold (30) <= score < 60 → quality_score persisted and the
600
+ // injection sort re-ranks by importance * quality_score/100 (degraded)
601
+ // score < 30 → archived + tagged low_quality (still explicitly searchable)
602
+ // Meta-memory markers, near-duplicates and repetitive filler lose points.
603
+ memoryQualityFilter: z.object({
604
+ enabled: z.boolean().default(true),
605
+ archiveThreshold: z.natural().min(1).max(100).default(30),
606
+ degradeThreshold: z.natural().min(1).max(100).default(60),
607
+ minContentLength: z.natural().min(1).max(1000).default(10),
608
+ // Issue #135 附属发现 1:importance ≥ 该值的记忆不参与静默自动归档——评分
609
+ // 与信号标签照常写入(可观测)、注入排序照常降权,但 setArchived 跳过。
610
+ // 报告实测 150 条低分归档里 128 条 importance ≥ 4(51 条 = 5)。设 1 = 全部
611
+ // 豁免(等效关闭自动归档),设 5 = 仅最重要的豁免。
612
+ exemptImportance: z.natural().min(1).max(5).default(4)
613
+ }).default({}),
614
+
615
+ // --- LLM audit trail (Bug8) ------------------------------------------------
616
+ // Records every background LLM call into llm_audit_logs: tokens, duration,
617
+ // status and which trigger produced it. Failures are recorded as
618
+ // status='error' and never block the feature. retentionDays bounds the table:
619
+ // older rows are purged on boot.
620
+ //
621
+ // Covered trigger_source values: autoDream (consolidation + summary),
622
+ // autoSummarize (compression), sleep (conflict + pattern) and entityExtract
623
+ // (issue #250 — the last two were structurally missing the hook, not a
624
+ // deliberate narrowing). entityExtract runs on every memory write, so row
625
+ // growth scales with write volume; retentionDays caps the ceiling but not the
626
+ // rate. If that turns out too fast, sample by operation_type or give it a
627
+ // separate retention — do not add a second gate: this flag is the single one.
628
+ llmAudit: z.object({
629
+ enabled: z.boolean().default(true),
630
+ retentionDays: z.natural().min(1).max(3650).default(90)
631
+ }).default({}),
632
+
633
+ // --- write admission (#254) ------------------------------------------------
634
+ // 写入准入:把「这条该不该进库」前移到 LLM 之前。与 memoryQualityFilter 的分工
635
+ // 是时点不是判据——那个是**写入后**打分归档(扣分可以错,只影响排序与归档),
636
+ // 这个是**写入前**判定(硬拒不能错)。
637
+ //
638
+ // 两个开关都默认关(验收第 1 条「默认关零行为变化」):
639
+ // enabled — 跑第 1 级确定性判据。关时 evaluate 的返回与只计量那一阶段逐字段
640
+ // 一致,写入路径完全不碰。
641
+ // enforce — 命中时真的拒绝。关时判据照跑、审计照留,决策回落 allow(仅告警)。
642
+ // 读审计时「metadata.deny 非空且 metadata.decision=allow」就是这一档的指纹。
643
+ // 分层是为了能把「先观察」和「开始拦」分成两次上线:enforce 打开前先在真实流量
644
+ // 里看误杀面,而不是靠回滚开关当保险。
645
+ //
646
+ // 第 1 级的判据面刻意收窄到两类:密钥/PII(#164 A2 是判据来源,经
647
+ // createWriteAdmission 的 sensitiveScan 注入;本批只定义它怎么被消费)与空白/
648
+ // 纯噪声文本。去重键命中**不**进第 1 级——按维护者 09-22 的判定归 write-update
649
+ // 放行(2606.24535 的失效模式:同步近重复门跑在异步矛盾检测之前,把本该被裁决
650
+ // 的矛盾直接拒掉);G1/G2 的阈值继续只计量,等遥测有分布再单独提。
651
+ writeAdmission: z.object({
652
+ enabled: z.boolean().default(false),
653
+ enforce: z.boolean().default(false)
654
+ }).default({}),
655
+
656
+ // --- issue #380: pre-inject gate (opinion/stance judgment before injection) -
657
+ // 注入前判定:每帧注入候选出池后、进入 system prompt 前,一次池级 LLM 调用判出
658
+ // 「会向本次请求注入意见/立场」的记忆(E12 D1 口径,判定协议同源实验
659
+ // protocol-preinject.txt)。两级语义复用 writeAdmission:
660
+ // enabled — 跑判定。判定 async prefetch + cache、同步消费(宿主 systemPrompt
661
+ // 渲染是同步回调,冷缓存首帧降级为不判定原样注入,下一轮同 key 生效);
662
+ // 判定完成落 llm_audit_logs(trigger_source=preInjectGate,真实 token 用量,
663
+ // related_memory_ids=被标记记忆)——先在真实负载看意见占比分布。
664
+ // enforce — 缓存命中帧真的滤除被标记候选。E12 实测只有过滤过判据(D1 −24.4pp,
665
+ // 判定+标记 D2 比它差 +17.6pp——给模型看标记的线已被 E2/E3 关闭,本闸判定
666
+ // 结果绝不进模型上下文)。enforce 无 enabled 时无效果。
667
+ // 降级路径:判定失败/超时/不可解析 → 本帧原样注入 + 审计行 degraded=true 且不
668
+ // 缓存——防线故障永不阻塞注入。判定 LLM 走 agentDefaultModel 当前选择(entity
669
+ // adapter 同款装配),不设独立 provider/model 键;llmAudit.enabled=false 时本闸
670
+ // 不运行(无账目的判定不可见也不该花钱)。lightMode 强制关(LLM-per-turn 附加
671
+ // 路径与低资源档互斥)。
672
+ preInjectGate: z.object({
673
+ enabled: z.boolean().default(false),
674
+ enforce: z.boolean().default(false)
675
+ }).default({}),
676
+
677
+ // --- #164 A2: secret / PII scan at the write boundary ----------------------
678
+ // 写入边界的密钥 / PII 判据(src/sensitive-scan.js)自身的闸。与 writeAdmission
679
+ // 的 enabled 分开是有意的:那一个管 #254 第 1 级的空白 / 噪声判据,本键管 A2 这
680
+ // 一类判据,两批的误杀面差一个量级(空白 / 噪声没有解释空间,邮箱 / 手机号有),
681
+ // 绑在同一个开关上就没法单独观察 A2 的命中分布。
682
+ //
683
+ // 分层必须写清:判据的唯一调用点在 #254 第 1 级的闸门里(write-admission.js 的
684
+ // firstLevelHit),闸门不走第 1 级就没人来调这个扫描器。所以本键是「闸门内这一批
685
+ // 判据参不参与」,不是一条能独立跑的链路——单开本键就是零行为变化:
686
+ // writeAdmission.enabled 关 → 第 1 级整个不跑,本键开也没用
687
+ // enabled 开 + 本键关 → 只跑空白 / 噪声那一批
688
+ // enabled 开 + 本键开 + enforce 关 → 命中留审计,写入照常(观察档)
689
+ // enabled 开 + 本键开 + enforce 开 → 命中即拒绝
690
+ // 默认关 = 只计量那一阶段的行为逐字节保留(#332 合并时 sensitiveScan 就是 null)。
691
+ // #164 的「默认仅告警、拦截 opt-in」落在 enforce 上:命中落一条审计
692
+ // (metadata.deny.kind)但照常写入,真要拦得 enabled 与 enforce 同时开。
693
+ // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
694
+ sensitiveScanEnabled: z.boolean().default(false),
695
+
696
+ // --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
697
+ // Separate retrieval evaluation snapshots from the production recall audit.
698
+ // When false (default) evaluateRetrieval still computes precision/recall/mrr
699
+ // and returns them to the caller, but writes nothing to recall_evals — the
700
+ // eval table only grows when the operator opts in. Production searchMemories
701
+ // audits to recall_runs and NEVER touches recall_evals, regardless of this
702
+ // flag (production isolation is unconditional).
703
+ evalPersistTestResults: z.boolean().default(false),
704
+
705
+ // --- standalone external API (v0.7.12) ------------------------------------
706
+ // A plain node:http server for ecosystem integrations that cannot reach the
707
+ // DSH-internal webServer. Disabled by default; when enabled the Bearer token
708
+ // is persisted in the settings kv ("external_api"), auto-generated on first
709
+ // boot. Bind host: keep the loopback default — moving it to a non-loopback
710
+ // address exposes the whole memory store to the network and is the
711
+ // operator's responsibility.
712
+ externalApiEnabled: z.boolean().default(false),
713
+ externalApiPort: z.natural().default(8790),
714
+ externalApiHost: z.string().default("127.0.0.1"),
715
+
716
+ // --- light mode preset (v0.7.12) -------------------------------------------
717
+ // One switch for low-resource setups: turns off every background/semantic
718
+ // heavy path (dream consolidation, entity extraction, vector pipeline,
719
+ // reranker, BM25, semantic dedup / selective inject, sleep mode) while
720
+ // keeping the core loop (autoInject, autoSummarize, hot memory, quality
721
+ // filter, keyword search). Applied by applyLightModePreset before the config
722
+ // reaches any service; a persisted panel_mode="light" (settings kv) counts
723
+ // as lightMode=true too and wins over the bundle config.
724
+ lightMode: z.boolean().default(false),
725
+
726
+ // --- heat: v0.7.0 self-evolution (heat + interest drift) ----------------
727
+ // 总开关,默认关(v0.7.12+ 用户已习惯无 heat 行为,默认开=全员行为变更)。
728
+ // 开启后:提供热度字段 / sleep 降级联合判定保护 / 前端热度投影,并只在
729
+ // 注入排序的优先级层内乘 heat(#218 v1;store 的 order=chrono 分页序与
730
+ // 召回融合序不动)。关闭则跳过所有 heat 计算与热度触达,注入排序乘数
731
+ // 恒 1,sleep 降级退回纯时间分层。
732
+ // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
733
+ heatEnabled: z.boolean().default(false),
734
+ // issue #218 / E5 效用考卷:heat 乘进注入排序在真实年龄混合下饿死老约束
735
+ // (现行量级 ≡ 拟合参数,注入集逐条相同;importance-only 遵从 +12.7pp),
736
+ // 故注入侧 heat 改为独立 opt-in:默认关 = heatEnabled 开启后热度字段/sleep
737
+ // 保护/前端投影照常,但注入排序乘数恒 1(与 heat 关闭逐字节一致);
738
+ // 显式开启才恢复「层内乘 heat」的旧序。召回侧时钟(touchLastAccess)与
739
+ // sleep 降级联判不受本键影响——它们不参与注入排序。
740
+ injectHeatEnabled: z.boolean().default(false),
741
+ // 广义指数形状参数 β(heat = exp(-λ·Δt^β),issue #218 拍板):β=1 纯指数。
742
+ // 快慢以 Δt>1 小时为准——β<1 衰减更慢(亚线性长尾)、β>1 更快(超线性);
743
+ // 0<Δt<1 的首小时内方向相反(Δt^β 随 β 增大而变小)。专家调优项,不进面板白名单。
744
+ heatGlobalBeta: z.number().min(0.5).max(2).default(1.0),
745
+ // per-type 衰减因子 λ;λ=0 的类型免疫(热度恒 1.0,sleep 永不降级)。
746
+ // 未知类型走默认 0.002。dict 的键为 type 字符串、值为数字 λ。
747
+ heatTypeDecay: z.dict(z.number(), z.string()).default({ ...TYPE_DECAY_DEFAULTS }),
748
+ // sleep 降级联合判定的热度下限:heat < 该值 且 importance<5 才允许降级。
749
+ sleepHeatThreshold: z.number().min(0).max(1).default(0.05),
750
+ // recordRecall 默认值(recall_runs 记录默认开;显式传 false 的调用方不受影响)。
751
+ recallRecordDefault: z.boolean().default(true),
752
+ // recall_runs 滚动清理保留天数。
753
+ recallRetentionDays: z.natural().min(1).max(3650).default(90),
754
+
755
+ // --- tool exposure: 慢模型/轻量模型的工具往返节流(v0.8.5)----------------
756
+ // 跨会话记忆已由 inject.js 每轮自动注入系统提示词,memory_search 只用于
757
+ // 「注入里没有、需要深挖」的补充检索;memory_archive 是隐藏/恢复条目的整理
758
+ // 操作,正常会话里很少需要。轻量模型对「何时该用工具」判断弱,容易每轮
759
+ // 顺手调一遍——每次工具调用都是一次串行往返(生成参数→执行→回填→再生成),
760
+ // 在慢模型上会被放大成明显卡顿。这两个开关允许直接隐藏对应工具(默认全
761
+ // 开=行为不变),隐藏后模型根本看不到它,也就不会调。也走 feature_flags
762
+ // 白名单,面板可启停=线上回滚开关。
763
+ disableMemorySearch: z.boolean().default(false),
764
+ disableMemoryArchive: z.boolean().default(false),
765
+
766
+ // --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
767
+ // 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
768
+ // 逐字节不变。开启后 memory_save 写入 agent_scope(session header 的
769
+ // agentPreset)与 workspace_scope(registry 反查 canonical path,header.cwd
770
+ // 兜底,取不到 NULL——解析绝不阻塞写入),去重键扩展为 (type, title,
771
+ // agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
772
+ // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
773
+ scopeEnabled: z.boolean().default(false),
774
+ // strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见,
775
+ // 检索与注入排序同权重,issue #339 补齐注入通道);
776
+ // 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
777
+ // 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
778
+ // 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
779
+ // 打开才有意义(没有写入标注就无可过滤维度),但独立成键:可先开标注积累
780
+ // 数据、观察 A2 加权质量后再切硬过滤。
781
+ strictScope: z.boolean().default(false),
782
+ });
783
+
784
+ // Fields forced to false by the light-mode preset. Everything not listed here
785
+ // (autoInject, autoSummarize, hotMemory*, memoryQualityFilter, dream
786
+ // thresholds/delays, ...) is left untouched — those are the core loop.
787
+ const LIGHT_MODE_OFF = [
788
+ "entityExtractionEnabled",
789
+ "autoDream",
790
+ "sleepModeEnabled",
791
+ "rerankEnabled",
792
+ "autoReindexOnBoot",
793
+ "hybridInject",
794
+ // #249 第二批:能力说明在轻量档保持关闭——它是工具描述与提示段上的额外常驻
795
+ // 文本,轻量档(小模型 / 小上下文)不该因默认值翻转而多付这份提示成本。
796
+ "injectGuidanceEnabled",
797
+ "searchSemanticDedup",
798
+ "selectiveInjectEnabled",
799
+ "bm25SearchEnabled",
800
+ // 轻量模式不开图召回轴(#219,依赖实体抽取产出;抽取本身已被关掉)。
801
+ "entityRecallEnabled",
802
+ // 轻量模式不开图谱锚定层级联(#24 块1,依赖实体抽取 + 关系表,抽取已关则无边可扩)。
803
+ "graphAnchoringEnabled",
804
+ // 轻量模式不开边权重演化(#24 块2,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
805
+ "graphWeightEnabled",
806
+ // 轻量模式不开关联提示标注(#24 块3,常驻注入文本多一段提示成本)。
807
+ "graphInjectHint",
808
+ // 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
809
+ "graphPassiveConfirm",
810
+ // 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
811
+ "dreamNarrativeEnabled",
812
+ // 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
813
+ "documentMemoryEnabled",
814
+ // 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
815
+ "heatEnabled",
816
+ // 轻量模式不开注入前判定(#380:LLM-per-turn 附加路径,每帧注入多一次池级
817
+ // 判定调用——与低资源档互斥;预设给的默认值,用户显式勾选仍然赢)。点分键
818
+ // 由 applyLightModePreset 的嵌套赋值分支处理。
819
+ "preInjectGate.enabled",
820
+ // #249 N3:轻量档默认不开压缩边缘双落点——它往对话里追加消息(新的注入表面),
821
+ // 轻量档(小模型 / 小上下文)最不该再多一份注入物。这是预设给的默认值、不是强制:
822
+ // 用户显式勾选仍然赢(合并顺序「用户开关 > 轻量预设 > bundle 配置」,同
823
+ // injectGuidanceEnabled)。
824
+ "continuityRescueEnabled"
825
+ ];
826
+
827
+ /**
828
+ * Apply the light-mode preset to a resolved config object (pure function,
829
+ * exported for tests). When cfg.lightMode is not exactly true the config is
830
+ * returned unchanged; otherwise a shallow copy carries false for every heavy
831
+ * feature. Idempotent and side-effect free.
832
+ */
833
+ export function applyLightModePreset(cfg) {
834
+ if (cfg?.lightMode !== true) return cfg;
835
+ const preset = { ...cfg, lightMode: true };
836
+ for (const key of LIGHT_MODE_OFF) {
837
+ // 点分键(#380 preInjectGate.enabled):对象子键的预设翻转——浅拷贝父对象后
838
+ // 改子键,避免直接改到用户 config 里的同一引用。平铺键照旧直赋。
839
+ const dot = key.indexOf(".");
840
+ if (dot < 0) {
841
+ preset[key] = false;
842
+ continue;
843
+ }
844
+ const parent = key.slice(0, dot);
845
+ preset[parent] = { ...(preset[parent] ?? {}), [key.slice(dot + 1)]: false };
846
+ }
847
+ return preset;
848
+ }
849
+
850
+ /**
851
+ * #249 第二批:注入形态的父/子开关。父 = `autoInject`(既有的挂载总闸,默认
852
+ * 开)。子项按 #249 §10 的判据分档——看它是否引入**新的注入时机/新的注入
853
+ * 表面/额外成本**:
854
+ *
855
+ * - 只修正既有每轮块内的位(能力说明)→ 随父开关生效,键的默认值给开;
856
+ * - 需要新时机或新表面(N2 回合结束提醒、N3 压缩边缘)→ 独立开关、默认关,
857
+ * 实现时挂到这张表下(今天还没有这两个键,故表里只有能力说明一项)。
858
+ *
859
+ * 语义:**父关 = 子项一律不生效,但子项的持久值原样保留**(父关是「不生效」,
860
+ * 不是「重置用户配置」)。用户显式写进 kv 的值也不因父关被改写。
861
+ *
862
+ * 闸门放在消费点而不是合并后的 cfg 上:压 cfg 会让 `/features` 的 effective
863
+ * 失去「子项自己勾着、父关时当前不生效」这个状态,面板就显示不出来。跨文件
864
+ * 契约见 test/inject-parent-gate.test.js(含面板侧关系的漂移检查)。
865
+ *
866
+ * 表里只放**顶层扁平键**:点号键(如 `memoryQualityFilter.enabled`)在合并后的
867
+ * cfg 里是嵌套对象,`cfg?.["a.b"]` 取不到,挂进来会恒判不生效且不报错。N2/N3
868
+ * 是扁平键,不受这条限制。
869
+ */
870
+ export const INJECT_CHILD_FLAGS = Object.freeze({
871
+ autoInject: Object.freeze(["injectGuidanceEnabled", "continuityRescueEnabled"])
872
+ });
873
+
874
+ /** 子开关的运行时生效值:父开关显式关(false)时恒不生效。 */
875
+ export function injectChildEnabled(cfg, key) {
876
+ for (const [parent, children] of Object.entries(INJECT_CHILD_FLAGS)) {
877
+ if (children.includes(key) && cfg?.[parent] === false) return false;
878
+ }
879
+ return cfg?.[key] === true;
880
+ }