@modusensus/dsh-mneme 0.7.15 → 0.7.16
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.en.md +2 -2
- package/README.md +4 -3
- package/lib/dream/sleep.js +17 -15
- package/lib/dream.js +94 -48
- package/package.json +1 -1
- package/src/dream/sleep.js +17 -15
- package/src/dream.js +94 -48
- package/test/dream.test.js +1 -1
- package/test/llm-audit.test.js +34 -4
- package/test/reasoning-effort.test.js +64 -0
package/README.en.md
CHANGED
|
@@ -7,7 +7,7 @@ English | [简体中文](README.md)
|
|
|
7
7
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
10
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
11
11
|
|
|
12
12
|
> A cross-session memory plugin for DeepSeek Harness: it lets the Agent remember you, remember your projects, and organize memories automatically. **Mneme** (Μνήμη) — named after Mnemosyne, the Greek goddess of memory who presides over memory and dreams, just as autoDream consolidates memories in the background.
|
|
13
13
|
|
|
@@ -405,7 +405,7 @@ src/
|
|
|
405
405
|
lib/
|
|
406
406
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
407
407
|
└── *.js # src 的同步分发产物
|
|
408
|
-
test/ #
|
|
408
|
+
test/ # 648 个 node:test 测试(含审计与三轴线压测不变量)
|
|
409
409
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步
|
|
410
410
|
```
|
|
411
411
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
9
9
|
[](https://github.com/modusensus/dsh-mneme/actions)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
@@ -241,6 +241,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
241
241
|
|
|
242
242
|
| 版本 | 亮点 |
|
|
243
243
|
|------|------|
|
|
244
|
+
| **v0.7.16** | 修复 autoDream 在 thinking 模型上空体 failed(`no json array in llm output`):恢复 config-first 路由(设置面板「巩固模型」生效,Issue #25)+ reasoningEffort 被拒自动去掉重试一次 + 解析失败如实记 llm_audit error 并带原始输出头日志;648 测试全绿 |
|
|
244
245
|
| **v0.7.15** | 桌面端适配:记忆库面板重设计(撤对话 tab → 居中非全屏 sheet、卡片网格/时间线双视图、右侧详情抽屉、编辑/归档/关联实体)+ 侧边栏入口上移工作区上方(借用宿主原生类名对齐、收起态零位移)+ 功能开关 30 键上 UI(features API,巩固模型与 embedding 提供方可配)+ 状态页工作台(巩固卡/工作动态流/沉淀记忆/归档恢复)+ 导入导出(镜像同构 md 黄金闭环)+ Token 面板默认遮蔽;645 测试全绿 |
|
|
245
246
|
| **v0.7.14** | 安全修复(CWE-200):蒸馏不再把助手 `reasoning` 私有推理块送进蒸馏上下文(只采公开 `text`,防止记忆沉淀私有思考链);617 测试全绿 |
|
|
246
247
|
| **v0.7.13** | 编码记忆蒸馏(`codingRetrospect` 默认关):完整转录(用户+助手回答+工具调用/结果+代码执行)提炼原子记忆,新增 `rejected_solution`/`pitfall`/`constraint` 三类型,编码任务 `codingBoostFactor` 加权(cap 5)+ 智能调速器(蒸馏全局串行队列 + 429 指数退避自动重试)+ 原子记忆语义保留(宁可拆多条不合并丢细节,`distillMaxChars` 默认 24000)+ 修复 v0.7.12 CI 回归(恢复 c8 覆盖率);616 测试全绿 |
|
|
@@ -544,7 +545,7 @@ src/
|
|
|
544
545
|
├── client.js # Web 面板 bundle(ModuleLoader 自注册;v0.7.6 起 src 正源)
|
|
545
546
|
└── index.js # 插件接线
|
|
546
547
|
lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性,无手写例外)
|
|
547
|
-
test/ #
|
|
548
|
+
test/ # 648 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
|
|
548
549
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js 召回基准
|
|
549
550
|
```
|
|
550
551
|
|
|
@@ -553,7 +554,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
553
554
|
```bash
|
|
554
555
|
cd dsh-mneme
|
|
555
556
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
556
|
-
npm test # 运行
|
|
557
|
+
npm test # 运行 648 个测试
|
|
557
558
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
558
559
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
559
560
|
```
|
package/lib/dream/sleep.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { randomUUID, createHash } from "node:crypto";
|
|
18
18
|
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
19
|
import { findPotentialConflicts } from "./clustering.js";
|
|
20
|
-
import { buildReceipt } from "../dream.js";
|
|
20
|
+
import { buildReceipt, withEffortFallback } from "../dream.js";
|
|
21
21
|
|
|
22
22
|
const SUMMARY_MAX = 120;
|
|
23
23
|
// Conflict similarity threshold per strictness level (v0.4.0):
|
|
@@ -72,16 +72,18 @@ async function streamText(ctx, options) {
|
|
|
72
72
|
return text;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/** LLM route
|
|
76
|
-
*
|
|
77
|
-
* bulk passes without disturbing the dream
|
|
75
|
+
/** LLM route (Issue #25): explicit sleepProvider/Model wins, then the dream
|
|
76
|
+
* route as a shared explicit fallback, then the agent default model. Sleep
|
|
77
|
+
* can pin a cheaper model for its bulk passes without disturbing the dream
|
|
78
|
+
* route. Explicit config first — otherwise the config routes are dead code
|
|
79
|
+
* whenever agentDefaultModel resolves (see resolveRoute in dream.js). */
|
|
78
80
|
function resolveSleepRoute(ctx, config, logger) {
|
|
81
|
+
if (config.sleepProvider && config.sleepModel) return { provider: config.sleepProvider, model: config.sleepModel };
|
|
82
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
79
83
|
try {
|
|
80
84
|
const sel = ctx?.agentDefaultModel?.currentSelection?.();
|
|
81
85
|
if (sel?.provider && sel?.model) return { provider: sel.provider, model: sel.model };
|
|
82
|
-
} catch { /* fall through to
|
|
83
|
-
if (config.sleepProvider && config.sleepModel) return { provider: config.sleepProvider, model: config.sleepModel };
|
|
84
|
-
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
86
|
+
} catch { /* fall through to warn */ }
|
|
85
87
|
logger?.warn?.("dsh-mneme sleep: no llm route available");
|
|
86
88
|
return undefined;
|
|
87
89
|
}
|
|
@@ -182,19 +184,19 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
182
184
|
const listText = selected.map((p) =>
|
|
183
185
|
`候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`
|
|
184
186
|
).join("\n\n");
|
|
185
|
-
const
|
|
187
|
+
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
188
|
+
const runConflict = (withEffort) => streamText(ctx, {
|
|
186
189
|
provider: route.provider,
|
|
187
190
|
model: route.model,
|
|
188
191
|
purpose: "sleep-conflict",
|
|
189
192
|
maxTokens: 2048,
|
|
190
|
-
...(
|
|
191
|
-
? { reasoningEffort: config.sleepReasoningEffort }
|
|
192
|
-
: {}),
|
|
193
|
+
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
193
194
|
messages: [
|
|
194
195
|
{ role: "system", content: [{ type: "text", text: CONFLICT_PROMPT }] },
|
|
195
196
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
196
197
|
]
|
|
197
198
|
});
|
|
199
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runConflict(true), () => runConflict(false));
|
|
198
200
|
if (text === undefined) return { status: "failed", error: "llm failed" };
|
|
199
201
|
const decisions = parseJsonArray(text);
|
|
200
202
|
if (!decisions) return { status: "failed", error: "invalid decisions json" };
|
|
@@ -282,19 +284,19 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
282
284
|
.map((m) => `id=${m.id} | type=${m.type} | importance=${m.importance} | updated=${m.updated_at} | title=${m.title} | content=${m.content}`)
|
|
283
285
|
.join("\n");
|
|
284
286
|
const maxPatterns = config.sleepMaxPatternPerRun ?? 3;
|
|
285
|
-
const
|
|
287
|
+
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
288
|
+
const runPattern = (withEffort) => streamText(ctx, {
|
|
286
289
|
provider: route.provider,
|
|
287
290
|
model: route.model,
|
|
288
291
|
purpose: "sleep-pattern",
|
|
289
292
|
maxTokens: 2048,
|
|
290
|
-
...(
|
|
291
|
-
? { reasoningEffort: config.sleepReasoningEffort }
|
|
292
|
-
: {}),
|
|
293
|
+
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
293
294
|
messages: [
|
|
294
295
|
{ role: "system", content: [{ type: "text", text: PATTERN_PROMPT.replace("N", String(maxPatterns)) }] },
|
|
295
296
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
296
297
|
]
|
|
297
298
|
});
|
|
299
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runPattern(true), () => runPattern(false));
|
|
298
300
|
if (text === undefined) return { status: "failed", error: "llm failed" };
|
|
299
301
|
const decisions = parseJsonArray(text);
|
|
300
302
|
if (!decisions || decisions.length === 0) return { status: "skipped", reason: "no patterns found" };
|
package/lib/dream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
export { validateDecisions, applyDecisions };
|
|
4
|
+
export { validateDecisions, applyDecisions, withEffortFallback };
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -282,6 +282,15 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
282
282
|
// record it as error here so the audit shows the truth.
|
|
283
283
|
status = "error";
|
|
284
284
|
errorMessage = errorMessage ?? "llm stream aborted or errored";
|
|
285
|
+
} else if (typeof spec.auditError === "function") {
|
|
286
|
+
// A stream that returned text but yields nothing usable is still a
|
|
287
|
+
// failed call — record it as error, not the default success, so the
|
|
288
|
+
// audit no longer contradicts a failed run (dream "no json array").
|
|
289
|
+
const message = spec.auditError(result);
|
|
290
|
+
if (message) {
|
|
291
|
+
status = "error";
|
|
292
|
+
errorMessage = message;
|
|
293
|
+
}
|
|
285
294
|
}
|
|
286
295
|
return result;
|
|
287
296
|
} catch (error) {
|
|
@@ -311,20 +320,50 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
323
|
+
* Reasoning-effort rejection fallback (v0.8.1): a configured dreamReasoningEffort
|
|
324
|
+
* / sleepReasoningEffort may be rejected by the provider (volcano-engine returns
|
|
325
|
+
* UNSUPPORTED_REASONING_EFFORT for values it does not accept — "off" is known
|
|
326
|
+
* rejected there). When that happens, retry once WITHOUT the reasoning field
|
|
327
|
+
* instead of hard-failing the run, so effort config is safe to experiment with:
|
|
328
|
+
* accepted → reasoning capped; rejected → provider default (old behavior),
|
|
329
|
+
* logged so the rejection is observable.
|
|
330
|
+
*/
|
|
331
|
+
async function withEffortFallback(ctx, effort, attempt, fallback) {
|
|
332
|
+
if (!effort || effort === "none") return attempt();
|
|
333
|
+
try {
|
|
334
|
+
return await attempt();
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const message = String(error?.message ?? error);
|
|
337
|
+
// matches both "reasoning effort" (natural language) and the bare
|
|
338
|
+
// "UNSUPPORTED_REASONING_EFFORT" error code (underscore).
|
|
339
|
+
if (!/reasoning[\s_]*effort/i.test(message)) throw error;
|
|
340
|
+
ctx.logger?.warn?.(`dsh-mneme dream: reasoningEffort "${effort}" rejected (${message}); retrying without it`);
|
|
341
|
+
return fallback();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the LLM route (Issue #25): an explicit plugin config
|
|
347
|
+
* (dreamProvider/dreamModel) is the user's declared override and wins; the
|
|
348
|
+
* agent default model (deployment) is only a fallback when no config route is
|
|
349
|
+
* set. In a standard DSH install agentDefaultModel always resolves, so without
|
|
350
|
+
* this ordering the config route would be dead code and dreamProvider/dreamModel
|
|
351
|
+
* could never take effect (v0.7.11 regressed this; README §config documents
|
|
352
|
+
* config-first). Falls through to undefined when no route exists — runDream
|
|
353
|
+
* then fails safe. A config→default switch is logged so it is observable.
|
|
318
354
|
*/
|
|
319
355
|
function resolveRoute(ctx, config, logger) {
|
|
356
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
320
357
|
try {
|
|
321
358
|
const sel = ctx.agentDefaultModel?.currentSelection?.();
|
|
322
|
-
if (sel?.provider && sel?.model)
|
|
323
|
-
|
|
359
|
+
if (sel?.provider && sel?.model) {
|
|
360
|
+
logger?.info?.("dsh-mneme dream: no dreamProvider/dreamModel config, falling back to agent default");
|
|
361
|
+
return { provider: sel.provider, model: sel.model };
|
|
362
|
+
}
|
|
363
|
+
logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, no config route either");
|
|
324
364
|
} catch (error) {
|
|
325
|
-
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed
|
|
365
|
+
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed: ${String(error)}`);
|
|
326
366
|
}
|
|
327
|
-
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
328
367
|
return undefined;
|
|
329
368
|
}
|
|
330
369
|
|
|
@@ -585,28 +624,37 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
585
624
|
? CONSOLIDATION_PROMPT + `\n\n当前为「冲突冻结」模式:检测到内容矛盾的条目时,仍请输出 conflict,并以 winner/loser 作为候选、reason 说明理由;冲突不会被自动裁决,而会冻结待人工确认。`
|
|
586
625
|
: CONSOLIDATION_PROMPT;
|
|
587
626
|
let decisionText;
|
|
627
|
+
// 加固(v0.8.1):配置的 reasoningEffort 被 provider 拒收时回退重试一次
|
|
628
|
+
// (不带该字段),避免 thinking 模型配置 low/medium 直接整单失败。解析放
|
|
629
|
+
// 在 auditError 检查器里、闭包交回主流程,避免二次解析;解析失败同时如实
|
|
630
|
+
// 记 audit error 并在日志带原始输出前 300 字节,便于定位"推理吞预算返回空体"。
|
|
631
|
+
const effort = config.dreamReasoningEffort && config.dreamReasoningEffort !== "none" ? config.dreamReasoningEffort : null;
|
|
632
|
+
let decisions = null;
|
|
633
|
+
const runConsolidation = (withEffort) => runAuditedLlm(ctx, service, config, {
|
|
634
|
+
triggerSource: "autoDream",
|
|
635
|
+
operationType: "dream_consolidate",
|
|
636
|
+
modelId: `${route.provider}:${route.model}`,
|
|
637
|
+
relatedMemoryIds: [...snapshot.keys()],
|
|
638
|
+
auditError: (text) => {
|
|
639
|
+
decisions = extractJsonArray(text);
|
|
640
|
+
return Array.isArray(decisions) ? null : "no json array in llm output";
|
|
641
|
+
}
|
|
642
|
+
}, (reportUsage) => streamText(ctx, {
|
|
643
|
+
provider: route.provider,
|
|
644
|
+
model: route.model,
|
|
645
|
+
purpose: "compaction",
|
|
646
|
+
maxTokens: config.dreamMaxTokens ?? 4096,
|
|
647
|
+
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
648
|
+
messages: [
|
|
649
|
+
{ role: "system", content: [{ type: "text", text: consolidationPrompt }] },
|
|
650
|
+
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
651
|
+
]
|
|
652
|
+
}, reportUsage));
|
|
588
653
|
try {
|
|
589
654
|
// Bug8: the consolidation call is audited (tokens/time/status). A throw
|
|
590
655
|
// re-propagates to the catch below; an aborted stream returns undefined
|
|
591
656
|
// and is treated as a failed run after the check below.
|
|
592
|
-
decisionText = await
|
|
593
|
-
triggerSource: "autoDream",
|
|
594
|
-
operationType: "dream_consolidate",
|
|
595
|
-
modelId: `${route.provider}:${route.model}`,
|
|
596
|
-
relatedMemoryIds: [...snapshot.keys()]
|
|
597
|
-
}, (reportUsage) => streamText(ctx, {
|
|
598
|
-
provider: route.provider,
|
|
599
|
-
model: route.model,
|
|
600
|
-
purpose: "compaction",
|
|
601
|
-
maxTokens: config.dreamMaxTokens ?? 4096,
|
|
602
|
-
...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
|
|
603
|
-
? { reasoningEffort: config.dreamReasoningEffort }
|
|
604
|
-
: {}),
|
|
605
|
-
messages: [
|
|
606
|
-
{ role: "system", content: [{ type: "text", text: consolidationPrompt }] },
|
|
607
|
-
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
608
|
-
]
|
|
609
|
-
}, reportUsage));
|
|
657
|
+
decisionText = await withEffortFallback(ctx, effort, () => runConsolidation(true), () => runConsolidation(false));
|
|
610
658
|
} catch (error) {
|
|
611
659
|
logger?.warn?.(`dsh-mneme dream: consolidation llm call failed: ${String(error)}`);
|
|
612
660
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
@@ -615,10 +663,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
615
663
|
logger?.warn?.("dsh-mneme dream: consolidation llm stream aborted or errored");
|
|
616
664
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
617
665
|
}
|
|
618
|
-
|
|
619
|
-
const decisions = extractJsonArray(decisionText);
|
|
620
666
|
if (!Array.isArray(decisions)) {
|
|
621
|
-
|
|
667
|
+
const head = (decisionText ?? "").slice(0, 300).replace(/\s+/g, " ").trim();
|
|
668
|
+
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0}; head: ${head})`);
|
|
622
669
|
return finish({ ok: false, error: "no json array in llm output", summary: false });
|
|
623
670
|
}
|
|
624
671
|
const { ok, errors } = validateDecisions(decisions, snapshot, {
|
|
@@ -735,26 +782,25 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
735
782
|
// Summary generation (second LLM call). A throwing stream is reported as
|
|
736
783
|
// a failed run; summary:false marks a run that produced no summary.
|
|
737
784
|
let summaryText;
|
|
785
|
+
const runSummary = (withEffort) => runAuditedLlm(ctx, service, config, {
|
|
786
|
+
triggerSource: "autoDream",
|
|
787
|
+
operationType: "dream_summarize",
|
|
788
|
+
modelId: `${route.provider}:${route.model}`,
|
|
789
|
+
relatedMemoryIds: []
|
|
790
|
+
}, (reportUsage) => streamText(ctx, {
|
|
791
|
+
provider: route.provider,
|
|
792
|
+
model: route.model,
|
|
793
|
+
purpose: "compaction",
|
|
794
|
+
maxTokens: config.dreamMaxTokens ?? 2048,
|
|
795
|
+
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
796
|
+
messages: [
|
|
797
|
+
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
798
|
+
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
799
|
+
]
|
|
800
|
+
}, reportUsage));
|
|
738
801
|
try {
|
|
739
802
|
// Bug8: the summary call is audited too (operation dream_summarize).
|
|
740
|
-
summaryText = await
|
|
741
|
-
triggerSource: "autoDream",
|
|
742
|
-
operationType: "dream_summarize",
|
|
743
|
-
modelId: `${route.provider}:${route.model}`,
|
|
744
|
-
relatedMemoryIds: []
|
|
745
|
-
}, (reportUsage) => streamText(ctx, {
|
|
746
|
-
provider: route.provider,
|
|
747
|
-
model: route.model,
|
|
748
|
-
purpose: "compaction",
|
|
749
|
-
maxTokens: config.dreamMaxTokens ?? 2048,
|
|
750
|
-
...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
|
|
751
|
-
? { reasoningEffort: config.dreamReasoningEffort }
|
|
752
|
-
: {}),
|
|
753
|
-
messages: [
|
|
754
|
-
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
755
|
-
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
756
|
-
]
|
|
757
|
-
}, reportUsage));
|
|
803
|
+
summaryText = await withEffortFallback(ctx, effort, () => runSummary(true), () => runSummary(false));
|
|
758
804
|
} catch (error) {
|
|
759
805
|
logger?.warn?.(`dsh-mneme dream: summary llm call failed: ${String(error)}`);
|
|
760
806
|
return finish({ ok: false, error: "llm failed", applied, decisions: auditDecisions, outcome, frozen: frozenCount, summary: false });
|
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.7.
|
|
4
|
+
"version": "0.7.16",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/dream/sleep.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { randomUUID, createHash } from "node:crypto";
|
|
18
18
|
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
19
|
import { findPotentialConflicts } from "./clustering.js";
|
|
20
|
-
import { buildReceipt } from "../dream.js";
|
|
20
|
+
import { buildReceipt, withEffortFallback } from "../dream.js";
|
|
21
21
|
|
|
22
22
|
const SUMMARY_MAX = 120;
|
|
23
23
|
// Conflict similarity threshold per strictness level (v0.4.0):
|
|
@@ -72,16 +72,18 @@ async function streamText(ctx, options) {
|
|
|
72
72
|
return text;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/** LLM route
|
|
76
|
-
*
|
|
77
|
-
* bulk passes without disturbing the dream
|
|
75
|
+
/** LLM route (Issue #25): explicit sleepProvider/Model wins, then the dream
|
|
76
|
+
* route as a shared explicit fallback, then the agent default model. Sleep
|
|
77
|
+
* can pin a cheaper model for its bulk passes without disturbing the dream
|
|
78
|
+
* route. Explicit config first — otherwise the config routes are dead code
|
|
79
|
+
* whenever agentDefaultModel resolves (see resolveRoute in dream.js). */
|
|
78
80
|
function resolveSleepRoute(ctx, config, logger) {
|
|
81
|
+
if (config.sleepProvider && config.sleepModel) return { provider: config.sleepProvider, model: config.sleepModel };
|
|
82
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
79
83
|
try {
|
|
80
84
|
const sel = ctx?.agentDefaultModel?.currentSelection?.();
|
|
81
85
|
if (sel?.provider && sel?.model) return { provider: sel.provider, model: sel.model };
|
|
82
|
-
} catch { /* fall through to
|
|
83
|
-
if (config.sleepProvider && config.sleepModel) return { provider: config.sleepProvider, model: config.sleepModel };
|
|
84
|
-
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
86
|
+
} catch { /* fall through to warn */ }
|
|
85
87
|
logger?.warn?.("dsh-mneme sleep: no llm route available");
|
|
86
88
|
return undefined;
|
|
87
89
|
}
|
|
@@ -182,19 +184,19 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
182
184
|
const listText = selected.map((p) =>
|
|
183
185
|
`候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`
|
|
184
186
|
).join("\n\n");
|
|
185
|
-
const
|
|
187
|
+
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
188
|
+
const runConflict = (withEffort) => streamText(ctx, {
|
|
186
189
|
provider: route.provider,
|
|
187
190
|
model: route.model,
|
|
188
191
|
purpose: "sleep-conflict",
|
|
189
192
|
maxTokens: 2048,
|
|
190
|
-
...(
|
|
191
|
-
? { reasoningEffort: config.sleepReasoningEffort }
|
|
192
|
-
: {}),
|
|
193
|
+
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
193
194
|
messages: [
|
|
194
195
|
{ role: "system", content: [{ type: "text", text: CONFLICT_PROMPT }] },
|
|
195
196
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
196
197
|
]
|
|
197
198
|
});
|
|
199
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runConflict(true), () => runConflict(false));
|
|
198
200
|
if (text === undefined) return { status: "failed", error: "llm failed" };
|
|
199
201
|
const decisions = parseJsonArray(text);
|
|
200
202
|
if (!decisions) return { status: "failed", error: "invalid decisions json" };
|
|
@@ -282,19 +284,19 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
282
284
|
.map((m) => `id=${m.id} | type=${m.type} | importance=${m.importance} | updated=${m.updated_at} | title=${m.title} | content=${m.content}`)
|
|
283
285
|
.join("\n");
|
|
284
286
|
const maxPatterns = config.sleepMaxPatternPerRun ?? 3;
|
|
285
|
-
const
|
|
287
|
+
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
288
|
+
const runPattern = (withEffort) => streamText(ctx, {
|
|
286
289
|
provider: route.provider,
|
|
287
290
|
model: route.model,
|
|
288
291
|
purpose: "sleep-pattern",
|
|
289
292
|
maxTokens: 2048,
|
|
290
|
-
...(
|
|
291
|
-
? { reasoningEffort: config.sleepReasoningEffort }
|
|
292
|
-
: {}),
|
|
293
|
+
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
293
294
|
messages: [
|
|
294
295
|
{ role: "system", content: [{ type: "text", text: PATTERN_PROMPT.replace("N", String(maxPatterns)) }] },
|
|
295
296
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
296
297
|
]
|
|
297
298
|
});
|
|
299
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runPattern(true), () => runPattern(false));
|
|
298
300
|
if (text === undefined) return { status: "failed", error: "llm failed" };
|
|
299
301
|
const decisions = parseJsonArray(text);
|
|
300
302
|
if (!decisions || decisions.length === 0) return { status: "skipped", reason: "no patterns found" };
|
package/src/dream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
export { validateDecisions, applyDecisions };
|
|
4
|
+
export { validateDecisions, applyDecisions, withEffortFallback };
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -282,6 +282,15 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
282
282
|
// record it as error here so the audit shows the truth.
|
|
283
283
|
status = "error";
|
|
284
284
|
errorMessage = errorMessage ?? "llm stream aborted or errored";
|
|
285
|
+
} else if (typeof spec.auditError === "function") {
|
|
286
|
+
// A stream that returned text but yields nothing usable is still a
|
|
287
|
+
// failed call — record it as error, not the default success, so the
|
|
288
|
+
// audit no longer contradicts a failed run (dream "no json array").
|
|
289
|
+
const message = spec.auditError(result);
|
|
290
|
+
if (message) {
|
|
291
|
+
status = "error";
|
|
292
|
+
errorMessage = message;
|
|
293
|
+
}
|
|
285
294
|
}
|
|
286
295
|
return result;
|
|
287
296
|
} catch (error) {
|
|
@@ -311,20 +320,50 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
323
|
+
* Reasoning-effort rejection fallback (v0.8.1): a configured dreamReasoningEffort
|
|
324
|
+
* / sleepReasoningEffort may be rejected by the provider (volcano-engine returns
|
|
325
|
+
* UNSUPPORTED_REASONING_EFFORT for values it does not accept — "off" is known
|
|
326
|
+
* rejected there). When that happens, retry once WITHOUT the reasoning field
|
|
327
|
+
* instead of hard-failing the run, so effort config is safe to experiment with:
|
|
328
|
+
* accepted → reasoning capped; rejected → provider default (old behavior),
|
|
329
|
+
* logged so the rejection is observable.
|
|
330
|
+
*/
|
|
331
|
+
async function withEffortFallback(ctx, effort, attempt, fallback) {
|
|
332
|
+
if (!effort || effort === "none") return attempt();
|
|
333
|
+
try {
|
|
334
|
+
return await attempt();
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const message = String(error?.message ?? error);
|
|
337
|
+
// matches both "reasoning effort" (natural language) and the bare
|
|
338
|
+
// "UNSUPPORTED_REASONING_EFFORT" error code (underscore).
|
|
339
|
+
if (!/reasoning[\s_]*effort/i.test(message)) throw error;
|
|
340
|
+
ctx.logger?.warn?.(`dsh-mneme dream: reasoningEffort "${effort}" rejected (${message}); retrying without it`);
|
|
341
|
+
return fallback();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the LLM route (Issue #25): an explicit plugin config
|
|
347
|
+
* (dreamProvider/dreamModel) is the user's declared override and wins; the
|
|
348
|
+
* agent default model (deployment) is only a fallback when no config route is
|
|
349
|
+
* set. In a standard DSH install agentDefaultModel always resolves, so without
|
|
350
|
+
* this ordering the config route would be dead code and dreamProvider/dreamModel
|
|
351
|
+
* could never take effect (v0.7.11 regressed this; README §config documents
|
|
352
|
+
* config-first). Falls through to undefined when no route exists — runDream
|
|
353
|
+
* then fails safe. A config→default switch is logged so it is observable.
|
|
318
354
|
*/
|
|
319
355
|
function resolveRoute(ctx, config, logger) {
|
|
356
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
320
357
|
try {
|
|
321
358
|
const sel = ctx.agentDefaultModel?.currentSelection?.();
|
|
322
|
-
if (sel?.provider && sel?.model)
|
|
323
|
-
|
|
359
|
+
if (sel?.provider && sel?.model) {
|
|
360
|
+
logger?.info?.("dsh-mneme dream: no dreamProvider/dreamModel config, falling back to agent default");
|
|
361
|
+
return { provider: sel.provider, model: sel.model };
|
|
362
|
+
}
|
|
363
|
+
logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, no config route either");
|
|
324
364
|
} catch (error) {
|
|
325
|
-
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed
|
|
365
|
+
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed: ${String(error)}`);
|
|
326
366
|
}
|
|
327
|
-
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
328
367
|
return undefined;
|
|
329
368
|
}
|
|
330
369
|
|
|
@@ -585,28 +624,37 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
585
624
|
? CONSOLIDATION_PROMPT + `\n\n当前为「冲突冻结」模式:检测到内容矛盾的条目时,仍请输出 conflict,并以 winner/loser 作为候选、reason 说明理由;冲突不会被自动裁决,而会冻结待人工确认。`
|
|
586
625
|
: CONSOLIDATION_PROMPT;
|
|
587
626
|
let decisionText;
|
|
627
|
+
// 加固(v0.8.1):配置的 reasoningEffort 被 provider 拒收时回退重试一次
|
|
628
|
+
// (不带该字段),避免 thinking 模型配置 low/medium 直接整单失败。解析放
|
|
629
|
+
// 在 auditError 检查器里、闭包交回主流程,避免二次解析;解析失败同时如实
|
|
630
|
+
// 记 audit error 并在日志带原始输出前 300 字节,便于定位"推理吞预算返回空体"。
|
|
631
|
+
const effort = config.dreamReasoningEffort && config.dreamReasoningEffort !== "none" ? config.dreamReasoningEffort : null;
|
|
632
|
+
let decisions = null;
|
|
633
|
+
const runConsolidation = (withEffort) => runAuditedLlm(ctx, service, config, {
|
|
634
|
+
triggerSource: "autoDream",
|
|
635
|
+
operationType: "dream_consolidate",
|
|
636
|
+
modelId: `${route.provider}:${route.model}`,
|
|
637
|
+
relatedMemoryIds: [...snapshot.keys()],
|
|
638
|
+
auditError: (text) => {
|
|
639
|
+
decisions = extractJsonArray(text);
|
|
640
|
+
return Array.isArray(decisions) ? null : "no json array in llm output";
|
|
641
|
+
}
|
|
642
|
+
}, (reportUsage) => streamText(ctx, {
|
|
643
|
+
provider: route.provider,
|
|
644
|
+
model: route.model,
|
|
645
|
+
purpose: "compaction",
|
|
646
|
+
maxTokens: config.dreamMaxTokens ?? 4096,
|
|
647
|
+
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
648
|
+
messages: [
|
|
649
|
+
{ role: "system", content: [{ type: "text", text: consolidationPrompt }] },
|
|
650
|
+
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
651
|
+
]
|
|
652
|
+
}, reportUsage));
|
|
588
653
|
try {
|
|
589
654
|
// Bug8: the consolidation call is audited (tokens/time/status). A throw
|
|
590
655
|
// re-propagates to the catch below; an aborted stream returns undefined
|
|
591
656
|
// and is treated as a failed run after the check below.
|
|
592
|
-
decisionText = await
|
|
593
|
-
triggerSource: "autoDream",
|
|
594
|
-
operationType: "dream_consolidate",
|
|
595
|
-
modelId: `${route.provider}:${route.model}`,
|
|
596
|
-
relatedMemoryIds: [...snapshot.keys()]
|
|
597
|
-
}, (reportUsage) => streamText(ctx, {
|
|
598
|
-
provider: route.provider,
|
|
599
|
-
model: route.model,
|
|
600
|
-
purpose: "compaction",
|
|
601
|
-
maxTokens: config.dreamMaxTokens ?? 4096,
|
|
602
|
-
...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
|
|
603
|
-
? { reasoningEffort: config.dreamReasoningEffort }
|
|
604
|
-
: {}),
|
|
605
|
-
messages: [
|
|
606
|
-
{ role: "system", content: [{ type: "text", text: consolidationPrompt }] },
|
|
607
|
-
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
608
|
-
]
|
|
609
|
-
}, reportUsage));
|
|
657
|
+
decisionText = await withEffortFallback(ctx, effort, () => runConsolidation(true), () => runConsolidation(false));
|
|
610
658
|
} catch (error) {
|
|
611
659
|
logger?.warn?.(`dsh-mneme dream: consolidation llm call failed: ${String(error)}`);
|
|
612
660
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
@@ -615,10 +663,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
615
663
|
logger?.warn?.("dsh-mneme dream: consolidation llm stream aborted or errored");
|
|
616
664
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
617
665
|
}
|
|
618
|
-
|
|
619
|
-
const decisions = extractJsonArray(decisionText);
|
|
620
666
|
if (!Array.isArray(decisions)) {
|
|
621
|
-
|
|
667
|
+
const head = (decisionText ?? "").slice(0, 300).replace(/\s+/g, " ").trim();
|
|
668
|
+
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0}; head: ${head})`);
|
|
622
669
|
return finish({ ok: false, error: "no json array in llm output", summary: false });
|
|
623
670
|
}
|
|
624
671
|
const { ok, errors } = validateDecisions(decisions, snapshot, {
|
|
@@ -735,26 +782,25 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
735
782
|
// Summary generation (second LLM call). A throwing stream is reported as
|
|
736
783
|
// a failed run; summary:false marks a run that produced no summary.
|
|
737
784
|
let summaryText;
|
|
785
|
+
const runSummary = (withEffort) => runAuditedLlm(ctx, service, config, {
|
|
786
|
+
triggerSource: "autoDream",
|
|
787
|
+
operationType: "dream_summarize",
|
|
788
|
+
modelId: `${route.provider}:${route.model}`,
|
|
789
|
+
relatedMemoryIds: []
|
|
790
|
+
}, (reportUsage) => streamText(ctx, {
|
|
791
|
+
provider: route.provider,
|
|
792
|
+
model: route.model,
|
|
793
|
+
purpose: "compaction",
|
|
794
|
+
maxTokens: config.dreamMaxTokens ?? 2048,
|
|
795
|
+
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
796
|
+
messages: [
|
|
797
|
+
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
798
|
+
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
799
|
+
]
|
|
800
|
+
}, reportUsage));
|
|
738
801
|
try {
|
|
739
802
|
// Bug8: the summary call is audited too (operation dream_summarize).
|
|
740
|
-
summaryText = await
|
|
741
|
-
triggerSource: "autoDream",
|
|
742
|
-
operationType: "dream_summarize",
|
|
743
|
-
modelId: `${route.provider}:${route.model}`,
|
|
744
|
-
relatedMemoryIds: []
|
|
745
|
-
}, (reportUsage) => streamText(ctx, {
|
|
746
|
-
provider: route.provider,
|
|
747
|
-
model: route.model,
|
|
748
|
-
purpose: "compaction",
|
|
749
|
-
maxTokens: config.dreamMaxTokens ?? 2048,
|
|
750
|
-
...(config.dreamReasoningEffort && config.dreamReasoningEffort !== "none"
|
|
751
|
-
? { reasoningEffort: config.dreamReasoningEffort }
|
|
752
|
-
: {}),
|
|
753
|
-
messages: [
|
|
754
|
-
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
755
|
-
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
756
|
-
]
|
|
757
|
-
}, reportUsage));
|
|
803
|
+
summaryText = await withEffortFallback(ctx, effort, () => runSummary(true), () => runSummary(false));
|
|
758
804
|
} catch (error) {
|
|
759
805
|
logger?.warn?.(`dsh-mneme dream: summary llm call failed: ${String(error)}`);
|
|
760
806
|
return finish({ ok: false, error: "llm failed", applied, decisions: auditDecisions, outcome, frozen: frozenCount, summary: false });
|
package/test/dream.test.js
CHANGED
|
@@ -870,7 +870,7 @@ test("Bug8: runDream records llm_audit_logs rows for consolidation and summary",
|
|
|
870
870
|
assert.deepEqual(summarize.related_memory_ids, [], "summary audit has no related ids");
|
|
871
871
|
for (const row of rows) {
|
|
872
872
|
assert.equal(row.status, "success");
|
|
873
|
-
assert.equal(row.model_id, "
|
|
873
|
+
assert.equal(row.model_id, "deepseek:deepseek-chat", "config-first route (Issue #25): dreamProvider/dreamModel wins");
|
|
874
874
|
assert.ok(Number.isInteger(row.duration_ms) && row.duration_ms >= 0, "duration recorded");
|
|
875
875
|
assert.equal(row.input_tokens, 0);
|
|
876
876
|
assert.equal(row.output_tokens, 0);
|
package/test/llm-audit.test.js
CHANGED
|
@@ -50,10 +50,11 @@ test("autoDream writes llm_audit_logs rows for consolidation and summary", async
|
|
|
50
50
|
assert.equal(summarize.trigger_source, "autoDream");
|
|
51
51
|
assert.equal(consolidate.status, "success");
|
|
52
52
|
assert.equal(summarize.status, "success");
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
assert.equal(
|
|
53
|
+
// config-first (Issue #25): dreamSetup sets dreamProvider/dreamModel, so it
|
|
54
|
+
// wins over mockCtx's agentDefaultModel (mock:stress-model) — assert the
|
|
55
|
+
// actually-used config route.
|
|
56
|
+
assert.equal(consolidate.model_id, "deepseek:deepseek-chat");
|
|
57
|
+
assert.equal(summarize.model_id, "deepseek:deepseek-chat");
|
|
57
58
|
assert.ok(Array.isArray(consolidate.related_memory_ids) && consolidate.related_memory_ids.length === 2,
|
|
58
59
|
"consolidation audit records the snapshot ids");
|
|
59
60
|
assert.ok(consolidate.total_tokens >= 0 && summarize.total_tokens >= 0);
|
|
@@ -110,6 +111,35 @@ test("autoDream throwing LLM is audited as status=error", async () => {
|
|
|
110
111
|
store.close();
|
|
111
112
|
});
|
|
112
113
|
|
|
114
|
+
test("autoDream parse failure is audited as status=error, not fake success", async () => {
|
|
115
|
+
const { store, service, config } = dreamSetup();
|
|
116
|
+
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
117
|
+
// Stream completes fine but returns no JSON array — the run fails, and the
|
|
118
|
+
// audit must NOT claim success (the old bug: llm_audit said dream_consolidate
|
|
119
|
+
// success while the run recorded failed).
|
|
120
|
+
const warnings = [];
|
|
121
|
+
const ctx = {
|
|
122
|
+
logger: { warn: (m) => warnings.push(m) },
|
|
123
|
+
llm: {
|
|
124
|
+
async *stream() {
|
|
125
|
+
yield { type: "text-delta", index: 0, text: "抱歉,我无法解析成 JSON。" };
|
|
126
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
131
|
+
const result = await dream.runDream(ctx, service, config);
|
|
132
|
+
assert.equal(result.ok, false);
|
|
133
|
+
assert.equal(result.error, "no json array in llm output");
|
|
134
|
+
const rows = service.listLlmAudits();
|
|
135
|
+
assert.equal(rows.length, 1, "failed consolidation still audited");
|
|
136
|
+
assert.equal(rows[0].operation_type, "dream_consolidate");
|
|
137
|
+
assert.equal(rows[0].status, "error", "stream succeeded but output unusable → audit error");
|
|
138
|
+
assert.match(rows[0].error_message, /no json array in llm output/);
|
|
139
|
+
assert.ok(warnings.some((m) => m.includes("head: 抱歉,我无法解析成")), "raw output head logged for diagnosis");
|
|
140
|
+
store.close();
|
|
141
|
+
});
|
|
142
|
+
|
|
113
143
|
test("autoDream audit is skipped when llmAudit.enabled === false", async () => {
|
|
114
144
|
const { store, service, config } = dreamSetup({ llmAudit: { enabled: false } });
|
|
115
145
|
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
@@ -107,6 +107,70 @@ test("issue#9: dream forwards dreamReasoningEffort on both LLM calls", async ()
|
|
|
107
107
|
store.close();
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
test("issue#25: dreamProvider/dreamModel config wins over the agentDefaultModel route", async () => {
|
|
111
|
+
const store = createStore(":memory:");
|
|
112
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
113
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
114
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
115
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
116
|
+
const captured = [];
|
|
117
|
+
const ctx = dreamCtx({
|
|
118
|
+
captured,
|
|
119
|
+
onConsolidation: () => JSON.stringify([
|
|
120
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
121
|
+
])
|
|
122
|
+
});
|
|
123
|
+
// dreamCtx's agentDefaultModel resolves (mock:mock-model), but the explicit
|
|
124
|
+
// config route must win — otherwise dreamProvider/dreamModel is dead code in
|
|
125
|
+
// a standard DSH install and the dream can never be moved off a thinking model.
|
|
126
|
+
const result = await dream.runDream(ctx, service, { dreamProvider: "volcano", dreamModel: "deepseek-v3" });
|
|
127
|
+
assert.equal(result.ok, true);
|
|
128
|
+
assert.ok(captured.length >= 2, "consolidation + summary both hit the LLM");
|
|
129
|
+
for (const options of captured) {
|
|
130
|
+
assert.equal(options.provider, "volcano");
|
|
131
|
+
assert.equal(options.model, "deepseek-v3", "config route wins over agentDefaultModel (mock:mock-model)");
|
|
132
|
+
}
|
|
133
|
+
store.close();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("issue#9: rejected reasoningEffort retries once without it and still consolidates", async () => {
|
|
137
|
+
const store = createStore(":memory:");
|
|
138
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
139
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
140
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
141
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
142
|
+
const calls = [];
|
|
143
|
+
const ctx = {
|
|
144
|
+
logger: { warn: () => {} },
|
|
145
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
146
|
+
llm: {
|
|
147
|
+
async *stream(options) {
|
|
148
|
+
calls.push(options);
|
|
149
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
150
|
+
if (userText.startsWith("id=")) {
|
|
151
|
+
// First attempt forwards reasoningEffort: the provider rejects it.
|
|
152
|
+
if (options.reasoningEffort) {
|
|
153
|
+
throw new Error("UNSUPPORTED_REASONING_EFFORT: DeepSeek does not support reasoning effort \"low\"");
|
|
154
|
+
}
|
|
155
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
156
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
157
|
+
]) };
|
|
158
|
+
} else {
|
|
159
|
+
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
160
|
+
}
|
|
161
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
166
|
+
assert.equal(result.ok, true, "run survives the effort rejection via the fallback retry");
|
|
167
|
+
assert.ok(result.applied > 0, "consolidation still lands changes");
|
|
168
|
+
assert.equal(calls.length, 3, "consolidation tried (rejected) + retried without effort + summary");
|
|
169
|
+
assert.equal(calls[0].reasoningEffort, "low", "first consolidation attempt forwards the effort");
|
|
170
|
+
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
171
|
+
store.close();
|
|
172
|
+
});
|
|
173
|
+
|
|
110
174
|
// ---------------------------------------------------------------- sleep passthrough
|
|
111
175
|
|
|
112
176
|
function sleepSetup() {
|