@modusensus/dsh-mneme 0.4.2 → 0.4.3

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/README.md CHANGED
@@ -54,6 +54,18 @@
54
54
  - **Fail-safe**:非法 LLM 输出(未知 id / 非法 action / 跨类型合并 / 越界 importance)拒绝整单,绝不破坏记忆库
55
55
  - **裁决审计**:每次运行写入 `dream_runs` 审计表(输入快照 sha256 digest + 完整输入快照 + 决策清单 + 逐 id 去向 + receipt),可离线回放;merge / conflict / update 幂等应用,重放/并发重复执行无累积副作用;update 记录 `_before` 快照
56
56
 
57
+ #### dreamMaxTokens 调优指南
58
+
59
+ 默认 `4096` 已覆盖常规记忆库。当**记忆量大**(数万字符以上)时,决策清单与摘要可能超过默认预算,建议按规模调大:
60
+
61
+ | 记忆库规模 | 建议 `dreamMaxTokens` |
62
+ |-----------|----------------------|
63
+ | 常规(<1 万字) | `4096`(默认) |
64
+ | 中等(1 万-5 万字) | `65536` |
65
+ | 大型(5 万字以上) | `131072`(上限) |
66
+
67
+ > 若使用**思考型模型**(如 DeepSeek-R1 类),模型可能把全部预算花在 reasoning 上导致正文为空(日志出现 `no json array in llm output`)。此时把 `dreamReasoningEffort` 设为 `low` 可压制推理开销、把预算留给正文输出;sleep 侧对应 `sleepReasoningEffort`。默认 `none` 不传该字段,完全沿用模型自身默认,行为与旧版本一致。
68
+
57
69
  ### Sleep Mode 系统级睡眠 💤(v0.4.0,opt-in)
58
70
 
59
71
  从 autoDream 的"被动阈值触发"升级为"主动定时维护 + 分层压缩"。系统空闲 `sleepIdleMinutes` 分钟自动执行深度维护,**默认关闭**(`sleepModeEnabled: false`),开启后行为:
@@ -149,6 +161,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
149
161
  | v0.3.9 | ✅ 完成 | 审计加固 A/B/D/F | compareAndUpdate 同事务原子性、degraded 回执、逐 type 物理终态、整数 fail-closed、并发初始化稳定 |
150
162
  | **v0.4.0** | ✅ 完成 | 系统级睡眠 Sleep Mode | 空闲触发的四阶段深度维护(冲突消解 / 归档降级 / 模式发现 / 关系补全)、分层压缩、可中断串行 fail-safe;471 测试全绿 |
151
163
  | **v0.4.2** | ✅ 完成 | autoSummarize 自定义模型 | `summarizeProvider`/`summarizeModel` 配置项支持,可独立指定轻量模型(如 qwen3.6-plus)用于会话摘要,节省主模型 token;473 测试全绿 |
164
+ | **v0.4.3** | ✅ 完成 | autoDream 大记忆量修复 | issue#9 B+A:`dreamMaxTokens` 上限 32768→131072 + `dreamReasoningEffort`/`sleepReasoningEffort` 思考开关(none 默认,主对话不受影响);478 测试全绿 |
152
165
  | **v0.5.0+** | 🚀 远期 | 自进化记忆 | 兴趣漂移跟踪 + 跨 workspace 记忆共享(等 DSH 支持) |
153
166
 
154
167
  > 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
@@ -219,7 +232,8 @@ dsh web
219
232
  | `dreamThresholdChars` | `5000` | 触发整理的总字符阈值 |
220
233
  | `dreamDelayMs` | `2000` | 整理异步延迟(去抖) |
221
234
  | `dreamProvider` / `dreamModel` | 空 | dream 的 LLM 路由回退(默认用 agent 默认模型) |
222
- | `dreamMaxTokens` | `4096` | dream LLM 调用最大 token |
235
+ | `dreamMaxTokens` | `4096` | dream LLM 调用最大 token 数(上限 131072;大记忆量建议调大,见下方调优指南) |
236
+ | `dreamReasoningEffort` | `none` | dream LLM 推理强度透传:`low` / `medium` / `high` / `none`(`none`=不传该字段,使用模型默认;思考型模型预算被推理耗尽导致正文为空时可设 `low`) |
223
237
  | `apiToken` | 空 | 可选 API 鉴权 token;设置后写操作与密钥接口要求 `Authorization: Bearer <apiToken>` |
224
238
  | `embedProvider` | `openai` | 语义后端:`openai`(默认,兼容 v0.1)/ `local`(ONNX 离线)/ `ollama` |
225
239
  | `localEmbedModel` | `Xenova/bge-small-zh-v1.5` | 本地 ONNX embedding 模型 |
package/lib/config.js CHANGED
@@ -17,7 +17,18 @@ export const Config = z.object({
17
17
  dreamDelayMs: z.natural().min(0).max(60000).default(2000),
18
18
  dreamProvider: z.string(),
19
19
  dreamModel: z.string(),
20
- dreamMaxTokens: z.natural().min(256).max(32768).default(4096),
20
+ dreamMaxTokens: z.natural().min(256).max(131072).default(4096),
21
+ // Pass-through reasoning effort for dream's LLM calls. 'none' (default)
22
+ // omits the field so the provider's own default applies; low/medium/high
23
+ // are forwarded verbatim. Useful to cap reasoning spend on thinking-type
24
+ // models that would otherwise drain the whole token budget and return an
25
+ // empty body ("no json array in llm output").
26
+ dreamReasoningEffort: z.union([
27
+ z.const("low"),
28
+ z.const("medium"),
29
+ z.const("high"),
30
+ z.const("none")
31
+ ]).default("none"),
21
32
  // Rule version for dream adjudication: when this bumps, older dream_runs
22
33
  // degrade to historical evidence (their receipts no longer drive live
23
34
  // decisions). Default 0 = no versioning in use yet.
@@ -134,4 +145,13 @@ export const Config = z.object({
134
145
  // route / agent default model).
135
146
  sleepProvider: z.string().default(""),
136
147
  sleepModel: z.string().default(""),
148
+ // Pass-through reasoning effort for sleep's LLM passes, same semantics as
149
+ // dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
150
+ // are forwarded verbatim.
151
+ sleepReasoningEffort: z.union([
152
+ z.const("low"),
153
+ z.const("medium"),
154
+ z.const("high"),
155
+ z.const("none")
156
+ ]).default("none"),
137
157
  });
@@ -187,6 +187,9 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
187
187
  model: route.model,
188
188
  purpose: "sleep-conflict",
189
189
  maxTokens: 2048,
190
+ ...(config.sleepReasoningEffort && config.sleepReasoningEffort !== "none"
191
+ ? { reasoningEffort: config.sleepReasoningEffort }
192
+ : {}),
190
193
  messages: [
191
194
  { role: "system", content: [{ type: "text", text: CONFLICT_PROMPT }] },
192
195
  { role: "user", content: [{ type: "text", text: listText }] }
@@ -286,6 +289,9 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
286
289
  model: route.model,
287
290
  purpose: "sleep-pattern",
288
291
  maxTokens: 2048,
292
+ ...(config.sleepReasoningEffort && config.sleepReasoningEffort !== "none"
293
+ ? { reasoningEffort: config.sleepReasoningEffort }
294
+ : {}),
289
295
  messages: [
290
296
  { role: "system", content: [{ type: "text", text: PATTERN_PROMPT.replace("N", String(maxPatterns)) }] },
291
297
  { role: "user", content: [{ type: "text", text: listText }] }
package/lib/dream.js CHANGED
@@ -457,6 +457,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
457
457
  model: route.model,
458
458
  purpose: "compaction",
459
459
  maxTokens: config.dreamMaxTokens ?? 4096,
460
+ ...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
461
+ ? { reasoningEffort: config.dreamReasoningEffort }
462
+ : {}),
460
463
  messages: [
461
464
  { role: "system", content: [{ type: "text", text: consolidationPrompt }] },
462
465
  { role: "user", content: [{ type: "text", text: listText }] }
@@ -600,6 +603,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
600
603
  model: route.model,
601
604
  purpose: "compaction",
602
605
  maxTokens: config.dreamMaxTokens ?? 2048,
606
+ ...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
607
+ ? { reasoningEffort: config.dreamReasoningEffort }
608
+ : {}),
603
609
  messages: [
604
610
  { role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
605
611
  { role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@modusensus/dsh-mneme",
3
3
  "description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
4
- "version": "0.4.2",
4
+ "version": "0.4.3",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -64,4 +64,4 @@
64
64
  "overrides": {
65
65
  "adm-zip": "0.6.0"
66
66
  }
67
- }
67
+ }
package/src/config.js CHANGED
@@ -17,7 +17,18 @@ export const Config = z.object({
17
17
  dreamDelayMs: z.natural().min(0).max(60000).default(2000),
18
18
  dreamProvider: z.string(),
19
19
  dreamModel: z.string(),
20
- dreamMaxTokens: z.natural().min(256).max(32768).default(4096),
20
+ dreamMaxTokens: z.natural().min(256).max(131072).default(4096),
21
+ // Pass-through reasoning effort for dream's LLM calls. 'none' (default)
22
+ // omits the field so the provider's own default applies; low/medium/high
23
+ // are forwarded verbatim. Useful to cap reasoning spend on thinking-type
24
+ // models that would otherwise drain the whole token budget and return an
25
+ // empty body ("no json array in llm output").
26
+ dreamReasoningEffort: z.union([
27
+ z.const("low"),
28
+ z.const("medium"),
29
+ z.const("high"),
30
+ z.const("none")
31
+ ]).default("none"),
21
32
  // Rule version for dream adjudication: when this bumps, older dream_runs
22
33
  // degrade to historical evidence (their receipts no longer drive live
23
34
  // decisions). Default 0 = no versioning in use yet.
@@ -134,4 +145,13 @@ export const Config = z.object({
134
145
  // route / agent default model).
135
146
  sleepProvider: z.string().default(""),
136
147
  sleepModel: z.string().default(""),
148
+ // Pass-through reasoning effort for sleep's LLM passes, same semantics as
149
+ // dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
150
+ // are forwarded verbatim.
151
+ sleepReasoningEffort: z.union([
152
+ z.const("low"),
153
+ z.const("medium"),
154
+ z.const("high"),
155
+ z.const("none")
156
+ ]).default("none"),
137
157
  });
@@ -187,6 +187,9 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
187
187
  model: route.model,
188
188
  purpose: "sleep-conflict",
189
189
  maxTokens: 2048,
190
+ ...(config.sleepReasoningEffort && config.sleepReasoningEffort !== "none"
191
+ ? { reasoningEffort: config.sleepReasoningEffort }
192
+ : {}),
190
193
  messages: [
191
194
  { role: "system", content: [{ type: "text", text: CONFLICT_PROMPT }] },
192
195
  { role: "user", content: [{ type: "text", text: listText }] }
@@ -286,6 +289,9 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
286
289
  model: route.model,
287
290
  purpose: "sleep-pattern",
288
291
  maxTokens: 2048,
292
+ ...(config.sleepReasoningEffort && config.sleepReasoningEffort !== "none"
293
+ ? { reasoningEffort: config.sleepReasoningEffort }
294
+ : {}),
289
295
  messages: [
290
296
  { role: "system", content: [{ type: "text", text: PATTERN_PROMPT.replace("N", String(maxPatterns)) }] },
291
297
  { role: "user", content: [{ type: "text", text: listText }] }
package/src/dream.js CHANGED
@@ -457,6 +457,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
457
457
  model: route.model,
458
458
  purpose: "compaction",
459
459
  maxTokens: config.dreamMaxTokens ?? 4096,
460
+ ...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
461
+ ? { reasoningEffort: config.dreamReasoningEffort }
462
+ : {}),
460
463
  messages: [
461
464
  { role: "system", content: [{ type: "text", text: consolidationPrompt }] },
462
465
  { role: "user", content: [{ type: "text", text: listText }] }
@@ -600,6 +603,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
600
603
  model: route.model,
601
604
  purpose: "compaction",
602
605
  maxTokens: config.dreamMaxTokens ?? 2048,
606
+ ...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
607
+ ? { reasoningEffort: config.dreamReasoningEffort }
608
+ : {}),
603
609
  messages: [
604
610
  { role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
605
611
  { role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
@@ -0,0 +1,172 @@
1
+ // Regression for issue #9:
2
+ // - B: dreamMaxTokens cap widened (min 256, max 131072) so large memory
3
+ // libraries no longer starve the consolidation output.
4
+ // - A: dreamReasoningEffort / sleepReasoningEffort pass-through. Default
5
+ // 'none' must OMIT the reasoningEffort field entirely (the provider's own
6
+ // default applies); low/medium/high are forwarded verbatim on every dream /
7
+ // sleep LLM call. Asserted by capturing the options each llm.stream() sees.
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { Config } from "../src/config.js";
11
+ import { createDreamScheduler } from "../src/dream.js";
12
+ import { runSleep } from "../src/dream/sleep.js";
13
+ import { createStore } from "../src/store.js";
14
+ import { createService } from "../src/service.js";
15
+ import { createVectorIndex } from "../src/vector-index.js";
16
+
17
+ const embedder = {
18
+ embedSingle: async () => [1, 0, 0],
19
+ embed: async () => [1, 0, 0],
20
+ schedule: () => {},
21
+ modelHash: "mock#1",
22
+ dimension: 3
23
+ };
24
+
25
+ // ---------------------------------------------------------------- config schema
26
+
27
+ test("issue#9: dreamMaxTokens accepts the widened 131072 cap and defaults to 4096", () => {
28
+ assert.equal(Config({}).dreamMaxTokens, 4096, "default unchanged");
29
+ assert.equal(Config({ dreamMaxTokens: 131072 }).dreamMaxTokens, 131072, "new upper bound accepted");
30
+ assert.equal(Config({ dreamMaxTokens: 65536 }).dreamMaxTokens, 65536, "intermediate value accepted");
31
+ });
32
+
33
+ test("issue#9: reasoningEffort config defaults to none and rejects unknown values", () => {
34
+ const cfg = Config({});
35
+ assert.equal(cfg.dreamReasoningEffort, "none");
36
+ assert.equal(cfg.sleepReasoningEffort, "none");
37
+ assert.equal(Config({ dreamReasoningEffort: "high" }).dreamReasoningEffort, "high");
38
+ assert.equal(Config({ sleepReasoningEffort: "medium" }).sleepReasoningEffort, "medium");
39
+ assert.throws(() => Config({ dreamReasoningEffort: "bogus" }), "invalid effort rejected");
40
+ assert.throws(() => Config({ sleepReasoningEffort: "ultra" }), "invalid effort rejected");
41
+ });
42
+
43
+ // ---------------------------------------------------------------- dream passthrough
44
+
45
+ /** dream ctx that records every llm.stream() call's options for inspection. */
46
+ function dreamCtx({ onConsolidation, summaryText = "记忆库总览:用户偏好中文。", captured = [] } = {}) {
47
+ return {
48
+ logger: { warn: () => {} },
49
+ agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
50
+ llm: {
51
+ async *stream(options) {
52
+ captured.push(options);
53
+ const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
54
+ if (userText.startsWith("id=")) {
55
+ yield { type: "text-delta", index: 0, text: onConsolidation ? onConsolidation(userText) : "[]" };
56
+ } else {
57
+ yield { type: "text-delta", index: 0, text: summaryText };
58
+ }
59
+ yield { type: "finish", reason: { kind: "stop" } };
60
+ }
61
+ }
62
+ };
63
+ }
64
+
65
+ test("issue#9: dream omits reasoningEffort under default 'none' and still consolidates (applied>0)", async () => {
66
+ const store = createStore(":memory:");
67
+ const service = createService({ store, mirror: null, config: {} });
68
+ const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
69
+ const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
70
+ const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
71
+ const captured = [];
72
+ const ctx = dreamCtx({
73
+ captured,
74
+ onConsolidation: () => JSON.stringify([
75
+ { action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
76
+ ])
77
+ });
78
+ const result = await dream.runDream(ctx, service, {});
79
+ assert.equal(result.ok, true);
80
+ assert.ok(result.applied > 0, "end-to-end dream run still lands changes");
81
+ assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
82
+ for (const options of captured) {
83
+ assert.equal("reasoningEffort" in options, false, `default 'none' must not forward reasoningEffort (${options.purpose})`);
84
+ }
85
+ store.close();
86
+ });
87
+
88
+ test("issue#9: dream forwards dreamReasoningEffort on both LLM calls", async () => {
89
+ const store = createStore(":memory:");
90
+ const service = createService({ store, mirror: null, config: {} });
91
+ const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
92
+ const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
93
+ const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
94
+ const captured = [];
95
+ const ctx = dreamCtx({
96
+ captured,
97
+ onConsolidation: () => JSON.stringify([
98
+ { action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
99
+ ])
100
+ });
101
+ const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
102
+ assert.equal(result.ok, true);
103
+ assert.equal(captured.length, 2);
104
+ for (const options of captured) {
105
+ assert.equal(options.reasoningEffort, "high", `reasoningEffort forwarded on ${options.purpose}`);
106
+ }
107
+ store.close();
108
+ });
109
+
110
+ // ---------------------------------------------------------------- sleep passthrough
111
+
112
+ function sleepSetup() {
113
+ const store = createStore(":memory:");
114
+ const service = createService({ store, mirror: null, config: {} });
115
+ const vectorIndex = createVectorIndex({ store });
116
+ service.setEmbedder(embedder);
117
+ service.setVectorIndex(vectorIndex);
118
+ return { store, service, vectorIndex };
119
+ }
120
+
121
+ function baseConfig(overrides = {}) {
122
+ return {
123
+ sleepModeEnabled: true,
124
+ sleepIdleMinutes: 5,
125
+ sleepMinIntervalHours: 8,
126
+ sleepConflictStrictness: "normal",
127
+ sleepArchiveDays: 30,
128
+ sleepCompressDays: 90,
129
+ sleepPatternMinMemories: 10,
130
+ sleepMaxPatternPerRun: 3,
131
+ ...overrides
132
+ };
133
+ }
134
+
135
+ /** sleep ctx that records every llm.stream() call's options. */
136
+ function sleepCtx(onConsolidation, selection = { provider: "mock", model: "sleep-model" }, captured = []) {
137
+ return {
138
+ logger: { warn: () => {}, info: () => {} },
139
+ agentDefaultModel: { currentSelection: () => selection },
140
+ llm: {
141
+ async *stream(options) {
142
+ captured.push(options);
143
+ const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
144
+ yield { type: "text-delta", index: 0, text: onConsolidation ? onConsolidation(userText) : "[]" };
145
+ yield { type: "finish", reason: { kind: "stop" } };
146
+ }
147
+ }
148
+ };
149
+ }
150
+
151
+ test("issue#9: sleep forwards sleepReasoningEffort on its LLM passes", async () => {
152
+ const { store, service, vectorIndex } = sleepSetup();
153
+ const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
154
+ const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
155
+ vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
156
+ vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
157
+ const captured = [];
158
+ const ctx = sleepCtx(
159
+ (userText) => userText.startsWith("候选冲突")
160
+ ? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
161
+ : "[]",
162
+ { provider: "mock", model: "sleep-model" },
163
+ captured
164
+ );
165
+ const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "medium" }), ctx.logger, { embedder, vectorIndex }, null);
166
+ assert.equal(result.status, "ok");
167
+ assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
168
+ for (const options of captured) {
169
+ assert.equal(options.reasoningEffort, "medium", `reasoningEffort forwarded on ${options.purpose}`);
170
+ }
171
+ store.close();
172
+ });