@modusensus/dsh-mneme 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/lib/dream.js +34 -6
- package/lib/store.js +1 -1
- package/package.json +1 -1
- package/src/dream.js +34 -6
- package/src/store.js +1 -1
- package/test/audit.test.js +66 -0
- package/test/dream.test.js +21 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
9
9
|
|
|
10
10
|
> 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
|
|
11
11
|
|
|
@@ -225,7 +225,7 @@ src/
|
|
|
225
225
|
lib/
|
|
226
226
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
227
227
|
└── *.js # src 的同步分发产物
|
|
228
|
-
test/ #
|
|
228
|
+
test/ # 263 个 node:test 测试(含审计与三轴线压测不变量)
|
|
229
229
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步
|
|
230
230
|
```
|
|
231
231
|
|
|
@@ -234,7 +234,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
234
234
|
```bash
|
|
235
235
|
cd dsh-mneme
|
|
236
236
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
237
|
-
npm test # 运行
|
|
237
|
+
npm test # 运行 263 个测试
|
|
238
238
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
239
239
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
240
240
|
```
|
package/lib/dream.js
CHANGED
|
@@ -68,7 +68,7 @@ export function parseReceipt(receipt) {
|
|
|
68
68
|
// reconcile = decisions validated but one or more did not commit (CAS
|
|
69
69
|
// conflict / transaction rollback) — the store diverges from the decision
|
|
70
70
|
// list and the run must be reconciled, never reported as a fake ok.
|
|
71
|
-
if (!runId || !/^(ok|
|
|
71
|
+
if (!runId || !/^(ok|noop|degraded|reconcile|failed)$/.test(status)) return undefined;
|
|
72
72
|
const count = Number(inputCount);
|
|
73
73
|
const appliedN = Number(applied);
|
|
74
74
|
if (!Number.isInteger(count) || !Number.isInteger(appliedN)) return undefined;
|
|
@@ -286,9 +286,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
286
286
|
// replayable too.
|
|
287
287
|
const finish = (result) => {
|
|
288
288
|
// status is derived from what actually committed: ok only when the full
|
|
289
|
-
// decision list landed
|
|
290
|
-
//
|
|
291
|
-
//
|
|
289
|
+
// decision list landed (or a summary was refreshed); noop when nothing
|
|
290
|
+
// changed; degraded when real changes landed without a summary;
|
|
291
|
+
// reconcile when decisions were validated but some did not commit (CAS
|
|
292
|
+
// conflict / rollback); failed on any LLM/validation error. No fake "ok"
|
|
293
|
+
// for an empty or partial run.
|
|
292
294
|
const status = result.status ?? (result.ok ? "ok" : "failed");
|
|
293
295
|
const applied = result.applied ?? 0;
|
|
294
296
|
const summaryStored = result.summary ?? false;
|
|
@@ -432,6 +434,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
432
434
|
const outcome = { ...buildOutcome(committed), conflicts, failures };
|
|
433
435
|
// Decisions validated but not fully committed → reconcile (not ok).
|
|
434
436
|
const partial = conflicts.length > 0 || failures.length > 0;
|
|
437
|
+
// No decision landed (all-keep, or every decision skipped as an idempotent
|
|
438
|
+
// replay) → nothing substantive changed. Distinct from a success: such a
|
|
439
|
+
// run must never be reported as ok, or the audit claims work that never
|
|
440
|
+
// happened and the scheduler refreshes the baseline on a false positive.
|
|
441
|
+
const noChange = applied === 0 && committed.every((c) => c.action === "keep");
|
|
435
442
|
|
|
436
443
|
// Keep the vector index consistent with the post-dream store state.
|
|
437
444
|
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
@@ -476,9 +483,30 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
476
483
|
} catch { /* best-effort */ }
|
|
477
484
|
}
|
|
478
485
|
}
|
|
486
|
+
// Honest status assignment (never a fake ok):
|
|
487
|
+
// reconcile — some decisions validated but did not commit (CAS/rollback).
|
|
488
|
+
// noop — nothing changed and no summary persisted: truly an empty
|
|
489
|
+
// run. ok:false keeps the scheduler from moving the baseline.
|
|
490
|
+
// ok — either real changes landed, or a fresh summary was stored
|
|
491
|
+
// (all-keep + summary is a substantive summary refresh).
|
|
492
|
+
// degraded — real consolidation landed but the summary came back empty/
|
|
493
|
+
// missing: the store was absorbed (ok for the baseline) but
|
|
494
|
+
// the run did not produce its full output (marked, not faked).
|
|
495
|
+
let status;
|
|
496
|
+
let okResult;
|
|
497
|
+
if (partial) {
|
|
498
|
+
status = "reconcile";
|
|
499
|
+
okResult = false;
|
|
500
|
+
} else if (noChange) {
|
|
501
|
+
status = summaryStored ? "ok" : "noop";
|
|
502
|
+
okResult = summaryStored;
|
|
503
|
+
} else {
|
|
504
|
+
status = summaryStored ? "ok" : "degraded";
|
|
505
|
+
okResult = true;
|
|
506
|
+
}
|
|
479
507
|
return finish({
|
|
480
|
-
ok:
|
|
481
|
-
status
|
|
508
|
+
ok: okResult,
|
|
509
|
+
status,
|
|
482
510
|
applied,
|
|
483
511
|
decisions: auditDecisions,
|
|
484
512
|
outcome,
|
package/lib/store.js
CHANGED
|
@@ -26,7 +26,7 @@ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
|
26
26
|
CREATE TABLE IF NOT EXISTS dream_runs (
|
|
27
27
|
id TEXT PRIMARY KEY,
|
|
28
28
|
created_at TEXT NOT NULL,
|
|
29
|
-
status TEXT NOT NULL, -- ok | failed
|
|
29
|
+
status TEXT NOT NULL, -- ok | noop | degraded | reconcile | failed
|
|
30
30
|
error TEXT,
|
|
31
31
|
provider TEXT,
|
|
32
32
|
model TEXT,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 6 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.7",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
package/src/dream.js
CHANGED
|
@@ -68,7 +68,7 @@ export function parseReceipt(receipt) {
|
|
|
68
68
|
// reconcile = decisions validated but one or more did not commit (CAS
|
|
69
69
|
// conflict / transaction rollback) — the store diverges from the decision
|
|
70
70
|
// list and the run must be reconciled, never reported as a fake ok.
|
|
71
|
-
if (!runId || !/^(ok|
|
|
71
|
+
if (!runId || !/^(ok|noop|degraded|reconcile|failed)$/.test(status)) return undefined;
|
|
72
72
|
const count = Number(inputCount);
|
|
73
73
|
const appliedN = Number(applied);
|
|
74
74
|
if (!Number.isInteger(count) || !Number.isInteger(appliedN)) return undefined;
|
|
@@ -286,9 +286,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
286
286
|
// replayable too.
|
|
287
287
|
const finish = (result) => {
|
|
288
288
|
// status is derived from what actually committed: ok only when the full
|
|
289
|
-
// decision list landed
|
|
290
|
-
//
|
|
291
|
-
//
|
|
289
|
+
// decision list landed (or a summary was refreshed); noop when nothing
|
|
290
|
+
// changed; degraded when real changes landed without a summary;
|
|
291
|
+
// reconcile when decisions were validated but some did not commit (CAS
|
|
292
|
+
// conflict / rollback); failed on any LLM/validation error. No fake "ok"
|
|
293
|
+
// for an empty or partial run.
|
|
292
294
|
const status = result.status ?? (result.ok ? "ok" : "failed");
|
|
293
295
|
const applied = result.applied ?? 0;
|
|
294
296
|
const summaryStored = result.summary ?? false;
|
|
@@ -432,6 +434,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
432
434
|
const outcome = { ...buildOutcome(committed), conflicts, failures };
|
|
433
435
|
// Decisions validated but not fully committed → reconcile (not ok).
|
|
434
436
|
const partial = conflicts.length > 0 || failures.length > 0;
|
|
437
|
+
// No decision landed (all-keep, or every decision skipped as an idempotent
|
|
438
|
+
// replay) → nothing substantive changed. Distinct from a success: such a
|
|
439
|
+
// run must never be reported as ok, or the audit claims work that never
|
|
440
|
+
// happened and the scheduler refreshes the baseline on a false positive.
|
|
441
|
+
const noChange = applied === 0 && committed.every((c) => c.action === "keep");
|
|
435
442
|
|
|
436
443
|
// Keep the vector index consistent with the post-dream store state.
|
|
437
444
|
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
@@ -476,9 +483,30 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
476
483
|
} catch { /* best-effort */ }
|
|
477
484
|
}
|
|
478
485
|
}
|
|
486
|
+
// Honest status assignment (never a fake ok):
|
|
487
|
+
// reconcile — some decisions validated but did not commit (CAS/rollback).
|
|
488
|
+
// noop — nothing changed and no summary persisted: truly an empty
|
|
489
|
+
// run. ok:false keeps the scheduler from moving the baseline.
|
|
490
|
+
// ok — either real changes landed, or a fresh summary was stored
|
|
491
|
+
// (all-keep + summary is a substantive summary refresh).
|
|
492
|
+
// degraded — real consolidation landed but the summary came back empty/
|
|
493
|
+
// missing: the store was absorbed (ok for the baseline) but
|
|
494
|
+
// the run did not produce its full output (marked, not faked).
|
|
495
|
+
let status;
|
|
496
|
+
let okResult;
|
|
497
|
+
if (partial) {
|
|
498
|
+
status = "reconcile";
|
|
499
|
+
okResult = false;
|
|
500
|
+
} else if (noChange) {
|
|
501
|
+
status = summaryStored ? "ok" : "noop";
|
|
502
|
+
okResult = summaryStored;
|
|
503
|
+
} else {
|
|
504
|
+
status = summaryStored ? "ok" : "degraded";
|
|
505
|
+
okResult = true;
|
|
506
|
+
}
|
|
479
507
|
return finish({
|
|
480
|
-
ok:
|
|
481
|
-
status
|
|
508
|
+
ok: okResult,
|
|
509
|
+
status,
|
|
482
510
|
applied,
|
|
483
511
|
decisions: auditDecisions,
|
|
484
512
|
outcome,
|
package/src/store.js
CHANGED
|
@@ -26,7 +26,7 @@ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
|
26
26
|
CREATE TABLE IF NOT EXISTS dream_runs (
|
|
27
27
|
id TEXT PRIMARY KEY,
|
|
28
28
|
created_at TEXT NOT NULL,
|
|
29
|
-
status TEXT NOT NULL, -- ok | failed
|
|
29
|
+
status TEXT NOT NULL, -- ok | noop | degraded | reconcile | failed
|
|
30
30
|
error TEXT,
|
|
31
31
|
provider TEXT,
|
|
32
32
|
model TEXT,
|
package/test/audit.test.js
CHANGED
|
@@ -288,3 +288,69 @@ test("buildOutcome over actually-committed sub-steps never claims a rolled-back
|
|
|
288
288
|
assert.equal(outcome.byId.m2, "merge-archived");
|
|
289
289
|
assert.equal(outcome.byId.k, "keep");
|
|
290
290
|
});
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------- noop & degraded (F-03)
|
|
293
|
+
|
|
294
|
+
test("runDream records status noop when all decisions are keep and summary is empty", async () => {
|
|
295
|
+
const { store, service, dream } = setup();
|
|
296
|
+
const { memory: m } = service.saveWithDedupe({ type: "preference", title: "语言", content: "中文" });
|
|
297
|
+
const ctx = mockCtx({
|
|
298
|
+
onConsolidation: () => JSON.stringify([{ action: "keep", ids: [m.id] }]),
|
|
299
|
+
summaryText: ""
|
|
300
|
+
});
|
|
301
|
+
const result = await dream.runDream(ctx, service, {});
|
|
302
|
+
assert.equal(result.ok, false, "no-change + empty summary is not a success");
|
|
303
|
+
assert.equal(result.status, "noop", "explicit noop status");
|
|
304
|
+
assert.equal(result.applied, 0);
|
|
305
|
+
assert.equal(result.summary, false);
|
|
306
|
+
|
|
307
|
+
const run = store.listDreamRuns()[0];
|
|
308
|
+
assert.equal(run.status, "noop", "audit row records noop, never ok");
|
|
309
|
+
assert.equal(run.applied, 0);
|
|
310
|
+
assert.equal(run.summary_stored, false);
|
|
311
|
+
const parsed = parseReceipt(run.receipt);
|
|
312
|
+
assert.equal(parsed.status, "noop", "receipt records noop");
|
|
313
|
+
assert.equal(parsed.applied, 0);
|
|
314
|
+
assert.equal(parsed.summaryStored, false);
|
|
315
|
+
assert.equal(run.outcome.byId[m.id], "keep", "keep disposition still recorded for replay");
|
|
316
|
+
store.close();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("runDream records status ok when all decisions are keep but a summary was stored", async () => {
|
|
320
|
+
const { store, service, dream } = setup();
|
|
321
|
+
const { memory: m } = service.saveWithDedupe({ type: "preference", title: "语言", content: "中文" });
|
|
322
|
+
const ctx = mockCtx({ onConsolidation: () => JSON.stringify([{ action: "keep", ids: [m.id] }]) });
|
|
323
|
+
const result = await dream.runDream(ctx, service, {});
|
|
324
|
+
assert.equal(result.status, "ok", "summary refresh is substantive output");
|
|
325
|
+
assert.equal(result.ok, true);
|
|
326
|
+
assert.equal(result.applied, 0);
|
|
327
|
+
assert.equal(result.summary, true);
|
|
328
|
+
const run = store.listDreamRuns()[0];
|
|
329
|
+
assert.equal(run.status, "ok");
|
|
330
|
+
assert.equal(parseReceipt(run.receipt).status, "ok");
|
|
331
|
+
assert.equal(parseReceipt(run.receipt).summaryStored, true);
|
|
332
|
+
store.close();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("runDream records status degraded when changes land but the summary is empty", async () => {
|
|
336
|
+
const { store, service, dream } = setup();
|
|
337
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "旧", content: "A", importance: 3 });
|
|
338
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "新", content: "B", importance: 4 });
|
|
339
|
+
const ctx = mockCtx({
|
|
340
|
+
onConsolidation: () => JSON.stringify([
|
|
341
|
+
{ action: "merge", ids: [a.id, b.id], title: "合并", content: "合并内容", importance: 4, keepSource: b.id }
|
|
342
|
+
]),
|
|
343
|
+
summaryText: ""
|
|
344
|
+
});
|
|
345
|
+
const result = await dream.runDream(ctx, service, {});
|
|
346
|
+
assert.equal(result.status, "degraded", "real changes without a summary are not a clean ok");
|
|
347
|
+
assert.equal(result.ok, true, "consolidation landed so the baseline may advance");
|
|
348
|
+
assert.equal(result.applied, 1);
|
|
349
|
+
assert.equal(result.summary, false, "summary honestly reported missing");
|
|
350
|
+
const run = store.listDreamRuns()[0];
|
|
351
|
+
assert.equal(run.status, "degraded", "audit row records degraded");
|
|
352
|
+
assert.equal(run.summary_stored, false);
|
|
353
|
+
assert.equal(parseReceipt(run.receipt).status, "degraded");
|
|
354
|
+
assert.equal(parseReceipt(run.receipt).summaryStored, false);
|
|
355
|
+
store.close();
|
|
356
|
+
});
|
package/test/dream.test.js
CHANGED
|
@@ -296,6 +296,27 @@ test("failed run does not refresh baseline: next write re-triggers", async () =>
|
|
|
296
296
|
store.close();
|
|
297
297
|
});
|
|
298
298
|
|
|
299
|
+
test("noop run (nothing changed) does not refresh baseline: next write re-triggers", async () => {
|
|
300
|
+
const { store, service } = dreamSetup();
|
|
301
|
+
let calls = 0;
|
|
302
|
+
const dream = createDreamScheduler({
|
|
303
|
+
onRun: async () => { calls++; return { ok: false, status: "noop", applied: 0, summary: false }; },
|
|
304
|
+
thresholdCount: 2, thresholdChars: 5000, delayMs: 5,
|
|
305
|
+
logger: { warn: () => {} }
|
|
306
|
+
});
|
|
307
|
+
service.saveWithDedupe({ type: "project", title: "a", content: "x".repeat(10) });
|
|
308
|
+
service.saveWithDedupe({ type: "project", title: "b", content: "y".repeat(10) });
|
|
309
|
+
assert.equal(dream.maybeSchedule(service), true, "scheduled");
|
|
310
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
311
|
+
assert.equal(calls, 1, "run attempted once");
|
|
312
|
+
// a noop is not a success: the baseline stays put so the accumulated writes
|
|
313
|
+
// are still owed and the next write re-schedules instead of being absorbed
|
|
314
|
+
assert.equal(dream.maybeSchedule(service), true, "noop keeps baseline, re-schedules");
|
|
315
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
316
|
+
assert.equal(calls, 2, "retried after noop");
|
|
317
|
+
store.close();
|
|
318
|
+
});
|
|
319
|
+
|
|
299
320
|
test("throwing run does not refresh baseline and is logged", async () => {
|
|
300
321
|
const { store, service } = dreamSetup();
|
|
301
322
|
const warnings = [];
|