@modusensus/dsh-mneme 0.7.21 → 0.7.23

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.
package/src/config.js CHANGED
@@ -47,9 +47,14 @@ export const Config = z.object({
47
47
  dreamThresholdCount: z.natural().min(1).max(1000).default(10),
48
48
  dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
49
49
  dreamDelayMs: z.natural().min(0).max(60000).default(2000),
50
+ // autoDream 触发最小间隔(分钟,0 = 不限制,Issue #89 请求 2):高频写入
51
+ // 场景下防止巩固调用(含失败重试)连发刷爆配额。间隔从每次实际开跑时刻
52
+ // 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次
53
+ // 写入事件会重新评估。
54
+ dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
50
55
  dreamProvider: z.string(),
51
56
  dreamModel: z.string(),
52
- dreamMaxTokens: z.natural().min(256).max(131072).default(8192),
57
+ dreamMaxTokens: z.natural().min(256).max(131072).default(32768),
53
58
  // Pass-through reasoning effort for dream's LLM calls. 'none' (default)
54
59
  // omits the field so the provider's own default applies; low/medium/high
55
60
  // are forwarded verbatim. Useful to cap reasoning spend on thinking-type
@@ -74,6 +79,13 @@ export const Config = z.object({
74
79
  // → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
75
80
  // 默认 0.5(至少显式覆盖一半 snapshot)。
76
81
  dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
82
+ // Issue #89:v0.6.9(Issue #26)的宽容路径回归。默认跳过单条非法决策
83
+ // (未知 id / 跨类型合并等)、应用合法子集、run 记 degraded;设 false 恢复
84
+ // 整单拒绝的严格模式。全局上限与覆盖率下限不受此开关影响、始终整单拒绝。
85
+ dreamSkipInvalid: z.boolean().default(true),
86
+ // 显式开启后放宽跨类型合并检查(类型边界由用户自行承担);配合
87
+ // dreamSkipInvalid 理解:关闭 skipInvalid 时跨类型 merge 直接整单拒绝。
88
+ allowCrossTypeMerge: z.boolean().default(false),
77
89
  // Rule version for dream adjudication: when this bumps, older dream_runs
78
90
  // degrade to historical evidence (their receipts no longer drive live
79
91
  // decisions). Default 0 = no versioning in use yet.
@@ -7,120 +7,153 @@ const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
7
7
 
8
8
  /**
9
9
  * Validate a dream decision list against a snapshot of eligible memories.
10
- * @param decisions - LLM-produced decision list.
10
+ * @param decisions - LLM-produced decision list. In skipInvalid mode, invalid
11
+ * entries are spliced out of this array in place (the caller reuses the same
12
+ * reference downstream for apply/audit); implicit keeps are appended here too.
11
13
  * @param snapshot - Map<id, memory> of eligible (non-archived, non-summary) entries.
12
- * @returns {{ok: boolean, errors: string[]}}
14
+ * @returns {{ok: boolean, errors: string[], skipped?: Array<{index, action, ids, error}>}}
13
15
  */
14
16
  export function validateDecisions(decisions, snapshot, options = {}) {
15
17
  const errors = [];
18
+ // Issue #89 回归修复(v0.6.9 / Issue #26 的 skipInvalid 路径原样移植,该路径
19
+ // 在 v0.7.11 重写中丢失):skipInvalid 开启时,单条非法决策只跳过该条(记录
20
+ // 到 skipped、不 claim 任何 id),合法子集照常应用,调用方据此把 run 记为
21
+ // degraded。全局信号(update/create 上限、显式覆盖率下限)依旧整单拒绝——
22
+ // 刷爆上限或覆盖率不达标的输出是模型坏了,不是轻微 schema 漂移。
23
+ const skipped = [];
24
+ const survivors = [];
25
+ const skipInvalid = options.skipInvalid === true;
16
26
  const maxUpdatePerRun = options.maxUpdatePerRun ?? 2;
17
27
  const minAgeHours = options.minAgeHours ?? 24;
18
- if (!Array.isArray(decisions) || decisions.length === 0) {
19
- return { ok: false, errors: ["decision list must be a non-empty array"] };
28
+ if (!Array.isArray(decisions)) {
29
+ return { ok: false, errors: ["decision list must be an array"] };
30
+ }
31
+ // 空决策 = 模型完整评估后确认无需操作(CONSOLIDATION_PROMPT 明确允许"无问题的
32
+ // 条目无需输出")。合法 JSON [] 不是空体(那是无输出/截断),也不是"残缺输出"
33
+ // ——显式短路直接 ok,避免隐式 keep 的覆盖率检查把 0% 误判为模型坏了。与
34
+ // sleep 的空模式(skipped no-op)语义对齐:下游 applied=0、audit 记 ok。
35
+ if (decisions.length === 0) {
36
+ return { ok: true, errors: [], skipped: [] };
20
37
  }
21
38
  const claimed = new Set();
22
39
  for (const [index, d] of decisions.entries()) {
23
40
  const at = `decision[${index}]`;
41
+ // 单条校验错误先进 local:skipInvalid 模式下整条跳过,严格模式下才并入
42
+ // 全局 errors(沿用 v0.6.9 的双轨结构)。
43
+ const local = [];
44
+ const ids = d && d.action === "conflict" ? [d.winner, d.loser] : (d?.ids ?? []);
24
45
  if (!d || typeof d !== "object" || !ACTIONS.has(d.action)) {
25
- errors.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
26
- continue;
27
- }
28
- const ids = d.action === "conflict" ? [d.winner, d.loser] : (d.ids ?? []);
29
- if (d.action === "conflict") {
46
+ local.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
47
+ } else if (d.action === "conflict") {
30
48
  if (!d.winner || !d.loser || d.winner === d.loser) {
31
- errors.push(`${at}: conflict needs distinct winner and loser`);
32
- continue;
49
+ local.push(`${at}: conflict needs distinct winner and loser`);
33
50
  }
34
51
  } else if (d.action === "create") {
35
52
  // Mint a fresh memory (sleep pattern discovery). Claims no existing id,
36
53
  // so it skips the claiming loop below; evidence is optional provenance
37
54
  // (already filtered to real ids by the caller) and is stored in content.
38
55
  if (typeof d.title !== "string" || !d.title.trim()) {
39
- errors.push(`${at}: create needs non-empty title`);
40
- continue;
56
+ local.push(`${at}: create needs non-empty title`);
41
57
  }
42
58
  if (typeof d.content !== "string" || !d.content.trim()) {
43
- errors.push(`${at}: create needs non-empty content`);
44
- continue;
59
+ local.push(`${at}: create needs non-empty content`);
45
60
  }
46
61
  if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
47
- errors.push(`${at}: create importance must be an integer 1-5 when provided`);
62
+ local.push(`${at}: create importance must be an integer 1-5 when provided`);
48
63
  }
49
64
  if (typeof d.type !== "string" || !d.type.trim()) {
50
- errors.push(`${at}: create needs non-empty type`);
65
+ local.push(`${at}: create needs non-empty type`);
51
66
  }
52
- continue;
53
67
  } else if (!Array.isArray(d.ids) || d.ids.length === 0) {
54
- errors.push(`${at}: ${d.action} needs non-empty ids`);
55
- continue;
68
+ local.push(`${at}: ${d.action} needs non-empty ids`);
56
69
  }
57
70
  // update-specific field validation runs BEFORE claiming ids, so a failing
58
71
  // update never pollutes the claimed set (which drives the "every id must
59
72
  // appear in a decision" check below).
60
- if (d.action === "update") {
73
+ if (local.length === 0 && d?.action === "update") {
61
74
  // 只能更新单条
62
75
  if (!Array.isArray(d.ids) || d.ids.length !== 1) {
63
- errors.push(`${at}: update must target exactly one id`);
64
- continue;
65
- }
66
- // 必须产生实际变化
67
- const mem = snapshot.get(d.ids[0]);
68
- const hasChange = (d.title !== undefined && d.title !== mem?.title)
69
- || (d.content !== undefined && d.content !== mem?.content)
70
- || (d.importance !== undefined && d.importance !== mem?.importance);
71
- if (!hasChange) {
72
- errors.push(`${at}: update must change at least one field`);
73
- continue;
74
- }
75
- // 不能更新 summary
76
- if (mem?.type === "summary") {
77
- errors.push(`${at}: cannot update summary via update action`);
78
- continue;
79
- }
80
- // 保护期:新建记忆不可立即被 update(可配置)
81
- const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
82
- if (ageHours < minAgeHours) {
83
- errors.push(`${at}: memory too young (< ${minAgeHours}h)`);
84
- continue;
76
+ local.push(`${at}: update must target exactly one id`);
77
+ } else {
78
+ // 必须产生实际变化
79
+ const mem = snapshot.get(d.ids[0]);
80
+ const hasChange = (d.title !== undefined && d.title !== mem?.title)
81
+ || (d.content !== undefined && d.content !== mem?.content)
82
+ || (d.importance !== undefined && d.importance !== mem?.importance);
83
+ if (!hasChange) {
84
+ local.push(`${at}: update must change at least one field`);
85
+ } else if (mem?.type === "summary") {
86
+ // 不能更新 summary
87
+ local.push(`${at}: cannot update summary via update action`);
88
+ } else {
89
+ // 保护期:新建记忆不可立即被 update(可配置)
90
+ const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
91
+ if (ageHours < minAgeHours) {
92
+ local.push(`${at}: memory too young (< ${minAgeHours}h)`);
93
+ }
94
+ }
85
95
  }
86
96
  }
87
- for (const id of ids) {
88
- const mem = snapshot.get(id);
89
- if (!mem) {
90
- errors.push(`${at}: unknown id ${JSON.stringify(id)}`);
91
- } else if (mem.archived || mem.type === "summary") {
92
- errors.push(`${at}: id ${JSON.stringify(id)} is archived or summary (not eligible)`);
97
+ if (local.length === 0 && d && d.action !== "create") {
98
+ // seen 捕获同一条决策内的重复 id;claimed 只含先前存活决策的 id
99
+ // (被跳过的决策不 claim,其目标留给其它合法决策/隐式 keep)。
100
+ const seen = new Set();
101
+ for (const id of ids) {
102
+ const mem = snapshot.get(id);
103
+ if (!mem) {
104
+ local.push(`${at}: unknown id ${JSON.stringify(id)}`);
105
+ } else if (mem.archived || mem.type === "summary") {
106
+ local.push(`${at}: id ${JSON.stringify(id)} is archived or summary (not eligible)`);
107
+ }
108
+ if (claimed.has(id) || seen.has(id)) {
109
+ local.push(`${at}: id ${JSON.stringify(id)} claimed by multiple decisions`);
110
+ }
111
+ seen.add(id);
93
112
  }
94
- if (claimed.has(id)) {
95
- errors.push(`${at}: id ${JSON.stringify(id)} claimed by multiple decisions`);
113
+ if (local.length === 0 && d.action === "merge") {
114
+ if (!d.keepSource || !d.ids.includes(d.keepSource)) {
115
+ local.push(`${at}: merge keepSource must be one of ids`);
116
+ }
117
+ if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
118
+ local.push(`${at}: merge needs non-empty title and content`);
119
+ }
120
+ if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
121
+ local.push(`${at}: merge importance must be an integer 1-5 when provided`);
122
+ }
123
+ // Merging across types would blur preference/project/decision boundaries
124
+ // in the injected context; the snapshot carries each entry's type.
125
+ // Issue #26 (P1):默认禁止跨类型合并。用户显式开启 allowCrossTypeMerge
126
+ // 后放宽该检查,类型边界由用户自行承担。
127
+ const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
128
+ if (mergeTypes.size > 1 && options.allowCrossTypeMerge !== true) {
129
+ local.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
130
+ }
96
131
  }
97
- claimed.add(id);
98
132
  }
99
- if (d.action === "merge") {
100
- if (!d.keepSource || !d.ids.includes(d.keepSource)) {
101
- errors.push(`${at}: merge keepSource must be one of ids`);
102
- }
103
- if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
104
- errors.push(`${at}: merge needs non-empty title and content`);
105
- }
106
- if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
107
- errors.push(`${at}: merge importance must be an integer 1-5 when provided`);
108
- }
109
- // Merging across types would blur preference/project/decision boundaries
110
- // in the injected context; the snapshot carries each entry's type.
111
- const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
112
- if (mergeTypes.size > 1) {
113
- errors.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
133
+ if (local.length > 0) {
134
+ if (skipInvalid) {
135
+ // 单条非法 跳过该决策,不 claim id(其目标记忆留给其它合法决策/隐式
136
+ // keep),并记录到 skipped 供调用方日志/审计。信息性跳过绝不写入全局
137
+ // errors,否则会误触发下方的整单拒绝。
138
+ skipped.push({ index, action: d?.action, ids, error: local.join("; ") });
139
+ } else {
140
+ errors.push(...local);
114
141
  }
142
+ continue;
143
+ }
144
+ if (d.action !== "create") {
145
+ for (const id of ids) claimed.add(id);
115
146
  }
147
+ survivors.push(d);
116
148
  }
117
- // Cap update churn: too many edits in one cycle signals a runaway model
118
- const updateCount = decisions.filter((d) => d.action === "update").length;
149
+ // Cap update churn: too many edits in one cycle signals a runaway model.
150
+ // 全局信号——skipInvalid 模式下依旧整单拒绝(见函数头注释)。
151
+ const updateCount = survivors.filter((d) => d.action === "update").length;
119
152
  if (updateCount > maxUpdatePerRun) {
120
153
  errors.push(`too many update decisions: ${updateCount} > ${maxUpdatePerRun}`);
121
154
  }
122
155
  // Cap pattern minting per run (sleepMaxPatternPerRun passes through here).
123
- const createCount = decisions.filter((d) => d.action === "create").length;
156
+ const createCount = survivors.filter((d) => d.action === "create").length;
124
157
  const maxCreatePerRun = options.maxCreatePerRun ?? 5;
125
158
  if (createCount > maxCreatePerRun) {
126
159
  errors.push(`too many create decisions: ${createCount} > ${maxCreatePerRun}`);
@@ -137,25 +170,31 @@ export function validateDecisions(decisions, snapshot, options = {}) {
137
170
  // snapshot(claimed.size / snapshot.size < dreamMinExplicitCoverage)时整单拒绝,
138
171
  // 而不是用 keep 把绝大部分 snapshot 全部"通过"。
139
172
  if (errors.length > 0) {
140
- return { ok: false, errors };
173
+ return { ok: false, errors, skipped };
141
174
  }
142
175
  const minCoverage = options.dreamMinExplicitCoverage ?? 0.5;
143
176
  if (options.dreamImplicitKeep !== false) {
144
177
  const coverage = snapshot.size > 0 ? claimed.size / snapshot.size : 1;
145
178
  if (coverage < minCoverage) {
146
179
  errors.push(`explicit decision coverage ${Math.round(coverage * 100)}% < minimum ${Math.round(minCoverage * 100)}%`);
147
- return { ok: false, errors };
180
+ return { ok: false, errors, skipped };
148
181
  }
149
182
  for (const id of snapshot.keys()) {
150
- if (!claimed.has(id)) decisions.push({ action: "keep", ids: [id] });
183
+ if (!claimed.has(id)) survivors.push({ action: "keep", ids: [id] });
151
184
  }
152
185
  } else {
153
186
  for (const id of snapshot.keys()) {
154
187
  if (!claimed.has(id)) errors.push(`memory ${JSON.stringify(id)} missing from decisions`);
155
188
  }
156
- if (errors.length > 0) return { ok: false, errors };
189
+ if (errors.length > 0) return { ok: false, errors, skipped };
157
190
  }
158
- return { ok: true, errors };
191
+ // 调用方下游(apply/audit)复用同一 decisions 引用:就地同步为 survivors——
192
+ // 在 skipInvalid 模式下去掉被跳过的非法决策;在隐式 keep 下追加补齐的 keep。
193
+ // 不能以 survivors.length !== decisions.length 作为是否 splice 的判据:
194
+ // 当"被跳过的非法决策数 == 隐式补齐的 keep 数"时长度回到相等但内容已变,
195
+ // 被跳过的决策会残留进 apply/audit。一律无条件 splice 最安全。
196
+ decisions.splice(0, decisions.length, ...survivors);
197
+ return { ok: true, errors, skipped };
159
198
  }
160
199
 
161
200
  /** Marker thrown when a decision target changed since the run snapshot. */
package/src/dream.js CHANGED
@@ -468,12 +468,15 @@ async function maintainIndexAfterDream(decisions, service, semantic) {
468
468
  if (embedder.modelHash) vectorIndex.markModel?.(embedder.modelHash, embedder.dimension);
469
469
  }
470
470
 
471
- export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, logger, semantic = null }) {
471
+ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, logger, semantic = null }) {
472
472
  let pendingTimer = null;
473
473
  let running = false;
474
474
  let disposed = false;
475
475
  let baseline = { count: 0, chars: 0 };
476
476
  let inFlight = null;
477
+ // Issue #89(请求 2):上一次实际开跑时刻。失败/degraded 的 run 也占用
478
+ // 最小间隔——节流的目的正是防止失败调用连发;间隔内的触发静默跳过。
479
+ let lastRunAt = 0;
477
480
 
478
481
  function shouldTrigger(service) {
479
482
  const memories = service.all().filter((m) => !m.archived && m.type !== "summary");
@@ -486,11 +489,14 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
486
489
 
487
490
  function maybeSchedule(service) {
488
491
  if (disposed || running || pendingTimer) return false;
492
+ // Issue #89(请求 2):最小触发间隔闸门。
493
+ if (minIntervalMs > 0 && Date.now() - lastRunAt < minIntervalMs) return false;
489
494
  const { trigger, count, chars } = shouldTrigger(service);
490
495
  if (!trigger) return false;
491
496
  pendingTimer = setTimeout(() => {
492
497
  pendingTimer = null;
493
498
  running = true;
499
+ lastRunAt = Date.now();
494
500
  // Defer the onRun invocation so a synchronous throw cannot escape the
495
501
  // timer callback (which would crash the process) and skip the teardown.
496
502
  // Errors are logged, never swallowed silently. inFlight lets dispose()
@@ -704,18 +710,26 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
704
710
  logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0}; head: ${head})`);
705
711
  return finish({ ok: false, error: "no json array in llm output", summary: false });
706
712
  }
707
- const { ok, errors } = validateDecisions(decisions, snapshot, {
713
+ const { ok, errors, skipped } = validateDecisions(decisions, snapshot, {
708
714
  maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
709
715
  minAgeHours: config.reflectionUpdateMinAgeHours,
710
716
  // v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
711
717
  // 真正生效,dreamMinExplicitCoverage 决定隐式 keep 下的覆盖率下限。
712
718
  dreamImplicitKeep: config.dreamImplicitKeep,
713
- dreamMinExplicitCoverage: config.dreamMinExplicitCoverage
719
+ dreamMinExplicitCoverage: config.dreamMinExplicitCoverage,
720
+ // Issue #89:v0.6.9(Issue #26)的宽容路径在 v0.7.11 重写中丢失——单条
721
+ // 非法决策重新只跳过该条、合法子集照常应用(run 记为 degraded)。
722
+ skipInvalid: config.dreamSkipInvalid !== false,
723
+ allowCrossTypeMerge: config.allowCrossTypeMerge === true
714
724
  });
715
725
  if (!ok) {
716
726
  logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
717
727
  return finish({ ok: false, error: `invalid decisions: ${errors.length} errors`, summary: false });
718
728
  }
729
+ const skippedInvalid = skipped.length > 0;
730
+ if (skippedInvalid) {
731
+ logger?.warn?.(`dsh-mneme dream: ${skipped.length} invalid decision(s) skipped (run degrades): ${skipped.map((s) => s.error).join("; ")}`);
732
+ }
719
733
 
720
734
  // Capture pre-update snapshots so the audit records what each update changed.
721
735
  const updateSnapshots = {};
@@ -872,9 +886,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
872
886
  // run. ok:false keeps the scheduler from moving the baseline.
873
887
  // ok — either real changes landed, or a fresh summary was stored
874
888
  // (all-keep + summary is a substantive summary refresh).
875
- // degraded — real consolidation landed but the summary came back empty/
876
- // missing: the store was absorbed (ok for the baseline) but
877
- // the run did not produce its full output (marked, not faked).
889
+ // degraded — real consolidation landed but the run did not produce its
890
+ // full output: the summary came back empty/missing, or
891
+ // skipInvalid dropped individually-invalid decisions
892
+ // (Issue #89 — marked, not faked). The valid subset was
893
+ // absorbed (ok for the baseline).
878
894
  let status;
879
895
  let okResult;
880
896
  if (partial) {
@@ -884,7 +900,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
884
900
  status = summaryStored ? "ok" : "noop";
885
901
  okResult = summaryStored;
886
902
  } else {
887
- status = summaryStored ? "ok" : "degraded";
903
+ status = summaryStored && !skippedInvalid ? "ok" : "degraded";
888
904
  okResult = true;
889
905
  }
890
906
  return finish({
package/src/index.js CHANGED
@@ -21,7 +21,10 @@ import { join } from "node:path";
21
21
  import { homedir } from "node:os";
22
22
 
23
23
  export const name = "dsh-mneme";
24
- export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel", "commands"];
24
+ // webServer 为可选依赖(headless/无 UI 宿主兼容):从 inject 声明中去掉,cordis
25
+ // 不再等待它激活;运行时 ctx.webServer 为空则跳过 API 注册(下方 if 守卫),
26
+ // 记忆工具/注入/dream 全部照常工作。
27
+ export const inject = ["tools", "systemPrompt", "llm", "agentDefaultModel", "commands"];
25
28
  export { Config };
26
29
 
27
30
  // Arrow (not function declaration): cordis 4 treats any apply with a
@@ -283,6 +286,7 @@ export const apply = (ctx, config) => {
283
286
  thresholdCount: cfg.dreamThresholdCount,
284
287
  thresholdChars: cfg.dreamThresholdChars,
285
288
  delayMs: cfg.dreamDelayMs,
289
+ minIntervalMs: (cfg.dreamMinIntervalMinutes ?? 0) * 60000,
286
290
  logger: ctx.logger,
287
291
  semantic: { embedder, vectorIndex },
288
292
  onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
package/src/settings.js CHANGED
@@ -58,6 +58,9 @@ const FEATURE_FLAG_BOOLEANS = [
58
58
  "bm25SearchEnabled",
59
59
  "conflictFreezeEnabled",
60
60
  "trustEpistemicWeighting",
61
+ // Issue #89:宽容校验回归(默认开)+ 跨类型合并显式放宽(默认关)。
62
+ "dreamSkipInvalid",
63
+ "allowCrossTypeMerge",
61
64
  // 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
62
65
  // 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
63
66
  // 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
@@ -70,7 +73,9 @@ const FEATURE_FLAG_INT_RANGES = {
70
73
  distillRateLimitRetries: [0, 10],
71
74
  distillRateLimitBaseDelayMs: [100, 60000],
72
75
  distillMaxChars: [1000, 200000],
73
- codingBoostFactor: [1, 5]
76
+ codingBoostFactor: [1, 5],
77
+ dreamMinIntervalMinutes: [0, 10080],
78
+ dreamMaxTokens: [256, 131072]
74
79
  };
75
80
  // 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符,
76
81
  // 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。
package/test/api.test.js CHANGED
@@ -527,10 +527,16 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
527
527
  assert.equal(res.statusCode, 200);
528
528
  const data = JSON.parse(res.body);
529
529
  assert.deepEqual(data.overrides, {});
530
- // effective 覆盖全部 31 个白名单键(含 v0.7.20 新增的 heatEnabled),未覆盖时
531
- // 取 bundle 配置的解析默认值;dreamProvider/dreamModel 无 schema 默认值
532
- // (Config({}) 解析为 undefined),不编造给前端 → 31 - 2 = 29
533
- assert.equal(Object.keys(data.effective).length, 29);
530
+ // effective 覆盖全部 35 个白名单键(含 v0.7.20 heatEnabled、Issue #89 新增
531
+ // dreamSkipInvalid/allowCrossTypeMerge/dreamMinIntervalMinutes 与本轮补入面板
532
+ // dreamMaxTokens),未覆盖时取 bundle 配置的解析默认值;
533
+ // dreamProvider/dreamModel 无 schema 默认值(Config({}) 解析为 undefined),
534
+ // 不编造给前端 → 35 - 2 = 33
535
+ assert.equal(Object.keys(data.effective).length, 33);
536
+ assert.equal(data.effective.dreamSkipInvalid, true);
537
+ assert.equal(data.effective.allowCrossTypeMerge, false);
538
+ assert.equal(data.effective.dreamMinIntervalMinutes, 0);
539
+ assert.equal(data.effective.dreamMaxTokens, 32768);
534
540
  assert.equal(data.effective.autoInject, true);
535
541
  assert.equal(data.effective.codingRetrospect, false);
536
542
  assert.equal(data.effective.distillMaxChars, 24000);