@modusensus/dsh-mneme 0.8.3 → 0.8.5

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 (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +727 -636
  3. package/bin/cli.mjs +0 -0
  4. package/bin/dsh-mneme-mcp.mjs +510 -0
  5. package/cordis.patch.yml +21 -21
  6. package/lib/api-standalone.js +506 -316
  7. package/lib/api.js +1403 -1316
  8. package/lib/bootstrap.js +211 -0
  9. package/lib/client.js +4657 -4366
  10. package/lib/commands.js +95 -95
  11. package/lib/config.js +615 -510
  12. package/lib/document.js +221 -0
  13. package/lib/dream/clustering.js +118 -118
  14. package/lib/dream/decisions.js +658 -658
  15. package/lib/dream/narratives.js +44 -0
  16. package/lib/dream/sleep.js +702 -700
  17. package/lib/dream.js +147 -16
  18. package/lib/embedding.js +177 -177
  19. package/lib/entities/extractor.js +249 -249
  20. package/lib/guide.js +44 -0
  21. package/lib/heat.js +144 -136
  22. package/lib/hot-memory.js +46 -46
  23. package/lib/index.js +515 -515
  24. package/lib/inject.js +375 -295
  25. package/lib/lang.js +208 -179
  26. package/lib/local-embedder.js +320 -320
  27. package/lib/mirror.js +198 -198
  28. package/lib/organize.js +450 -0
  29. package/lib/quality-filter.js +126 -126
  30. package/lib/recall-stats.js +125 -0
  31. package/lib/reranker.js +269 -269
  32. package/lib/runtime/adopt.js +176 -176
  33. package/lib/runtime/closure.js +172 -172
  34. package/lib/runtime/download.js +247 -247
  35. package/lib/runtime/layout.js +265 -235
  36. package/lib/runtime/loader.js +252 -249
  37. package/lib/runtime/model-fetch.js +361 -361
  38. package/lib/runtime/provision.js +237 -237
  39. package/lib/runtime/tarball.js +130 -130
  40. package/lib/runtime/verify.js +204 -191
  41. package/lib/scope.js +118 -118
  42. package/lib/search/adaptive.js +60 -22
  43. package/lib/search/bm25.js +96 -96
  44. package/lib/service.js +2324 -2087
  45. package/lib/settings.js +459 -420
  46. package/lib/store.js +2549 -2367
  47. package/lib/summarize.js +820 -449
  48. package/lib/tools.js +702 -574
  49. package/lib/vector-index.js +110 -110
  50. package/lib/version-check.js +122 -122
  51. package/package.json +109 -108
  52. package/runtime-manifest.json +526 -596
  53. package/scripts/benchmark-embed.js +201 -201
  54. package/scripts/benchmark-recall.js +199 -199
  55. package/scripts/benchmark-rerank.js +166 -166
  56. package/scripts/build-runtime-manifest.mjs +224 -217
  57. package/scripts/check-sync.js +42 -42
  58. package/scripts/e2e-dsh.js +220 -220
  59. package/scripts/mneme-runtime.mjs +277 -277
  60. package/scripts/stress-dsh.js +255 -255
  61. package/scripts/sync-lib.js +47 -47
  62. package/scripts/sync-test-badge.mjs +67 -61
  63. package/src/api-standalone.js +506 -316
  64. package/src/api.js +1403 -1316
  65. package/src/bootstrap.js +211 -0
  66. package/src/commands.js +95 -95
  67. package/src/config.js +615 -510
  68. package/src/document.js +221 -0
  69. package/src/dream/clustering.js +118 -118
  70. package/src/dream/decisions.js +658 -658
  71. package/src/dream/narratives.js +44 -0
  72. package/src/dream/sleep.js +702 -700
  73. package/src/dream.js +147 -16
  74. package/src/embedding.js +177 -177
  75. package/src/entities/extractor.js +249 -249
  76. package/src/guide.js +44 -0
  77. package/src/heat.js +144 -136
  78. package/src/hot-memory.js +46 -46
  79. package/src/index.js +515 -515
  80. package/src/inject.js +375 -295
  81. package/src/lang.js +208 -179
  82. package/src/local-embedder.js +320 -320
  83. package/src/mirror.js +198 -198
  84. package/src/organize.js +450 -0
  85. package/src/quality-filter.js +126 -126
  86. package/src/recall-stats.js +125 -0
  87. package/src/reranker.js +269 -269
  88. package/src/runtime/adopt.js +176 -176
  89. package/src/runtime/closure.js +172 -172
  90. package/src/runtime/download.js +247 -247
  91. package/src/runtime/layout.js +265 -235
  92. package/src/runtime/loader.js +252 -249
  93. package/src/runtime/model-fetch.js +361 -361
  94. package/src/runtime/provision.js +237 -237
  95. package/src/runtime/tarball.js +130 -130
  96. package/src/runtime/verify.js +204 -191
  97. package/src/scope.js +118 -118
  98. package/src/search/adaptive.js +60 -22
  99. package/src/search/bm25.js +96 -96
  100. package/src/service.js +2324 -2087
  101. package/src/settings.js +459 -420
  102. package/src/store.js +2549 -2367
  103. package/src/summarize.js +820 -449
  104. package/src/tools.js +702 -574
  105. package/src/vector-index.js +110 -110
  106. package/src/version-check.js +122 -122
  107. package/test/adaptive-inject-budget.test.js +85 -0
  108. package/test/api-routes-gaps.test.js +218 -218
  109. package/test/api.test.js +1640 -1482
  110. package/test/audit.test.js +448 -448
  111. package/test/benchmark.test.js +94 -94
  112. package/test/bootstrap.test.js +220 -0
  113. package/test/client.test.js +860 -736
  114. package/test/clustering.test.js +100 -100
  115. package/test/commands.test.js +106 -106
  116. package/test/config.test.js +66 -66
  117. package/test/conflict-freeze.test.js +300 -300
  118. package/test/document.test.js +523 -0
  119. package/test/dream-archive-guardrail.test.js +116 -116
  120. package/test/dream-candidate-hybrid.test.js +281 -281
  121. package/test/dream-coverage-degrade.test.js +86 -86
  122. package/test/dream.test.js +1308 -1254
  123. package/test/embedding-ready.test.js +196 -196
  124. package/test/entities.test.js +547 -547
  125. package/test/entity-recall.test.js +179 -0
  126. package/test/epistemic.test.js +298 -298
  127. package/test/fnew-0112.test.js +311 -311
  128. package/test/fnew-03.test.js +422 -422
  129. package/test/graph-api.test.js +175 -175
  130. package/test/heat.test.js +175 -148
  131. package/test/helpers/dream-mock.js +82 -82
  132. package/test/helpers/peer-worker.mjs +25 -25
  133. package/test/helpers/runtime-source.js +60 -60
  134. package/test/helpers/tar-builder.js +80 -80
  135. package/test/hot-memory.test.js +145 -145
  136. package/test/inject-content-cap.test.js +91 -0
  137. package/test/inject-first-round-bm25.test.js +89 -89
  138. package/test/inject-heat.test.js +61 -0
  139. package/test/inject-pools.test.js +170 -0
  140. package/test/inject-recall.test.js +105 -0
  141. package/test/inject-rotation.test.js +161 -161
  142. package/test/inject.test.js +283 -249
  143. package/test/lang.test.js +87 -87
  144. package/test/lib-smoke.test.js +96 -96
  145. package/test/llm-audit.test.js +462 -309
  146. package/test/llm-message-source.test.js +177 -177
  147. package/test/local-embedder.test.js +249 -249
  148. package/test/mcp-stdio.test.js +385 -0
  149. package/test/mirror-dirty.test.js +424 -424
  150. package/test/mirror-edit-digest.test.js +187 -187
  151. package/test/mirror-generation.test.js +499 -499
  152. package/test/mirror.test.js +249 -249
  153. package/test/narrative-bars.test.js +162 -0
  154. package/test/organize.test.js +418 -0
  155. package/test/peer-blockers.test.js +193 -193
  156. package/test/policy-epoch.test.js +259 -259
  157. package/test/quality-filter.test.js +186 -186
  158. package/test/reasoning-effort.test.js +746 -746
  159. package/test/recall-evals.test.js +235 -235
  160. package/test/recall-layer.test.js +326 -326
  161. package/test/recall-stats.test.js +123 -0
  162. package/test/receipt-chain.test.js +451 -451
  163. package/test/reflection.test.js +226 -226
  164. package/test/reindex-backfill.test.js +62 -62
  165. package/test/reranker-single-logit.test.js +73 -73
  166. package/test/reranker.test.js +197 -197
  167. package/test/resident-status.test.js +83 -0
  168. package/test/runtime-adopt.test.js +206 -206
  169. package/test/runtime-cli.test.js +89 -89
  170. package/test/runtime-closure.test.js +193 -193
  171. package/test/runtime-download.test.js +302 -302
  172. package/test/runtime-layout.test.js +163 -163
  173. package/test/runtime-loader.test.js +255 -237
  174. package/test/runtime-manifest.test.js +159 -158
  175. package/test/runtime-model-fetch.test.js +348 -348
  176. package/test/runtime-provision.test.js +101 -101
  177. package/test/runtime-tarball.test.js +81 -81
  178. package/test/runtime-verify.test.js +279 -214
  179. package/test/scope-candidates.test.js +239 -239
  180. package/test/scope-explicit.test.js +452 -452
  181. package/test/scope-retrieval.test.js +197 -197
  182. package/test/scope-storage.test.js +255 -255
  183. package/test/scope-strict.test.js +231 -231
  184. package/test/search-fusion.test.js +90 -90
  185. package/test/semantic.test.js +124 -124
  186. package/test/service-search.test.js +174 -174
  187. package/test/service.test.js +405 -405
  188. package/test/settings.test.js +261 -261
  189. package/test/skip-invalid-restore.test.js +262 -262
  190. package/test/sleep-action-set.test.js +422 -422
  191. package/test/sleep-heat.test.js +126 -125
  192. package/test/sleep-scheduler-rearm.test.js +89 -89
  193. package/test/sleep.test.js +410 -373
  194. package/test/standalone-api.test.js +544 -359
  195. package/test/store-vector-cache.test.js +98 -98
  196. package/test/store.test.js +396 -383
  197. package/test/stress.test.js +209 -209
  198. package/test/summarize.test.js +1288 -575
  199. package/test/tools-dto-schema-matrix.test.js +325 -0
  200. package/test/tools-memory-get-schema.test.js +65 -65
  201. package/test/tools.test.js +374 -341
  202. package/test/updated-at-semantics.test.js +113 -113
  203. package/test/vector-index.test.js +221 -221
  204. package/test/version-check.test.js +152 -152
  205. package/test/webServer-optional.test.js +82 -82
package/src/config.js CHANGED
@@ -1,510 +1,615 @@
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
- // 落库前去重档位:off(默认,等同现状)/ title(零成本,仅拦完全同名)/
28
- // vector(复用已有 embedding 列做同会话语义近邻,无 LLM 调用)。
29
- summarizeDedupeMode: z.union([z.const("off"), z.const("title"), z.const("vector")]).default("off"),
30
- summarizeDedupeMinSim: z.number().min(0.5).max(0.99).default(0.92),
31
- summarizeDedupeWindowHours: z.natural().min(0).max(168).default(24),
32
- // 智能调速器(429 保护,默认开):蒸馏 LLM 调用全局串行排队,相邻请求
33
- // 间隔 distillRateLimitIntervalMs(默认 1s 一次);命中 429 限流时按
34
- // distillRateLimitBaseDelayMs 指数退避(1s→2s→4s…)自动重试
35
- // distillRateLimitRetries 次,全程对用户透明,不把 429 错误码抛给用户。
36
- distillRateLimitIntervalMs: z.natural().min(0).max(60000).default(1000),
37
- distillRateLimitRetries: z.natural().min(0).max(10).default(3),
38
- distillRateLimitBaseDelayMs: z.natural().min(100).max(60000).default(1000),
39
- maxInjectedItems: z.natural().min(1).max(20).default(5),
40
- // Issue #205:注入位跨轮轮换。同一会话里,最近 N 个不同用户查询轮次注入过
41
- // 的记忆本轮不再优先(新鲜优先、原序回填,槽位数不变);长会话中避免相邻
42
- // 轮次反复注入同一条(实测 15.5h 会话 86 次注入 93% 重复)。0 = 关闭(默认,
43
- // 保持既有行为)。会话边界自动重置:新会话从零开始。
44
- injectRotationTurns: z.natural().min(0).max(20).default(0),
45
- importanceThreshold: z.natural().min(1).max(5).default(3),
46
- // 编码记忆蒸馏(codingRetrospect,opt-in,默认关)。开启时,turn/end 蒸馏
47
- // 额外提取三类编码专属记忆:rejected_solution(被否决方案)/ pitfall(踩坑)/
48
- // constraint(工程约束)。蒸馏上下文为整轮完整对话(用户输入 → 助手思考/回答
49
- // → 工具调用与结果 → 代码执行),不再只看用户消息,便于提炼踩坑根因。
50
- // 关闭时行为与之前完全一致。
51
- codingRetrospect: z.boolean().default(false),
52
- // 编码任务识别词表(读取侧门控用):命中即视为编码类任务,编码记忆才注入。
53
- codingKeywords: z.array(z.string()).default([
54
- "代码", "编码", "写一个", "写个", "实现", "函数", "方法", "类",
55
- "接口", "bug", "调试", "报错", "错误", "异常", "堆栈", "脚本",
56
- "python", "javascript", "typescript", "node", "js", "ts",
57
- "sql", "sqlite", "数据库", "算法", "重构", "优化", "性能",
58
- "测试", "单测", "修复", "补丁", "依赖", "npm", "pip",
59
- "命令行", "shell", "配置", "配置文件", "yaml", "json",
60
- "插件", "开发", "编译", "构建", "部署", "git", "commit",
61
- "review", "前端", "后端", "页面", "组件", "dsh", "memos"
62
- ]),
63
- // 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
64
- // 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
65
- codingBoostFactor: z.number().min(1).max(5).default(2),
66
- // 注入边界花括号转义(issue #162,v0.7.4 曾修后被 v0.7.11 重构误删):默认开启,
67
- // 在 memory / 短期上下文 / 用户设置三处注入出口对连续 2+ 个花括号插 `\`
68
- // (`{{a}}`→`{\{a\}\}`),让 DSH 的 interpolate() 不再扫到非法变量名而 throw。
69
- // 关闭时按原样透传。
70
- escapePromptVariables: z.boolean().default(true),
71
- autoDream: z.boolean().default(true),
72
- dreamThresholdCount: z.natural().min(1).max(1000).default(10),
73
- dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
74
- dreamDelayMs: z.natural().min(0).max(60000).default(2000),
75
- // autoDream 触发最小间隔(分钟,0 = 不限制,Issue #89 请求 2):高频写入
76
- // 场景下防止巩固调用(含失败重试)连发刷爆配额。间隔从每次实际开跑时刻
77
- // 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次
78
- // 写入事件会重新评估。
79
- dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
80
- // 巩固模型路由(settings panel「巩固模型」/ dreamProvider+dreamModel):
81
- // dream 的记忆沉淀专用 LLM 路由,显式配置优先于 agent 默认模型(config-first,
82
- // Issue #25)。模型分类声明:
83
- // - 非思考模型(推荐,如 glm-5-2 类):无 reasoning 声明,effort 请求被 harness
84
- // 拒绝后 withEffortFallback 去掉字段重试即成功;空体/no json array 风险最低。
85
- // - 思考模型(如 deepseek-v4-flash-ga 等 v4-flash-ga 系):默认开推理,可能烧光
86
- // token 预算返回空体;且部分(如 v4-flash-ga)在 harness 侧被声明为不接受任何
87
- // reasoning effort —— 即使去掉 effort 重试,harness 的 defaultEffort 也会顶上来
88
- // 再次拒绝(UNSUPPORTED_REASONING_EFFORT),插件 fallback 无法绕开。
89
- // 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
90
- dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
91
- dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
92
- // Issue #135 建议 3:#9 只抬了 max(8192→131072),default 停在 32768。思考型
93
- // 模型的推理与正文共享该预算,effort 未压低时 32768 恰好被推理烧光 → 空体
94
- // ("no json array in llm output",报告实测 146 轮因此失败)。默认抬到上限,
95
- // 让默认配置也留足正文预算;流式计费按实际用量,不按上限。
96
- dreamMaxTokens: z.natural().min(256).max(131072).default(131072),
97
- // Pass-through reasoning effort for dream's LLM calls (Issue #135 建议 4/5).
98
- // Unset (no value) resolves to the LOWEST effort the model declares before
99
- // streaming: background consolidation must produce JSON, and omitting the
100
- // field lets the harness substitute the model's defaultEffort (thinking-type
101
- // models often default to high/max) — the reasoning then drains the whole
102
- // token budget and the run dies with an empty body. Explicit 'none' still
103
- // omits the field (provider default applies); off/low/medium/high are
104
- // forwarded verbatim, unsupported values are remapped (v0.7.26+).
105
- // Caveat: on some thinking models (e.g. v4-flash-ga) the harness declares NO
106
- // supported effort, so even the fallback retry (field stripped) is rejected
107
- // again via its defaultEffort — prefer a non-reasoning dreamProvider/dreamModel.
108
- dreamReasoningEffort: z.union([
109
- z.const("off"),
110
- z.const("low"),
111
- z.const("medium"),
112
- z.const("high"),
113
- z.const("none")
114
- ]).description("巩固模型的推理档位:未配置 = 自动取模型支持的最低档(避免思考模型用自带默认档烧光预算返回空体,#135);显式 'none' = 不发送字段、用服务商自带默认;off/low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
115
- // 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
116
- // 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
117
- // 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
118
- dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
119
- // Issue #104 方向 2:单轮 archive 决策上限。一次性大扫除(12 条互不相关主题
120
- // 被批量归档,其中 8 条疑似误伤长保留类型)是失控信号,与 update 上限同类的
121
- // 全局闸门:skipInvalid 也不豁免,超限整单拒绝。正常清理可调高。
122
- dreamMaxArchivePerRun: z.natural().min(1).max(200).default(8),
123
- // Issue #125:候选集构造方式。"window"(默认,等同现状)只取最近
124
- // dreamMaxSnapshotSize 条;"hybrid" 在此基础上并入**向量翻出的高相似组**——
125
- // 本机实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口(窗口覆盖率 11.7%),
126
- // 纯时间窗口让"该合并的一对"几乎永远碰不到面。候选总量仍由下面的上限封顶、
127
- // 与库总量解耦,这是相对"调大窗口"的核心收益:输入成本不随库增长。
128
- dreamCandidateMode: z.union([
129
- z.const("window"),
130
- z.const("hybrid")
131
- ]).default("window"),
132
- // hybrid 的候选总量上限;0 = 复用 dreamMaxSnapshotSize(不迁移,需要时显式覆盖)。
133
- dreamCandidateMax: z.natural().min(0).max(5000).default(0),
134
- // hybrid 判"高相似"的阈值;0.85 与 sleep normal 档对齐——两个模块对"高相似"
135
- // 保持同一个定义。
136
- dreamCandidateMinSim: z.number().min(0.5).max(0.99).default(0.85),
137
- // 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
138
- // 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
139
- // (未覆盖即拒绝整单)。
140
- dreamImplicitKeep: z.boolean().default(true),
141
- // 显式决策覆盖率下限(v0.4.4 fix):dreamImplicitKeep 开启时,LLM 输出被
142
- // 截断只显式 claim 少量 snapshot 记忆(claimed.size / snapshot.size < 该阈值)
143
- // → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
144
- // 默认 0.5(至少显式覆盖一半 snapshot)。
145
- dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
146
- // Issue #89:v0.6.9(Issue #26)的宽容路径回归。默认跳过单条非法决策
147
- // (未知 id / 跨类型合并等)、应用合法子集、run 记 degraded;设 false 恢复
148
- // 整单拒绝的严格模式。全局上限与覆盖率下限不受此开关影响、始终整单拒绝。
149
- dreamSkipInvalid: z.boolean().default(true),
150
- // 显式开启后放宽跨类型合并检查(类型边界由用户自行承担);配合
151
- // dreamSkipInvalid 理解:关闭 skipInvalid 时跨类型 merge 直接整单拒绝。
152
- allowCrossTypeMerge: z.boolean().default(false),
153
- // Rule version for dream adjudication: when this bumps, older dream_runs
154
- // degrade to historical evidence (their receipts no longer drive live
155
- // decisions). Default 0 = no versioning in use yet.
156
- policyEpoch: z.natural().min(0).max(1000000).default(0),
157
-
158
- // --- API protection ------------------------------------------------------
159
- // Optional shared token for the plugin's HTTP API. Empty (default) keeps
160
- // the API open (DSH binds to 127.0.0.1 and has no built-in auth); when set,
161
- // sensitive endpoints (vector-config, vector-reindex, and all write ops on
162
- // profile/rules/commands) require `Authorization: Bearer <apiToken>` (or
163
- // `X-DSH-Mneme-Token`). Read-only list/search/semantic stay open so the
164
- // Web panel keeps working without the token.
165
- apiToken: z.string(),
166
-
167
- // --- semantic: local embedding provider (v0.2) --------------------------
168
- // "openai" keeps the legacy external-API path (settings vector config);
169
- // "local" runs an ONNX model in-process; "ollama" calls a local Ollama.
170
- embedProvider: z.union([z.const("openai"), z.const("local"), z.const("ollama")]).default("openai"),
171
-
172
- // Local ONNX embedder (transformers.js / onnxruntime).
173
- localEmbedModel: z.string().default("Xenova/bge-small-zh-v1.5"),
174
- localEmbedDimension: z.natural().default(512),
175
- localEmbedDevice: z.union([z.const("cpu"), z.const("gpu")]).default("cpu"),
176
- localEmbedBatchSize: z.natural().min(1).max(64).default(8),
177
-
178
- // Ollama embedder.
179
- ollamaBaseUrl: z.string().default("http://localhost:11434"),
180
- ollamaModel: z.string().default("nomic-embed-text"),
181
-
182
- // Model download/cache. When empty (default), models are cached under the
183
- // user-level path ~/.dsh/mneme/models (resolved in local-embedder/reranker);
184
- // a non-empty value is used verbatim.
185
- embedModelCacheDir: z.string().default(""),
186
- embedModelMirror: z.string().default("https://hf-mirror.com"),
187
-
188
- // issue #194:模型文件下载的断点续传与重试。开启时 transformers.js 的 env.fetch 被
189
- // 替换为带断点续传的实现(已收字节落盘、Range/If-Range 续传、空闲看门狗),大文件
190
- // 中断后不必整份重来;关闭 = env.fetch 保持原样(线上回滚开关)。
191
- resilientModelDownload: z.boolean().default(true),
192
-
193
- // 自管运行时目录(issue #131)。空 = ~/.dsh/mneme/runtime:里面放收编或下载来的
194
- // transformers + onnxruntime 闭包,插件经 src/runtime/loader.js 的三层解析加载它。
195
- // 目的就是让这条重依赖不必留在宿主 profile 的依赖图里——profile 是所有插件共用的
196
- // 依赖图,留在里面会让「装任何插件都要替它重走一遍这条链」。
197
- runtimeDir: z.string().default(""),
198
-
199
- // 运行时取件的两个来源(issue #131 / PR-C)。都是「克制」的默认:留空 = 不额外改变行为。
200
- // - runtimeTarballDir:本地 .tgz 目录。某个包网络下不到时(例如 onnxruntime-node),
201
- // 把 `npm pack <pkg>` 出来的 tgz 丢进这个目录即可离线取件;有它就优先于联网。
202
- // - runtimeMirror:registry 镜像前缀,例如 https://npmmirror.com/mirrors/npm/ 。
203
- // 留空则用 runtime-manifest.json 里写死的官方地址(那是随包发布的固定清单)。
204
- runtimeTarballDir: z.string().default(""),
205
- runtimeMirror: z.string().default(""),
206
-
207
- // Vector search tuning.
208
- vectorSearchTopK: z.natural().min(1).max(100).default(20),
209
- vectorSearchThreshold: z.number().min(0).max(1).default(0.65),
210
- hybridSearchVectorWeight: z.number().min(0).max(1).default(0.6),
211
- hybridSearchKeywordWeight: z.number().min(0).max(1).default(0.4),
212
- // Lazy auto-backfill of missing embeddings on boot (Bug2): when the vector
213
- // API is configured and rows still lack an embedding, the index is rebuilt
214
- // in the background after a short delay, rate-limited in batches. On by
215
- // default; set false to keep the backfill manual only.
216
- autoReindexOnBoot: z.boolean().default(true),
217
- // Semantic-first injection (Bug4): when enabled, injectCandidates with a
218
- // non-empty query recalls via the vector index first and falls back to the
219
- // rule-based pick to fill/dedupe. Empty query / no vector → legacy behavior.
220
- hybridInject: z.boolean().default(true),
221
-
222
- // --- recall optimization (v0.5.0) ----------------------------------------
223
- // BM25 third recall path beside vector + LIKE keyword (1.1): per-token IDF
224
- // scoring recalls rows whose query terms are scattered — identifiers, code
225
- // fragments, mixed CJK/ASCII — where substring LIKE cannot match.
226
- bm25SearchEnabled: z.boolean().default(true),
227
- // Query-aware vector cutoff (1.2) replacing the fixed 0.65: entity:/attr:
228
- // prefixes loosen to 0.5, short queries tighten to 0.7, long queries loosen
229
- // to 0.6, and a decisive top-1/top-5 score gap loosens to 0.5 so the tail
230
- // still reaches the reranker. Off = legacy fixed threshold behavior.
231
- adaptiveThresholdEnabled: z.boolean().default(true),
232
- // Session-scoped hot memory (1.3): the latest N dialogue rounds rendered
233
- // ahead of the long-term recall block — short-term context that never
234
- // enters the memory store.
235
- hotMemoryEnabled: z.boolean().default(true),
236
- hotMemoryRounds: z.natural().min(1).max(50).default(5),
237
- hotMemoryMaxTokens: z.natural().min(200).max(32000).default(2000),
238
- // Topic-ranked injection (2.2): when a query vector is available the whole
239
- // injection candidate list is re-ordered by similarity to the current
240
- // query instead of keeping the rule-based order.
241
- selectiveInjectEnabled: z.boolean().default(true),
242
- // Search-time semantic dedup (2.3): greedy pass over the merged candidate
243
- // list dropping rows whose embedding cosine-similarity to an already-kept
244
- // row exceeds the threshold — duplicates are filtered at recall time
245
- // instead of waiting for a dream consolidation. Opt-in aggressive mode:
246
- // small embedding models can collapse legitimately distinct rows, so the
247
- // default keeps every recalled row.
248
- searchSemanticDedup: z.boolean().default(false),
249
- searchSemanticDedupThreshold: z.number().min(0.5).max(1).default(0.95),
250
-
251
- // Recall fusion recipe (plan #1): how the keyword/vector/BM25 ranked lists
252
- // are combined into the final ranking. `blend` (default) is the legacy
253
- // behavior — weighted sum for vector/hybrid, union backfill for auto —
254
- // unchanged. `rrf` (Reciprocal Rank Fusion) and `minmax` (min-max normalized
255
- // weighted sum) are rank/scale-aware recipes that fix the unit mismatch the
256
- // issue describes (raw cosine vs keyword score vs normalized IDF are added
257
- // directly). Off by default so existing behavior holds exactly.
258
- recallFusion: z.union([z.const("blend"), z.const("rrf"), z.const("minmax")]).default("blend"),
259
- // Attach a `signals` object { keyword, vector, bm25, final } to each search
260
- // result for transparency/debugging (plan #2). Default off; when on it only
261
- // decorates the returned rows, never changes the ranking.
262
- signalTransparency: z.boolean().default(false),
263
-
264
- // --- semantic: rerank layer (v0.2) --------------------------------------
265
- // Opt-in by default (item ⑥): the local cross-encoder pulls in onnxruntime
266
- // (transformers.js) at init, so a bare install must not load it. Only an
267
- // explicit rerankEnabled=true + rerankProvider="local" constructs LocalReranker.
268
- rerankEnabled: z.boolean().default(false),
269
- rerankProvider: z.union([z.const("local"), z.const("none")]).default("none"),
270
- rerankModel: z.string().default("Xenova/bge-reranker-base"),
271
- rerankBatchSize: z.natural().min(1).max(64).default(8),
272
- rerankMaxCandidates: z.natural().min(5).max(100).default(30),
273
- rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
274
- // #188:量化档,与嵌入层的 useDtype 同语义(q8 = model_quantized.onnx,约为
275
- // fp32 体积的 1/4)。此前重排层不传 dtype,即使缓存里已有量化文件也会去下载
276
- // 1GB 级的 model.onnx。非法值由 transformers 在 init 时抛出 → 重排降级并告警。
277
- rerankDtype: z.string().default("q8"),
278
-
279
- // --- reflection: update decision + failure tracking (v0.2.1) ------------
280
- reflectionUpdateEnabled: z.boolean().default(true),
281
- reflectionFailureTracking: z.boolean().default(true),
282
- reflectionUpdateMaxPerRun: z.natural().min(0).max(5).default(2),
283
- reflectionUpdateMinAgeHours: z.natural().min(0).max(168).default(24),
284
-
285
- // --- conflict freeze: manual review for conflicting memories (v0.2.1) ---
286
- // Opt-in by default: when true, conflicting memories are not auto-merged
287
- // and are marked as pending manual review instead.
288
- conflictFreezeEnabled: z.boolean().default(false),
289
- // Maximum number of frozen conflicts to keep pending for manual review.
290
- conflictFreezeMaxPending: z.natural().min(1).max(1000).default(100),
291
-
292
- // --- entity gene (v0.3.0) -----------------------------------------------
293
- // Opt-in: when false (default) nothing in the pipeline extracts entities.
294
- // The storage layer (entities/entity_attrs/entity_relations tables + CRUD)
295
- // is always available regardless of this flag.
296
- entityExtractionEnabled: z.boolean().default(false),
297
- // Optional provider override for entity extraction; empty = use the caller's
298
- // default provider/model. Combined with entityExtractionModel — provider
299
- // without model (or vice versa) falls through to the caller default.
300
- entityExtractionProvider: z.string().default(""),
301
- // Optional model override for entity extraction; empty = use the caller's
302
- // default provider/model.
303
- entityExtractionModel: z.string().default(""),
304
- // Reasoning effort for entity extraction (issue #109), mirrors
305
- // dreamReasoningEffort pass-through: 'none' omits the field / provider
306
- // default; off/low/medium/high passed through. A provider that rejects the
307
- // effort retries once without it, so opting in is safe to experiment with.
308
- entityExtractionReasoning: z.union([
309
- z.const("low"),
310
- z.const("medium"),
311
- z.const("high"),
312
- z.const("none")
313
- ]).default("none"),
314
- // Cap on entities per extraction pass and attributes per entity.
315
- entityExtractionMaxEntities: z.natural().min(1).max(20).default(10),
316
- entityExtractionMaxAttrs: z.natural().min(1).max(50).default(20),
317
- // Prefix/semantic search over entity names (used by recall).
318
- entitySearchEnabled: z.boolean().default(true),
319
-
320
- // --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
321
- // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
322
- // sleep fires when the store has been quiet for sleepIdleMinutes and deep-
323
- // maintains the whole library: conflict resolution, archival demotion,
324
- // pattern discovery and entity relation completion. Abortable on user
325
- // activity, audited into dream_runs (run_type='sleep'), and serialized with
326
- // autoDream so the two never overlap.
327
- sleepModeEnabled: z.boolean().default(false),
328
- // Quiet window before a cycle fires (minutes).
329
- sleepIdleMinutes: z.natural().min(1).max(60).default(5),
330
- // Minimum gap between two sleep runs (hours) — a second idle window within
331
- // this interval does not retrigger.
332
- sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
333
- // Conflict adjudication strictness:
334
- // gentle only high-confidence conflicts (threshold 0.92) are resolved
335
- // normal standard dream-level (threshold 0.85)
336
- // aggressive low-confidence pairs are also adjudicated (threshold 0.75)
337
- sleepConflictStrictness: z.union([
338
- z.const("gentle"),
339
- z.const("normal"),
340
- z.const("aggressive")
341
- ]).default("normal"),
342
- // Issue #126:冲突阶段的动作集。"conflict"(默认 = 现状)只用 conflict/keep——
343
- // 后台自动流程不静默扩张行为;"full" 开放六分支(merge / update / supersede /
344
- // differentiate / conflict / keep),互补型与演进型重复因此有了正确出口,不再
345
- // 被迫"输赢化"(实测抽样 10 对里 2 对属演进型/互补型,用 conflict 处理会丢信息)。
346
- sleepActionSet: z.union([
347
- z.const("conflict"),
348
- z.const("full")
349
- ]).default("conflict"),
350
- // Archival demotion tiering (days since last access):
351
- // >= sleepArchiveDays → shrink to summary, full body kept in _full_content
352
- // >= sleepCompressDays → archived outright (entity relations preserved)
353
- sleepArchiveDays: z.natural().min(7).max(365).default(30),
354
- sleepCompressDays: z.natural().min(7).max(365).default(90),
355
- // Pattern discovery scan window (most recent memories to scan).
356
- sleepPatternMinMemories: z.natural().min(10).max(1000).default(100),
357
- // How far back pattern discovery considers entity attr changes (days).
358
- sleepPatternLookbackDays: z.natural().min(1).max(90).default(30),
359
- // Max pattern memories minted per run (0 = disabled).
360
- sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
361
- // Optional LLM route override for sleep's bulk passes (empty = use dream
362
- // route / agent default model).
363
- sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
364
- sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
365
- // Pass-through reasoning effort for sleep's LLM passes, same semantics as
366
- // dreamReasoningEffort (Issue #135 建议 4/5): unset resolves to the lowest
367
- // effort the model declares; explicit 'none' omits the field; off/low/
368
- // medium/high are forwarded verbatim.
369
- sleepReasoningEffort: z.union([
370
- z.const("off"),
371
- z.const("low"),
372
- z.const("medium"),
373
- z.const("high"),
374
- z.const("none")
375
- ]).description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,未配置 = 自动取模型支持的最低档;显式 'none' = 不发送字段、用服务商自带默认。"),
376
-
377
- // --- epistemic trust: memory source credibility (v0.4.5) -----------------
378
- // Distinguish memories by source: observation (measured / witnessed),
379
- // subjective (opinion / guess) and inferred (derived from other evidence).
380
- // Opt-in by default: when false (default) retrieval ranking, injection
381
- // marking and dream merge/conflict keepSource are untouched and
382
- // epistemic_status stays inert data (still written + inferred on save, just
383
- // never used to influence behavior).
384
- trustEpistemicWeighting: z.boolean().default(false),
385
-
386
- // --- memory quality filter (Bug7) ------------------------------------------
387
- // Heuristic gate on what deserves the injection/recall surface. When enabled,
388
- // saveWithDedupe scores each new memory after dedupe and before write:
389
- // score >= degradeThreshold (60) → stored normally
390
- // archiveThreshold (30) <= score < 60 → quality_score persisted and the
391
- // injection sort re-ranks by importance * quality_score/100 (degraded)
392
- // score < 30 → archived + tagged low_quality (still explicitly searchable)
393
- // Meta-memory markers, near-duplicates and repetitive filler lose points.
394
- memoryQualityFilter: z.object({
395
- enabled: z.boolean().default(true),
396
- archiveThreshold: z.natural().min(1).max(100).default(30),
397
- degradeThreshold: z.natural().min(1).max(100).default(60),
398
- minContentLength: z.natural().min(1).max(1000).default(10),
399
- // Issue #135 附属发现 1:importance ≥ 该值的记忆不参与静默自动归档——评分
400
- // 与信号标签照常写入(可观测)、注入排序照常降权,但 setArchived 跳过。
401
- // 报告实测 150 条低分归档里 128 条 importance ≥ 4(51 条 = 5)。设 1 = 全部
402
- // 豁免(等效关闭自动归档),设 5 = 仅最重要的豁免。
403
- exemptImportance: z.natural().min(1).max(5).default(4)
404
- }).default({}),
405
-
406
- // --- LLM audit trail (Bug8) ------------------------------------------------
407
- // Records every background LLM call (autoDream consolidation + summary,
408
- // autoSummarize compression) into llm_audit_logs: tokens, duration, status
409
- // and which trigger produced it. Failures are recorded as status=error and
410
- // never block the feature. retentionDays bounds the table: older rows are
411
- // purged on boot.
412
- llmAudit: z.object({
413
- enabled: z.boolean().default(true),
414
- retentionDays: z.natural().min(1).max(3650).default(90)
415
- }).default({}),
416
-
417
- // --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
418
- // Separate retrieval evaluation snapshots from the production recall audit.
419
- // When false (default) evaluateRetrieval still computes precision/recall/mrr
420
- // and returns them to the caller, but writes nothing to recall_evals — the
421
- // eval table only grows when the operator opts in. Production searchMemories
422
- // audits to recall_runs and NEVER touches recall_evals, regardless of this
423
- // flag (production isolation is unconditional).
424
- evalPersistTestResults: z.boolean().default(false),
425
-
426
- // --- standalone external API (v0.7.12) ------------------------------------
427
- // A plain node:http server for ecosystem integrations that cannot reach the
428
- // DSH-internal webServer. Disabled by default; when enabled the Bearer token
429
- // is persisted in the settings kv ("external_api"), auto-generated on first
430
- // boot. Bind host: keep the loopback default — moving it to a non-loopback
431
- // address exposes the whole memory store to the network and is the
432
- // operator's responsibility.
433
- externalApiEnabled: z.boolean().default(false),
434
- externalApiPort: z.natural().default(8790),
435
- externalApiHost: z.string().default("127.0.0.1"),
436
-
437
- // --- light mode preset (v0.7.12) -------------------------------------------
438
- // One switch for low-resource setups: turns off every background/semantic
439
- // heavy path (dream consolidation, entity extraction, vector pipeline,
440
- // reranker, BM25, semantic dedup / selective inject, sleep mode) while
441
- // keeping the core loop (autoInject, autoSummarize, hot memory, quality
442
- // filter, keyword search). Applied by applyLightModePreset before the config
443
- // reaches any service; a persisted panel_mode="light" (settings kv) counts
444
- // as lightMode=true too and wins over the bundle config.
445
- lightMode: z.boolean().default(false),
446
-
447
- // --- heat: v0.7.0 self-evolution (heat + interest drift) ----------------
448
- // 总开关,默认关(v0.7.12+ 用户已习惯无 heat 行为,默认开=全员行为变更)。
449
- // 开启后:提供热度字段 / sleep 降级联合判定保护 / 前端热度投影,不改变
450
- // 召回排序。关闭则跳过所有 heat 计算与热度触达,sleep 降级退回纯时间分层。
451
- // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
452
- heatEnabled: z.boolean().default(false),
453
- // 幂律形状参数 α(heat = 1/(1+λΔt)^α),越大衰减越快。
454
- heatGlobalAlpha: z.number().min(0.1).max(5).default(1.2),
455
- // per-type 衰减因子 λ;λ=0 的类型免疫(热度恒 1.0,sleep 永不降级)。
456
- // 未知类型走默认 0.002。dict 的键为 type 字符串、值为数字 λ。
457
- heatTypeDecay: z.dict(z.number(), z.string()).default({ ...TYPE_DECAY_DEFAULTS }),
458
- // sleep 降级联合判定的热度下限:heat < 该值 且 importance<5 才允许降级。
459
- sleepHeatThreshold: z.number().min(0).max(1).default(0.05),
460
- // recordRecall 默认值(recall_runs 记录默认开;显式传 false 的调用方不受影响)。
461
- recallRecordDefault: z.boolean().default(true),
462
- // recall_runs 滚动清理保留天数。
463
- recallRetentionDays: z.natural().min(1).max(3650).default(90),
464
-
465
- // --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
466
- // 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
467
- // 逐字节不变。开启后 memory_save 写入 agent_scope(session header 的
468
- // agentPreset)与 workspace_scope(registry 反查 canonical path,header.cwd
469
- // 兜底,取不到 NULL——解析绝不阻塞写入),去重键扩展为 (type, title,
470
- // agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
471
- // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
472
- scopeEnabled: z.boolean().default(false),
473
- // strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见);
474
- // 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
475
- // 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
476
- // 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
477
- // 打开才有意义(没有写入标注就无可过滤维度),但独立成键:可先开标注积累
478
- // 数据、观察 A2 加权质量后再切硬过滤。
479
- strictScope: z.boolean().default(false),
480
- });
481
-
482
- // Fields forced to false by the light-mode preset. Everything not listed here
483
- // (autoInject, autoSummarize, hotMemory*, memoryQualityFilter, dream
484
- // thresholds/delays, ...) is left untouched — those are the core loop.
485
- const LIGHT_MODE_OFF = [
486
- "entityExtractionEnabled",
487
- "autoDream",
488
- "sleepModeEnabled",
489
- "rerankEnabled",
490
- "autoReindexOnBoot",
491
- "hybridInject",
492
- "searchSemanticDedup",
493
- "selectiveInjectEnabled",
494
- "bm25SearchEnabled",
495
- // 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
496
- "heatEnabled"
497
- ];
498
-
499
- /**
500
- * Apply the light-mode preset to a resolved config object (pure function,
501
- * exported for tests). When cfg.lightMode is not exactly true the config is
502
- * returned unchanged; otherwise a shallow copy carries false for every heavy
503
- * feature. Idempotent and side-effect free.
504
- */
505
- export function applyLightModePreset(cfg) {
506
- if (cfg?.lightMode !== true) return cfg;
507
- const preset = { ...cfg, lightMode: true };
508
- for (const key of LIGHT_MODE_OFF) preset[key] = false;
509
- return preset;
510
- }
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
+ importanceThreshold: z.natural().min(1).max(5).default(3),
75
+ // Issue #239(第 5 项):注入条数的查询自适应(默认关)。确定性强的话题收缩注入
76
+ // 条数(减半、下限 1),模糊话题(回指/时间线索,或极短查询)维持
77
+ // maxInjectedItems 上限——只做**单向收缩**,绝不越过用户配置的上限;判据只看
78
+ // 查询本身,不做额外检索(先探针检索等于白付一次 fuseRecall)。
79
+ injectUncertaintyAdaptive: z.boolean().default(false),
80
+ // #249(第一批):能力说明——「怎么用记忆」的判断指引。落两个零注入成本的
81
+ // 位:①`memory_search` / `memory_save` 的工具描述补一句判断指引(工具描述是
82
+ // 常驻文本,不进每轮上下文);②一段 order 150 的系统提示段(一次性、同会话
83
+ // 内不随轮次变化,因此不作废前缀缓存),只讲总则(优先序、何时查、何时写、
84
+ // 何时 no-op)。默认关:关闭时工具描述与提示段与既有行为逐字节一致。
85
+ injectGuidanceEnabled: z.boolean().default(false),
86
+ // #249(第一批):B1 pin 池预算——约束/偏好类注入条目的独立小上限。约束与
87
+ // 偏好被静默降级是本议题的立项核心(同类知识与情景日志同池同速率摘要,实测
88
+ // 一轮压缩后仅保 53%、五轮 10%),故这两类不进相关性竞争、不参与跨轮轮换、
89
+ // 逐字保真(仅受超大条目的硬顶保护,截断仍带提示)并排在块内排序之前。
90
+ // 独立预算的意义:pin 不占 maxInjectedItems 名额,不会把当前任务需要的情景
91
+ // 候选挤出预算(另一种「批量塞历史」)。0(默认)= 关闭,注入块构成与既有
92
+ // 行为逐字节一致;超出预算的条数在块内如实标注未展示条数,绝不静默。
93
+ pinnedInjectBudget: z.natural().min(0).max(5).default(0),
94
+ // 编码记忆蒸馏(codingRetrospect,opt-in,默认关)。开启时,turn/end 蒸馏
95
+ // 额外提取三类编码专属记忆:rejected_solution(被否决方案)/ pitfall(踩坑)/
96
+ // constraint(工程约束)。蒸馏上下文为整轮完整对话(用户输入 → 助手思考/回答
97
+ // → 工具调用与结果 → 代码执行),不再只看用户消息,便于提炼踩坑根因。
98
+ // 关闭时行为与之前完全一致。
99
+ codingRetrospect: z.boolean().default(false),
100
+ // 编码任务识别词表(读取侧门控用):命中即视为编码类任务,编码记忆才注入。
101
+ codingKeywords: z.array(z.string()).default([
102
+ "代码", "编码", "写一个", "写个", "实现", "函数", "方法", "类",
103
+ "接口", "bug", "调试", "报错", "错误", "异常", "堆栈", "脚本",
104
+ "python", "javascript", "typescript", "node", "js", "ts",
105
+ "sql", "sqlite", "数据库", "算法", "重构", "优化", "性能",
106
+ "测试", "单测", "修复", "补丁", "依赖", "npm", "pip",
107
+ "命令行", "shell", "配置", "配置文件", "yaml", "json",
108
+ "插件", "开发", "编译", "构建", "部署", "git", "commit",
109
+ "review", "前端", "后端", "页面", "组件", "dsh", "memos"
110
+ ]),
111
+ // 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
112
+ // 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
113
+ codingBoostFactor: z.number().min(1).max(5).default(2),
114
+ // 注入边界花括号转义(issue #162,v0.7.4 曾修后被 v0.7.11 重构误删):默认开启,
115
+ // 在 memory / 短期上下文 / 用户设置三处注入出口对连续 2+ 个花括号插 `\`
116
+ // (`{{a}}`→`{\{a\}\}`),让 DSH 的 interpolate() 不再扫到非法变量名而 throw。
117
+ // 关闭时按原样透传。
118
+ escapePromptVariables: z.boolean().default(true),
119
+ autoDream: z.boolean().default(true),
120
+ dreamThresholdCount: z.natural().min(1).max(1000).default(10),
121
+ dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
122
+ dreamDelayMs: z.natural().min(0).max(60000).default(2000),
123
+ // autoDream 触发最小间隔(分钟,0 = 不限制,Issue #89 请求 2):高频写入
124
+ // 场景下防止巩固调用(含失败重试)连发刷爆配额。间隔从每次实际开跑时刻
125
+ // 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次
126
+ // 写入事件会重新评估。
127
+ dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
128
+ // 巩固模型路由(settings panel「巩固模型」/ dreamProvider+dreamModel):
129
+ // dream 的记忆沉淀专用 LLM 路由,显式配置优先于 agent 默认模型(config-first,
130
+ // Issue #25)。模型分类声明:
131
+ // - 非思考模型(推荐,如 glm-5-2 类):无 reasoning 声明,effort 请求被 harness
132
+ // 拒绝后 withEffortFallback 去掉字段重试即成功;空体/no json array 风险最低。
133
+ // - 思考模型(如 deepseek-v4-flash-ga 等 v4-flash-ga 系):默认开推理,可能烧光
134
+ // token 预算返回空体;且部分(如 v4-flash-ga)在 harness 侧被声明为不接受任何
135
+ // reasoning effort —— 即使去掉 effort 重试,harness 的 defaultEffort 也会顶上来
136
+ // 再次拒绝(UNSUPPORTED_REASONING_EFFORT),插件 fallback 无法绕开。
137
+ // 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
138
+ dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
139
+ dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
140
+ // Issue #135 建议 3:#9 只抬了 max(8192→131072),default 停在 32768。思考型
141
+ // 模型的推理与正文共享该预算,effort 未压低时 32768 恰好被推理烧光 → 空体
142
+ // ("no json array in llm output",报告实测 146 轮因此失败)。默认抬到上限,
143
+ // 让默认配置也留足正文预算;流式计费按实际用量,不按上限。
144
+ dreamMaxTokens: z.natural().min(256).max(131072).default(131072),
145
+ // Pass-through reasoning effort for dream's LLM calls (Issue #135 建议 4/5).
146
+ // Unset (no value) resolves to the LOWEST effort the model declares before
147
+ // streaming: background consolidation must produce JSON, and omitting the
148
+ // field lets the harness substitute the model's defaultEffort (thinking-type
149
+ // models often default to high/max) — the reasoning then drains the whole
150
+ // token budget and the run dies with an empty body. Explicit 'none' still
151
+ // omits the field (provider default applies); off/low/medium/high are
152
+ // forwarded verbatim, unsupported values are remapped (v0.7.26+).
153
+ // Caveat: on some thinking models (e.g. v4-flash-ga) the harness declares NO
154
+ // supported effort, so even the fallback retry (field stripped) is rejected
155
+ // again via its defaultEffort — prefer a non-reasoning dreamProvider/dreamModel.
156
+ dreamReasoningEffort: z.union([
157
+ z.const("off"),
158
+ z.const("low"),
159
+ z.const("medium"),
160
+ z.const("high"),
161
+ z.const("none")
162
+ ]).description("巩固模型的推理档位:未配置 = 自动取模型支持的最低档(避免思考模型用自带默认档烧光预算返回空体,#135);显式 'none' = 不发送字段、用服务商自带默认;off/low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
163
+ // 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
164
+ // 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
165
+ // 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
166
+ dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
167
+ // Issue #104 方向 2:单轮 archive 决策上限。一次性大扫除(12 条互不相关主题
168
+ // 被批量归档,其中 8 条疑似误伤长保留类型)是失控信号,与 update 上限同类的
169
+ // 全局闸门:skipInvalid 也不豁免,超限整单拒绝。正常清理可调高。
170
+ dreamMaxArchivePerRun: z.natural().min(1).max(200).default(8),
171
+ // Issue #125:候选集构造方式。"window"(默认,等同现状)只取最近
172
+ // dreamMaxSnapshotSize 条;"hybrid" 在此基础上并入**向量翻出的高相似组**——
173
+ // 本机实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口(窗口覆盖率 11.7%),
174
+ // 纯时间窗口让"该合并的一对"几乎永远碰不到面。候选总量仍由下面的上限封顶、
175
+ // 与库总量解耦,这是相对"调大窗口"的核心收益:输入成本不随库增长。
176
+ dreamCandidateMode: z.union([
177
+ z.const("window"),
178
+ z.const("hybrid")
179
+ ]).default("window"),
180
+ // hybrid 的候选总量上限;0 = 复用 dreamMaxSnapshotSize(不迁移,需要时显式覆盖)。
181
+ dreamCandidateMax: z.natural().min(0).max(5000).default(0),
182
+ // Issue #258:总览(dream_summarize)独立路由。consolidate 有 dreamMaxSnapshotSize
183
+ // 窗口、总览原为全库无界——两者对 ctx 的需求差 4 倍以上却强制共用 dream 路由,
184
+ // dreamProvider 指向小 ctx 模型时总览当场 CONTEXT_WINDOW_EXCEEDED(实测 120,969
185
+ // tokens > 32,768),指向大 ctx 模型则 consolidate 的卸载收益归零。未配置 = 回落
186
+ // dream 路由,行为逐字节不变。
187
+ dreamSummaryProvider: z.string().default("").description("记忆总览(dream_summarize)专用模型服务商,留空用巩固模型。总览输入为全库(或 dreamSummaryMaxInputs 上限),ctx 需求远大于 consolidation,建议大 ctx 模型(issue #258)。"),
188
+ dreamSummaryModel: z.string().default("").description("记忆总览(dream_summarize)专用模型,留空用巩固模型;总览输入随库增长,建议大 ctx 非思考模型(issue #258)。"),
189
+ // Issue #258:总览输入条数硬上限。0 = 不设上限(历史行为,库增长可能撑爆小
190
+ // ctx 模型);>0 时按 updated_at 倒序保留最新的 N 条(与 consolidate 窗口同一
191
+ // 排序口径)。总览是常驻叙述而非逐条巩固,限输入只影响口径脚注里的条数。
192
+ dreamSummaryMaxInputs: z.natural().min(0).max(100000).default(0),
193
+ // hybrid 判"高相似"的阈值;0.85 与 sleep normal 档对齐——两个模块对"高相似"
194
+ // 保持同一个定义。
195
+ dreamCandidateMinSim: z.number().min(0.5).max(0.99).default(0.85),
196
+ // 叙述条(#164 对齐,opt-in):dream 期间按共享 tag 主题聚类(dream/
197
+ // narratives.js),每簇 LLM 合成一条叙述落库(source=narrative、evidence
198
+ // 回链簇内原子记忆、求交防捏造)。按需检索、不常驻注入。默认关=行为与
199
+ // 此前一致;lightMode 强制关闭。
200
+ dreamNarrativeEnabled: z.boolean().default(false),
201
+ // 成簇门槛:共享同一 tag 的记忆 ≥ 此值才合成叙述条。
202
+ dreamNarrativeMinCluster: z.natural().min(2).max(20).default(3),
203
+ // document 型记忆(#164/#230,opt-in):agent 产长文档的指针行——注册校验
204
+ // (文件存在 + 路径合法 + evidence 求交)、摘要 + doc_path 落库、C2 比对
205
+ // 去重、supersede 记账。全文归 agent,管线零触碰;默认关=行为与此前一致;
206
+ // lightMode 强制关闭。
207
+ documentMemoryEnabled: z.boolean().default(false),
208
+ // document 摘要行的注入预算(#230 拍板):次优先档内最多注入的 document
209
+ // 行数,超预算跳过由后续候选补位。只约束注入,不约束检索。
210
+ documentInjectBudget: z.natural().min(1).max(5).default(2),
211
+ // 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
212
+ // 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
213
+ // (未覆盖即拒绝整单)。
214
+ dreamImplicitKeep: z.boolean().default(true),
215
+ // 显式决策覆盖率下限(v0.4.4 fix):dreamImplicitKeep 开启时,LLM 输出被
216
+ // 截断只显式 claim 少量 snapshot 记忆(claimed.size / snapshot.size < 该阈值)
217
+ // → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
218
+ // 默认 0.5(至少显式覆盖一半 snapshot)。
219
+ dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
220
+ // Issue #89:v0.6.9(Issue #26)的宽容路径回归。默认跳过单条非法决策
221
+ // (未知 id / 跨类型合并等)、应用合法子集、run 记 degraded;设 false 恢复
222
+ // 整单拒绝的严格模式。全局上限与覆盖率下限不受此开关影响、始终整单拒绝。
223
+ dreamSkipInvalid: z.boolean().default(true),
224
+ // 显式开启后放宽跨类型合并检查(类型边界由用户自行承担);配合
225
+ // dreamSkipInvalid 理解:关闭 skipInvalid 时跨类型 merge 直接整单拒绝。
226
+ allowCrossTypeMerge: z.boolean().default(false),
227
+ // Rule version for dream adjudication: when this bumps, older dream_runs
228
+ // degrade to historical evidence (their receipts no longer drive live
229
+ // decisions). Default 0 = no versioning in use yet.
230
+ policyEpoch: z.natural().min(0).max(1000000).default(0),
231
+
232
+ // --- API protection ------------------------------------------------------
233
+ // Optional shared token for the plugin's HTTP API. Empty (default) keeps
234
+ // the API open (DSH binds to 127.0.0.1 and has no built-in auth); when set,
235
+ // sensitive endpoints (vector-config, vector-reindex, and all write ops on
236
+ // profile/rules/commands) require `Authorization: Bearer <apiToken>` (or
237
+ // `X-DSH-Mneme-Token`). Read-only list/search/semantic stay open so the
238
+ // Web panel keeps working without the token.
239
+ apiToken: z.string(),
240
+
241
+ // --- semantic: local embedding provider (v0.2) --------------------------
242
+ // "openai" keeps the legacy external-API path (settings vector config);
243
+ // "local" runs an ONNX model in-process; "ollama" calls a local Ollama.
244
+ embedProvider: z.union([z.const("openai"), z.const("local"), z.const("ollama")]).default("openai"),
245
+
246
+ // Local ONNX embedder (transformers.js / onnxruntime).
247
+ localEmbedModel: z.string().default("Xenova/bge-small-zh-v1.5"),
248
+ localEmbedDimension: z.natural().default(512),
249
+ localEmbedDevice: z.union([z.const("cpu"), z.const("gpu")]).default("cpu"),
250
+ localEmbedBatchSize: z.natural().min(1).max(64).default(8),
251
+
252
+ // Ollama embedder.
253
+ ollamaBaseUrl: z.string().default("http://localhost:11434"),
254
+ ollamaModel: z.string().default("nomic-embed-text"),
255
+
256
+ // Model download/cache. When empty (default), models are cached under the
257
+ // user-level path ~/.dsh/mneme/models (resolved in local-embedder/reranker);
258
+ // a non-empty value is used verbatim.
259
+ embedModelCacheDir: z.string().default(""),
260
+ embedModelMirror: z.string().default("https://hf-mirror.com"),
261
+
262
+ // issue #194:模型文件下载的断点续传与重试。开启时 transformers.js 的 env.fetch 被
263
+ // 替换为带断点续传的实现(已收字节落盘、Range/If-Range 续传、空闲看门狗),大文件
264
+ // 中断后不必整份重来;关闭 = env.fetch 保持原样(线上回滚开关)。
265
+ resilientModelDownload: z.boolean().default(true),
266
+
267
+ // 自管运行时目录(issue #131)。空 = ~/.dsh/mneme/runtime:里面放收编或下载来的
268
+ // transformers + onnxruntime 闭包,插件经 src/runtime/loader.js 的三层解析加载它。
269
+ // 目的就是让这条重依赖不必留在宿主 profile 的依赖图里——profile 是所有插件共用的
270
+ // 依赖图,留在里面会让「装任何插件都要替它重走一遍这条链」。
271
+ runtimeDir: z.string().default(""),
272
+
273
+ // 运行时取件的两个来源(issue #131 / PR-C)。都是「克制」的默认:留空 = 不额外改变行为。
274
+ // - runtimeTarballDir:本地 .tgz 目录。某个包网络下不到时(例如 onnxruntime-node),
275
+ // 把 `npm pack <pkg>` 出来的 tgz 丢进这个目录即可离线取件;有它就优先于联网。
276
+ // - runtimeMirror:registry 镜像前缀,例如 https://npmmirror.com/mirrors/npm/ 。
277
+ // 留空则用 runtime-manifest.json 里写死的官方地址(那是随包发布的固定清单)。
278
+ runtimeTarballDir: z.string().default(""),
279
+ runtimeMirror: z.string().default(""),
280
+
281
+ // Vector search tuning.
282
+ vectorSearchTopK: z.natural().min(1).max(100).default(20),
283
+ vectorSearchThreshold: z.number().min(0).max(1).default(0.65),
284
+ hybridSearchVectorWeight: z.number().min(0).max(1).default(0.6),
285
+ hybridSearchKeywordWeight: z.number().min(0).max(1).default(0.4),
286
+ // Lazy auto-backfill of missing embeddings on boot (Bug2): when the vector
287
+ // API is configured and rows still lack an embedding, the index is rebuilt
288
+ // in the background after a short delay, rate-limited in batches. On by
289
+ // default; set false to keep the backfill manual only.
290
+ autoReindexOnBoot: z.boolean().default(true),
291
+ // Semantic-first injection (Bug4): when enabled, injectCandidates with a
292
+ // non-empty query recalls via the vector index first and falls back to the
293
+ // rule-based pick to fill/dedupe. Empty query / no vector → legacy behavior.
294
+ hybridInject: z.boolean().default(true),
295
+
296
+ // --- recall optimization (v0.5.0) ----------------------------------------
297
+ // BM25 third recall path beside vector + LIKE keyword (1.1): per-token IDF
298
+ // scoring recalls rows whose query terms are scattered — identifiers, code
299
+ // fragments, mixed CJK/ASCII — where substring LIKE cannot match.
300
+ bm25SearchEnabled: z.boolean().default(true),
301
+ // Query-aware vector cutoff (1.2) replacing the fixed 0.65: entity:/attr:
302
+ // prefixes loosen to 0.5, short queries tighten to 0.7, long queries loosen
303
+ // to 0.6, and a decisive top-1/top-5 score gap loosens to 0.5 so the tail
304
+ // still reaches the reranker. Off = legacy fixed threshold behavior.
305
+ adaptiveThresholdEnabled: z.boolean().default(true),
306
+ // Session-scoped hot memory (1.3): the latest N dialogue rounds rendered
307
+ // ahead of the long-term recall block — short-term context that never
308
+ // enters the memory store.
309
+ hotMemoryEnabled: z.boolean().default(true),
310
+ hotMemoryRounds: z.natural().min(1).max(50).default(5),
311
+ hotMemoryMaxTokens: z.natural().min(200).max(32000).default(2000),
312
+ // Topic-ranked injection (2.2): when a query vector is available the whole
313
+ // injection candidate list is re-ordered by similarity to the current
314
+ // query instead of keeping the rule-based order.
315
+ selectiveInjectEnabled: z.boolean().default(true),
316
+ // Search-time semantic dedup (2.3): greedy pass over the merged candidate
317
+ // list dropping rows whose embedding cosine-similarity to an already-kept
318
+ // row exceeds the threshold — duplicates are filtered at recall time
319
+ // instead of waiting for a dream consolidation. Opt-in aggressive mode:
320
+ // small embedding models can collapse legitimately distinct rows, so the
321
+ // default keeps every recalled row.
322
+ searchSemanticDedup: z.boolean().default(false),
323
+ searchSemanticDedupThreshold: z.number().min(0.5).max(1).default(0.95),
324
+
325
+ // Recall fusion recipe (plan #1): how the keyword/vector/BM25 ranked lists
326
+ // are combined into the final ranking. `blend` (default) is the legacy
327
+ // behavior — weighted sum for vector/hybrid, union backfill for auto —
328
+ // unchanged. `rrf` (Reciprocal Rank Fusion) and `minmax` (min-max normalized
329
+ // weighted sum) are rank/scale-aware recipes that fix the unit mismatch the
330
+ // issue describes (raw cosine vs keyword score vs normalized IDF are added
331
+ // directly). Off by default so existing behavior holds exactly.
332
+ recallFusion: z.union([z.const("blend"), z.const("rrf"), z.const("minmax")]).default("blend"),
333
+ // Attach a `signals` object { keyword, vector, bm25, final } to each search
334
+ // result for transparency/debugging (plan #2). Default off; when on it only
335
+ // decorates the returned rows, never changes the ranking.
336
+ signalTransparency: z.boolean().default(false),
337
+
338
+ // --- semantic: rerank layer (v0.2) --------------------------------------
339
+ // Opt-in by default (item ⑥): the local cross-encoder pulls in onnxruntime
340
+ // (transformers.js) at init, so a bare install must not load it. Only an
341
+ // explicit rerankEnabled=true + rerankProvider="local" constructs LocalReranker.
342
+ rerankEnabled: z.boolean().default(false),
343
+ rerankProvider: z.union([z.const("local"), z.const("none")]).default("none"),
344
+ rerankModel: z.string().default("Xenova/bge-reranker-base"),
345
+ rerankBatchSize: z.natural().min(1).max(64).default(8),
346
+ rerankMaxCandidates: z.natural().min(5).max(100).default(30),
347
+ rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
348
+ // #188:量化档,与嵌入层的 useDtype 同语义(q8 = model_quantized.onnx,约为
349
+ // fp32 体积的 1/4)。此前重排层不传 dtype,即使缓存里已有量化文件也会去下载
350
+ // 1GB 级的 model.onnx。非法值由 transformers 在 init 时抛出 → 重排降级并告警。
351
+ rerankDtype: z.string().default("q8"),
352
+
353
+ // --- reflection: update decision + failure tracking (v0.2.1) ------------
354
+ reflectionUpdateEnabled: z.boolean().default(true),
355
+ reflectionFailureTracking: z.boolean().default(true),
356
+ reflectionUpdateMaxPerRun: z.natural().min(0).max(5).default(2),
357
+ reflectionUpdateMinAgeHours: z.natural().min(0).max(168).default(24),
358
+
359
+ // --- conflict freeze: manual review for conflicting memories (v0.2.1) ---
360
+ // Opt-in by default: when true, conflicting memories are not auto-merged
361
+ // and are marked as pending manual review instead.
362
+ conflictFreezeEnabled: z.boolean().default(false),
363
+ // Maximum number of frozen conflicts to keep pending for manual review.
364
+ conflictFreezeMaxPending: z.natural().min(1).max(1000).default(100),
365
+
366
+ // --- entity gene (v0.3.0) -----------------------------------------------
367
+ // Opt-in: when false (default) nothing in the pipeline extracts entities.
368
+ // The storage layer (entities/entity_attrs/entity_relations tables + CRUD)
369
+ // is always available regardless of this flag.
370
+ entityExtractionEnabled: z.boolean().default(false),
371
+ // Optional provider override for entity extraction; empty = use the caller's
372
+ // default provider/model. Combined with entityExtractionModel — provider
373
+ // without model (or vice versa) falls through to the caller default.
374
+ entityExtractionProvider: z.string().default(""),
375
+ // Optional model override for entity extraction; empty = use the caller's
376
+ // default provider/model.
377
+ entityExtractionModel: z.string().default(""),
378
+ // Reasoning effort for entity extraction (issue #109), mirrors
379
+ // dreamReasoningEffort pass-through: 'none' omits the field / provider
380
+ // default; off/low/medium/high passed through. A provider that rejects the
381
+ // effort retries once without it, so opting in is safe to experiment with.
382
+ entityExtractionReasoning: z.union([
383
+ z.const("low"),
384
+ z.const("medium"),
385
+ z.const("high"),
386
+ z.const("none")
387
+ ]).default("none"),
388
+ // Cap on entities per extraction pass and attributes per entity.
389
+ entityExtractionMaxEntities: z.natural().min(1).max(20).default(10),
390
+ entityExtractionMaxAttrs: z.natural().min(1).max(50).default(20),
391
+ // Prefix/semantic search over entity names (used by recall).
392
+ entitySearchEnabled: z.boolean().default(true),
393
+ // 图召回轴(issue #219):查询文本命中实体名时,把该实体挂联的记忆并入
394
+ // 检索融合池(与 BM25 同级的确认/回填信号)。默认关=检索行为与 #219 前
395
+ // 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
396
+ entityRecallEnabled: z.boolean().default(false),
397
+
398
+ // --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
399
+ // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
400
+ // sleep fires when the store has been quiet for sleepIdleMinutes and deep-
401
+ // maintains the whole library: conflict resolution, archival demotion,
402
+ // pattern discovery and entity relation completion. Abortable on user
403
+ // activity, audited into dream_runs (run_type='sleep'), and serialized with
404
+ // autoDream so the two never overlap.
405
+ sleepModeEnabled: z.boolean().default(false),
406
+ // Quiet window before a cycle fires (minutes).
407
+ sleepIdleMinutes: z.natural().min(1).max(60).default(5),
408
+ // Minimum gap between two sleep runs (hours) — a second idle window within
409
+ // this interval does not retrigger.
410
+ sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
411
+ // Conflict adjudication strictness:
412
+ // gentle only high-confidence conflicts (threshold 0.92) are resolved
413
+ // normal standard dream-level (threshold 0.85)
414
+ // aggressive low-confidence pairs are also adjudicated (threshold 0.75)
415
+ sleepConflictStrictness: z.union([
416
+ z.const("gentle"),
417
+ z.const("normal"),
418
+ z.const("aggressive")
419
+ ]).default("normal"),
420
+ // Issue #126:冲突阶段的动作集。"conflict"(默认 = 现状)只用 conflict/keep——
421
+ // 后台自动流程不静默扩张行为;"full" 开放六分支(merge / update / supersede /
422
+ // differentiate / conflict / keep),互补型与演进型重复因此有了正确出口,不再
423
+ // 被迫"输赢化"(实测抽样 10 对里 2 对属演进型/互补型,用 conflict 处理会丢信息)。
424
+ sleepActionSet: z.union([
425
+ z.const("conflict"),
426
+ z.const("full")
427
+ ]).default("conflict"),
428
+ // Archival demotion tiering (days since last access):
429
+ // >= sleepArchiveDays → shrink to summary, full body kept in _full_content
430
+ // >= sleepCompressDays → archived outright (entity relations preserved)
431
+ sleepArchiveDays: z.natural().min(7).max(365).default(30),
432
+ sleepCompressDays: z.natural().min(7).max(365).default(90),
433
+ // Pattern discovery scan window (most recent memories to scan).
434
+ sleepPatternMinMemories: z.natural().min(10).max(1000).default(100),
435
+ // How far back pattern discovery considers entity attr changes (days).
436
+ sleepPatternLookbackDays: z.natural().min(1).max(90).default(30),
437
+ // Max pattern memories minted per run (0 = disabled).
438
+ sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
439
+ // Optional LLM route override for sleep's bulk passes (empty = use dream
440
+ // route / agent default model).
441
+ sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
442
+ sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
443
+ // Pass-through reasoning effort for sleep's LLM passes, same semantics as
444
+ // dreamReasoningEffort (Issue #135 建议 4/5): unset resolves to the lowest
445
+ // effort the model declares; explicit 'none' omits the field; off/low/
446
+ // medium/high are forwarded verbatim.
447
+ sleepReasoningEffort: z.union([
448
+ z.const("off"),
449
+ z.const("low"),
450
+ z.const("medium"),
451
+ z.const("high"),
452
+ z.const("none")
453
+ ]).description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,未配置 = 自动取模型支持的最低档;显式 'none' = 不发送字段、用服务商自带默认。"),
454
+ // Issue #257:sleep 冲突/模式两阶段的输出预算(原硬编码 2048)。实测默认档
455
+ // 每对裁决约 90 token、24 对 2097——2048 恰好压在边界(53 次运行 48 败);
456
+ // full 档六分支实测约 290 token/对、24 对 6967,2048 必然截断。默认 8192
457
+ // 覆盖实测峰值(候选对按「每记忆至多一对」去重,饱和在 ~25 对、不随库无限
458
+ // 增长);流式计费按实际用量,不按上限。
459
+ sleepMaxTokens: z.natural().min(256).max(131072).default(8192).description("sleep 冲突消解与模式发现阶段的 LLM 输出预算上限(token)。原为硬编码 2048,sleepActionSet=full 实测需约 7000 导致裁决被截断而整轮失败;流式计费按实际用量,调大不增加成本。"),
460
+
461
+ // --- epistemic trust: memory source credibility (v0.4.5) -----------------
462
+ // Distinguish memories by source: observation (measured / witnessed),
463
+ // subjective (opinion / guess) and inferred (derived from other evidence).
464
+ // Opt-in by default: when false (default) retrieval ranking, injection
465
+ // marking and dream merge/conflict keepSource are untouched and
466
+ // epistemic_status stays inert data (still written + inferred on save, just
467
+ // never used to influence behavior).
468
+ trustEpistemicWeighting: z.boolean().default(false),
469
+
470
+ // --- memory quality filter (Bug7) ------------------------------------------
471
+ // Heuristic gate on what deserves the injection/recall surface. When enabled,
472
+ // saveWithDedupe scores each new memory after dedupe and before write:
473
+ // score >= degradeThreshold (60) → stored normally
474
+ // archiveThreshold (30) <= score < 60 → quality_score persisted and the
475
+ // injection sort re-ranks by importance * quality_score/100 (degraded)
476
+ // score < 30 → archived + tagged low_quality (still explicitly searchable)
477
+ // Meta-memory markers, near-duplicates and repetitive filler lose points.
478
+ memoryQualityFilter: z.object({
479
+ enabled: z.boolean().default(true),
480
+ archiveThreshold: z.natural().min(1).max(100).default(30),
481
+ degradeThreshold: z.natural().min(1).max(100).default(60),
482
+ minContentLength: z.natural().min(1).max(1000).default(10),
483
+ // Issue #135 附属发现 1:importance ≥ 该值的记忆不参与静默自动归档——评分
484
+ // 与信号标签照常写入(可观测)、注入排序照常降权,但 setArchived 跳过。
485
+ // 报告实测 150 条低分归档里 128 条 importance ≥ 4(51 条 = 5)。设 1 = 全部
486
+ // 豁免(等效关闭自动归档),设 5 = 仅最重要的豁免。
487
+ exemptImportance: z.natural().min(1).max(5).default(4)
488
+ }).default({}),
489
+
490
+ // --- LLM audit trail (Bug8) ------------------------------------------------
491
+ // Records every background LLM call (autoDream consolidation + summary,
492
+ // autoSummarize compression) into llm_audit_logs: tokens, duration, status
493
+ // and which trigger produced it. Failures are recorded as status=error and
494
+ // never block the feature. retentionDays bounds the table: older rows are
495
+ // purged on boot.
496
+ llmAudit: z.object({
497
+ enabled: z.boolean().default(true),
498
+ retentionDays: z.natural().min(1).max(3650).default(90)
499
+ }).default({}),
500
+
501
+ // --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
502
+ // Separate retrieval evaluation snapshots from the production recall audit.
503
+ // When false (default) evaluateRetrieval still computes precision/recall/mrr
504
+ // and returns them to the caller, but writes nothing to recall_evals — the
505
+ // eval table only grows when the operator opts in. Production searchMemories
506
+ // audits to recall_runs and NEVER touches recall_evals, regardless of this
507
+ // flag (production isolation is unconditional).
508
+ evalPersistTestResults: z.boolean().default(false),
509
+
510
+ // --- standalone external API (v0.7.12) ------------------------------------
511
+ // A plain node:http server for ecosystem integrations that cannot reach the
512
+ // DSH-internal webServer. Disabled by default; when enabled the Bearer token
513
+ // is persisted in the settings kv ("external_api"), auto-generated on first
514
+ // boot. Bind host: keep the loopback default — moving it to a non-loopback
515
+ // address exposes the whole memory store to the network and is the
516
+ // operator's responsibility.
517
+ externalApiEnabled: z.boolean().default(false),
518
+ externalApiPort: z.natural().default(8790),
519
+ externalApiHost: z.string().default("127.0.0.1"),
520
+
521
+ // --- light mode preset (v0.7.12) -------------------------------------------
522
+ // One switch for low-resource setups: turns off every background/semantic
523
+ // heavy path (dream consolidation, entity extraction, vector pipeline,
524
+ // reranker, BM25, semantic dedup / selective inject, sleep mode) while
525
+ // keeping the core loop (autoInject, autoSummarize, hot memory, quality
526
+ // filter, keyword search). Applied by applyLightModePreset before the config
527
+ // reaches any service; a persisted panel_mode="light" (settings kv) counts
528
+ // as lightMode=true too and wins over the bundle config.
529
+ lightMode: z.boolean().default(false),
530
+
531
+ // --- heat: v0.7.0 self-evolution (heat + interest drift) ----------------
532
+ // 总开关,默认关(v0.7.12+ 用户已习惯无 heat 行为,默认开=全员行为变更)。
533
+ // 开启后:提供热度字段 / sleep 降级联合判定保护 / 前端热度投影,并只在
534
+ // 注入排序的优先级层内乘 heat(#218 v1;store 的 order=chrono 分页序与
535
+ // 召回融合序不动)。关闭则跳过所有 heat 计算与热度触达,注入排序乘数
536
+ // 恒 1,sleep 降级退回纯时间分层。
537
+ // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
538
+ heatEnabled: z.boolean().default(false),
539
+ // 广义指数形状参数 β(heat = exp(-λ·Δt^β),issue #218 拍板):β=1 纯指数。
540
+ // 快慢以 Δt>1 小时为准——β<1 衰减更慢(亚线性长尾)、β>1 更快(超线性);
541
+ // 0<Δt<1 的首小时内方向相反(Δt^β 随 β 增大而变小)。专家调优项,不进面板白名单。
542
+ heatGlobalBeta: z.number().min(0.5).max(2).default(1.0),
543
+ // per-type 衰减因子 λ;λ=0 的类型免疫(热度恒 1.0,sleep 永不降级)。
544
+ // 未知类型走默认 0.002。dict 的键为 type 字符串、值为数字 λ。
545
+ heatTypeDecay: z.dict(z.number(), z.string()).default({ ...TYPE_DECAY_DEFAULTS }),
546
+ // sleep 降级联合判定的热度下限:heat < 该值 且 importance<5 才允许降级。
547
+ sleepHeatThreshold: z.number().min(0).max(1).default(0.05),
548
+ // recordRecall 默认值(recall_runs 记录默认开;显式传 false 的调用方不受影响)。
549
+ recallRecordDefault: z.boolean().default(true),
550
+ // recall_runs 滚动清理保留天数。
551
+ recallRetentionDays: z.natural().min(1).max(3650).default(90),
552
+
553
+ // --- tool exposure: 慢模型/轻量模型的工具往返节流(v0.8.5)----------------
554
+ // 跨会话记忆已由 inject.js 每轮自动注入系统提示词,memory_search 只用于
555
+ // 「注入里没有、需要深挖」的补充检索;memory_archive 是隐藏/恢复条目的整理
556
+ // 操作,正常会话里很少需要。轻量模型对「何时该用工具」判断弱,容易每轮
557
+ // 顺手调一遍——每次工具调用都是一次串行往返(生成参数→执行→回填→再生成),
558
+ // 在慢模型上会被放大成明显卡顿。这两个开关允许直接隐藏对应工具(默认全
559
+ // 开=行为不变),隐藏后模型根本看不到它,也就不会调。也走 feature_flags
560
+ // 白名单,面板可启停=线上回滚开关。
561
+ disableMemorySearch: z.boolean().default(false),
562
+ disableMemoryArchive: z.boolean().default(false),
563
+
564
+ // --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
565
+ // 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
566
+ // 逐字节不变。开启后 memory_save 写入 agent_scope(session header 的
567
+ // agentPreset)与 workspace_scope(registry 反查 canonical path,header.cwd
568
+ // 兜底,取不到 NULL——解析绝不阻塞写入),去重键扩展为 (type, title,
569
+ // agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
570
+ // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
571
+ scopeEnabled: z.boolean().default(false),
572
+ // strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见);
573
+ // 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
574
+ // 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
575
+ // 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
576
+ // 打开才有意义(没有写入标注就无可过滤维度),但独立成键:可先开标注积累
577
+ // 数据、观察 A2 加权质量后再切硬过滤。
578
+ strictScope: z.boolean().default(false),
579
+ });
580
+
581
+ // Fields forced to false by the light-mode preset. Everything not listed here
582
+ // (autoInject, autoSummarize, hotMemory*, memoryQualityFilter, dream
583
+ // thresholds/delays, ...) is left untouched — those are the core loop.
584
+ const LIGHT_MODE_OFF = [
585
+ "entityExtractionEnabled",
586
+ "autoDream",
587
+ "sleepModeEnabled",
588
+ "rerankEnabled",
589
+ "autoReindexOnBoot",
590
+ "hybridInject",
591
+ "searchSemanticDedup",
592
+ "selectiveInjectEnabled",
593
+ "bm25SearchEnabled",
594
+ // 轻量模式不开图召回轴(#219,依赖实体抽取产出;抽取本身已被关掉)。
595
+ "entityRecallEnabled",
596
+ // 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
597
+ "dreamNarrativeEnabled",
598
+ // 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
599
+ "documentMemoryEnabled",
600
+ // 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
601
+ "heatEnabled"
602
+ ];
603
+
604
+ /**
605
+ * Apply the light-mode preset to a resolved config object (pure function,
606
+ * exported for tests). When cfg.lightMode is not exactly true the config is
607
+ * returned unchanged; otherwise a shallow copy carries false for every heavy
608
+ * feature. Idempotent and side-effect free.
609
+ */
610
+ export function applyLightModePreset(cfg) {
611
+ if (cfg?.lightMode !== true) return cfg;
612
+ const preset = { ...cfg, lightMode: true };
613
+ for (const key of LIGHT_MODE_OFF) preset[key] = false;
614
+ return preset;
615
+ }