@modusensus/dsh-mneme 0.7.25 → 0.7.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +12 -6
- package/README.md +12 -6
- package/lib/api.js +99 -0
- package/lib/client.js +132 -4
- package/lib/config.js +6 -6
- package/lib/dream/sleep.js +3 -3
- package/lib/dream.js +56 -2
- package/lib/settings.js +4 -0
- package/package.json +1 -1
- package/src/api.js +99 -0
- package/src/config.js +6 -6
- package/src/dream/sleep.js +3 -3
- package/src/dream.js +56 -2
- package/src/settings.js +4 -0
- package/test/api.test.js +171 -6
- package/test/client.test.js +72 -1
- package/test/reasoning-effort.test.js +163 -0
|
@@ -413,3 +413,166 @@ test("sleep passes the stream failure accessor so a stream-level effort rejectio
|
|
|
413
413
|
assert.equal("reasoningEffort" in captured[1], false, "conflict retry omits the rejected effort field");
|
|
414
414
|
store.close();
|
|
415
415
|
});
|
|
416
|
+
|
|
417
|
+
// ------------------------------------------------------------------ defaultEffort trap
|
|
418
|
+
// DSH Desktop's volcano-engine adapter declares reasoning.defaultEffort="low"
|
|
419
|
+
// for a model that rejects "low", so omitting the field is NOT a safe retry —
|
|
420
|
+
// the harness substitutes the poison default and fails again. resolveDreamEffort
|
|
421
|
+
// queries resolveModelInfo up front and forwards a value that is actually in
|
|
422
|
+
// the model's declared efforts, so the first attempt already carries a
|
|
423
|
+
// supported effort and never trips UNSUPPORTED_REASONING_EFFORT.
|
|
424
|
+
|
|
425
|
+
test("defaultEffort trap: configured 'low' remapped to the first supported effort when default is poison", async () => {
|
|
426
|
+
const store = createStore(":memory:");
|
|
427
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
428
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
429
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
430
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
431
|
+
const captured = [];
|
|
432
|
+
const infoCalls = [];
|
|
433
|
+
const ctx = dreamCtx({
|
|
434
|
+
captured,
|
|
435
|
+
onConsolidation: () => JSON.stringify([
|
|
436
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
437
|
+
])
|
|
438
|
+
});
|
|
439
|
+
// The adapter's capability report: defaultEffort "low" is NOT in efforts
|
|
440
|
+
// (the model rejects it) — exactly the volcano-engine/deepseek-v4-flash trap.
|
|
441
|
+
ctx.llm.resolveModelInfo = async (provider, model) => {
|
|
442
|
+
infoCalls.push([provider, model]);
|
|
443
|
+
return {
|
|
444
|
+
provider,
|
|
445
|
+
model,
|
|
446
|
+
reasoning: {
|
|
447
|
+
efforts: [{ id: "medium" }, { id: "high" }],
|
|
448
|
+
defaultEffort: "low"
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
453
|
+
assert.equal(result.ok, true, "run succeeds without ever tripping the poison default");
|
|
454
|
+
assert.ok(result.applied > 0, "consolidation lands changes");
|
|
455
|
+
assert.deepEqual(infoCalls[0], ["mock", "mock-model"], "capability queried for the exact dream route");
|
|
456
|
+
assert.equal(captured[0].reasoningEffort, "medium", "poison 'low' remapped to the first supported effort");
|
|
457
|
+
assert.equal(captured[1].reasoningEffort, "medium", "summary pass uses the same resolved effort");
|
|
458
|
+
store.close();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("defaultEffort trap: model with no reasoning capability omits the field entirely", async () => {
|
|
462
|
+
const store = createStore(":memory:");
|
|
463
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
464
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
465
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
466
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
467
|
+
const captured = [];
|
|
468
|
+
const ctx = dreamCtx({
|
|
469
|
+
captured,
|
|
470
|
+
onConsolidation: () => JSON.stringify([
|
|
471
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
472
|
+
])
|
|
473
|
+
});
|
|
474
|
+
// Non-thinking model (e.g. deepseek-v4-flash): adapter reports no reasoning
|
|
475
|
+
// capability, so ANY explicit effort would be rejected — the helper must
|
|
476
|
+
// drop it, which is the harness's safe "no reasoning" path.
|
|
477
|
+
ctx.llm.resolveModelInfo = async () => ({ provider: "mock", model: "mock-model", reasoning: undefined });
|
|
478
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
479
|
+
assert.equal(result.ok, true);
|
|
480
|
+
for (const options of captured) {
|
|
481
|
+
assert.equal("reasoningEffort" in options, false, "no reasoning capability -> effort omitted, never rejected");
|
|
482
|
+
}
|
|
483
|
+
store.close();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("defaultEffort trap: configured effort supported is forwarded verbatim", async () => {
|
|
487
|
+
const store = createStore(":memory:");
|
|
488
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
489
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
490
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
491
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
492
|
+
const captured = [];
|
|
493
|
+
const ctx = dreamCtx({
|
|
494
|
+
captured,
|
|
495
|
+
onConsolidation: () => JSON.stringify([
|
|
496
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
497
|
+
])
|
|
498
|
+
});
|
|
499
|
+
ctx.llm.resolveModelInfo = async () => ({
|
|
500
|
+
provider: "mock",
|
|
501
|
+
model: "mock-model",
|
|
502
|
+
reasoning: { efforts: [{ id: "high" }, { id: "low" }], defaultEffort: "low" }
|
|
503
|
+
});
|
|
504
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
505
|
+
assert.equal(result.ok, true);
|
|
506
|
+
for (const options of captured) {
|
|
507
|
+
assert.equal(options.reasoningEffort, "high", "supported configured value untouched");
|
|
508
|
+
}
|
|
509
|
+
store.close();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("defaultEffort trap: capability query failure falls back to configured effort (retry still guards)", async () => {
|
|
513
|
+
const store = createStore(":memory:");
|
|
514
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
515
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
516
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
517
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
518
|
+
const calls = [];
|
|
519
|
+
const warnings = [];
|
|
520
|
+
const ctx = {
|
|
521
|
+
logger: { warn: (m) => warnings.push(String(m)) },
|
|
522
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
523
|
+
llm: {
|
|
524
|
+
async *stream(options) {
|
|
525
|
+
calls.push(options);
|
|
526
|
+
if (options.reasoningEffort) {
|
|
527
|
+
throw new Error("UNSUPPORTED_REASONING_EFFORT: mock does not support reasoning effort \"high\"");
|
|
528
|
+
}
|
|
529
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
530
|
+
if (userText.startsWith("id=")) {
|
|
531
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
532
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
533
|
+
]) };
|
|
534
|
+
} else {
|
|
535
|
+
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
536
|
+
}
|
|
537
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
538
|
+
},
|
|
539
|
+
// Adapter knows nothing about the model — helper must not crash, and the
|
|
540
|
+
// configured effort flows through so withEffortFallback still retries.
|
|
541
|
+
resolveModelInfo: async () => { throw new Error("adapter not reachable"); }
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
545
|
+
assert.equal(result.ok, true, "run succeeds via the no-effort retry");
|
|
546
|
+
assert.equal(calls[0].reasoningEffort, "high", "configured effort forwarded when capability query fails");
|
|
547
|
+
assert.equal("reasoningEffort" in calls[1], false, "rejected effort retried without the field");
|
|
548
|
+
assert.ok(warnings.some((w) => w.includes("resolveModelInfo failed")), "capability-query failure is logged");
|
|
549
|
+
store.close();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("defaultEffort trap: sleep conflict pass remaps a poison effort too", async () => {
|
|
553
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
554
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
555
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
556
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
557
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
558
|
+
const captured = [];
|
|
559
|
+
const ctx = sleepCtx(
|
|
560
|
+
(userText) => userText.startsWith("候选冲突")
|
|
561
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
562
|
+
: "[]",
|
|
563
|
+
{ provider: "mock", model: "sleep-model" },
|
|
564
|
+
captured
|
|
565
|
+
);
|
|
566
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
567
|
+
provider,
|
|
568
|
+
model,
|
|
569
|
+
reasoning: { efforts: [{ id: "medium" }, { id: "high" }], defaultEffort: "low" }
|
|
570
|
+
});
|
|
571
|
+
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "low" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
572
|
+
assert.equal(result.status, "ok");
|
|
573
|
+
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
574
|
+
for (const options of captured) {
|
|
575
|
+
assert.equal(options.reasoningEffort, "medium", "poison 'low' remapped on sleep passes too");
|
|
576
|
+
}
|
|
577
|
+
store.close();
|
|
578
|
+
});
|