From d2df61db36bf692550d3ef96da3a942dc95a49ea Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 27 Sep 2026 02:25:02 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(dream):=20autoDream=20=E8=BF=9E?= =?UTF-8?q?=E7=BB=AD=E5=A4=B1=E8=B4=A5=E6=8C=87=E6=95=B0=E9=80=80=E9=81=BF?= =?UTF-8?q?=EF=BC=88#292=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #89 的最小间隔闸对失败 run 也生效,但间隔恒定——恒定失败的模型(#135 空体面)按固定节奏连发刷配额。新增 opt-in 键 autoDreamFailureBackoff (默认关 = 行为逐字节不变):有效最小间隔 = dreamMinIntervalMinutes × 2^连续失败数,封顶 30 分钟(只拦增长,不把用户配得更大的基数压小); 成功一次清零,重启归零(跨重启冷却归 #291 的 lastRunAtSeed)。被退避 推迟的触发没有调用发生,不写审计行(与 #89 间隔内跳过同口径)。 config.js schema 与 settings.js 白名单成对注册;api.test.js 旗标计数锁 按累加口径 +1 并补 effective 断言;新增 test/dream-failure-backoff.test.js 5 例(注入时钟,同 dream-peak-hours 房型):默认关间隔不变 / 连败 2 次 第 3 次推迟 / 成功清零恢复 / 连败 10 次封顶 30 分钟 / 0 基数不产生间隔。 --- dsh-mneme/CHANGELOG.md | 11 ++ dsh-mneme/lib/config.js | 9 + dsh-mneme/lib/dream.js | 37 +++- dsh-mneme/lib/index.js | 2 + dsh-mneme/lib/settings.js | 3 + dsh-mneme/src/config.js | 9 + dsh-mneme/src/dream.js | 37 +++- dsh-mneme/src/index.js | 2 + dsh-mneme/src/settings.js | 3 + dsh-mneme/test/api.test.js | 6 +- dsh-mneme/test/dream-failure-backoff.test.js | 173 +++++++++++++++++++ 11 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 dsh-mneme/test/dream-failure-backoff.test.js diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index 3b960de..dbb8aa9 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -83,6 +83,17 @@ ## 🆕 新增 +- **autoDream 连续失败退避(issue #292,#135 派生)**:新增 opt-in 键 + `autoDreamFailureBackoff`(默认关 = 行为与此前逐字节一致)。#89 的最小间隔闸对失败 + run 也生效,但间隔恒定——恒定失败的模型(#135 空体面)会按固定节奏连发刷爆配额; + 开启后调度器对连续失败做指数退避:有效最小间隔 = `dreamMinIntervalMinutes` × + 2^连续失败数(封顶 30 分钟,只拦增长、不把用户配得更大的基数压小),成功一次清零 + 恢复基数。基数取 `dreamMinIntervalMinutes`:基数为 0 时无闸可翻倍,本键不自己产生 + 间隔。计数是调度器内存变量、宿主重启归零(跨重启的冷却由 #291 的 lastRunAt 持久化 + 负责,两不重叠);被退避推迟的触发没有任何调用发生,也不写审计行(与 #89 间隔内 + 跳过同口径)。settings 白名单注册(面板可启停)。新增回归 5 条 + (`test/dream-failure-backoff.test.js`,注入时钟,同 dream-peak-hours 房型)。 + - **蒸馏思考强度设置项(issue #315)**:蒸馏(会话总结提炼)LLM 新增 `summarizeReasoningEffort`(`off`/`low`/`medium`/`high`/`none`,默认 `none` = 不发送字段、服务商默认生效,行为与此前一致)。思考型模型蒸馏时推理会烧光输出预算、总结为空或截断(#9 同款失败面,此前仅巩固/睡眠/实体抽取三链路有档位控制),配 `off`/`low` 可封顶推理。档位被模型拒收时自动去掉字段重试一次(与巩固/睡眠同一降级策略,`withEffortFallback` 共享、拒收判别式 `EFFORT_REJECT_RE` 提为单一来源);面板「功能开关 → 自动总结」下新增档位下拉(opt-in 语义与实体抽取 `entityExtractionReasoning` 对齐,settings 白名单注册)。新增回归 5 条(`test/summarize-reasoning-effort.test.js`)。 - **压缩边缘双落点(issue #249 N3)**:上下文即将被宿主压缩前抢救「正在做什么」,新增 diff --git a/dsh-mneme/lib/config.js b/dsh-mneme/lib/config.js index 4c68f9a..075aa2e 100644 --- a/dsh-mneme/lib/config.js +++ b/dsh-mneme/lib/config.js @@ -145,6 +145,15 @@ export const Config = z.object({ // 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次 // 写入事件会重新评估。 dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0), + // Issue #292(#135 派生):autoDream 连续失败退避(opt-in,默认关 = 行为与 + // 现状逐字节一致)。开启后调度器对连续失败做指数退避:有效最小间隔 = + // dreamMinIntervalMinutes × 2^连续失败数(成功一次清零恢复),封顶 30 分钟。 + // #89 的最小间隔闸失败 run 也占用,但间隔恒定——恒定失败的模型(#135 空体 + // 面)会按固定节奏连发刷爆配额;退避把下次重试按失败次数指数推远。基数取 + // dreamMinIntervalMinutes:基数为 0 时无闸可翻倍,本键不自己产生间隔(先配 + // dreamMinIntervalMinutes 再开本键)。与 dreamPeakHours / dreamMinIntervalMinutes + // 同族(节流阀,不新增任何 LLM 调用),故不进 LIGHT_MODE_OFF。 + autoDreamFailureBackoff: z.boolean().default(false), // Issue #239(第 4 项,错峰队列)镜像到巩固:高峰期不做梦。与 // summarizePeakHours 同一份时段语法(复用 src/summarize.js 的 parsePeakSpec / // isInPeakWindow / nextOffPeakAt,不另写解析器):逗号分隔、可带星期前缀、支持 diff --git a/dsh-mneme/lib/dream.js b/dsh-mneme/lib/dream.js index 8ddc37c..be2fb29 100644 --- a/dsh-mneme/lib/dream.js +++ b/dsh-mneme/lib/dream.js @@ -756,7 +756,11 @@ export async function maintainIndexAfterDream(decisions, service, semantic) { if (embedder.modelHash) vectorIndex.markModel?.(embedder.modelHash, embedder.dimension); } -export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, logger, semantic = null, lastRunAtSeed = 0, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) { +// Issue #292:连续失败退避的间隔封顶(30 分钟)。封顶只拦指数「增长」,不把 +// 用户配得比这更大的 dreamMinIntervalMinutes 基数压小(见 effectiveMinIntervalMs)。 +const FAILURE_BACKOFF_CAP_MS = 30 * 60 * 1000; + +export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, failureBackoff = false, logger, semantic = null, lastRunAtSeed = 0, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) { let pendingTimer = null; // Issue #239(第 4 项)镜像到巩固:高峰顺延定时器。与 pendingTimer 分开——两者 // 语义不同(一个是「马上要跑」,一个是「等出高峰再跑」),合成一个变量会让 @@ -771,6 +775,21 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar // lastRunAtSeed:调用方从 dream_runs 审计表读出的上次开跑时刻—— // 内存变量进程重启即归零,闸门对新实例放行 → 重启后立刻连发(#89 根因)。 let lastRunAt = lastRunAtSeed; + // Issue #292(#135 派生):同会话内连续失败计数。失败(onRun 抛错或返回 + // ok:false)+1,成功清零;无返回结果的 run(no-op 桩)视为完成且无失败, + // 不动计数。纯内存变量、宿主重启归零——跨重启的冷却由 lastRunAtSeed(#291) + // 持久化负责,两者不重叠。 + let consecutiveFailures = 0; + + // Issue #292:有效最小间隔 = 基数 × 2^连续失败数,封顶 30 分钟。退避关闭或 + // 尚无失败时逐字节返回基数(默认关 = 行为与现状一致)。基数 0 无闸可翻倍 + // (本键不自己产生间隔);封顶取 max(基数, cap),指数再大也不会把用户配的 + // 大基数压小。2^N 溢出成 Infinity 由 Math.min 兜到 cap,无需另设上限位数。 + function effectiveMinIntervalMs() { + if (!failureBackoff || consecutiveFailures <= 0) return minIntervalMs; + const cap = Math.max(minIntervalMs, FAILURE_BACKOFF_CAP_MS); + return Math.min(minIntervalMs * 2 ** consecutiveFailures, cap); + } function shouldTrigger(service) { const memories = service.all().filter((m) => !m.archived && m.type !== "summary" && m.type !== "document"); @@ -783,8 +802,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar function maybeSchedule(service) { if (disposed || running || pendingTimer || deferTimer) return false; - // Issue #89(请求 2):最小触发间隔闸门。 - if (minIntervalMs > 0 && now() - lastRunAt < minIntervalMs) return false; + // Issue #89(请求 2):最小触发间隔闸门。#292 退避开启时改用指数放大的 + // 有效间隔:连续失败越多,下次放行越晚(恒定失败的模型不再按固定节奏连发, + // 只会越打越稀)。失败 run 本就占用间隔(lastRunAt 在开跑时刷新,见 #89), + // 这里放大的是同一道闸,不新增任何状态面。间隔内的触发静默跳过:没有调用 + // 发生,也就没有可审计的对象(与 #89 口径一致,不写审计行)。 + if (minIntervalMs > 0 && now() - lastRunAt < effectiveMinIntervalMs()) return false; const { trigger, count, chars } = shouldTrigger(service); if (!trigger) return false; // Issue #239(第 4 项,错峰队列)镜像到巩固:命中高峰就不调模型。与蒸馏的差别 @@ -856,17 +879,25 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar // keeps the old baseline. A run that reports nothing is treated as // completed without failure (no-op hooks / minimal test doubles). if (result && result.ok) { + consecutiveFailures = 0; // Issue #292:成功清零,下次触发回到基数间隔 try { baseline = shouldTrigger(service); } catch (error) { // Store closed mid-flight: keep the last known baseline. logger?.warn?.(`dsh-mneme dream: baseline refresh failed: ${String(error)}`); } + } else if (result) { + // Issue #292:ok:false(LLM 失败 / 空体 / 整单拒绝)计入连败;degraded + // 的 run 走 ok:true(LLM 本身成功了,只是决策被部分应用)→ 算成功、清零。 + // 退避关闭时该计数没有消费者,行为与此前逐字节一致。 + consecutiveFailures += 1; } + // result 为空(no-op 桩 / 最小测试替身)=完成且无失败:基线与连败计数都不动。 }) .catch((error) => { logger?.warn?.(`dsh-mneme dream: run failed: ${error?.message ?? error}`); // Failed runs do not refresh the baseline. + consecutiveFailures += 1; // Issue #292:抛错同样计入连败 }) .finally(() => { running = false; diff --git a/dsh-mneme/lib/index.js b/dsh-mneme/lib/index.js index db257cd..e814def 100644 --- a/dsh-mneme/lib/index.js +++ b/dsh-mneme/lib/index.js @@ -491,6 +491,8 @@ export const apply = (ctx, config) => { thresholdChars: cfg.dreamThresholdChars, delayMs: cfg.dreamDelayMs, minIntervalMs: (cfg.dreamMinIntervalMinutes ?? 0) * 60000, + // Issue #292:连续失败指数退避(opt-in,默认关 = 行为不变)。 + failureBackoff: cfg.autoDreamFailureBackoff === true, logger: ctx.logger, semantic: { embedder, vectorIndex }, lastRunAtSeed: store.lastDreamRunAt("auto"), diff --git a/dsh-mneme/lib/settings.js b/dsh-mneme/lib/settings.js index 6e6d6a8..9bf9b02 100644 --- a/dsh-mneme/lib/settings.js +++ b/dsh-mneme/lib/settings.js @@ -63,6 +63,9 @@ const FEATURE_FLAG_BOOLEANS = [ "documentMemoryEnabled", "codingRetrospect", "autoDream", + // Issue #292:autoDream 连续失败退避(默认关)。基数是 dreamMinIntervalMinutes, + // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 30 分钟;面板可启停。 + "autoDreamFailureBackoff", "sleepModeEnabled", "heatEnabled", "hybridInject", diff --git a/dsh-mneme/src/config.js b/dsh-mneme/src/config.js index 4c68f9a..075aa2e 100644 --- a/dsh-mneme/src/config.js +++ b/dsh-mneme/src/config.js @@ -145,6 +145,15 @@ export const Config = z.object({ // 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次 // 写入事件会重新评估。 dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0), + // Issue #292(#135 派生):autoDream 连续失败退避(opt-in,默认关 = 行为与 + // 现状逐字节一致)。开启后调度器对连续失败做指数退避:有效最小间隔 = + // dreamMinIntervalMinutes × 2^连续失败数(成功一次清零恢复),封顶 30 分钟。 + // #89 的最小间隔闸失败 run 也占用,但间隔恒定——恒定失败的模型(#135 空体 + // 面)会按固定节奏连发刷爆配额;退避把下次重试按失败次数指数推远。基数取 + // dreamMinIntervalMinutes:基数为 0 时无闸可翻倍,本键不自己产生间隔(先配 + // dreamMinIntervalMinutes 再开本键)。与 dreamPeakHours / dreamMinIntervalMinutes + // 同族(节流阀,不新增任何 LLM 调用),故不进 LIGHT_MODE_OFF。 + autoDreamFailureBackoff: z.boolean().default(false), // Issue #239(第 4 项,错峰队列)镜像到巩固:高峰期不做梦。与 // summarizePeakHours 同一份时段语法(复用 src/summarize.js 的 parsePeakSpec / // isInPeakWindow / nextOffPeakAt,不另写解析器):逗号分隔、可带星期前缀、支持 diff --git a/dsh-mneme/src/dream.js b/dsh-mneme/src/dream.js index 8ddc37c..be2fb29 100644 --- a/dsh-mneme/src/dream.js +++ b/dsh-mneme/src/dream.js @@ -756,7 +756,11 @@ export async function maintainIndexAfterDream(decisions, service, semantic) { if (embedder.modelHash) vectorIndex.markModel?.(embedder.modelHash, embedder.dimension); } -export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, logger, semantic = null, lastRunAtSeed = 0, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) { +// Issue #292:连续失败退避的间隔封顶(30 分钟)。封顶只拦指数「增长」,不把 +// 用户配得比这更大的 dreamMinIntervalMinutes 基数压小(见 effectiveMinIntervalMs)。 +const FAILURE_BACKOFF_CAP_MS = 30 * 60 * 1000; + +export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, failureBackoff = false, logger, semantic = null, lastRunAtSeed = 0, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) { let pendingTimer = null; // Issue #239(第 4 项)镜像到巩固:高峰顺延定时器。与 pendingTimer 分开——两者 // 语义不同(一个是「马上要跑」,一个是「等出高峰再跑」),合成一个变量会让 @@ -771,6 +775,21 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar // lastRunAtSeed:调用方从 dream_runs 审计表读出的上次开跑时刻—— // 内存变量进程重启即归零,闸门对新实例放行 → 重启后立刻连发(#89 根因)。 let lastRunAt = lastRunAtSeed; + // Issue #292(#135 派生):同会话内连续失败计数。失败(onRun 抛错或返回 + // ok:false)+1,成功清零;无返回结果的 run(no-op 桩)视为完成且无失败, + // 不动计数。纯内存变量、宿主重启归零——跨重启的冷却由 lastRunAtSeed(#291) + // 持久化负责,两者不重叠。 + let consecutiveFailures = 0; + + // Issue #292:有效最小间隔 = 基数 × 2^连续失败数,封顶 30 分钟。退避关闭或 + // 尚无失败时逐字节返回基数(默认关 = 行为与现状一致)。基数 0 无闸可翻倍 + // (本键不自己产生间隔);封顶取 max(基数, cap),指数再大也不会把用户配的 + // 大基数压小。2^N 溢出成 Infinity 由 Math.min 兜到 cap,无需另设上限位数。 + function effectiveMinIntervalMs() { + if (!failureBackoff || consecutiveFailures <= 0) return minIntervalMs; + const cap = Math.max(minIntervalMs, FAILURE_BACKOFF_CAP_MS); + return Math.min(minIntervalMs * 2 ** consecutiveFailures, cap); + } function shouldTrigger(service) { const memories = service.all().filter((m) => !m.archived && m.type !== "summary" && m.type !== "document"); @@ -783,8 +802,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar function maybeSchedule(service) { if (disposed || running || pendingTimer || deferTimer) return false; - // Issue #89(请求 2):最小触发间隔闸门。 - if (minIntervalMs > 0 && now() - lastRunAt < minIntervalMs) return false; + // Issue #89(请求 2):最小触发间隔闸门。#292 退避开启时改用指数放大的 + // 有效间隔:连续失败越多,下次放行越晚(恒定失败的模型不再按固定节奏连发, + // 只会越打越稀)。失败 run 本就占用间隔(lastRunAt 在开跑时刷新,见 #89), + // 这里放大的是同一道闸,不新增任何状态面。间隔内的触发静默跳过:没有调用 + // 发生,也就没有可审计的对象(与 #89 口径一致,不写审计行)。 + if (minIntervalMs > 0 && now() - lastRunAt < effectiveMinIntervalMs()) return false; const { trigger, count, chars } = shouldTrigger(service); if (!trigger) return false; // Issue #239(第 4 项,错峰队列)镜像到巩固:命中高峰就不调模型。与蒸馏的差别 @@ -856,17 +879,25 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar // keeps the old baseline. A run that reports nothing is treated as // completed without failure (no-op hooks / minimal test doubles). if (result && result.ok) { + consecutiveFailures = 0; // Issue #292:成功清零,下次触发回到基数间隔 try { baseline = shouldTrigger(service); } catch (error) { // Store closed mid-flight: keep the last known baseline. logger?.warn?.(`dsh-mneme dream: baseline refresh failed: ${String(error)}`); } + } else if (result) { + // Issue #292:ok:false(LLM 失败 / 空体 / 整单拒绝)计入连败;degraded + // 的 run 走 ok:true(LLM 本身成功了,只是决策被部分应用)→ 算成功、清零。 + // 退避关闭时该计数没有消费者,行为与此前逐字节一致。 + consecutiveFailures += 1; } + // result 为空(no-op 桩 / 最小测试替身)=完成且无失败:基线与连败计数都不动。 }) .catch((error) => { logger?.warn?.(`dsh-mneme dream: run failed: ${error?.message ?? error}`); // Failed runs do not refresh the baseline. + consecutiveFailures += 1; // Issue #292:抛错同样计入连败 }) .finally(() => { running = false; diff --git a/dsh-mneme/src/index.js b/dsh-mneme/src/index.js index db257cd..e814def 100644 --- a/dsh-mneme/src/index.js +++ b/dsh-mneme/src/index.js @@ -491,6 +491,8 @@ export const apply = (ctx, config) => { thresholdChars: cfg.dreamThresholdChars, delayMs: cfg.dreamDelayMs, minIntervalMs: (cfg.dreamMinIntervalMinutes ?? 0) * 60000, + // Issue #292:连续失败指数退避(opt-in,默认关 = 行为不变)。 + failureBackoff: cfg.autoDreamFailureBackoff === true, logger: ctx.logger, semantic: { embedder, vectorIndex }, lastRunAtSeed: store.lastDreamRunAt("auto"), diff --git a/dsh-mneme/src/settings.js b/dsh-mneme/src/settings.js index 6e6d6a8..9bf9b02 100644 --- a/dsh-mneme/src/settings.js +++ b/dsh-mneme/src/settings.js @@ -63,6 +63,9 @@ const FEATURE_FLAG_BOOLEANS = [ "documentMemoryEnabled", "codingRetrospect", "autoDream", + // Issue #292:autoDream 连续失败退避(默认关)。基数是 dreamMinIntervalMinutes, + // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 30 分钟;面板可启停。 + "autoDreamFailureBackoff", "sleepModeEnabled", "heatEnabled", "hybridInject", diff --git a/dsh-mneme/test/api.test.js b/dsh-mneme/test/api.test.js index 5bd7b91..5765c6b 100644 --- a/dsh-mneme/test/api.test.js +++ b/dsh-mneme/test/api.test.js @@ -666,11 +666,13 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d // pinnedInjectBudget、issue #249 N3 新增 continuityRescueEnabled, // v0.8.5 新增 disableMemorySearch/disableMemoryArchive, // 本地嵌入池化新增 localEmbedPooling,issue #315 新增 summarizeReasoningEffort, - // issue #239 第 4 项镜像到巩固新增 dreamPeakHours/dreamPeakMaxDeferMinutes) - assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2); + // issue #239 第 4 项镜像到巩固新增 dreamPeakHours/dreamPeakMaxDeferMinutes, + // issue #292 新增 autoDreamFailureBackoff) + assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1); assert.equal(data.effective.dreamSkipInvalid, true); assert.equal(data.effective.allowCrossTypeMerge, false); assert.equal(data.effective.dreamMinIntervalMinutes, 0); + assert.equal(data.effective.autoDreamFailureBackoff, false); assert.equal(data.effective.dreamMaxTokens, 131072); assert.equal(data.effective.sleepProvider, ""); assert.equal(data.effective.sleepModel, ""); diff --git a/dsh-mneme/test/dream-failure-backoff.test.js b/dsh-mneme/test/dream-failure-backoff.test.js new file mode 100644 index 0000000..dae8621 --- /dev/null +++ b/dsh-mneme/test/dream-failure-backoff.test.js @@ -0,0 +1,173 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createDreamScheduler } from "../src/dream.js"; +import { createStore } from "../src/store.js"; +import { createService } from "../src/service.js"; + +// Issue #292:autoDream 同会话内连续失败退避(autoDreamFailureBackoff,opt-in)。 +// +// 这些用例锁的是「调度器节流语义」,不是实现细节——防的回归是: +// ① 退避闸失效/写反 → 恒定失败的模型(#135 空体面)按 dreamMinIntervalMinutes +// 的固定节奏连发刷配额(#89 只挡了间隔内重试,间隔本身不会变长); +// ② 成功后连败计数不清零 → 模型恢复正常后仍被旧失败拖着重试推迟(巩固变相停摆); +// ③ 30 分钟封顶失效 → 指数无限翻倍,一整天都不再巩固; +// ④ 默认关路径被顺手改掉 → 未 opt-in 用户的重试间隔漂移(默认关 = 行为与 +// 现状逐字节一致的承诺); +// ⑤ 0 基数被偷偷补了间隔 → 未配 dreamMinIntervalMinutes 的用户开退避即被限流 +// (文档口径:基数 0 时本键不自己产生间隔)。 +// +// 时钟与定时器全部注入(同 test/dream-peak-hours.test.js 的房型):「失败 → 推迟 +// → 放行」才能被确定性覆盖,也不在 CI 上留真实等待。退避从上次开跑(lastRunAt) +// 起算:失败 run 本就占用基数间隔(#89 现状,lastRunAt 在开跑时刷新),退避放大 +// 的是同一道闸,不新增状态面。 +function dreamSetup() { + const store = createStore(":memory:"); + const service = createService({ store, mirror: null, config: {} }); + return { store, service }; +} + +function fakeClock(startMs) { + let nowMs = startMs; + const timers = []; + let seq = 1; + return { + now: () => nowMs, + setNow: (v) => { nowMs = v; }, + timers, + setTimeoutFn: (fn, delay) => { const t = { id: seq++, at: nowMs + delay, fn }; timers.push(t); return t.id; }, + clearTimeoutFn: (id) => { const i = timers.findIndex((t) => t.id === id); if (i >= 0) timers.splice(i, 1); } + }; +} + +const T0 = new Date(2026, 8, 27, 10, 0, 0, 0).getTime(); + +/** + * 触发一次并等 run 真正落完:断言闸门这次是放行的、恰好挂了一个 delayMs=0 的 + * 执行定时器,点火后用 setImmediate 冲掉 startRun 的 Promise 链(纯微任务冲刷, + * 不留真实等待)。失败路径下 baseline 不刷新,同一份写入可持续触发。 + */ +async function runOnce(clock, dream, service) { + assert.equal(dream.maybeSchedule(service), true, "前置:阈值已过、间隔闸放行"); + const ts = clock.timers.splice(0); + assert.equal(ts.length, 1, "前置:恰好挂了一个执行定时器"); + ts[0].fn(); + await new Promise((r) => setImmediate(r)); +} + +test("默认关:失败后重试间隔与现状一致(基数,不翻倍)", async () => { + const { store, service } = dreamSetup(); + const clock = fakeClock(T0); + let runs = 0; + const dream = createDreamScheduler({ + onRun: async () => { runs++; return { ok: false, error: "llm failed" }; }, + thresholdCount: 1, thresholdChars: 0, delayMs: 0, minIntervalMs: 100, + failureBackoff: false, // 显式默认值:锁「默认关 = 行为逐字节不变」 + logger: { warn: () => {} }, + now: clock.now, setTimeoutFn: clock.setTimeoutFn, clearTimeoutFn: clock.clearTimeoutFn + }); + service.saveWithDedupe({ type: "project", title: "a", content: "x" }); + await runOnce(clock, dream, service); // 失败一次:失败 run 占用基数间隔(#89 现状) + assert.equal(runs, 1); + clock.setNow(T0 + 50); + assert.equal(dream.maybeSchedule(service), false, "基数间隔内照旧跳过"); + clock.setNow(T0 + 100); + assert.equal(dream.maybeSchedule(service), true, "关键:基数间隔一到即放行——不翻倍"); + store.close(); +}); + +test("开启:连续失败 2 次 → 第 3 次触发被指数推迟", async () => { + const { store, service } = dreamSetup(); + const clock = fakeClock(T0); + let runs = 0; + const dream = createDreamScheduler({ + onRun: async () => { runs++; return { ok: false, error: "empty body" }; }, + thresholdCount: 1, thresholdChars: 0, delayMs: 0, minIntervalMs: 100, + failureBackoff: true, + logger: { warn: () => {} }, + now: clock.now, setTimeoutFn: clock.setTimeoutFn, clearTimeoutFn: clock.clearTimeoutFn + }); + service.saveWithDedupe({ type: "project", title: "a", content: "x" }); + await runOnce(clock, dream, service); // 败 1:effective = 100 × 2^1 = 200 + assert.equal(runs, 1); + clock.setNow(T0 + 100); + assert.equal(dream.maybeSchedule(service), false, "关键:基数到点不放行(现状会在 +100 放行)"); + clock.setNow(T0 + 200); + await runOnce(clock, dream, service); // 败 2:effective = 100 × 2^2 = 400 + assert.equal(runs, 2); + clock.setNow(T0 + 200 + 300); + assert.equal(dream.maybeSchedule(service), false, "关键:2 次连败后 400ms 内不放行"); + clock.setNow(T0 + 200 + 400); + assert.equal(dream.maybeSchedule(service), true, "自上次开跑起满 400ms 才放行第 3 次"); + store.close(); +}); + +test("成功一次后计数清零、间隔恢复基数", async () => { + const { store, service } = dreamSetup(); + const clock = fakeClock(T0); + let failNext = true; + const dream = createDreamScheduler({ + onRun: async () => ({ ok: !failNext }), + thresholdCount: 1, thresholdChars: 0, delayMs: 0, minIntervalMs: 100, + failureBackoff: true, + logger: { warn: () => {} }, + now: clock.now, setTimeoutFn: clock.setTimeoutFn, clearTimeoutFn: clock.clearTimeoutFn + }); + service.saveWithDedupe({ type: "project", title: "a", content: "x" }); + await runOnce(clock, dream, service); // 败 1:effective = 200 + clock.setNow(T0 + 100); + assert.equal(dream.maybeSchedule(service), false, "前置:退避确实在挡(基数到点不放行)"); + clock.setNow(T0 + 200); + failNext = false; + await runOnce(clock, dream, service); // 成功:连败清零、baseline 刷新为当前库量 + service.saveWithDedupe({ type: "project", title: "b", content: "y" }); // 补过刷新后的阈值 + clock.setNow(T0 + 300); // 距上次开跑 +100 = 基数 + assert.equal(dream.maybeSchedule(service), true, "关键:成功清零后基数即放行(未清零要等 +400)"); + store.close(); +}); + +test("cap:连败 10 次 → 推迟量封顶 30 分钟(未封顶需等约 34 小时)", async () => { + const { store, service } = dreamSetup(); + const clock = fakeClock(T0); + const BASE = 2 * 60000; // 2 分钟基数:2^10 × 基数 ≈ 34h,30 分钟封顶必被触发 + let runs = 0; + const dream = createDreamScheduler({ + onRun: async () => { runs++; return { ok: false }; }, + thresholdCount: 1, thresholdChars: 0, delayMs: 0, minIntervalMs: BASE, + failureBackoff: true, + logger: { warn: () => {} }, + now: clock.now, setTimeoutFn: clock.setTimeoutFn, clearTimeoutFn: clock.clearTimeoutFn + }); + service.saveWithDedupe({ type: "project", title: "a", content: "x" }); + let lastFailAt = T0; + for (let i = 1; i <= 10; i++) { + if (i > 1) { + const wait = Math.min(BASE * 2 ** (i - 1), 30 * 60000); // 与实现同一口径的期望间隔 + clock.setNow(lastFailAt + wait); + } + await runOnce(clock, dream, service); + lastFailAt = clock.now(); + } + assert.equal(runs, 10); + clock.setNow(lastFailAt + 30 * 60000 - 1); + assert.equal(dream.maybeSchedule(service), false, "关键:封顶后 30 分钟内仍不放行"); + clock.setNow(lastFailAt + 30 * 60000); + assert.equal(dream.maybeSchedule(service), true, "封顶生效:恰好 30 分钟放行,不再随失败数翻倍"); + store.close(); +}); + +test("基数为 0 时退避不自己产生间隔(需先配 dreamMinIntervalMinutes)", async () => { + const { store, service } = dreamSetup(); + const clock = fakeClock(T0); + const dream = createDreamScheduler({ + onRun: async () => ({ ok: false }), + thresholdCount: 1, thresholdChars: 0, delayMs: 0, minIntervalMs: 0, + failureBackoff: true, + logger: { warn: () => {} }, + now: clock.now, setTimeoutFn: clock.setTimeoutFn, clearTimeoutFn: clock.clearTimeoutFn + }); + service.saveWithDedupe({ type: "project", title: "a", content: "x" }); + await runOnce(clock, dream, service); // 失败一次 + clock.setNow(T0); + assert.equal(dream.maybeSchedule(service), true, "关键:0 基数无闸可翻倍,开退避也不限流"); + store.close(); +}); From 809b3eae66cfa681d8920bdf53b223fdb68c0077 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 27 Sep 2026 02:25:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(config):=20=E9=85=8D=E7=BD=AE=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E4=B8=80=E9=A1=B5=EF=BC=8C147=20=E9=94=AE=E9=80=90?= =?UTF-8?q?=E9=94=AE=E8=A6=86=E7=9B=96=EF=BC=88#290=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 dsh-mneme/docs/CONFIGURATION.md:以 src/config.js schema 为唯一 正本,按功能面分组(全局与存储 / scope 隔离 / 蒸馏 / 注入 / 巩固 / 睡眠 / 反思与冲突 / 实体 / 检索 / 热度 / API 与工具面 / 运行时),每键给默认、 作用与开启后果/冲突;lightMode 联动键逐行标注并附 LIGHT_MODE_OFF 完整 清单;与 settings.js 白名单的对应关系在页首一句话交代。反向核对确认 schema 顶层 147 键无遗漏。两个 README 的文档索引各加一行链接。 --- README.md | 2 + dsh-mneme/CHANGELOG.md | 7 + dsh-mneme/README.md | 1 + dsh-mneme/docs/CONFIGURATION.md | 238 ++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 dsh-mneme/docs/CONFIGURATION.md diff --git a/README.md b/README.md index 26e2ea5..d8a762e 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ dsh web |------|------| | 插件完整文档(功能 / 安装 / 配置 / 架构) | [dsh-mneme/README.md](dsh-mneme/README.md) | | stdio MCP server——Claude Code / Cursor 等任意 MCP 客户端接入记忆六件套 | [dsh-mneme/README.md · MCP Server](dsh-mneme/README.md#mcp-server任意-mcp-客户端接入) | +| 配置说明(全键参考) | [dsh-mneme/docs/CONFIGURATION.md](dsh-mneme/docs/CONFIGURATION.md) | | 实体结构化设计 | [dsh-mneme/docs/ENTITIES.md](dsh-mneme/docs/ENTITIES.md) | | 语义架构 | [dsh-mneme/docs/SEMANTIC.md](dsh-mneme/docs/SEMANTIC.md) | | 本地模型部署指南 | [dsh-mneme/docs/LOCAL_MODEL.md](dsh-mneme/docs/LOCAL_MODEL.md) | @@ -337,6 +338,7 @@ The plugin ships a zero-dependency stdio MCP server (standalone npm package **`m |-----|------| | Full plugin docs (features / install / config / architecture) | [dsh-mneme/README.md](dsh-mneme/README.md)(中文) | | stdio MCP server — plug the six memory tools into any MCP client (Claude Code / Cursor / …) | [dsh-mneme/README.md · MCP Server](dsh-mneme/README.md#mcp-server任意-mcp-客户端接入)(中文) | +| Configuration reference (all keys) | [dsh-mneme/docs/CONFIGURATION.md](dsh-mneme/docs/CONFIGURATION.md)(中文) | | Entity structure design | [dsh-mneme/docs/ENTITIES.md](dsh-mneme/docs/ENTITIES.md) | | Semantic architecture | [dsh-mneme/docs/SEMANTIC.md](dsh-mneme/docs/SEMANTIC.md) | | Local model guide | [dsh-mneme/docs/LOCAL_MODEL.md](dsh-mneme/docs/LOCAL_MODEL.md) | diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index dbb8aa9..abc7035 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -94,6 +94,13 @@ 跳过同口径)。settings 白名单注册(面板可启停)。新增回归 5 条 (`test/dream-failure-backoff.test.js`,注入时钟,同 dream-peak-hours 房型)。 +- **配置说明一页(issue #290)**:新增 [docs/CONFIGURATION.md](docs/CONFIGURATION.md), + 以 `src/config.js` schema 为唯一正本逐键过(147 键全覆盖):按功能面分组(全局与 + 存储 / scope 隔离 / 蒸馏 / 注入 / 巩固 / 睡眠 / 反思与冲突 / 实体 / 检索 / 热度 / + API 与工具面 / 运行时),每键给默认值、作用与开启后果/冲突;lightMode 联动键显式 + 标注(文末附 `LIGHT_MODE_OFF` 完整清单),opt-in 默认关的键可见即知,与 settings.js + 白名单的对应关系在页首一句话交代。两个 README 的文档索引各加一行链接。 + - **蒸馏思考强度设置项(issue #315)**:蒸馏(会话总结提炼)LLM 新增 `summarizeReasoningEffort`(`off`/`low`/`medium`/`high`/`none`,默认 `none` = 不发送字段、服务商默认生效,行为与此前一致)。思考型模型蒸馏时推理会烧光输出预算、总结为空或截断(#9 同款失败面,此前仅巩固/睡眠/实体抽取三链路有档位控制),配 `off`/`low` 可封顶推理。档位被模型拒收时自动去掉字段重试一次(与巩固/睡眠同一降级策略,`withEffortFallback` 共享、拒收判别式 `EFFORT_REJECT_RE` 提为单一来源);面板「功能开关 → 自动总结」下新增档位下拉(opt-in 语义与实体抽取 `entityExtractionReasoning` 对齐,settings 白名单注册)。新增回归 5 条(`test/summarize-reasoning-effort.test.js`)。 - **压缩边缘双落点(issue #249 N3)**:上下文即将被宿主压缩前抢救「正在做什么」,新增 diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index 9e8c656..baa3959 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -606,6 +606,7 @@ npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动 > 设计文档位于仓库根 `docs/`,链接以 `../docs/` 相对路径指向(GitHub 上从本目录打开可正常跳转)。 +- [配置说明(全键参考)](docs/CONFIGURATION.md) - [实体结构化记忆设计](docs/ENTITIES.md) - [语义增强架构](docs/SEMANTIC.md) - [本地模型部署指南](docs/LOCAL_MODEL.md) diff --git a/dsh-mneme/docs/CONFIGURATION.md b/dsh-mneme/docs/CONFIGURATION.md new file mode 100644 index 0000000..4ad45a8 --- /dev/null +++ b/dsh-mneme/docs/CONFIGURATION.md @@ -0,0 +1,238 @@ +# 配置说明(全键参考) + +> 正本是 `src/config.js` 的 schema——本文与其同步维护,键名、默认值、取值范围以代码为准; +> 行为动机与踩坑出处在 schema 注释里(含 issue 编号),本文只保留「怎么配、配了会怎样」。 +> 改 schema 时顺手更新对应行(新改动自查清单第 2 条)。 + +**怎么读这张表** + +- 「默认」列即 `config.js` schema 的解析默认值;默认值本身 = 未配置时的行为。 +- 标 **light** 的键会被 lightMode 预设强制关闭(文末附完整清单);lightMode 是预设不是 + 强制——用户显式写在 feature_flags 里的值优先于预设。 +- 行为开关(布尔键)默认关的即 opt-in:不开 = 行为与该键存在之前逐字节一致。 +- 绝大多数行为开关与部分阈值/路由键在 `settings.js` 的白名单里,可在面板「功能开关」 + 逐项启停(线上回滚开关);未入白名单的键(路径、词典、枚举外的杂项)只能改 bundle 配置。 + +## 全局与存储 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `language` | `zh` | 记忆语言:生成条目、注入标题、后台提示词所用语言(`zh`/`en`) | 切换后重启生效 | +| `memoryDir` | `~/.dsh/memory` | SQLite 记忆库目录 | WAL + busy_timeout 已为多进程就绪;两个宿主共用库时 `externalApiEnabled` 与 `autoDream` 只能一边开 | +| `lightMode` | `false` | 轻量档总闸:一键关掉全部重型路径,保留核心循环(见文末清单) | 面板 `panel_mode=light` 等效为 `true` 且优先于 bundle 配置 | + +## scope 隔离(issue #17) + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `scopeEnabled` | `false` | 存储层总开关:写入标注 agent_scope / workspace_scope,去重键扩展 | opt-in;关 = 写入不标注、行为与 A1 前逐字节一致 | +| `strictScope` | `false` | 硬过滤模式(A3):他 scope 完全不可见、未标注恒可见(fail-closed) | 依赖 `scopeEnabled` 打开才有意义;关 = A2 软隔离(他 scope 降权保留可见) | + +## 审计 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `llmAudit.enabled` | `true` | 后台 LLM 调用全量落 `llm_audit_logs`(tokens / 时长 / 状态 / 触发源) | 覆盖 autoDream / autoSummarize / sleep / entityExtract 与写入准入测量点;关 = 一行不写 | +| `llmAudit.retentionDays` | `90` | 审计表滚动清理保留天数(≤3650) | 启动时清理旧行 | + +## 蒸馏(会话 → 记忆) + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `autoSummarize` | `true` | 会话/回合结束自动蒸馏记忆 | 关 = 只剩工具与命令写入口 | +| `summarizeProvider` / `summarizeModel` | 空 | 蒸馏专用模型路由 | 两者都非空才生效;空 = 用会话当前模型 | +| `distillMaxChars` | `24000` | 喂给蒸馏 LLM 的完整转录上限(字符,1000–200000) | 调大更完整、更贵 | +| `summarizeMinIntervalMinutes` | `0` | 同会话两次蒸馏的最小间隔(分钟,≤10080) | 0 = 不限(#127 节流三键之一) | +| `summarizeMaxEntriesPerRun` | `0` | 单次蒸馏最多落库条数(≤50) | 0 = 不限 | +| `summarizeMinWindowChars` | `0` | 窗口可蒸馏文本下限(字符),不足直接跳过 LLM(#239) | 被挡窗口照常消费游标并留 skip 审计 | +| `summarizeMaxRunsPerSession` | `0` | 每会话蒸馏次数预算,只计真实发起的调用(≤1000) | 0 = 不限;进程内计数,重启清零 | +| `summarizePeakHours` | `""` | 高峰时段串(`"09:00-18:00"` / `"mon-fri 08:00-12:00,14:00-18:00"`,支持跨零点) | 命中高峰不调 LLM、顺延补跑;任一写法非法整串按未配置处理 | +| `summarizePeakMaxDeferMinutes` | `120` | 高峰顺延上限(分钟,≤1440) | 0 = 不设上限;到点仍处高峰照跑(bypassPeak) | +| `summarizeDedupeMode` | `off` | 落库前去重档位:`off` / `title`(拦完全同名)/ `vector`(同会话语义近邻) | `vector` 复用已有 embedding 列,无 LLM 调用 | +| `summarizeDedupeMinSim` | `0.92` | `vector` 档并入阈值(0.5–0.99) | 仅 `vector` 档生效 | +| `summarizeDedupeWindowHours` | `24` | `vector` 档回看窗口(小时,≤168) | 仅 `vector` 档生效 | +| `distillRateLimitIntervalMs` | `1000` | 蒸馏请求全局串行排队的相邻间隔(毫秒) | 0 = 关闭排队(默认开的 429 保护随之失效) | +| `distillRateLimitRetries` | `3` | 命中 429 的指数退避重试次数(≤10) | 重试全程对用户透明 | +| `distillRateLimitBaseDelayMs` | `1000` | 429 退避基数(毫秒,100–60000) | 1s→2s→4s… | +| `summarizeReasoningEffort` | `none` | 蒸馏 LLM 推理档位:`off`/`low`/`medium`/`high`/`none`(#315) | `none` = 不发字段用服务商默认;思考模型建议 `off`/`low` 防推理烧光输出预算 | +| `codingRetrospect` | `false` | 编码记忆蒸馏:rejected_solution / pitfall / constraint 三类 | opt-in;开启后蒸馏上下文为整轮完整对话 | +| `codingKeywords` | 内置词表 | 编码任务识别词表(读取侧门控) | 命中才注入编码记忆 | +| `codingBoostFactor` | `2` | 编码任务时编码记忆的 importance 加权系数(1–5) | — | +| `memoryQualityFilter.enabled` | `true` | 写入质量打分:≥60 正常 / 30–60 降权 / <30 归档打 low_quality 标 | 归档行仍可显式搜到 | +| `memoryQualityFilter.archiveThreshold` | `30` | 自动归档线 | 与 `exemptImportance` 联动 | +| `memoryQualityFilter.degradeThreshold` | `60` | 降权线(注入排序乘 quality_score) | — | +| `memoryQualityFilter.minContentLength` | `10` | 最短内容长度(字符) | — | +| `memoryQualityFilter.exemptImportance` | `4` | importance ≥ 此值豁免静默自动归档(#135) | 设 1 = 等效关自动归档 | + +## 注入 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `autoInject` | `true` | 每轮自动注入记忆块的父总闸 | 关 = 两个子开关一并失效(持久值保留,不重置) | +| `maxInjectedItems` | `5` | 注入条数上限(1–20) | — | +| `importanceThreshold` | `3` | 注入候选的 importance 下限(1–5) | 低于该值的记忆不进注入面 | +| `injectRotationTurns` | `0` | 跨轮轮换:最近 N 轮注入过的不再优先(#205) | opt-in;会话边界自动重置 | +| `injectContentMaxChars` | `300` | 单条正文截断上限(60–4000,#164①) | 截断带「上限/原长/全文指引」提示,不静默 | +| `injectUncertaintyAdaptive` | `false` | 确定性强的话题收缩注入条数(减半、下限 1,#239 第 5 项) | 只做单向收缩,绝不越过 `maxInjectedItems` | +| `injectGuidanceEnabled` **light** | `true` | 工具描述 + order 150 系统段讲「何时查/何时写」(#249) | `autoInject` 子开关;lightMode 强制关 | +| `continuityRescueEnabled` **light** | `false` | 压缩边缘双落点:连续性提案落库 + 追加注入(#249 N3) | opt-in(新注入表面);`autoInject` 子开关;订阅宿主压缩事件,无该时机则降级为持久规则 | +| `pinnedInjectBudget` | `0` | 约束/偏好 pin 池独立预算(0–5,#249 B1) | 0 = 关;pin 不占 `maxInjectedItems` 名额 | +| `hotMemoryEnabled` | `true` | 会话近期对话渲染在长期记忆块之前 | — | +| `hotMemoryRounds` | `5` | 热记忆覆盖最近几轮对话(≤50) | — | +| `hotMemoryMaxTokens` | `2000` | 热记忆块 token 上限(200–32000) | — | +| `escapePromptVariables` | `true` | 注入内容连续花括号转义(#162) | 关 = 原样透传(DSH interpolate 可能 throw) | + +## 巩固(autoDream) + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `autoDream` **light** | `true` | 阈值触发的后台巩固(LLM 合并/归档/降重) | lightMode 强制关;与 sleepMode 串行、不重叠 | +| `dreamThresholdCount` / `dreamThresholdChars` | `10` / `5000` | 触发阈值(新增条数 / 字符,双过才跑) | — | +| `dreamDelayMs` | `2000` | 触发后的延迟(毫秒) | — | +| `dreamMinIntervalMinutes` | `0` | 两次开跑最小间隔(分钟,≤10080,#89) | 失败/degraded run 也占用;0 = 不限 | +| `autoDreamFailureBackoff` | `false` | 连续失败指数退避:有效间隔 = 基数 × 2^连败,封顶 30 分钟(#292) | opt-in;基数取 `dreamMinIntervalMinutes`(0 = 无闸可翻倍);成功一次清零;重启归零 | +| `dreamPeakHours` | `""` | 巩固错峰,与 `summarizePeakHours` 同一份时段语法(#239) | 命中高峰不做梦、阈值继续累积、留 skip 审计;非法写法按未配置 | +| `dreamPeakMaxDeferMinutes` | `120` | 巩固顺延上限(分钟) | 0 = 不设上限;到点仍处高峰照跑 | +| `dreamProvider` / `dreamModel` | 空 | 巩固专用路由(settings「巩固模型」) | 建议非思考模型——思考模型易烧光预算返回空体(#135) | +| `dreamMaxTokens` | `131072` | 巩固输出预算(256–131072) | 流式计费按实际用量,调大不增加成本 | +| `dreamReasoningEffort` | 未配置 | 巩固推理档位(同 `summarizeReasoningEffort` 的枚举) | 未配置 = 自动取模型支持的最低档(#135);显式 `none` = 不发字段 | +| `dreamSummaryProvider` / `dreamSummaryModel` | 空 | 记忆总览(dream_summarize)专用路由(#258) | 空 = 回落巩固路由;总览输入为全库,建议大 ctx 非思考模型 | +| `dreamSummaryMaxInputs` | `0` | 总览输入条数硬上限(按 updated_at 倒序取最新 N 条) | 0 = 不设上限(库增长可能撑爆小 ctx 模型) | +| `dreamMaxSnapshotSize` | `200` | 滑动窗口:每次只巩固最近 N 条(≤1000) | 窗口外旧记忆不进快照 | +| `dreamCandidateMode` | `window` | 候选集构造:`window` / `hybrid`(并入向量高相似组,#125) | `hybrid` 让该合并的对能碰面;输入成本不随库增长 | +| `dreamCandidateMax` | `0` | `hybrid` 候选总量上限(≤5000) | 0 = 复用 `dreamMaxSnapshotSize` | +| `dreamCandidateMinSim` | `0.85` | `hybrid` 判「高相似」的阈值 | 与 sleep normal 档同一个定义 | +| `dreamMaxArchivePerRun` | `8` | 单轮 archive 决策上限(≤200,#104) | 超限整单拒绝,`dreamSkipInvalid` 不豁免 | +| `dreamImplicitKeep` | `true` | LLM 未提及的快照记忆自动补 keep | `false` = 恢复严格校验(未覆盖即拒绝整单) | +| `dreamMinExplicitCoverage` | `0.5` | 显式决策覆盖率下限(0–1) | 防截断输出被隐式 keep 洗白 | +| `dreamSkipInvalid` | `true` | 跳过单条非法决策、应用合法子集、run 记 degraded(#89) | `false` = 恢复整单拒绝 | +| `allowCrossTypeMerge` | `false` | 放宽跨类型合并检查 | opt-in;`dreamSkipInvalid` 关时跨类型 merge 直接整单拒绝 | +| `dreamNarrativeEnabled` **light** | `false` | dream 期间按共享 tag 聚类合成叙述条(#164 对齐) | opt-in;按需检索、不常驻注入 | +| `dreamNarrativeMinCluster` | `3` | 成簇门槛(共享同一 tag 的记忆数,2–20) | — | +| `documentMemoryEnabled` **light** | `false` | document 型记忆:长文档指针行,全文归 agent(#230) | opt-in;注册校验 + C2 去重 + supersede 记账 | +| `documentInjectBudget` | `2` | document 摘要行的注入预算(1–5) | 只约束注入,不约束检索 | +| `documentDir` | `""` | managed 落盘目录(#296) | 空 = `/documents/`;目录之外只登记指针、正文零读零写 | +| `policyEpoch` | `0` | 裁决规则版本号 | bump 后旧 dream_runs 降为历史证据(receipt 不再驱动现行决策) | + +## 睡眠模式(sleep,空闲深维护) + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `sleepModeEnabled` **light** | `false` | 库静默后全库深维护:冲突消解/归档降级/模式发现/关系补全 | opt-in;可被用户活动中止,与 autoDream 串行 | +| `sleepIdleMinutes` | `5` | 静默多久触发(1–60 分钟) | — | +| `sleepMinIntervalHours` | `8` | 两次 run 最小间隔(小时,≤168) | — | +| `sleepConflictStrictness` | `normal` | 冲突裁决档:`gentle`(0.92) / `normal`(0.85) / `aggressive`(0.75) | — | +| `sleepActionSet` | `conflict` | 动作集:`conflict` / `full`(六分支,#126) | `full` 让互补型/演进型重复有正确出口 | +| `sleepArchiveDays` / `sleepCompressDays` | `30` / `90` | 归档降级分层(天):先缩为摘要、再彻底归档 | 实体关系保留 | +| `sleepPatternMinMemories` | `100` | 模式发现的扫描窗口(条) | — | +| `sleepPatternLookbackDays` | `30` | 属性变更回看(天) | — | +| `sleepMaxPatternPerRun` | `3` | 单轮模式条上限(0–10) | 0 = 关闭模式发现 | +| `sleepProvider` / `sleepModel` | 空 | sleep 专用路由 | 空 = 用巩固路由 / 当前模型 | +| `sleepReasoningEffort` | 未配置 | 同 `dreamReasoningEffort` | — | +| `sleepMaxTokens` | `8192` | 冲突/模式阶段输出预算(#257) | 原硬编码 2048 对 `full` 档必然截断 | +| `sleepHeatThreshold` | `0.05` | 降级联合判定的热度下限(heat < 值且 importance<5 才降) | `heatEnabled` 关时退回纯时间分层 | + +## 反思与冲突(dream 配套) + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `reflectionUpdateEnabled` | `true` | 检索后的反思更新既有记忆 | — | +| `reflectionFailureTracking` | `true` | 失败经验追踪 | — | +| `reflectionUpdateMaxPerRun` | `2` | 单轮 update 上限(0–5) | — | +| `reflectionUpdateMinAgeHours` | `24` | 可更新记忆的最短年龄(小时,≤168) | — | +| `conflictFreezeEnabled` | `false` | 冲突不自动合并、冻结待人工复核 | opt-in | +| `conflictFreezeMaxPending` | `100` | 冻结队列上限(≤1000) | — | + +## 实体 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `entityExtractionEnabled` **light** | `false` | 写入时 LLM 抽取实体/属性/关系 | opt-in;存储三表与 CRUD 恒可用,本键只闸抽取 | +| `entityExtractionProvider` / `entityExtractionModel` | 空 | 抽取专用路由 | 单边为空时回落调用方默认 | +| `entityExtractionReasoning` | `none` | 抽取推理档位(#109) | `none` = 不发字段;拒收自动去掉字段重试一次 | +| `entityExtractionMaxEntities` / `entityExtractionMaxAttrs` | `10` / `20` | 单次抽取的实体数 / 每实体属性数上限 | — | +| `entitySearchEnabled` | `true` | 实体名前缀/语义搜索(供召回) | — | +| `entityRecallEnabled` **light** | `false` | 图召回轴:查询命中实体名时并入融合池(#219) | opt-in;依赖实体抽取产出(lightMode 下抽取已关) | + +## 检索 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `embedProvider` | `openai` | 嵌入提供方:`openai`(外部 API)/ `local`(ONNX)/ `ollama` | — | +| `vectorSearchTopK` | `20` | 向量召回条数(≤100) | — | +| `vectorSearchThreshold` | `0.65` | 固定向量阈值 | `adaptiveThresholdEnabled` 开启时被动态阈值取代 | +| `adaptiveThresholdEnabled` | `true` | 查询感知动态阈值(实体前缀放宽、短查询收紧等) | 关 = 回到固定 0.65 | +| `bm25SearchEnabled` **light** | `true` | BM25 第三召回路径(标识符/代码碎片/混排) | — | +| `hybridSearchVectorWeight` / `hybridSearchKeywordWeight` | `0.6` / `0.4` | 混合检索加权 | — | +| `recallFusion` | `blend` | 融合配方:`blend`(现状)/ `rrf` / `minmax` | `rrf`/`minmax` 修「raw 余弦 + 关键词分直接相加」的量纲失配 | +| `signalTransparency` | `false` | 每条结果附带 `{keyword, vector, bm25, final}` 信号 | 只装饰返回行,不改排序 | +| `hybridInject` **light** | `true` | 注入前语义优先召回、规则序回填 | — | +| `selectiveInjectEnabled` **light** | `true` | 有查询向量时按相似度重排注入候选 | — | +| `searchSemanticDedup` **light** | `false` | 检索期语义去重(贪心剔除近重复) | opt-in 激进档:小嵌入模型易把不同条目坍缩 | +| `searchSemanticDedupThreshold` | `0.95` | 去重阈值 | — | +| `evalPersistTestResults` | `false` | 检索评测快照落 `recall_evals` | 生产检索恒只进 `recall_runs`,与此键无关 | +| `recallRecordDefault` | `true` | `recall_runs` 记录默认开 | 显式传 `false` 的调用方不受影响 | +| `recallRetentionDays` | `90` | `recall_runs` 滚动清理保留天数 | — | +| `trustEpistemicWeighting` | `false` | 来源可信度(observation/subjective/inferred)参与排序与标注 | opt-in;关时 epistemic_status 只是惰性数据 | + +### 重排层 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `rerankEnabled` **light** | `false` | 本地交叉编码器重排 | opt-in:显式 `true` + `local` 才拉起 onnxruntime | +| `rerankProvider` | `none` | `local` / `none` | — | +| `rerankModel` | `Xenova/bge-reranker-base` | 重排模型 | — | +| `rerankBatchSize` | `8` | 批大小(≤64) | — | +| `rerankMaxCandidates` | `30` | 参与重排的候选上限(5–100) | — | +| `rerankScoreThreshold` | `0.1` | 重排分数阈值 | — | +| `rerankDtype` | `q8` | 量化档(#188) | 非法值由 transformers 抛出 → 重排降级告警 | + +## 热度 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `heatEnabled` **light** | `false` | 热度曲线(遗忘):热度字段 / sleep 降级联判 / 注入优先级层乘 heat(#218) | opt-in;关 = 注入乘数恒 1、sleep 退回纯时间分层 | +| `heatGlobalBeta` | `1.0` | 广义指数形状参数 β(0.5–2) | 专家调优项,不进面板白名单 | +| `heatTypeDecay` | 内置默认 | per-type 衰减 λ(键为 type 字符串) | λ=0 的类型免疫(热度恒 1、sleep 永不降级) | + +## API 与工具面 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `apiToken` | 空 | 宿主内 API 敏感端点的共享 token | 空 = 只读端点保持开放(DSH 绑 127.0.0.1 且无内建鉴权) | +| `externalApiEnabled` | `false` | 独立 `node:http` 数据面(生态集成用) | opt-in;双宿主共用库时只能一边开(端口冲突) | +| `externalApiPort` | `8790` | 端口 | — | +| `externalApiHost` | `127.0.0.1` | 绑定地址 | 改成非回环 = 把整个记忆库暴露给网络,自负其责 | +| `disableMemorySearch` | `false` | 对模型隐藏 `memory_search` 工具(v0.8.5) | 慢/轻量模型的工具往返节流;隐藏即不可调 | +| `disableMemoryArchive` | `false` | 对模型隐藏 `memory_archive` 工具 | 同上 | + +## 运行时与本地模型 + +| 键 | 默认 | 作用 | 开启后果 / 冲突 | +|---|---|---|---| +| `localEmbedModel` | `Xenova/bge-small-zh-v1.5` | 本地嵌入模型(`embedProvider=local` 时) | — | +| `localEmbedDimension` | `512` | 向量维度 | — | +| `localEmbedDevice` | `cpu` | `cpu` / `gpu` | — | +| `localEmbedBatchSize` | `8` | 批大小(≤64) | — | +| `localEmbedPooling` | `auto` | 池化:`auto`(BGE→cls,其余→mean)/ `cls` / `mean` | 池化决定向量空间——改动会变 modelHash、触发既有索引重建 | +| `ollamaBaseUrl` | `http://localhost:11434` | Ollama 地址 | 只接受 http/https(SSRF 防线) | +| `ollamaModel` | `nomic-embed-text` | Ollama 嵌入模型 | — | +| `embedModelCacheDir` | `""` | 模型缓存目录 | 空 = `~/.dsh/mneme/models` | +| `embedModelMirror` | `https://hf-mirror.com` | 模型下载镜像 | — | +| `resilientModelDownload` | `true` | 模型下载断点续传 + 空闲看门狗(#194) | 关 = 恢复原生 fetch(线上回滚开关) | +| `runtimeDir` | `""` | 自管运行时目录(transformers + onnxruntime 闭包,#131) | 空 = `~/.dsh/mneme/runtime`;把重依赖挪出宿主 profile 的依赖图 | +| `runtimeTarballDir` | `""` | 本地 `.tgz` 离线取件目录 | 有货优先于联网(弱网下装 onnxruntime-node 用) | +| `runtimeMirror` | `""` | registry 镜像前缀(如 `https://npmmirror.com/mirrors/npm/`) | 空 = manifest 里的官方地址 | + +## lightMode 强制关闭清单(`LIGHT_MODE_OFF`) + +`lightMode: true`(或面板 `panel_mode=light`)时,以下键被预设强制为 `false`——都是 +重型路径;核心循环(autoInject、autoSummarize、hotMemory*、质量过滤、关键词检索)不动: + +`entityExtractionEnabled` · `autoDream` · `sleepModeEnabled` · `rerankEnabled` · +`autoReindexOnBoot` · `hybridInject` · `injectGuidanceEnabled` · `searchSemanticDedup` · +`selectiveInjectEnabled` · `bm25SearchEnabled` · `entityRecallEnabled` · +`dreamNarrativeEnabled` · `documentMemoryEnabled` · `heatEnabled` · `continuityRescueEnabled` + +预设只是默认值而非强制:用户显式写进 feature_flags 的值在装配顺序上后展开、仍然生效 +(「用户开关 > 轻量预设 > bundle 配置」)。