@modusensus/dsh-mneme 0.7.31 → 0.8.0

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 (93) hide show
  1. package/README.md +65 -27
  2. package/lib/api.js +139 -6
  3. package/lib/client.js +369 -32
  4. package/lib/commands.js +36 -5
  5. package/lib/config.js +101 -14
  6. package/lib/dream/decisions.js +147 -14
  7. package/lib/dream/sleep.js +84 -34
  8. package/lib/dream.js +250 -60
  9. package/lib/embedding.js +22 -2
  10. package/lib/index.js +68 -37
  11. package/lib/inject.js +54 -13
  12. package/lib/lang.js +168 -0
  13. package/lib/local-embedder.js +23 -9
  14. package/lib/mirror.js +17 -14
  15. package/lib/reranker.js +23 -8
  16. package/lib/runtime/adopt.js +176 -0
  17. package/lib/runtime/closure.js +172 -0
  18. package/lib/runtime/download.js +247 -0
  19. package/lib/runtime/layout.js +235 -0
  20. package/lib/runtime/loader.js +249 -0
  21. package/lib/runtime/provision.js +237 -0
  22. package/lib/runtime/tarball.js +130 -0
  23. package/lib/runtime/verify.js +191 -0
  24. package/lib/scope.js +83 -0
  25. package/lib/service.js +374 -55
  26. package/lib/settings.js +43 -2
  27. package/lib/store.js +126 -13
  28. package/lib/summarize.js +108 -35
  29. package/lib/tools.js +205 -12
  30. package/package.json +7 -2
  31. package/runtime-manifest.json +596 -0
  32. package/scripts/build-runtime-manifest.mjs +217 -0
  33. package/scripts/mneme-runtime.mjs +277 -0
  34. package/src/api.js +139 -6
  35. package/src/commands.js +36 -5
  36. package/src/config.js +101 -14
  37. package/src/dream/decisions.js +147 -14
  38. package/src/dream/sleep.js +84 -34
  39. package/src/dream.js +250 -60
  40. package/src/embedding.js +22 -2
  41. package/src/index.js +68 -37
  42. package/src/inject.js +54 -13
  43. package/src/lang.js +168 -0
  44. package/src/local-embedder.js +23 -9
  45. package/src/mirror.js +17 -14
  46. package/src/reranker.js +23 -8
  47. package/src/runtime/adopt.js +176 -0
  48. package/src/runtime/closure.js +172 -0
  49. package/src/runtime/download.js +247 -0
  50. package/src/runtime/layout.js +235 -0
  51. package/src/runtime/loader.js +249 -0
  52. package/src/runtime/provision.js +237 -0
  53. package/src/runtime/tarball.js +130 -0
  54. package/src/runtime/verify.js +191 -0
  55. package/src/scope.js +83 -0
  56. package/src/service.js +374 -55
  57. package/src/settings.js +43 -2
  58. package/src/store.js +126 -13
  59. package/src/summarize.js +108 -35
  60. package/src/tools.js +205 -12
  61. package/test/api.test.js +175 -9
  62. package/test/client.test.js +152 -10
  63. package/test/commands.test.js +40 -3
  64. package/test/config.test.js +17 -1
  65. package/test/dream-candidate-hybrid.test.js +281 -0
  66. package/test/dream.test.js +172 -0
  67. package/test/embedding-ready.test.js +196 -0
  68. package/test/helpers/runtime-source.js +60 -0
  69. package/test/helpers/tar-builder.js +80 -0
  70. package/test/inject.test.js +130 -1
  71. package/test/lang.test.js +87 -0
  72. package/test/quality-filter.test.js +68 -0
  73. package/test/reasoning-effort.test.js +106 -15
  74. package/test/reindex-backfill.test.js +62 -0
  75. package/test/runtime-adopt.test.js +206 -0
  76. package/test/runtime-cli.test.js +89 -0
  77. package/test/runtime-closure.test.js +193 -0
  78. package/test/runtime-download.test.js +302 -0
  79. package/test/runtime-layout.test.js +163 -0
  80. package/test/runtime-loader.test.js +237 -0
  81. package/test/runtime-manifest.test.js +158 -0
  82. package/test/runtime-provision.test.js +101 -0
  83. package/test/runtime-tarball.test.js +82 -0
  84. package/test/runtime-verify.test.js +214 -0
  85. package/test/scope-retrieval.test.js +197 -0
  86. package/test/scope-storage.test.js +247 -0
  87. package/test/scope-strict.test.js +165 -0
  88. package/test/service.test.js +76 -0
  89. package/test/sleep-action-set.test.js +422 -0
  90. package/test/store.test.js +14 -0
  91. package/test/summarize.test.js +224 -0
  92. package/test/tools.test.js +30 -5
  93. package/test/webServer-optional.test.js +34 -1
package/src/api.js CHANGED
@@ -3,8 +3,11 @@ import { readFileSync } from "node:fs";
3
3
  import { timingSafeEqual, randomBytes } from "node:crypto";
4
4
  import { FEATURE_FLAG_SPEC } from "./settings.js";
5
5
  import { TYPE_FILE, renderMirrorText, parseHumanEdits } from "./mirror.js";
6
+ import { langOf } from "./lang.js";
6
7
  import { computeHeat } from "./heat.js";
7
8
  import { describeStreamFailure, resolveRoute } from "./dream.js";
9
+ import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
10
+ import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
8
11
 
9
12
  // headers:少数端点(/export 附件下载)需要追加 Content-Disposition 等响应头。
10
13
  function sendJson(res, status, payload, headers = {}) {
@@ -113,6 +116,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
113
116
  const flagKeys = [
114
117
  ...FEATURE_FLAG_SPEC.booleans,
115
118
  ...Object.keys(FEATURE_FLAG_SPEC.ints),
119
+ ...Object.keys(FEATURE_FLAG_SPEC.numbers),
116
120
  ...FEATURE_FLAG_SPEC.strings,
117
121
  ...FEATURE_FLAG_SPEC.urls,
118
122
  ...Object.keys(FEATURE_FLAG_SPEC.enums)
@@ -604,18 +608,87 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
604
608
  handler(req, res) {
605
609
  try {
606
610
  const stats = semantic?.vectorIndex?.getStats?.() ?? null;
611
+ // #118: expose readiness so the status card can tell "initializing /
612
+ // unreachable" from "disabled". An embedder carrying no `ready` prop at
613
+ // all (test doubles, third-party adapters) is assumed usable.
614
+ // issue #135: 但「可用」和「配了」是两件事 —— legacy OpenAI embedder
615
+ // 此前恒报 ready,于是 baseUrl/apiKey/model 全空时状态卡依然是绿的
616
+ // ("绿色假阳性")。下面把两者分开,并附上索引覆盖度。
617
+ const ready = embedder ? ("ready" in embedder ? embedder.ready === true : true) : null;
618
+ // local/ollama 的配置来自 settings 兜底,没有「未配置」态:它们的
619
+ // ready=false 表示「还在初始化」,不是「没配」。
620
+ const configured = embedder ? (typeof embedder.configured === "boolean" ? embedder.configured : true) : null;
621
+ const embedded = Number(stats?.embeddedCount ?? 0);
622
+ const total = Number(stats?.totalCount ?? 0);
607
623
  sendJson(res, 200, {
608
- // #118: expose readiness so the status card can tell "initializing /
609
- // unreachable" from "disabled". Legacy OpenAI embedder has no `ready`
610
- // prop and is immediately usable, so treat that as ready.
611
- ready: embedder ? ("ready" in embedder ? embedder.ready === true : true) : null,
624
+ ready,
625
+ configured,
626
+ // 机器可读的原因;null = 一切正常。面板按它选文案,不用猜布尔组合。
627
+ reason: !embedder ? "no-embedder"
628
+ : configured === false ? "not-configured"
629
+ : ready !== true ? "initializing"
630
+ : null,
631
+ // 空库时 ratio 必须是 null 而非 0 —— 0 会被读成「一条都没嵌上」。
632
+ coverage: { embedded, total, ratio: total > 0 ? embedded / total : null },
633
+ // 配了、库里有东西可嵌、却一条都没嵌上 = 真的降级(本机此前长期
634
+ // 处于 ready:true + embeddedCount:0,面板却显示正常)。
635
+ degraded: configured === true && total > 0 && embedded === 0,
612
636
  // Explicit display name first: the legacy OpenAI-compatible embedder
613
637
  // is an object literal, so constructor.name would be "Object".
614
638
  embedProvider: embedder ? (embedder.name ?? embedder.constructor?.name ?? "unknown") : null,
615
639
  modelHash: embedder?.modelHash ?? null,
616
640
  dimension: embedder?.dimension ?? null,
617
641
  reranker: semantic?.reranker ? "ready" : null,
618
- index: stats
642
+ index: stats,
643
+ // issue #131 / PR-A:自管运行时的状态。无论当前 provider 是不是 local
644
+ // 都报 —— 面板需要区分「已收编但没启用」和「要用本地却没收编」。
645
+ // 这里只做结构检查(不跑推理),失败降级成状态对象,不会让本端点 500。
646
+ // 必须经 publicRuntimeStatus 投影:本端点是免鉴权的,绝对路径与原始错误
647
+ // 文本不能出去(CWE-200)。完整诊断走本机 CLI 的 status。
648
+ localRuntime: publicRuntimeStatus(describeLocalRuntime({ runtimeDir: config?.runtimeDir ?? "" }))
649
+ });
650
+ } catch {
651
+ sendJson(res, 500, { error: "internal" });
652
+ }
653
+ }
654
+ });
655
+
656
+ // --- 一键取回自管运行时(issue #131 / PR-C:三档来源)-------------------------
657
+ // 写路径(requireAuth):它会往 ~/.dsh/mneme/runtime/ 落一份依赖闭包。
658
+ //
659
+ // 第一档源固定取本 profile 的 node_modules(由插件自身路径推出),不接受客户端传源目录 ——
660
+ // 「从任意目录往运行时目录里搬东西」没有必要成为对外能力。
661
+ //
662
+ // 一律回 200:请求本身合法,成不成看 body 里的 ok/status/reason。让 HTTP 码只表达
663
+ // 「有没有鉴权/路径对不对」,结论交给 ok —— 面板要能显示「收编了但闭包缺件」这种
664
+ // 中间状态,用一个 4xx 概括会把信息压掉。
665
+ register({
666
+ kind: "exact",
667
+ path: "/api/dsh-mneme/runtime/provision",
668
+ handler(req, res) {
669
+ try {
670
+ if (req.method !== "POST") {
671
+ sendJson(res, 405, { error: "method-not-allowed" });
672
+ return;
673
+ }
674
+ if (!requireAuth(req, res, apiToken)) return;
675
+ return readBody(req).then(async (text) => {
676
+ const body = parseBody(text);
677
+ const runtimeDir = config?.runtimeDir ?? "";
678
+ // 三档来源依次尝试(收编本机 node_modules → 本地 .tgz 目录 → registry)。
679
+ // 所以这个端点会**联网**:它可能拉一份几十到上百 MB 的闭包。
680
+ const result = await provisionRuntime({
681
+ hostModulesDir: hostModulesDir(import.meta.url),
682
+ runtimeDir,
683
+ localTarballDir: config?.runtimeTarballDir ?? "",
684
+ mirror: config?.runtimeMirror ?? "",
685
+ overwrite: body?.overwrite === true
686
+ });
687
+ // 顺手带回最新状态,面板不必再打一次 /semantic(且它走的是免鉴权投影)。
688
+ sendJson(res, 200, {
689
+ ...result,
690
+ localRuntime: publicRuntimeStatus(describeLocalRuntime({ runtimeDir }))
691
+ });
619
692
  });
620
693
  } catch {
621
694
  sendJson(res, 500, { error: "internal" });
@@ -853,7 +926,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
853
926
  }
854
927
  const sections = [];
855
928
  for (const type of Object.keys(TYPE_FILE)) {
856
- if (byType[type]?.length) sections.push(renderMirrorText(type, byType[type]));
929
+ if (byType[type]?.length) sections.push(renderMirrorText(type, byType[type], langOf(config)));
857
930
  }
858
931
  sendAttachment(res, 200, "text/markdown; charset=utf-8", `dsh-mneme-export-${stamp}.md`, sections.join("\n"));
859
932
  } catch {
@@ -941,6 +1014,66 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
941
1014
  }
942
1015
  });
943
1016
 
1017
+ // --- 冲突集中处理(v0.8.0):队列 + 人工确认 --------------------------------
1018
+ // GET /conflicts:未解决冲突 + 双方当前内容(此前面板只有计数与徽章,
1019
+ // resolveConflictPending 在存储层躺着没有任何调用方)。POST /conflicts/resolve:
1020
+ // {id, winner: "a"|"b"|null, apply}——盖章审计,apply 时按 dream 非冻结
1021
+ // conflict 路径同款处置(service.resolveConflictPending 内部复用
1022
+ // applyDecisions:CAS + 事务 + 注记 + 幂等)。认证与其他写路由一致。
1023
+ register({
1024
+ kind: "exact",
1025
+ path: "/api/dsh-mneme/conflicts",
1026
+ handler(req, res) {
1027
+ try {
1028
+ if (req.method !== "GET") {
1029
+ sendJson(res, 404, { error: "not-found" });
1030
+ return;
1031
+ }
1032
+ if (!requireAuth(req, res, apiToken)) return;
1033
+ const url = new URL(req.url, "http://localhost");
1034
+ const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") ?? 50) || 50));
1035
+ const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0) || 0);
1036
+ const items = service.listConflictQueue({ limit, offset });
1037
+ sendJson(res, 200, { items, total: service.countConflictPending?.() ?? items.length });
1038
+ } catch {
1039
+ sendJson(res, 500, { error: "internal" });
1040
+ }
1041
+ }
1042
+ });
1043
+
1044
+ register({
1045
+ kind: "exact",
1046
+ path: "/api/dsh-mneme/conflicts/resolve",
1047
+ handler(req, res) {
1048
+ try {
1049
+ if (req.method !== "POST") {
1050
+ sendJson(res, 404, { error: "not-found" });
1051
+ return;
1052
+ }
1053
+ if (!requireAuth(req, res, apiToken)) return;
1054
+ return readBody(req).then((text) => {
1055
+ let body = {};
1056
+ try { body = JSON.parse(text || "{}"); } catch { /* 坏 JSON → 走缺参分支 */ }
1057
+ const id = typeof body.id === "string" ? body.id.trim() : "";
1058
+ if (!id) {
1059
+ sendJson(res, 400, { error: "missing-id" });
1060
+ return;
1061
+ }
1062
+ const winner = body.winner === "a" || body.winner === "b" ? body.winner : null;
1063
+ const apply = body.apply !== false;
1064
+ const result = service.resolveConflictPending(id, { winner, apply });
1065
+ if (!result) {
1066
+ sendJson(res, 404, { error: "not-found" });
1067
+ return;
1068
+ }
1069
+ sendJson(res, 200, { ok: true, conflict: result });
1070
+ }).catch(() => sendJson(res, 500, { error: "internal" }));
1071
+ } catch {
1072
+ sendJson(res, 500, { error: "internal" });
1073
+ }
1074
+ }
1075
+ });
1076
+
944
1077
  // --- health: mirror sync state (F-NEW-03 / v0.3.6) ---
945
1078
  // Auth-gated; only returns a sanitized error code (never raw last_error which
946
1079
  // may leak paths/token-like strings/internal hosts). On state read failure it
package/src/commands.js CHANGED
@@ -1,10 +1,18 @@
1
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
+ import { STR } from "./lang.js";
3
+
1
4
  // Custom slash-command manager: keeps the DSH command registry in sync with
2
5
  // user-defined commands persisted in SQLite. Commands are registered on boot
3
6
  // and (re)registered on add/remove through the API.
4
7
  //
5
- // Each custom command's handler returns the user-authored instruction as a
6
- // success result; the DSH UI surfaces it as a model-directed instruction.
7
- export function createCommandManager({ ctx, settings, logger }) {
8
+ // Per the DSH command contract, a handler's CommandResult text is only a UI
9
+ // settlement notice and never reaches the model. To actually drive the agent,
10
+ // the handler submits the user-authored instruction itself as a user followup
11
+ // message on the receiving agent (same pattern as the official /goal command).
12
+ // The notice just acknowledges the submission; an optional rawInput suffix
13
+ // typed after the command is appended for the model.
14
+
15
+ export function createCommandManager({ ctx, settings, logger, language = "zh" }) {
8
16
  const registered = new Map(); // name -> disposer
9
17
 
10
18
  function registerOne(command) {
@@ -13,8 +21,10 @@ export function createCommandManager({ ctx, settings, logger }) {
13
21
  try {
14
22
  dispose = ctx.commands.register({
15
23
  name: command.name,
16
- description: command.description || `自定义指令 ${command.name}`,
17
- handler: () => ({ kind: "success", text: command.instruction })
24
+ // 缺省描述随实例语言(命令描述进命令注册表,对 LLM 可见)。
25
+ description: command.description || STR.commandFallbackDesc[language](command.name),
26
+ input: { hint: STR.commandInputHint[language] },
27
+ handler: (invocation) => submit(command, invocation)
18
28
  });
19
29
  } catch (error) {
20
30
  logger?.warn?.(`dsh-mneme: failed to register command /${command.name}: ${String(error)}`);
@@ -23,6 +33,27 @@ export function createCommandManager({ ctx, settings, logger }) {
23
33
  registered.set(command.name, dispose);
24
34
  }
25
35
 
36
+ /** Submit the instruction (plus optional rawInput suffix) to the receiving agent. */
37
+ function submit(command, invocation) {
38
+ const suffix = typeof invocation?.rawInput === "string" ? invocation.rawInput.trim() : "";
39
+ const text = suffix ? `${command.instruction}\n\n${suffix}` : command.instruction;
40
+ const agent = invocation?.agent;
41
+ if (typeof agent?.followup !== "function") {
42
+ // 兜底:宿主未提供接收 agent 时退回旧行为,至少把指令展示在 notice 里。
43
+ return { kind: "success", text };
44
+ }
45
+ try {
46
+ agent.followup(createUserMessage({
47
+ content: [{ type: "text", text }],
48
+ source: { kind: "user" }
49
+ }));
50
+ } catch (error) {
51
+ logger?.warn?.(`dsh-mneme: failed to submit /${command.name} to agent: ${String(error)}`);
52
+ return { kind: "error", text: STR.commandSubmitFailed[language](command.name) };
53
+ }
54
+ return { kind: "success", text: STR.commandSubmitted[language](command.name) };
55
+ }
56
+
26
57
  function unregisterOne(name) {
27
58
  const dispose = registered.get(name);
28
59
  if (dispose) {
package/src/config.js CHANGED
@@ -2,6 +2,10 @@ import z from "@deepseek-ai/schemastery";
2
2
  import { TYPE_DECAY_DEFAULTS } from "./heat.js";
3
3
 
4
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=英文)。"),
5
9
  memoryDir: z.string().default("~/.dsh/memory"),
6
10
  autoInject: z.boolean().default(true),
7
11
  autoSummarize: z.boolean().default(true),
@@ -14,6 +18,17 @@ export const Config = z.object({
14
18
  // 蒸馏把完整对话上下文交给 LLM 提炼,不硬裁到 8000 字就截断语义;默认
15
19
  // 24000 字符(约覆盖一整轮中等对话),需要更完整可调大。
16
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),
17
32
  // 智能调速器(429 保护,默认开):蒸馏 LLM 调用全局串行排队,相邻请求
18
33
  // 间隔 distillRateLimitIntervalMs(默认 1s 一次);命中 429 限流时按
19
34
  // distillRateLimitBaseDelayMs 指数退避(1s→2s→4s…)自动重试
@@ -43,6 +58,11 @@ export const Config = z.object({
43
58
  // 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
44
59
  // 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
45
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),
46
66
  autoDream: z.boolean().default(true),
47
67
  dreamThresholdCount: z.natural().min(1).max(1000).default(10),
48
68
  dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
@@ -64,25 +84,47 @@ export const Config = z.object({
64
84
  // 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
65
85
  dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
66
86
  dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
67
- dreamMaxTokens: z.natural().min(256).max(131072).default(32768),
68
- // Pass-through reasoning effort for dream's LLM calls. 'none' (default)
69
- // omits the field so the provider's own default applies; low/medium/high
70
- // are forwarded verbatim. Useful to cap reasoning spend on thinking-type
71
- // models that would otherwise drain the whole token budget and return an
72
- // empty body ("no json array in llm output").
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+).
73
100
  // Caveat: on some thinking models (e.g. v4-flash-ga) the harness declares NO
74
101
  // supported effort, so even the fallback retry (field stripped) is rejected
75
102
  // again via its defaultEffort — prefer a non-reasoning dreamProvider/dreamModel.
76
103
  dreamReasoningEffort: z.union([
104
+ z.const("off"),
77
105
  z.const("low"),
78
106
  z.const("medium"),
79
107
  z.const("high"),
80
108
  z.const("none")
81
- ]).default("none").description("巩固模型的推理档位:'none'(默认)用服务商自带默认;low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
109
+ ]).description("巩固模型的推理档位:未配置 = 自动取模型支持的最低档(避免思考模型用自带默认档烧光预算返回空体,#135);显式 'none' = 不发送字段、用服务商自带默认;off/low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
82
110
  // 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
83
111
  // 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
84
112
  // 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
85
113
  dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
114
+ // Issue #125:候选集构造方式。"window"(默认,等同现状)只取最近
115
+ // dreamMaxSnapshotSize 条;"hybrid" 在此基础上并入**向量翻出的高相似组**——
116
+ // 本机实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口(窗口覆盖率 11.7%),
117
+ // 纯时间窗口让"该合并的一对"几乎永远碰不到面。候选总量仍由下面的上限封顶、
118
+ // 与库总量解耦,这是相对"调大窗口"的核心收益:输入成本不随库增长。
119
+ dreamCandidateMode: z.union([
120
+ z.const("window"),
121
+ z.const("hybrid")
122
+ ]).default("window"),
123
+ // hybrid 的候选总量上限;0 = 复用 dreamMaxSnapshotSize(不迁移,需要时显式覆盖)。
124
+ dreamCandidateMax: z.natural().min(0).max(5000).default(0),
125
+ // hybrid 判"高相似"的阈值;0.85 与 sleep normal 档对齐——两个模块对"高相似"
126
+ // 保持同一个定义。
127
+ dreamCandidateMinSim: z.number().min(0.5).max(0.99).default(0.85),
86
128
  // 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
87
129
  // 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
88
130
  // (未覆盖即拒绝整单)。
@@ -134,6 +176,20 @@ export const Config = z.object({
134
176
  embedModelCacheDir: z.string().default(""),
135
177
  embedModelMirror: z.string().default("https://hf-mirror.com"),
136
178
 
179
+ // 自管运行时目录(issue #131)。空 = ~/.dsh/mneme/runtime:里面放收编或下载来的
180
+ // transformers + onnxruntime 闭包,插件经 src/runtime/loader.js 的三层解析加载它。
181
+ // 目的就是让这条重依赖不必留在宿主 profile 的依赖图里——profile 是所有插件共用的
182
+ // 依赖图,留在里面会让「装任何插件都要替它重走一遍这条链」。
183
+ runtimeDir: z.string().default(""),
184
+
185
+ // 运行时取件的两个来源(issue #131 / PR-C)。都是「克制」的默认:留空 = 不额外改变行为。
186
+ // - runtimeTarballDir:本地 .tgz 目录。某个包网络下不到时(例如 onnxruntime-node),
187
+ // 把 `npm pack <pkg>` 出来的 tgz 丢进这个目录即可离线取件;有它就优先于联网。
188
+ // - runtimeMirror:registry 镜像前缀,例如 https://npmmirror.com/mirrors/npm/ 。
189
+ // 留空则用 runtime-manifest.json 里写死的官方地址(那是随包发布的固定清单)。
190
+ runtimeTarballDir: z.string().default(""),
191
+ runtimeMirror: z.string().default(""),
192
+
137
193
  // Vector search tuning.
138
194
  vectorSearchTopK: z.natural().min(1).max(100).default(20),
139
195
  vectorSearchThreshold: z.number().min(0).max(1).default(0.65),
@@ -228,9 +284,9 @@ export const Config = z.object({
228
284
  // default provider/model.
229
285
  entityExtractionModel: z.string().default(""),
230
286
  // Reasoning effort for entity extraction (issue #109), mirrors
231
- // dreamReasoningEffort: 'none' (default) omits the field / provider default;
232
- // low/medium/high passed through. A provider that rejects the effort retries
233
- // once without it, so opting in is safe to experiment with.
287
+ // dreamReasoningEffort pass-through: 'none' omits the field / provider
288
+ // default; off/low/medium/high passed through. A provider that rejects the
289
+ // effort retries once without it, so opting in is safe to experiment with.
234
290
  entityExtractionReasoning: z.union([
235
291
  z.const("low"),
236
292
  z.const("medium"),
@@ -265,6 +321,14 @@ export const Config = z.object({
265
321
  z.const("normal"),
266
322
  z.const("aggressive")
267
323
  ]).default("normal"),
324
+ // Issue #126:冲突阶段的动作集。"conflict"(默认 = 现状)只用 conflict/keep——
325
+ // 后台自动流程不静默扩张行为;"full" 开放六分支(merge / update / supersede /
326
+ // differentiate / conflict / keep),互补型与演进型重复因此有了正确出口,不再
327
+ // 被迫"输赢化"(实测抽样 10 对里 2 对属演进型/互补型,用 conflict 处理会丢信息)。
328
+ sleepActionSet: z.union([
329
+ z.const("conflict"),
330
+ z.const("full")
331
+ ]).default("conflict"),
268
332
  // Archival demotion tiering (days since last access):
269
333
  // >= sleepArchiveDays → shrink to summary, full body kept in _full_content
270
334
  // >= sleepCompressDays → archived outright (entity relations preserved)
@@ -281,14 +345,16 @@ export const Config = z.object({
281
345
  sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
282
346
  sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
283
347
  // Pass-through reasoning effort for sleep's LLM passes, same semantics as
284
- // dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
285
- // are forwarded verbatim.
348
+ // dreamReasoningEffort (Issue #135 建议 4/5): unset resolves to the lowest
349
+ // effort the model declares; explicit 'none' omits the field; off/low/
350
+ // medium/high are forwarded verbatim.
286
351
  sleepReasoningEffort: z.union([
352
+ z.const("off"),
287
353
  z.const("low"),
288
354
  z.const("medium"),
289
355
  z.const("high"),
290
356
  z.const("none")
291
- ]).default("none").description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,'none' 用服务商默认。"),
357
+ ]).description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,未配置 = 自动取模型支持的最低档;显式 'none' = 不发送字段、用服务商自带默认。"),
292
358
 
293
359
  // --- epistemic trust: memory source credibility (v0.4.5) -----------------
294
360
  // Distinguish memories by source: observation (measured / witnessed),
@@ -311,7 +377,12 @@ export const Config = z.object({
311
377
  enabled: z.boolean().default(true),
312
378
  archiveThreshold: z.natural().min(1).max(100).default(30),
313
379
  degradeThreshold: z.natural().min(1).max(100).default(60),
314
- minContentLength: z.natural().min(1).max(1000).default(10)
380
+ minContentLength: z.natural().min(1).max(1000).default(10),
381
+ // Issue #135 附属发现 1:importance ≥ 该值的记忆不参与静默自动归档——评分
382
+ // 与信号标签照常写入(可观测)、注入排序照常降权,但 setArchived 跳过。
383
+ // 报告实测 150 条低分归档里 128 条 importance ≥ 4(51 条 = 5)。设 1 = 全部
384
+ // 豁免(等效关闭自动归档),设 5 = 仅最重要的豁免。
385
+ exemptImportance: z.natural().min(1).max(5).default(4)
315
386
  }).default({}),
316
387
 
317
388
  // --- LLM audit trail (Bug8) ------------------------------------------------
@@ -372,6 +443,22 @@ export const Config = z.object({
372
443
  recallRecordDefault: z.boolean().default(true),
373
444
  // recall_runs 滚动清理保留天数。
374
445
  recallRetentionDays: z.natural().min(1).max(3650).default(90),
446
+
447
+ // --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
448
+ // 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
449
+ // 逐字节不变。开启后 memory_save 写入 agent_scope(session header 的
450
+ // agentPreset)与 workspace_scope(registry 反查 canonical path,header.cwd
451
+ // 兜底,取不到 NULL——解析绝不阻塞写入),去重键扩展为 (type, title,
452
+ // agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
453
+ // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
454
+ scopeEnabled: z.boolean().default(false),
455
+ // strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见);
456
+ // 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
457
+ // 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
458
+ // 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
459
+ // 打开才有意义(没有写入标注就无可过滤维度),但独立成键:可先开标注积累
460
+ // 数据、观察 A2 加权质量后再切硬过滤。
461
+ strictScope: z.boolean().default(false),
375
462
  });
376
463
 
377
464
  // Fields forced to false by the light-mode preset. Everything not listed here