@modusensus/dsh-mneme 0.8.2 → 0.8.4

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