@modusensus/dsh-mneme 0.8.14 → 0.8.15
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/LICENSE +21 -21
- package/README.md +655 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +689 -689
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +263 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -0
- package/docs/ENTITIES.md +245 -0
- package/docs/LOCAL_MODEL.md +222 -0
- package/docs/SEMANTIC.md +256 -0
- package/docs/SLEEP.md +164 -0
- package/docs/STORAGE.md +89 -0
- package/lib/api-standalone.js +673 -673
- package/lib/api.js +1437 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +5444 -5295
- package/lib/commands.js +95 -95
- package/lib/config.js +880 -843
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +220 -220
- package/lib/document-index.js +94 -94
- package/lib/document.js +303 -303
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +674 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1501 -1501
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +252 -252
- package/lib/graph/anchoring.js +179 -179
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +535 -503
- package/lib/inject.js +464 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +142 -142
- package/lib/mirror.js +330 -330
- package/lib/organize.js +450 -450
- package/lib/peak-hours.js +117 -117
- package/lib/placement.js +13 -0
- package/lib/pre-inject-gate.js +340 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +175 -175
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -204
- package/lib/scope.js +146 -146
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +229 -229
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +215 -215
- package/lib/service.js +2663 -2605
- package/lib/settings.js +517 -511
- package/lib/store.js +3007 -3007
- package/lib/summarize.js +891 -856
- package/lib/tools.js +743 -737
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/lib/write-admission.js +402 -402
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +118 -118
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +36 -36
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +44 -44
- package/src/api-standalone.js +673 -673
- package/src/api.js +1437 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +880 -843
- package/src/content-hash.js +54 -54
- package/src/continuity.js +220 -220
- package/src/document-index.js +94 -94
- package/src/document.js +303 -303
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +674 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1501 -1501
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +252 -252
- package/src/graph/anchoring.js +179 -179
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +46 -46
- package/src/index.js +535 -503
- package/src/inject.js +464 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +142 -142
- package/src/mirror.js +330 -330
- package/src/organize.js +450 -450
- package/src/peak-hours.js +117 -117
- package/src/placement.js +13 -0
- package/src/pre-inject-gate.js +340 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +175 -175
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -204
- package/src/scope.js +146 -146
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +229 -229
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +215 -215
- package/src/service.js +2663 -2605
- package/src/settings.js +517 -511
- package/src/store.js +3007 -3007
- package/src/summarize.js +891 -856
- package/src/tools.js +743 -737
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/src/write-admission.js +402 -402
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1702 -1690
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -220
- package/test/changelog-prep.test.js +60 -60
- package/test/client.test.js +1230 -1169
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +333 -333
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +671 -671
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1385 -1383
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +627 -547
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +175 -175
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +308 -308
- package/test/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +106 -106
- package/test/inject-pools.test.js +194 -194
- package/test/inject-recall.test.js +105 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +420 -420
- package/test/lang.test.js +87 -87
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +598 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +391 -391
- package/test/mcp-stdio.test.js +385 -385
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize.test.js +418 -418
- package/test/peer-blockers.test.js +193 -193
- package/test/placement.test.js +36 -0
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +434 -0
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +189 -189
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -279
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +207 -207
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +196 -196
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +405 -405
- package/test/settings.test.js +261 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -126
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +527 -527
- package/test/standalone-api.test.js +751 -751
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +396 -396
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -0
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +1465 -1465
- package/test/supersede-state-tracking.test.js +394 -0
- package/test/test-count-sync.test.js +60 -60
- package/test/tools-dto-schema-matrix.test.js +325 -325
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +428 -374
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/write-admission.test.js +668 -668
|
@@ -1,752 +1,752 @@
|
|
|
1
|
-
// Regression for issue #9, extended by issue #135 (建议 3/4/5):
|
|
2
|
-
// - B: dreamMaxTokens cap widened (min 256, max 131072) so large memory
|
|
3
|
-
// libraries no longer starve the consolidation output. #135 raises the
|
|
4
|
-
// DEFAULT to 131072 too — 32768 was exactly what thinking-model reasoning
|
|
5
|
-
// burnt, leaving an empty body.
|
|
6
|
-
// - A: dreamReasoningEffort / sleepReasoningEffort pass-through. Explicit
|
|
7
|
-
// 'none' omits the reasoningEffort field entirely (provider default
|
|
8
|
-
// applies); low/medium/high are forwarded verbatim on every dream /
|
|
9
|
-
// sleep LLM call. #135: UNSET now resolves to the LOWEST effort the model
|
|
10
|
-
// declares (the old default 'none' let the harness substitute the model's
|
|
11
|
-
// defaultEffort, burning the budget on reasoning). Asserted by capturing
|
|
12
|
-
// the options each llm.stream() sees.
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { Config } from "../src/config.js";
|
|
16
|
-
import { createDreamScheduler, parseReceipt } from "../src/dream.js";
|
|
17
|
-
import { runSleep } from "../src/dream/sleep.js";
|
|
18
|
-
import { createStore } from "../src/store.js";
|
|
19
|
-
import { createService } from "../src/service.js";
|
|
20
|
-
import { createVectorIndex } from "../src/vector-index.js";
|
|
21
|
-
import { createEntityStreamAdapter } from "../src/index.js";
|
|
22
|
-
|
|
23
|
-
const embedder = {
|
|
24
|
-
embedSingle: async () => [1, 0, 0],
|
|
25
|
-
embed: async () => [1, 0, 0],
|
|
26
|
-
schedule: () => {},
|
|
27
|
-
modelHash: "mock#1",
|
|
28
|
-
dimension: 3
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------- config schema
|
|
32
|
-
|
|
33
|
-
test("issue#135: dreamMaxTokens defaults to 131072 and accepts values up to 131072", () => {
|
|
34
|
-
assert.equal(Config({}).dreamMaxTokens, 131072, "old default 32768 was exactly what thinking-model reasoning burnt (Issue #135 建议 3)");
|
|
35
|
-
assert.equal(Config({ dreamMaxTokens: 131072 }).dreamMaxTokens, 131072, "upper bound accepted");
|
|
36
|
-
assert.equal(Config({ dreamMaxTokens: 65536 }).dreamMaxTokens, 65536, "intermediate value accepted");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("issue#9: dreamMaxTokens clamps to [256, 131072], out-of-range values are rejected", () => {
|
|
40
|
-
assert.equal(Config({ dreamMaxTokens: 256 }).dreamMaxTokens, 256, "lower bound accepted");
|
|
41
|
-
assert.equal(Config({ dreamMaxTokens: 100000 }).dreamMaxTokens, 100000, "raised default tier accepted");
|
|
42
|
-
assert.throws(() => Config({ dreamMaxTokens: 255 }), "below min rejected");
|
|
43
|
-
assert.throws(() => Config({ dreamMaxTokens: 131073 }), "above max rejected");
|
|
44
|
-
assert.throws(() => Config({ dreamMaxTokens: 0 }), "zero rejected");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("issue#135: reasoningEffort unset = undefined (auto-lowest at resolve time); explicit values preserved", () => {
|
|
48
|
-
const cfg = Config({});
|
|
49
|
-
assert.equal(cfg.dreamReasoningEffort, undefined, "unset must be distinguishable from explicit 'none'");
|
|
50
|
-
assert.equal(cfg.sleepReasoningEffort, undefined);
|
|
51
|
-
// issue #315: the distill knob defaults to 'none' (entity-extraction shape,
|
|
52
|
-
// no auto-lowest resolution — distill keeps its current behavior until the
|
|
53
|
-
// user opts in).
|
|
54
|
-
assert.equal(cfg.summarizeReasoningEffort, "none", "distill effort defaults to 'none'");
|
|
55
|
-
assert.equal(Config({ summarizeReasoningEffort: "off" }).summarizeReasoningEffort, "off");
|
|
56
|
-
assert.equal(Config({ dreamReasoningEffort: "none" }).dreamReasoningEffort, "none", "explicit 'none' kept");
|
|
57
|
-
assert.equal(Config({ dreamReasoningEffort: "off" }).dreamReasoningEffort, "off", "'off' now a valid explicit choice");
|
|
58
|
-
assert.equal(Config({ sleepReasoningEffort: "medium" }).sleepReasoningEffort, "medium");
|
|
59
|
-
assert.throws(() => Config({ dreamReasoningEffort: "bogus" }), "invalid effort rejected");
|
|
60
|
-
assert.throws(() => Config({ sleepReasoningEffort: "ultra" }), "invalid effort rejected");
|
|
61
|
-
assert.throws(() => Config({ summarizeReasoningEffort: "max" }), "invalid distill effort rejected");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------- dream passthrough
|
|
65
|
-
|
|
66
|
-
/** dream ctx that records every llm.stream() call's options for inspection. */
|
|
67
|
-
function dreamCtx({ onConsolidation, summaryText = "记忆库总览:用户偏好中文。", captured = [] } = {}) {
|
|
68
|
-
return {
|
|
69
|
-
logger: { warn: () => {} },
|
|
70
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
71
|
-
llm: {
|
|
72
|
-
async *stream(options) {
|
|
73
|
-
captured.push(options);
|
|
74
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
75
|
-
if (userText.startsWith("id=")) {
|
|
76
|
-
yield { type: "text-delta", index: 0, text: onConsolidation ? onConsolidation(userText) : "[]" };
|
|
77
|
-
} else {
|
|
78
|
-
yield { type: "text-delta", index: 0, text: summaryText };
|
|
79
|
-
}
|
|
80
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
test("issue#135: dream omits reasoningEffort when unset without a capability API and still consolidates (applied>0)", async () => {
|
|
87
|
-
const store = createStore(":memory:");
|
|
88
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
89
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
90
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
91
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
92
|
-
const captured = [];
|
|
93
|
-
const ctx = dreamCtx({
|
|
94
|
-
captured,
|
|
95
|
-
onConsolidation: () => JSON.stringify([
|
|
96
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
97
|
-
])
|
|
98
|
-
});
|
|
99
|
-
const result = await dream.runDream(ctx, service, {});
|
|
100
|
-
assert.equal(result.ok, true);
|
|
101
|
-
assert.ok(result.applied > 0, "end-to-end dream run still lands changes");
|
|
102
|
-
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
103
|
-
for (const options of captured) {
|
|
104
|
-
assert.equal("reasoningEffort" in options, false, `unset without capability API must not forward reasoningEffort (${options.purpose})`);
|
|
105
|
-
}
|
|
106
|
-
store.close();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("issue#135: unset effort resolves to the LOWEST supported effort, never the poison default", async () => {
|
|
110
|
-
const store = createStore(":memory:");
|
|
111
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
112
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
113
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
114
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
115
|
-
const captured = [];
|
|
116
|
-
const ctx = dreamCtx({
|
|
117
|
-
captured,
|
|
118
|
-
onConsolidation: () => JSON.stringify([
|
|
119
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
120
|
-
])
|
|
121
|
-
});
|
|
122
|
-
// v4-flash 系能力报告:声明 off/low/high/max、defaultEffort=high。旧默认
|
|
123
|
-
// (省略字段)让 harness 顶上 high → 推理烧光预算返回空体;现在未配置应
|
|
124
|
-
// 自动取最低档 off,而不是服务商默认。
|
|
125
|
-
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
126
|
-
provider,
|
|
127
|
-
model,
|
|
128
|
-
reasoning: { efforts: [{ id: "high" }, { id: "off" }, { id: "low" }, { id: "max" }], defaultEffort: "high" }
|
|
129
|
-
});
|
|
130
|
-
const result = await dream.runDream(ctx, service, {});
|
|
131
|
-
assert.equal(result.ok, true);
|
|
132
|
-
assert.ok(result.applied > 0, "consolidation lands changes");
|
|
133
|
-
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
134
|
-
for (const options of captured) {
|
|
135
|
-
assert.equal(options.reasoningEffort, "off", `unset resolves to the lowest declared effort on ${options.purpose}`);
|
|
136
|
-
}
|
|
137
|
-
store.close();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("issue#135: explicit 'none' still omits the field even when the capability API is available", async () => {
|
|
141
|
-
const store = createStore(":memory:");
|
|
142
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
143
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
144
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
145
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
146
|
-
const captured = [];
|
|
147
|
-
const ctx = dreamCtx({
|
|
148
|
-
captured,
|
|
149
|
-
onConsolidation: () => JSON.stringify([
|
|
150
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
151
|
-
])
|
|
152
|
-
});
|
|
153
|
-
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
154
|
-
provider,
|
|
155
|
-
model,
|
|
156
|
-
reasoning: { efforts: [{ id: "off" }, { id: "low" }], defaultEffort: "high" }
|
|
157
|
-
});
|
|
158
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "none" });
|
|
159
|
-
assert.equal(result.ok, true);
|
|
160
|
-
for (const options of captured) {
|
|
161
|
-
assert.equal("reasoningEffort" in options, false, `explicit 'none' is a user decision to omit (${options.purpose})`);
|
|
162
|
-
}
|
|
163
|
-
store.close();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("issue#9: dream forwards dreamReasoningEffort on both LLM calls", async () => {
|
|
167
|
-
const store = createStore(":memory:");
|
|
168
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
169
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
170
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
171
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
172
|
-
const captured = [];
|
|
173
|
-
const ctx = dreamCtx({
|
|
174
|
-
captured,
|
|
175
|
-
onConsolidation: () => JSON.stringify([
|
|
176
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
177
|
-
])
|
|
178
|
-
});
|
|
179
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
180
|
-
assert.equal(result.ok, true);
|
|
181
|
-
assert.equal(captured.length, 2);
|
|
182
|
-
for (const options of captured) {
|
|
183
|
-
assert.equal(options.reasoningEffort, "high", `reasoningEffort forwarded on ${options.purpose}`);
|
|
184
|
-
}
|
|
185
|
-
store.close();
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("issue#9: dreamMaxTokens is forwarded as maxTokens on consolidation and summary calls", async () => {
|
|
189
|
-
const store = createStore(":memory:");
|
|
190
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
191
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
192
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
193
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
194
|
-
const captured = [];
|
|
195
|
-
const ctx = dreamCtx({
|
|
196
|
-
captured,
|
|
197
|
-
onConsolidation: () => JSON.stringify([
|
|
198
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
199
|
-
])
|
|
200
|
-
});
|
|
201
|
-
const result = await dream.runDream(ctx, service, { dreamMaxTokens: 100000 });
|
|
202
|
-
assert.equal(result.ok, true);
|
|
203
|
-
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
204
|
-
for (const options of captured) {
|
|
205
|
-
assert.equal(options.maxTokens, 100000, `raised budget forwarded on ${options.purpose}`);
|
|
206
|
-
}
|
|
207
|
-
store.close();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("issue#9: thinking-model empty body (no text emitted, budget burnt on reasoning) fails as no json array", async () => {
|
|
211
|
-
const store = createStore(":memory:");
|
|
212
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
213
|
-
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文" });
|
|
214
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
215
|
-
const ctx = dreamCtx({ onConsolidation: () => "" }); // 思考型模型把预算烧光 → 无正文
|
|
216
|
-
const result = await dream.runDream(ctx, service, { dreamMaxTokens: 32768 });
|
|
217
|
-
assert.equal(result.ok, false, "empty body is a hard failure, never faked ok");
|
|
218
|
-
assert.match(result.error, /no json array/);
|
|
219
|
-
const run = store.listDreamRuns()[0];
|
|
220
|
-
assert.equal(run.status, "failed", "audit row records failed");
|
|
221
|
-
assert.match(run.error, /no json array/, "audit error_message carries the empty-body cause");
|
|
222
|
-
assert.equal(parseReceipt(run.receipt).status, "failed", "receipt records failed");
|
|
223
|
-
store.close();
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("legal empty decision list [] is a no-op success, not a failure", async () => {
|
|
227
|
-
// 真实模型在记忆无冗余时合法输出 [](CONSOLIDATION_PROMPT 允许"无问题无需输出"),
|
|
228
|
-
// 此前被 validateDecisions 判 failed → 审计表反复失败。现在应 ok:true、applied 0、
|
|
229
|
-
// 审计记 ok,且 summary 照常产出。
|
|
230
|
-
const store = createStore(":memory:");
|
|
231
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
232
|
-
service.saveWithDedupe({ type: "project", title: "插件", content: "内容", importance: 3 });
|
|
233
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
234
|
-
const ctx = dreamCtx({ onConsolidation: () => "[]" }); // 模型:无需合并
|
|
235
|
-
const result = await dream.runDream(ctx, service, { dreamMaxTokens: 32768 });
|
|
236
|
-
assert.equal(result.ok, true, "empty [] is a valid no-op, never failed");
|
|
237
|
-
assert.equal(result.applied, 0, "no decisions to apply");
|
|
238
|
-
assert.equal(result.summary, true, "summary still produced");
|
|
239
|
-
const run = store.listDreamRuns()[0];
|
|
240
|
-
assert.equal(run.status, "ok", "audit row records ok, not failed");
|
|
241
|
-
assert.equal(parseReceipt(run.receipt).status, "ok", "receipt records ok");
|
|
242
|
-
store.close();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("issue#25: dreamProvider/dreamModel config wins over the agentDefaultModel route", async () => {
|
|
246
|
-
const store = createStore(":memory:");
|
|
247
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
248
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
249
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
250
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
251
|
-
const captured = [];
|
|
252
|
-
const ctx = dreamCtx({
|
|
253
|
-
captured,
|
|
254
|
-
onConsolidation: () => JSON.stringify([
|
|
255
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
256
|
-
])
|
|
257
|
-
});
|
|
258
|
-
// dreamCtx's agentDefaultModel resolves (mock:mock-model), but the explicit
|
|
259
|
-
// config route must win — otherwise dreamProvider/dreamModel is dead code in
|
|
260
|
-
// a standard DSH install and the dream can never be moved off a thinking model.
|
|
261
|
-
const result = await dream.runDream(ctx, service, { dreamProvider: "volcano", dreamModel: "deepseek-v3" });
|
|
262
|
-
assert.equal(result.ok, true);
|
|
263
|
-
assert.ok(captured.length >= 2, "consolidation + summary both hit the LLM");
|
|
264
|
-
for (const options of captured) {
|
|
265
|
-
assert.equal(options.provider, "volcano");
|
|
266
|
-
assert.equal(options.model, "deepseek-v3", "config route wins over agentDefaultModel (mock:mock-model)");
|
|
267
|
-
}
|
|
268
|
-
store.close();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("issue#9: rejected reasoningEffort retries once without it and still consolidates", async () => {
|
|
272
|
-
const store = createStore(":memory:");
|
|
273
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
274
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
275
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
276
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
277
|
-
const calls = [];
|
|
278
|
-
const ctx = {
|
|
279
|
-
logger: { warn: () => {} },
|
|
280
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
281
|
-
llm: {
|
|
282
|
-
async *stream(options) {
|
|
283
|
-
calls.push(options);
|
|
284
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
285
|
-
if (userText.startsWith("id=")) {
|
|
286
|
-
// First attempt forwards reasoningEffort: the provider rejects it.
|
|
287
|
-
if (options.reasoningEffort) {
|
|
288
|
-
throw new Error("UNSUPPORTED_REASONING_EFFORT: DeepSeek does not support reasoning effort \"low\"");
|
|
289
|
-
}
|
|
290
|
-
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
291
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
292
|
-
]) };
|
|
293
|
-
} else {
|
|
294
|
-
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
295
|
-
}
|
|
296
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
301
|
-
assert.equal(result.ok, true, "run survives the effort rejection via the fallback retry");
|
|
302
|
-
assert.ok(result.applied > 0, "consolidation still lands changes");
|
|
303
|
-
assert.equal(calls.length, 3, "consolidation tried (rejected) + retried without effort + summary");
|
|
304
|
-
assert.equal(calls[0].reasoningEffort, "low", "first consolidation attempt forwards the effort");
|
|
305
|
-
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
306
|
-
store.close();
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// ---------------------------------------------------------------- sleep passthrough
|
|
310
|
-
|
|
311
|
-
function sleepSetup() {
|
|
312
|
-
const store = createStore(":memory:");
|
|
313
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
314
|
-
const vectorIndex = createVectorIndex({ store });
|
|
315
|
-
service.setEmbedder(embedder);
|
|
316
|
-
service.setVectorIndex(vectorIndex);
|
|
317
|
-
return { store, service, vectorIndex };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function baseConfig(overrides = {}) {
|
|
321
|
-
return {
|
|
322
|
-
sleepModeEnabled: true,
|
|
323
|
-
sleepIdleMinutes: 5,
|
|
324
|
-
sleepMinIntervalHours: 8,
|
|
325
|
-
sleepConflictStrictness: "normal",
|
|
326
|
-
sleepArchiveDays: 30,
|
|
327
|
-
sleepCompressDays: 90,
|
|
328
|
-
sleepPatternMinMemories: 10,
|
|
329
|
-
sleepMaxPatternPerRun: 3,
|
|
330
|
-
...overrides
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** sleep ctx that records every llm.stream() call's options. */
|
|
335
|
-
function sleepCtx(onConsolidation, selection = { provider: "mock", model: "sleep-model" }, captured = []) {
|
|
336
|
-
return {
|
|
337
|
-
logger: { warn: () => {}, info: () => {} },
|
|
338
|
-
agentDefaultModel: { currentSelection: () => selection },
|
|
339
|
-
llm: {
|
|
340
|
-
async *stream(options) {
|
|
341
|
-
captured.push(options);
|
|
342
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
343
|
-
yield { type: "text-delta", index: 0, text: onConsolidation ? onConsolidation(userText) : "[]" };
|
|
344
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
test("issue#9: sleep forwards sleepReasoningEffort on its LLM passes", async () => {
|
|
351
|
-
const { store, service, vectorIndex } = sleepSetup();
|
|
352
|
-
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
353
|
-
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
354
|
-
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
355
|
-
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
356
|
-
const captured = [];
|
|
357
|
-
const ctx = sleepCtx(
|
|
358
|
-
(userText) => userText.startsWith("候选冲突")
|
|
359
|
-
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
360
|
-
: "[]",
|
|
361
|
-
{ provider: "mock", model: "sleep-model" },
|
|
362
|
-
captured
|
|
363
|
-
);
|
|
364
|
-
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "medium" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
365
|
-
assert.equal(result.status, "ok");
|
|
366
|
-
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
367
|
-
for (const options of captured) {
|
|
368
|
-
assert.equal(options.reasoningEffort, "medium", `reasoningEffort forwarded on ${options.purpose}`);
|
|
369
|
-
}
|
|
370
|
-
store.close();
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test("issue#135: sleep unset effort also resolves to the lowest supported", async () => {
|
|
374
|
-
const { store, service, vectorIndex } = sleepSetup();
|
|
375
|
-
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
376
|
-
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
377
|
-
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
378
|
-
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
379
|
-
const captured = [];
|
|
380
|
-
const ctx = sleepCtx(
|
|
381
|
-
(userText) => userText.startsWith("候选冲突")
|
|
382
|
-
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
383
|
-
: "[]",
|
|
384
|
-
{ provider: "mock", model: "sleep-model" },
|
|
385
|
-
captured
|
|
386
|
-
);
|
|
387
|
-
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
388
|
-
provider,
|
|
389
|
-
model,
|
|
390
|
-
reasoning: { efforts: [{ id: "low" }, { id: "high" }], defaultEffort: "high" }
|
|
391
|
-
});
|
|
392
|
-
const result = await runSleep(ctx, service, baseConfig(), ctx.logger, { embedder, vectorIndex }, null);
|
|
393
|
-
assert.equal(result.status, "ok");
|
|
394
|
-
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
395
|
-
for (const options of captured) {
|
|
396
|
-
assert.equal(options.reasoningEffort, "low", `unset sleep effort resolves to the lowest declared (${options.purpose})`);
|
|
397
|
-
}
|
|
398
|
-
store.close();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// ------------------------------------------------------------------ stream-level rejection
|
|
402
|
-
// dsh-llm rc.1 converts adapter-stage failures (including the provider's
|
|
403
|
-
// UNSUPPORTED_REASONING_EFFORT throw from resolveCallWithInfo) into a terminal
|
|
404
|
-
// error finish chunk inside adapterStream — the rejection NEVER reaches our
|
|
405
|
-
// catch. The v0.7.16 throw-based fallback was therefore dead code for the
|
|
406
|
-
// stream path; these tests pin the finish-chunk-based fallback.
|
|
407
|
-
|
|
408
|
-
test("rc.1 stream-level effort rejection (error finish chunk) also triggers the no-effort retry", async () => {
|
|
409
|
-
const store = createStore(":memory:");
|
|
410
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
411
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
412
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
413
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
414
|
-
const calls = [];
|
|
415
|
-
const warnings = [];
|
|
416
|
-
const ctx = {
|
|
417
|
-
logger: { warn: (m) => warnings.push(String(m)) },
|
|
418
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
419
|
-
llm: {
|
|
420
|
-
async *stream(options) {
|
|
421
|
-
calls.push(options);
|
|
422
|
-
if (options.reasoningEffort) {
|
|
423
|
-
yield {
|
|
424
|
-
type: "finish",
|
|
425
|
-
reason: {
|
|
426
|
-
kind: "error",
|
|
427
|
-
failure: {
|
|
428
|
-
code: "UNSUPPORTED_REASONING_EFFORT",
|
|
429
|
-
message: 'provider "mock" model "mock-model" does not support reasoning effort "low"'
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
436
|
-
if (userText.startsWith("id=")) {
|
|
437
|
-
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
438
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
439
|
-
]) };
|
|
440
|
-
} else {
|
|
441
|
-
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
442
|
-
}
|
|
443
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
448
|
-
assert.equal(result.ok, true, "run survives the stream-level effort rejection");
|
|
449
|
-
assert.ok(result.applied > 0, "consolidation still lands changes");
|
|
450
|
-
assert.equal(calls[0].reasoningEffort, "low", "first attempt forwards the effort");
|
|
451
|
-
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
452
|
-
assert.ok(warnings.some((w) => w.includes("rejected via stream")), "the stream-level rejection is logged");
|
|
453
|
-
store.close();
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test("non-effort stream failures are not retried and the finish-chunk cause reaches the audit row", async () => {
|
|
457
|
-
const store = createStore(":memory:");
|
|
458
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
459
|
-
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
460
|
-
const calls = [];
|
|
461
|
-
const ctx = {
|
|
462
|
-
logger: { warn: () => {} },
|
|
463
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
464
|
-
llm: {
|
|
465
|
-
async *stream(options) {
|
|
466
|
-
calls.push(options);
|
|
467
|
-
yield { type: "finish", reason: { kind: "error", failure: { code: "PROVIDER_GONE", message: "provider mock is not registered" } } };
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
472
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
473
|
-
assert.equal(result.ok, false);
|
|
474
|
-
assert.equal(result.error, "llm failed", "the run error stays the stable short string");
|
|
475
|
-
assert.equal(calls.length, 1, "no blind retry when the stream failure is not an effort rejection");
|
|
476
|
-
const row = service.listLlmAudits().find((r) => r.operation_type === "dream_consolidate");
|
|
477
|
-
assert.ok(row && row.status === "error", "failed consolidation still audited");
|
|
478
|
-
assert.ok(
|
|
479
|
-
String(row.error_message).includes("PROVIDER_GONE") && String(row.error_message).includes("provider mock is not registered"),
|
|
480
|
-
"audit error_message carries the finish-chunk cause"
|
|
481
|
-
);
|
|
482
|
-
store.close();
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
test("sleep passes the stream failure accessor so a stream-level effort rejection retries", async () => {
|
|
486
|
-
const { store, service, vectorIndex } = sleepSetup();
|
|
487
|
-
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
488
|
-
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
489
|
-
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
490
|
-
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
491
|
-
const captured = [];
|
|
492
|
-
const ctx = sleepCtx(null, { provider: "mock", model: "sleep-model" }, captured);
|
|
493
|
-
ctx.llm.stream = async function* (options) {
|
|
494
|
-
captured.push(options);
|
|
495
|
-
if (options.reasoningEffort) {
|
|
496
|
-
yield {
|
|
497
|
-
type: "finish",
|
|
498
|
-
reason: { kind: "error", failure: { code: "UNSUPPORTED_REASONING_EFFORT", message: 'provider "mock" model "sleep-model" does not support reasoning effort "low"' } }
|
|
499
|
-
};
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
503
|
-
yield { type: "text-delta", index: 0, text: userText.startsWith("候选冲突")
|
|
504
|
-
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
505
|
-
: "[]" };
|
|
506
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
507
|
-
};
|
|
508
|
-
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "low" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
509
|
-
assert.equal(result.status, "ok", "sleep survives the stream-level effort rejection");
|
|
510
|
-
assert.equal(captured[0].reasoningEffort, "low", "first conflict attempt forwards the effort");
|
|
511
|
-
assert.equal("reasoningEffort" in captured[1], false, "conflict retry omits the rejected effort field");
|
|
512
|
-
store.close();
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
// ------------------------------------------------------------------ defaultEffort trap
|
|
516
|
-
// DSH Desktop's volcano-engine adapter declares reasoning.defaultEffort="low"
|
|
517
|
-
// for a model that rejects "low", so omitting the field is NOT a safe retry —
|
|
518
|
-
// the harness substitutes the poison default and fails again. resolveDreamEffort
|
|
519
|
-
// queries resolveModelInfo up front and forwards a value that is actually in
|
|
520
|
-
// the model's declared efforts, so the first attempt already carries a
|
|
521
|
-
// supported effort and never trips UNSUPPORTED_REASONING_EFFORT.
|
|
522
|
-
|
|
523
|
-
test("defaultEffort trap: configured 'low' remapped to the first supported effort when default is poison", async () => {
|
|
524
|
-
const store = createStore(":memory:");
|
|
525
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
526
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
527
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
528
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
529
|
-
const captured = [];
|
|
530
|
-
const infoCalls = [];
|
|
531
|
-
const ctx = dreamCtx({
|
|
532
|
-
captured,
|
|
533
|
-
onConsolidation: () => JSON.stringify([
|
|
534
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
535
|
-
])
|
|
536
|
-
});
|
|
537
|
-
// The adapter's capability report: defaultEffort "low" is NOT in efforts
|
|
538
|
-
// (the model rejects it) — exactly the volcano-engine/deepseek-v4-flash trap.
|
|
539
|
-
ctx.llm.resolveModelInfo = async (provider, model) => {
|
|
540
|
-
infoCalls.push([provider, model]);
|
|
541
|
-
return {
|
|
542
|
-
provider,
|
|
543
|
-
model,
|
|
544
|
-
reasoning: {
|
|
545
|
-
efforts: [{ id: "medium" }, { id: "high" }],
|
|
546
|
-
defaultEffort: "low"
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
};
|
|
550
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
551
|
-
assert.equal(result.ok, true, "run succeeds without ever tripping the poison default");
|
|
552
|
-
assert.ok(result.applied > 0, "consolidation lands changes");
|
|
553
|
-
assert.deepEqual(infoCalls[0], ["mock", "mock-model"], "capability queried for the exact dream route");
|
|
554
|
-
assert.equal(captured[0].reasoningEffort, "medium", "poison 'low' remapped to the first supported effort");
|
|
555
|
-
assert.equal(captured[1].reasoningEffort, "medium", "summary pass uses the same resolved effort");
|
|
556
|
-
store.close();
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test("defaultEffort trap: model with no reasoning capability omits the field entirely", async () => {
|
|
560
|
-
const store = createStore(":memory:");
|
|
561
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
562
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
563
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
564
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
565
|
-
const captured = [];
|
|
566
|
-
const ctx = dreamCtx({
|
|
567
|
-
captured,
|
|
568
|
-
onConsolidation: () => JSON.stringify([
|
|
569
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
570
|
-
])
|
|
571
|
-
});
|
|
572
|
-
// Non-thinking model (e.g. deepseek-v4-flash): adapter reports no reasoning
|
|
573
|
-
// capability, so ANY explicit effort would be rejected — the helper must
|
|
574
|
-
// drop it, which is the harness's safe "no reasoning" path.
|
|
575
|
-
ctx.llm.resolveModelInfo = async () => ({ provider: "mock", model: "mock-model", reasoning: undefined });
|
|
576
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
577
|
-
assert.equal(result.ok, true);
|
|
578
|
-
for (const options of captured) {
|
|
579
|
-
assert.equal("reasoningEffort" in options, false, "no reasoning capability -> effort omitted, never rejected");
|
|
580
|
-
}
|
|
581
|
-
store.close();
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
test("defaultEffort trap: configured effort supported is forwarded verbatim", async () => {
|
|
585
|
-
const store = createStore(":memory:");
|
|
586
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
587
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
588
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
589
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
590
|
-
const captured = [];
|
|
591
|
-
const ctx = dreamCtx({
|
|
592
|
-
captured,
|
|
593
|
-
onConsolidation: () => JSON.stringify([
|
|
594
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
595
|
-
])
|
|
596
|
-
});
|
|
597
|
-
ctx.llm.resolveModelInfo = async () => ({
|
|
598
|
-
provider: "mock",
|
|
599
|
-
model: "mock-model",
|
|
600
|
-
reasoning: { efforts: [{ id: "high" }, { id: "low" }], defaultEffort: "low" }
|
|
601
|
-
});
|
|
602
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
603
|
-
assert.equal(result.ok, true);
|
|
604
|
-
for (const options of captured) {
|
|
605
|
-
assert.equal(options.reasoningEffort, "high", "supported configured value untouched");
|
|
606
|
-
}
|
|
607
|
-
store.close();
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
test("defaultEffort trap: capability query failure falls back to configured effort (retry still guards)", async () => {
|
|
611
|
-
const store = createStore(":memory:");
|
|
612
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
613
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
614
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
615
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
616
|
-
const calls = [];
|
|
617
|
-
const warnings = [];
|
|
618
|
-
const ctx = {
|
|
619
|
-
logger: { warn: (m) => warnings.push(String(m)) },
|
|
620
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
621
|
-
llm: {
|
|
622
|
-
async *stream(options) {
|
|
623
|
-
calls.push(options);
|
|
624
|
-
if (options.reasoningEffort) {
|
|
625
|
-
throw new Error("UNSUPPORTED_REASONING_EFFORT: mock does not support reasoning effort \"high\"");
|
|
626
|
-
}
|
|
627
|
-
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
628
|
-
if (userText.startsWith("id=")) {
|
|
629
|
-
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
630
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
631
|
-
]) };
|
|
632
|
-
} else {
|
|
633
|
-
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
634
|
-
}
|
|
635
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
636
|
-
},
|
|
637
|
-
// Adapter knows nothing about the model — helper must not crash, and the
|
|
638
|
-
// configured effort flows through so withEffortFallback still retries.
|
|
639
|
-
resolveModelInfo: async () => { throw new Error("adapter not reachable"); }
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
643
|
-
assert.equal(result.ok, true, "run succeeds via the no-effort retry");
|
|
644
|
-
assert.equal(calls[0].reasoningEffort, "high", "configured effort forwarded when capability query fails");
|
|
645
|
-
assert.equal("reasoningEffort" in calls[1], false, "rejected effort retried without the field");
|
|
646
|
-
assert.ok(warnings.some((w) => w.includes("resolveModelInfo failed")), "capability-query failure is logged");
|
|
647
|
-
store.close();
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
test("defaultEffort trap: sleep conflict pass remaps a poison effort too", async () => {
|
|
651
|
-
const { store, service, vectorIndex } = sleepSetup();
|
|
652
|
-
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
653
|
-
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
654
|
-
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
655
|
-
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
656
|
-
const captured = [];
|
|
657
|
-
const ctx = sleepCtx(
|
|
658
|
-
(userText) => userText.startsWith("候选冲突")
|
|
659
|
-
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
660
|
-
: "[]",
|
|
661
|
-
{ provider: "mock", model: "sleep-model" },
|
|
662
|
-
captured
|
|
663
|
-
);
|
|
664
|
-
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
665
|
-
provider,
|
|
666
|
-
model,
|
|
667
|
-
reasoning: { efforts: [{ id: "medium" }, { id: "high" }], defaultEffort: "low" }
|
|
668
|
-
});
|
|
669
|
-
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "low" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
670
|
-
assert.equal(result.status, "ok");
|
|
671
|
-
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
672
|
-
for (const options of captured) {
|
|
673
|
-
assert.equal(options.reasoningEffort, "medium", "poison 'low' remapped on sleep passes too");
|
|
674
|
-
}
|
|
675
|
-
store.close();
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// ------------------------------------------------- entity extraction adapter (issue #108/#109)
|
|
679
|
-
// The streamEntityText adapter lives in index.js; it maps the extractor's
|
|
680
|
-
// options onto a dsh-llm stream route and retries once without the effort
|
|
681
|
-
// when the first attempt is rejected. These tests drive the real exported
|
|
682
|
-
// factory, not a mock of it.
|
|
683
|
-
|
|
684
|
-
test("issue#109: entity extraction effort rejection retries once without the effort", async () => {
|
|
685
|
-
const calls = [];
|
|
686
|
-
const warnings = [];
|
|
687
|
-
const streamEntityText = createEntityStreamAdapter({
|
|
688
|
-
llm: {
|
|
689
|
-
async *stream(options) {
|
|
690
|
-
calls.push(options);
|
|
691
|
-
// First attempt carries the effort: the provider rejects it via an
|
|
692
|
-
// error finish chunk (the realistic stream-level rejection).
|
|
693
|
-
if (options.reasoningEffort) {
|
|
694
|
-
yield { type: "finish", reason: { kind: "error", message: "UNSUPPORTED_REASONING_EFFORT" } };
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
yield { type: "text-delta", index: 0, text: "{\"entities\":[{\"name\":\"张三\",\"type\":\"person\",\"attrs\":[{\"key\":\"职业\",\"value\":\"工程师\",\"confidence\":0.9}]}],\"relations\":[]}" };
|
|
698
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
699
|
-
}
|
|
700
|
-
},
|
|
701
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
702
|
-
logger: { warn: (m) => warnings.push(String(m)) }
|
|
703
|
-
});
|
|
704
|
-
const text = await streamEntityText(
|
|
705
|
-
[{ role: "user", content: [{ type: "text", text: "记忆内容" }] }],
|
|
706
|
-
{ reasoningEffort: "low" }
|
|
707
|
-
);
|
|
708
|
-
assert.equal(calls.length, 2, "attempt (rejected) + retry without effort");
|
|
709
|
-
assert.equal(calls[0].reasoningEffort, "low", "first attempt forwards the effort");
|
|
710
|
-
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
711
|
-
assert.equal(calls[0].provider, "mock", "route resolved from agentDefaultModel");
|
|
712
|
-
assert.equal(calls[0].model, "mock-model", "route model from agentDefaultModel");
|
|
713
|
-
assert.equal(calls[0].maxTokens, 4096, "extraction caps its output");
|
|
714
|
-
assert.ok(text.includes("张三"), "retry stream text is returned");
|
|
715
|
-
assert.ok(warnings.some((w) => w.includes("rejected, retrying without it")), "rejection is logged");
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
test("issue#109: entity extraction never retries blindly without an effort configured", async () => {
|
|
719
|
-
const calls = [];
|
|
720
|
-
const streamEntityText = createEntityStreamAdapter({
|
|
721
|
-
llm: {
|
|
722
|
-
async *stream(options) {
|
|
723
|
-
calls.push(options);
|
|
724
|
-
yield { type: "finish", reason: { kind: "error", message: "overloaded" } };
|
|
725
|
-
}
|
|
726
|
-
},
|
|
727
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
728
|
-
logger: { warn: () => {} }
|
|
729
|
-
});
|
|
730
|
-
const text = await streamEntityText([{ role: "user", content: [] }], {});
|
|
731
|
-
assert.equal(text, undefined, "failure yields no text");
|
|
732
|
-
assert.equal(calls.length, 1, "no blind retry when no effort was requested");
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
test("issue#109: entity extraction explicit provider/model win over the default route", async () => {
|
|
736
|
-
const calls = [];
|
|
737
|
-
const streamEntityText = createEntityStreamAdapter({
|
|
738
|
-
llm: {
|
|
739
|
-
async *stream(options) {
|
|
740
|
-
calls.push(options);
|
|
741
|
-
yield { type: "text-delta", index: 0, text: "{}" };
|
|
742
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
agentDefaultModel: { currentSelection: () => ({ provider: "default", model: "default-model" }) },
|
|
746
|
-
logger: { warn: () => {} }
|
|
747
|
-
});
|
|
748
|
-
await streamEntityText([{ role: "user", content: [] }], { provider: "volcano", model: "deepseek-v3" });
|
|
749
|
-
assert.equal(calls.length, 1);
|
|
750
|
-
assert.equal(calls[0].provider, "volcano", "explicit provider beats the default");
|
|
751
|
-
assert.equal(calls[0].model, "deepseek-v3", "explicit model beats the default");
|
|
752
|
-
});
|
|
1
|
+
// Regression for issue #9, extended by issue #135 (建议 3/4/5):
|
|
2
|
+
// - B: dreamMaxTokens cap widened (min 256, max 131072) so large memory
|
|
3
|
+
// libraries no longer starve the consolidation output. #135 raises the
|
|
4
|
+
// DEFAULT to 131072 too — 32768 was exactly what thinking-model reasoning
|
|
5
|
+
// burnt, leaving an empty body.
|
|
6
|
+
// - A: dreamReasoningEffort / sleepReasoningEffort pass-through. Explicit
|
|
7
|
+
// 'none' omits the reasoningEffort field entirely (provider default
|
|
8
|
+
// applies); low/medium/high are forwarded verbatim on every dream /
|
|
9
|
+
// sleep LLM call. #135: UNSET now resolves to the LOWEST effort the model
|
|
10
|
+
// declares (the old default 'none' let the harness substitute the model's
|
|
11
|
+
// defaultEffort, burning the budget on reasoning). Asserted by capturing
|
|
12
|
+
// the options each llm.stream() sees.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { Config } from "../src/config.js";
|
|
16
|
+
import { createDreamScheduler, parseReceipt } from "../src/dream.js";
|
|
17
|
+
import { runSleep } from "../src/dream/sleep.js";
|
|
18
|
+
import { createStore } from "../src/store.js";
|
|
19
|
+
import { createService } from "../src/service.js";
|
|
20
|
+
import { createVectorIndex } from "../src/vector-index.js";
|
|
21
|
+
import { createEntityStreamAdapter } from "../src/index.js";
|
|
22
|
+
|
|
23
|
+
const embedder = {
|
|
24
|
+
embedSingle: async () => [1, 0, 0],
|
|
25
|
+
embed: async () => [1, 0, 0],
|
|
26
|
+
schedule: () => {},
|
|
27
|
+
modelHash: "mock#1",
|
|
28
|
+
dimension: 3
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------- config schema
|
|
32
|
+
|
|
33
|
+
test("issue#135: dreamMaxTokens defaults to 131072 and accepts values up to 131072", () => {
|
|
34
|
+
assert.equal(Config({}).dreamMaxTokens, 131072, "old default 32768 was exactly what thinking-model reasoning burnt (Issue #135 建议 3)");
|
|
35
|
+
assert.equal(Config({ dreamMaxTokens: 131072 }).dreamMaxTokens, 131072, "upper bound accepted");
|
|
36
|
+
assert.equal(Config({ dreamMaxTokens: 65536 }).dreamMaxTokens, 65536, "intermediate value accepted");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("issue#9: dreamMaxTokens clamps to [256, 131072], out-of-range values are rejected", () => {
|
|
40
|
+
assert.equal(Config({ dreamMaxTokens: 256 }).dreamMaxTokens, 256, "lower bound accepted");
|
|
41
|
+
assert.equal(Config({ dreamMaxTokens: 100000 }).dreamMaxTokens, 100000, "raised default tier accepted");
|
|
42
|
+
assert.throws(() => Config({ dreamMaxTokens: 255 }), "below min rejected");
|
|
43
|
+
assert.throws(() => Config({ dreamMaxTokens: 131073 }), "above max rejected");
|
|
44
|
+
assert.throws(() => Config({ dreamMaxTokens: 0 }), "zero rejected");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("issue#135: reasoningEffort unset = undefined (auto-lowest at resolve time); explicit values preserved", () => {
|
|
48
|
+
const cfg = Config({});
|
|
49
|
+
assert.equal(cfg.dreamReasoningEffort, undefined, "unset must be distinguishable from explicit 'none'");
|
|
50
|
+
assert.equal(cfg.sleepReasoningEffort, undefined);
|
|
51
|
+
// issue #315: the distill knob defaults to 'none' (entity-extraction shape,
|
|
52
|
+
// no auto-lowest resolution — distill keeps its current behavior until the
|
|
53
|
+
// user opts in).
|
|
54
|
+
assert.equal(cfg.summarizeReasoningEffort, "none", "distill effort defaults to 'none'");
|
|
55
|
+
assert.equal(Config({ summarizeReasoningEffort: "off" }).summarizeReasoningEffort, "off");
|
|
56
|
+
assert.equal(Config({ dreamReasoningEffort: "none" }).dreamReasoningEffort, "none", "explicit 'none' kept");
|
|
57
|
+
assert.equal(Config({ dreamReasoningEffort: "off" }).dreamReasoningEffort, "off", "'off' now a valid explicit choice");
|
|
58
|
+
assert.equal(Config({ sleepReasoningEffort: "medium" }).sleepReasoningEffort, "medium");
|
|
59
|
+
assert.throws(() => Config({ dreamReasoningEffort: "bogus" }), "invalid effort rejected");
|
|
60
|
+
assert.throws(() => Config({ sleepReasoningEffort: "ultra" }), "invalid effort rejected");
|
|
61
|
+
assert.throws(() => Config({ summarizeReasoningEffort: "max" }), "invalid distill effort rejected");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------- dream passthrough
|
|
65
|
+
|
|
66
|
+
/** dream ctx that records every llm.stream() call's options for inspection. */
|
|
67
|
+
function dreamCtx({ onConsolidation, summaryText = "记忆库总览:用户偏好中文。", captured = [] } = {}) {
|
|
68
|
+
return {
|
|
69
|
+
logger: { warn: () => {} },
|
|
70
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
71
|
+
llm: {
|
|
72
|
+
async *stream(options) {
|
|
73
|
+
captured.push(options);
|
|
74
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
75
|
+
if (userText.startsWith("id=")) {
|
|
76
|
+
yield { type: "text-delta", index: 0, text: onConsolidation ? onConsolidation(userText) : "[]" };
|
|
77
|
+
} else {
|
|
78
|
+
yield { type: "text-delta", index: 0, text: summaryText };
|
|
79
|
+
}
|
|
80
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test("issue#135: dream omits reasoningEffort when unset without a capability API and still consolidates (applied>0)", async () => {
|
|
87
|
+
const store = createStore(":memory:");
|
|
88
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
89
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
90
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
91
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
92
|
+
const captured = [];
|
|
93
|
+
const ctx = dreamCtx({
|
|
94
|
+
captured,
|
|
95
|
+
onConsolidation: () => JSON.stringify([
|
|
96
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
97
|
+
])
|
|
98
|
+
});
|
|
99
|
+
const result = await dream.runDream(ctx, service, {});
|
|
100
|
+
assert.equal(result.ok, true);
|
|
101
|
+
assert.ok(result.applied > 0, "end-to-end dream run still lands changes");
|
|
102
|
+
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
103
|
+
for (const options of captured) {
|
|
104
|
+
assert.equal("reasoningEffort" in options, false, `unset without capability API must not forward reasoningEffort (${options.purpose})`);
|
|
105
|
+
}
|
|
106
|
+
store.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("issue#135: unset effort resolves to the LOWEST supported effort, never the poison default", async () => {
|
|
110
|
+
const store = createStore(":memory:");
|
|
111
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
112
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
113
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
114
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
115
|
+
const captured = [];
|
|
116
|
+
const ctx = dreamCtx({
|
|
117
|
+
captured,
|
|
118
|
+
onConsolidation: () => JSON.stringify([
|
|
119
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
120
|
+
])
|
|
121
|
+
});
|
|
122
|
+
// v4-flash 系能力报告:声明 off/low/high/max、defaultEffort=high。旧默认
|
|
123
|
+
// (省略字段)让 harness 顶上 high → 推理烧光预算返回空体;现在未配置应
|
|
124
|
+
// 自动取最低档 off,而不是服务商默认。
|
|
125
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
126
|
+
provider,
|
|
127
|
+
model,
|
|
128
|
+
reasoning: { efforts: [{ id: "high" }, { id: "off" }, { id: "low" }, { id: "max" }], defaultEffort: "high" }
|
|
129
|
+
});
|
|
130
|
+
const result = await dream.runDream(ctx, service, {});
|
|
131
|
+
assert.equal(result.ok, true);
|
|
132
|
+
assert.ok(result.applied > 0, "consolidation lands changes");
|
|
133
|
+
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
134
|
+
for (const options of captured) {
|
|
135
|
+
assert.equal(options.reasoningEffort, "off", `unset resolves to the lowest declared effort on ${options.purpose}`);
|
|
136
|
+
}
|
|
137
|
+
store.close();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("issue#135: explicit 'none' still omits the field even when the capability API is available", async () => {
|
|
141
|
+
const store = createStore(":memory:");
|
|
142
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
143
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
144
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
145
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
146
|
+
const captured = [];
|
|
147
|
+
const ctx = dreamCtx({
|
|
148
|
+
captured,
|
|
149
|
+
onConsolidation: () => JSON.stringify([
|
|
150
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
151
|
+
])
|
|
152
|
+
});
|
|
153
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
154
|
+
provider,
|
|
155
|
+
model,
|
|
156
|
+
reasoning: { efforts: [{ id: "off" }, { id: "low" }], defaultEffort: "high" }
|
|
157
|
+
});
|
|
158
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "none" });
|
|
159
|
+
assert.equal(result.ok, true);
|
|
160
|
+
for (const options of captured) {
|
|
161
|
+
assert.equal("reasoningEffort" in options, false, `explicit 'none' is a user decision to omit (${options.purpose})`);
|
|
162
|
+
}
|
|
163
|
+
store.close();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("issue#9: dream forwards dreamReasoningEffort on both LLM calls", async () => {
|
|
167
|
+
const store = createStore(":memory:");
|
|
168
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
169
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
170
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
171
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
172
|
+
const captured = [];
|
|
173
|
+
const ctx = dreamCtx({
|
|
174
|
+
captured,
|
|
175
|
+
onConsolidation: () => JSON.stringify([
|
|
176
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
177
|
+
])
|
|
178
|
+
});
|
|
179
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
180
|
+
assert.equal(result.ok, true);
|
|
181
|
+
assert.equal(captured.length, 2);
|
|
182
|
+
for (const options of captured) {
|
|
183
|
+
assert.equal(options.reasoningEffort, "high", `reasoningEffort forwarded on ${options.purpose}`);
|
|
184
|
+
}
|
|
185
|
+
store.close();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("issue#9: dreamMaxTokens is forwarded as maxTokens on consolidation and summary calls", async () => {
|
|
189
|
+
const store = createStore(":memory:");
|
|
190
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
191
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
192
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
193
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
194
|
+
const captured = [];
|
|
195
|
+
const ctx = dreamCtx({
|
|
196
|
+
captured,
|
|
197
|
+
onConsolidation: () => JSON.stringify([
|
|
198
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
199
|
+
])
|
|
200
|
+
});
|
|
201
|
+
const result = await dream.runDream(ctx, service, { dreamMaxTokens: 100000 });
|
|
202
|
+
assert.equal(result.ok, true);
|
|
203
|
+
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
204
|
+
for (const options of captured) {
|
|
205
|
+
assert.equal(options.maxTokens, 100000, `raised budget forwarded on ${options.purpose}`);
|
|
206
|
+
}
|
|
207
|
+
store.close();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("issue#9: thinking-model empty body (no text emitted, budget burnt on reasoning) fails as no json array", async () => {
|
|
211
|
+
const store = createStore(":memory:");
|
|
212
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
213
|
+
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文" });
|
|
214
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
215
|
+
const ctx = dreamCtx({ onConsolidation: () => "" }); // 思考型模型把预算烧光 → 无正文
|
|
216
|
+
const result = await dream.runDream(ctx, service, { dreamMaxTokens: 32768 });
|
|
217
|
+
assert.equal(result.ok, false, "empty body is a hard failure, never faked ok");
|
|
218
|
+
assert.match(result.error, /no json array/);
|
|
219
|
+
const run = store.listDreamRuns()[0];
|
|
220
|
+
assert.equal(run.status, "failed", "audit row records failed");
|
|
221
|
+
assert.match(run.error, /no json array/, "audit error_message carries the empty-body cause");
|
|
222
|
+
assert.equal(parseReceipt(run.receipt).status, "failed", "receipt records failed");
|
|
223
|
+
store.close();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("legal empty decision list [] is a no-op success, not a failure", async () => {
|
|
227
|
+
// 真实模型在记忆无冗余时合法输出 [](CONSOLIDATION_PROMPT 允许"无问题无需输出"),
|
|
228
|
+
// 此前被 validateDecisions 判 failed → 审计表反复失败。现在应 ok:true、applied 0、
|
|
229
|
+
// 审计记 ok,且 summary 照常产出。
|
|
230
|
+
const store = createStore(":memory:");
|
|
231
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
232
|
+
service.saveWithDedupe({ type: "project", title: "插件", content: "内容", importance: 3 });
|
|
233
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
234
|
+
const ctx = dreamCtx({ onConsolidation: () => "[]" }); // 模型:无需合并
|
|
235
|
+
const result = await dream.runDream(ctx, service, { dreamMaxTokens: 32768 });
|
|
236
|
+
assert.equal(result.ok, true, "empty [] is a valid no-op, never failed");
|
|
237
|
+
assert.equal(result.applied, 0, "no decisions to apply");
|
|
238
|
+
assert.equal(result.summary, true, "summary still produced");
|
|
239
|
+
const run = store.listDreamRuns()[0];
|
|
240
|
+
assert.equal(run.status, "ok", "audit row records ok, not failed");
|
|
241
|
+
assert.equal(parseReceipt(run.receipt).status, "ok", "receipt records ok");
|
|
242
|
+
store.close();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("issue#25: dreamProvider/dreamModel config wins over the agentDefaultModel route", async () => {
|
|
246
|
+
const store = createStore(":memory:");
|
|
247
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
248
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
249
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
250
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
251
|
+
const captured = [];
|
|
252
|
+
const ctx = dreamCtx({
|
|
253
|
+
captured,
|
|
254
|
+
onConsolidation: () => JSON.stringify([
|
|
255
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
256
|
+
])
|
|
257
|
+
});
|
|
258
|
+
// dreamCtx's agentDefaultModel resolves (mock:mock-model), but the explicit
|
|
259
|
+
// config route must win — otherwise dreamProvider/dreamModel is dead code in
|
|
260
|
+
// a standard DSH install and the dream can never be moved off a thinking model.
|
|
261
|
+
const result = await dream.runDream(ctx, service, { dreamProvider: "volcano", dreamModel: "deepseek-v3" });
|
|
262
|
+
assert.equal(result.ok, true);
|
|
263
|
+
assert.ok(captured.length >= 2, "consolidation + summary both hit the LLM");
|
|
264
|
+
for (const options of captured) {
|
|
265
|
+
assert.equal(options.provider, "volcano");
|
|
266
|
+
assert.equal(options.model, "deepseek-v3", "config route wins over agentDefaultModel (mock:mock-model)");
|
|
267
|
+
}
|
|
268
|
+
store.close();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("issue#9: rejected reasoningEffort retries once without it and still consolidates", async () => {
|
|
272
|
+
const store = createStore(":memory:");
|
|
273
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
274
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
275
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
276
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
277
|
+
const calls = [];
|
|
278
|
+
const ctx = {
|
|
279
|
+
logger: { warn: () => {} },
|
|
280
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
281
|
+
llm: {
|
|
282
|
+
async *stream(options) {
|
|
283
|
+
calls.push(options);
|
|
284
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
285
|
+
if (userText.startsWith("id=")) {
|
|
286
|
+
// First attempt forwards reasoningEffort: the provider rejects it.
|
|
287
|
+
if (options.reasoningEffort) {
|
|
288
|
+
throw new Error("UNSUPPORTED_REASONING_EFFORT: DeepSeek does not support reasoning effort \"low\"");
|
|
289
|
+
}
|
|
290
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
291
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
292
|
+
]) };
|
|
293
|
+
} else {
|
|
294
|
+
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
295
|
+
}
|
|
296
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
301
|
+
assert.equal(result.ok, true, "run survives the effort rejection via the fallback retry");
|
|
302
|
+
assert.ok(result.applied > 0, "consolidation still lands changes");
|
|
303
|
+
assert.equal(calls.length, 3, "consolidation tried (rejected) + retried without effort + summary");
|
|
304
|
+
assert.equal(calls[0].reasoningEffort, "low", "first consolidation attempt forwards the effort");
|
|
305
|
+
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
306
|
+
store.close();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------- sleep passthrough
|
|
310
|
+
|
|
311
|
+
function sleepSetup() {
|
|
312
|
+
const store = createStore(":memory:");
|
|
313
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
314
|
+
const vectorIndex = createVectorIndex({ store });
|
|
315
|
+
service.setEmbedder(embedder);
|
|
316
|
+
service.setVectorIndex(vectorIndex);
|
|
317
|
+
return { store, service, vectorIndex };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function baseConfig(overrides = {}) {
|
|
321
|
+
return {
|
|
322
|
+
sleepModeEnabled: true,
|
|
323
|
+
sleepIdleMinutes: 5,
|
|
324
|
+
sleepMinIntervalHours: 8,
|
|
325
|
+
sleepConflictStrictness: "normal",
|
|
326
|
+
sleepArchiveDays: 30,
|
|
327
|
+
sleepCompressDays: 90,
|
|
328
|
+
sleepPatternMinMemories: 10,
|
|
329
|
+
sleepMaxPatternPerRun: 3,
|
|
330
|
+
...overrides
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** sleep ctx that records every llm.stream() call's options. */
|
|
335
|
+
function sleepCtx(onConsolidation, selection = { provider: "mock", model: "sleep-model" }, captured = []) {
|
|
336
|
+
return {
|
|
337
|
+
logger: { warn: () => {}, info: () => {} },
|
|
338
|
+
agentDefaultModel: { currentSelection: () => selection },
|
|
339
|
+
llm: {
|
|
340
|
+
async *stream(options) {
|
|
341
|
+
captured.push(options);
|
|
342
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
343
|
+
yield { type: "text-delta", index: 0, text: onConsolidation ? onConsolidation(userText) : "[]" };
|
|
344
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
test("issue#9: sleep forwards sleepReasoningEffort on its LLM passes", async () => {
|
|
351
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
352
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
353
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
354
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
355
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
356
|
+
const captured = [];
|
|
357
|
+
const ctx = sleepCtx(
|
|
358
|
+
(userText) => userText.startsWith("候选冲突")
|
|
359
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
360
|
+
: "[]",
|
|
361
|
+
{ provider: "mock", model: "sleep-model" },
|
|
362
|
+
captured
|
|
363
|
+
);
|
|
364
|
+
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "medium" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
365
|
+
assert.equal(result.status, "ok");
|
|
366
|
+
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
367
|
+
for (const options of captured) {
|
|
368
|
+
assert.equal(options.reasoningEffort, "medium", `reasoningEffort forwarded on ${options.purpose}`);
|
|
369
|
+
}
|
|
370
|
+
store.close();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("issue#135: sleep unset effort also resolves to the lowest supported", async () => {
|
|
374
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
375
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
376
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
377
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
378
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
379
|
+
const captured = [];
|
|
380
|
+
const ctx = sleepCtx(
|
|
381
|
+
(userText) => userText.startsWith("候选冲突")
|
|
382
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
383
|
+
: "[]",
|
|
384
|
+
{ provider: "mock", model: "sleep-model" },
|
|
385
|
+
captured
|
|
386
|
+
);
|
|
387
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
388
|
+
provider,
|
|
389
|
+
model,
|
|
390
|
+
reasoning: { efforts: [{ id: "low" }, { id: "high" }], defaultEffort: "high" }
|
|
391
|
+
});
|
|
392
|
+
const result = await runSleep(ctx, service, baseConfig(), ctx.logger, { embedder, vectorIndex }, null);
|
|
393
|
+
assert.equal(result.status, "ok");
|
|
394
|
+
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
395
|
+
for (const options of captured) {
|
|
396
|
+
assert.equal(options.reasoningEffort, "low", `unset sleep effort resolves to the lowest declared (${options.purpose})`);
|
|
397
|
+
}
|
|
398
|
+
store.close();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ------------------------------------------------------------------ stream-level rejection
|
|
402
|
+
// dsh-llm rc.1 converts adapter-stage failures (including the provider's
|
|
403
|
+
// UNSUPPORTED_REASONING_EFFORT throw from resolveCallWithInfo) into a terminal
|
|
404
|
+
// error finish chunk inside adapterStream — the rejection NEVER reaches our
|
|
405
|
+
// catch. The v0.7.16 throw-based fallback was therefore dead code for the
|
|
406
|
+
// stream path; these tests pin the finish-chunk-based fallback.
|
|
407
|
+
|
|
408
|
+
test("rc.1 stream-level effort rejection (error finish chunk) also triggers the no-effort retry", async () => {
|
|
409
|
+
const store = createStore(":memory:");
|
|
410
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
411
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
412
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
413
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
414
|
+
const calls = [];
|
|
415
|
+
const warnings = [];
|
|
416
|
+
const ctx = {
|
|
417
|
+
logger: { warn: (m) => warnings.push(String(m)) },
|
|
418
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
419
|
+
llm: {
|
|
420
|
+
async *stream(options) {
|
|
421
|
+
calls.push(options);
|
|
422
|
+
if (options.reasoningEffort) {
|
|
423
|
+
yield {
|
|
424
|
+
type: "finish",
|
|
425
|
+
reason: {
|
|
426
|
+
kind: "error",
|
|
427
|
+
failure: {
|
|
428
|
+
code: "UNSUPPORTED_REASONING_EFFORT",
|
|
429
|
+
message: 'provider "mock" model "mock-model" does not support reasoning effort "low"'
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
436
|
+
if (userText.startsWith("id=")) {
|
|
437
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
438
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
439
|
+
]) };
|
|
440
|
+
} else {
|
|
441
|
+
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
442
|
+
}
|
|
443
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
448
|
+
assert.equal(result.ok, true, "run survives the stream-level effort rejection");
|
|
449
|
+
assert.ok(result.applied > 0, "consolidation still lands changes");
|
|
450
|
+
assert.equal(calls[0].reasoningEffort, "low", "first attempt forwards the effort");
|
|
451
|
+
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
452
|
+
assert.ok(warnings.some((w) => w.includes("rejected via stream")), "the stream-level rejection is logged");
|
|
453
|
+
store.close();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("non-effort stream failures are not retried and the finish-chunk cause reaches the audit row", async () => {
|
|
457
|
+
const store = createStore(":memory:");
|
|
458
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
459
|
+
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
460
|
+
const calls = [];
|
|
461
|
+
const ctx = {
|
|
462
|
+
logger: { warn: () => {} },
|
|
463
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
464
|
+
llm: {
|
|
465
|
+
async *stream(options) {
|
|
466
|
+
calls.push(options);
|
|
467
|
+
yield { type: "finish", reason: { kind: "error", failure: { code: "PROVIDER_GONE", message: "provider mock is not registered" } } };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
472
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
473
|
+
assert.equal(result.ok, false);
|
|
474
|
+
assert.equal(result.error, "llm failed", "the run error stays the stable short string");
|
|
475
|
+
assert.equal(calls.length, 1, "no blind retry when the stream failure is not an effort rejection");
|
|
476
|
+
const row = service.listLlmAudits().find((r) => r.operation_type === "dream_consolidate");
|
|
477
|
+
assert.ok(row && row.status === "error", "failed consolidation still audited");
|
|
478
|
+
assert.ok(
|
|
479
|
+
String(row.error_message).includes("PROVIDER_GONE") && String(row.error_message).includes("provider mock is not registered"),
|
|
480
|
+
"audit error_message carries the finish-chunk cause"
|
|
481
|
+
);
|
|
482
|
+
store.close();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("sleep passes the stream failure accessor so a stream-level effort rejection retries", async () => {
|
|
486
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
487
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
488
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
489
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
490
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
491
|
+
const captured = [];
|
|
492
|
+
const ctx = sleepCtx(null, { provider: "mock", model: "sleep-model" }, captured);
|
|
493
|
+
ctx.llm.stream = async function* (options) {
|
|
494
|
+
captured.push(options);
|
|
495
|
+
if (options.reasoningEffort) {
|
|
496
|
+
yield {
|
|
497
|
+
type: "finish",
|
|
498
|
+
reason: { kind: "error", failure: { code: "UNSUPPORTED_REASONING_EFFORT", message: 'provider "mock" model "sleep-model" does not support reasoning effort "low"' } }
|
|
499
|
+
};
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
503
|
+
yield { type: "text-delta", index: 0, text: userText.startsWith("候选冲突")
|
|
504
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
505
|
+
: "[]" };
|
|
506
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
507
|
+
};
|
|
508
|
+
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "low" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
509
|
+
assert.equal(result.status, "ok", "sleep survives the stream-level effort rejection");
|
|
510
|
+
assert.equal(captured[0].reasoningEffort, "low", "first conflict attempt forwards the effort");
|
|
511
|
+
assert.equal("reasoningEffort" in captured[1], false, "conflict retry omits the rejected effort field");
|
|
512
|
+
store.close();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ------------------------------------------------------------------ defaultEffort trap
|
|
516
|
+
// DSH Desktop's volcano-engine adapter declares reasoning.defaultEffort="low"
|
|
517
|
+
// for a model that rejects "low", so omitting the field is NOT a safe retry —
|
|
518
|
+
// the harness substitutes the poison default and fails again. resolveDreamEffort
|
|
519
|
+
// queries resolveModelInfo up front and forwards a value that is actually in
|
|
520
|
+
// the model's declared efforts, so the first attempt already carries a
|
|
521
|
+
// supported effort and never trips UNSUPPORTED_REASONING_EFFORT.
|
|
522
|
+
|
|
523
|
+
test("defaultEffort trap: configured 'low' remapped to the first supported effort when default is poison", async () => {
|
|
524
|
+
const store = createStore(":memory:");
|
|
525
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
526
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
527
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
528
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
529
|
+
const captured = [];
|
|
530
|
+
const infoCalls = [];
|
|
531
|
+
const ctx = dreamCtx({
|
|
532
|
+
captured,
|
|
533
|
+
onConsolidation: () => JSON.stringify([
|
|
534
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
535
|
+
])
|
|
536
|
+
});
|
|
537
|
+
// The adapter's capability report: defaultEffort "low" is NOT in efforts
|
|
538
|
+
// (the model rejects it) — exactly the volcano-engine/deepseek-v4-flash trap.
|
|
539
|
+
ctx.llm.resolveModelInfo = async (provider, model) => {
|
|
540
|
+
infoCalls.push([provider, model]);
|
|
541
|
+
return {
|
|
542
|
+
provider,
|
|
543
|
+
model,
|
|
544
|
+
reasoning: {
|
|
545
|
+
efforts: [{ id: "medium" }, { id: "high" }],
|
|
546
|
+
defaultEffort: "low"
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
551
|
+
assert.equal(result.ok, true, "run succeeds without ever tripping the poison default");
|
|
552
|
+
assert.ok(result.applied > 0, "consolidation lands changes");
|
|
553
|
+
assert.deepEqual(infoCalls[0], ["mock", "mock-model"], "capability queried for the exact dream route");
|
|
554
|
+
assert.equal(captured[0].reasoningEffort, "medium", "poison 'low' remapped to the first supported effort");
|
|
555
|
+
assert.equal(captured[1].reasoningEffort, "medium", "summary pass uses the same resolved effort");
|
|
556
|
+
store.close();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("defaultEffort trap: model with no reasoning capability omits the field entirely", async () => {
|
|
560
|
+
const store = createStore(":memory:");
|
|
561
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
562
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
563
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
564
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
565
|
+
const captured = [];
|
|
566
|
+
const ctx = dreamCtx({
|
|
567
|
+
captured,
|
|
568
|
+
onConsolidation: () => JSON.stringify([
|
|
569
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
570
|
+
])
|
|
571
|
+
});
|
|
572
|
+
// Non-thinking model (e.g. deepseek-v4-flash): adapter reports no reasoning
|
|
573
|
+
// capability, so ANY explicit effort would be rejected — the helper must
|
|
574
|
+
// drop it, which is the harness's safe "no reasoning" path.
|
|
575
|
+
ctx.llm.resolveModelInfo = async () => ({ provider: "mock", model: "mock-model", reasoning: undefined });
|
|
576
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
577
|
+
assert.equal(result.ok, true);
|
|
578
|
+
for (const options of captured) {
|
|
579
|
+
assert.equal("reasoningEffort" in options, false, "no reasoning capability -> effort omitted, never rejected");
|
|
580
|
+
}
|
|
581
|
+
store.close();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("defaultEffort trap: configured effort supported is forwarded verbatim", async () => {
|
|
585
|
+
const store = createStore(":memory:");
|
|
586
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
587
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
588
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
589
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
590
|
+
const captured = [];
|
|
591
|
+
const ctx = dreamCtx({
|
|
592
|
+
captured,
|
|
593
|
+
onConsolidation: () => JSON.stringify([
|
|
594
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
595
|
+
])
|
|
596
|
+
});
|
|
597
|
+
ctx.llm.resolveModelInfo = async () => ({
|
|
598
|
+
provider: "mock",
|
|
599
|
+
model: "mock-model",
|
|
600
|
+
reasoning: { efforts: [{ id: "high" }, { id: "low" }], defaultEffort: "low" }
|
|
601
|
+
});
|
|
602
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
603
|
+
assert.equal(result.ok, true);
|
|
604
|
+
for (const options of captured) {
|
|
605
|
+
assert.equal(options.reasoningEffort, "high", "supported configured value untouched");
|
|
606
|
+
}
|
|
607
|
+
store.close();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("defaultEffort trap: capability query failure falls back to configured effort (retry still guards)", async () => {
|
|
611
|
+
const store = createStore(":memory:");
|
|
612
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
613
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
614
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
615
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
616
|
+
const calls = [];
|
|
617
|
+
const warnings = [];
|
|
618
|
+
const ctx = {
|
|
619
|
+
logger: { warn: (m) => warnings.push(String(m)) },
|
|
620
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
621
|
+
llm: {
|
|
622
|
+
async *stream(options) {
|
|
623
|
+
calls.push(options);
|
|
624
|
+
if (options.reasoningEffort) {
|
|
625
|
+
throw new Error("UNSUPPORTED_REASONING_EFFORT: mock does not support reasoning effort \"high\"");
|
|
626
|
+
}
|
|
627
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
628
|
+
if (userText.startsWith("id=")) {
|
|
629
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
630
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
631
|
+
]) };
|
|
632
|
+
} else {
|
|
633
|
+
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
634
|
+
}
|
|
635
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
636
|
+
},
|
|
637
|
+
// Adapter knows nothing about the model — helper must not crash, and the
|
|
638
|
+
// configured effort flows through so withEffortFallback still retries.
|
|
639
|
+
resolveModelInfo: async () => { throw new Error("adapter not reachable"); }
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
643
|
+
assert.equal(result.ok, true, "run succeeds via the no-effort retry");
|
|
644
|
+
assert.equal(calls[0].reasoningEffort, "high", "configured effort forwarded when capability query fails");
|
|
645
|
+
assert.equal("reasoningEffort" in calls[1], false, "rejected effort retried without the field");
|
|
646
|
+
assert.ok(warnings.some((w) => w.includes("resolveModelInfo failed")), "capability-query failure is logged");
|
|
647
|
+
store.close();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("defaultEffort trap: sleep conflict pass remaps a poison effort too", async () => {
|
|
651
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
652
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
653
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
654
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
655
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
656
|
+
const captured = [];
|
|
657
|
+
const ctx = sleepCtx(
|
|
658
|
+
(userText) => userText.startsWith("候选冲突")
|
|
659
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
660
|
+
: "[]",
|
|
661
|
+
{ provider: "mock", model: "sleep-model" },
|
|
662
|
+
captured
|
|
663
|
+
);
|
|
664
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
665
|
+
provider,
|
|
666
|
+
model,
|
|
667
|
+
reasoning: { efforts: [{ id: "medium" }, { id: "high" }], defaultEffort: "low" }
|
|
668
|
+
});
|
|
669
|
+
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "low" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
670
|
+
assert.equal(result.status, "ok");
|
|
671
|
+
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
672
|
+
for (const options of captured) {
|
|
673
|
+
assert.equal(options.reasoningEffort, "medium", "poison 'low' remapped on sleep passes too");
|
|
674
|
+
}
|
|
675
|
+
store.close();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// ------------------------------------------------- entity extraction adapter (issue #108/#109)
|
|
679
|
+
// The streamEntityText adapter lives in index.js; it maps the extractor's
|
|
680
|
+
// options onto a dsh-llm stream route and retries once without the effort
|
|
681
|
+
// when the first attempt is rejected. These tests drive the real exported
|
|
682
|
+
// factory, not a mock of it.
|
|
683
|
+
|
|
684
|
+
test("issue#109: entity extraction effort rejection retries once without the effort", async () => {
|
|
685
|
+
const calls = [];
|
|
686
|
+
const warnings = [];
|
|
687
|
+
const streamEntityText = createEntityStreamAdapter({
|
|
688
|
+
llm: {
|
|
689
|
+
async *stream(options) {
|
|
690
|
+
calls.push(options);
|
|
691
|
+
// First attempt carries the effort: the provider rejects it via an
|
|
692
|
+
// error finish chunk (the realistic stream-level rejection).
|
|
693
|
+
if (options.reasoningEffort) {
|
|
694
|
+
yield { type: "finish", reason: { kind: "error", message: "UNSUPPORTED_REASONING_EFFORT" } };
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
yield { type: "text-delta", index: 0, text: "{\"entities\":[{\"name\":\"张三\",\"type\":\"person\",\"attrs\":[{\"key\":\"职业\",\"value\":\"工程师\",\"confidence\":0.9}]}],\"relations\":[]}" };
|
|
698
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
702
|
+
logger: { warn: (m) => warnings.push(String(m)) }
|
|
703
|
+
});
|
|
704
|
+
const text = await streamEntityText(
|
|
705
|
+
[{ role: "user", content: [{ type: "text", text: "记忆内容" }] }],
|
|
706
|
+
{ reasoningEffort: "low" }
|
|
707
|
+
);
|
|
708
|
+
assert.equal(calls.length, 2, "attempt (rejected) + retry without effort");
|
|
709
|
+
assert.equal(calls[0].reasoningEffort, "low", "first attempt forwards the effort");
|
|
710
|
+
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
711
|
+
assert.equal(calls[0].provider, "mock", "route resolved from agentDefaultModel");
|
|
712
|
+
assert.equal(calls[0].model, "mock-model", "route model from agentDefaultModel");
|
|
713
|
+
assert.equal(calls[0].maxTokens, 4096, "extraction caps its output");
|
|
714
|
+
assert.ok(text.includes("张三"), "retry stream text is returned");
|
|
715
|
+
assert.ok(warnings.some((w) => w.includes("rejected, retrying without it")), "rejection is logged");
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test("issue#109: entity extraction never retries blindly without an effort configured", async () => {
|
|
719
|
+
const calls = [];
|
|
720
|
+
const streamEntityText = createEntityStreamAdapter({
|
|
721
|
+
llm: {
|
|
722
|
+
async *stream(options) {
|
|
723
|
+
calls.push(options);
|
|
724
|
+
yield { type: "finish", reason: { kind: "error", message: "overloaded" } };
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
728
|
+
logger: { warn: () => {} }
|
|
729
|
+
});
|
|
730
|
+
const text = await streamEntityText([{ role: "user", content: [] }], {});
|
|
731
|
+
assert.equal(text, undefined, "failure yields no text");
|
|
732
|
+
assert.equal(calls.length, 1, "no blind retry when no effort was requested");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("issue#109: entity extraction explicit provider/model win over the default route", async () => {
|
|
736
|
+
const calls = [];
|
|
737
|
+
const streamEntityText = createEntityStreamAdapter({
|
|
738
|
+
llm: {
|
|
739
|
+
async *stream(options) {
|
|
740
|
+
calls.push(options);
|
|
741
|
+
yield { type: "text-delta", index: 0, text: "{}" };
|
|
742
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "default", model: "default-model" }) },
|
|
746
|
+
logger: { warn: () => {} }
|
|
747
|
+
});
|
|
748
|
+
await streamEntityText([{ role: "user", content: [] }], { provider: "volcano", model: "deepseek-v3" });
|
|
749
|
+
assert.equal(calls.length, 1);
|
|
750
|
+
assert.equal(calls[0].provider, "volcano", "explicit provider beats the default");
|
|
751
|
+
assert.equal(calls[0].model, "deepseek-v3", "explicit model beats the default");
|
|
752
|
+
});
|