@modusensus/dsh-mneme 0.2.5 → 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 CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
6
6
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
7
  [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
8
- [![tests](https://img.shields.io/badge/tests-258%20passed-success)](https://github.com/modusensus/dsh-mneme)
8
+ [![tests](https://img.shields.io/badge/tests-263%20passed-success)](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/ # 258 个 node: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 # 运行 258 个测试
237
+ npm test # 运行 263 个测试
238
238
  npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
239
239
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
240
240
  ```
package/lib/client.js CHANGED
@@ -178,7 +178,7 @@ window.__ModuleLoader__.load({
178
178
 
179
179
  const tabs = ["all", "preference", "project", "decision", "history"];
180
180
 
181
- const body = react.createElement("div", { style: embedded ? { ...styles.panel, width: "100%", maxWidth: "100%", maxHeight: "none" } : styles.panel },
181
+ const body = react.createElement("div", { style: embedded ? { width: "100%", display: "flex", flexDirection: "column" } : styles.panel },
182
182
  !embedded && react.createElement("div", { style: styles.header },
183
183
  react.createElement("span", { style: styles.title }, t("memory.panel.title")),
184
184
  react.createElement("button", { style: styles.close, onClick: onClose }, "×")
@@ -365,7 +365,7 @@ window.__ModuleLoader__.load({
365
365
  const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
366
366
  const rowStyle = { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)" };
367
367
 
368
- const body = h("div", { style: embedded ? { ...styles.panel, width: "100%", maxWidth: "100%", maxHeight: "none" } : styles.panel },
368
+ const body = h("div", { style: embedded ? { width: "100%", display: "flex", flexDirection: "column" } : styles.panel },
369
369
  !embedded && h("div", { style: styles.header },
370
370
  h("span", { style: styles.title }, t("memory.settings.title")),
371
371
  h("button", { style: styles.close, onClick: onClose }, "×")
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|failed|reconcile)$/.test(status)) return undefined;
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; reconcile when decisions were validated but some
290
- // did not commit (CAS conflict / rollback); failed on any LLM/validation
291
- // error. No fake "ok" for a partial commit.
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: !partial,
481
- status: partial ? "reconcile" : "ok",
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.5",
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|failed|reconcile)$/.test(status)) return undefined;
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; reconcile when decisions were validated but some
290
- // did not commit (CAS conflict / rollback); failed on any LLM/validation
291
- // error. No fake "ok" for a partial commit.
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: !partial,
481
- status: partial ? "reconcile" : "ok",
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,
@@ -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
+ });
@@ -24,3 +24,21 @@ test("client bundle is lib-only with no src counterpart", () => {
24
24
  assert.equal(existsSync(join(root, "src/client.js")), false, "src/ must not contain client.js");
25
25
  assert.equal(existsSync(join(root, "lib/client.js")), true, "lib/client.js must exist");
26
26
  });
27
+
28
+ // Both panels render in two modes: a portal modal (styles.panel, full chrome)
29
+ // and an embedded settings-section variant. The embedded variant must drop the
30
+ // modal-only chrome (background, radius, padding, shadow) — the settings host
31
+ // already provides its own surface, so reusing the modal panel style makes the
32
+ // section render as a floating card with an inset box and shadow inside the
33
+ // settings page.
34
+ test("embedded variant drops the modal panel chrome", () => {
35
+ const branches = clientSource.match(/embedded \? \{[^}]+\}/g);
36
+ assert.ok(branches, "panels must branch on the embedded flag");
37
+ assert.equal(branches.length, 2, "both MemoryPanel and SettingsPanel must branch");
38
+ for (const branch of branches) {
39
+ assert.equal(branch.includes("styles.panel"), false, "embedded branch must not reuse the modal panel style");
40
+ for (const chrome of ["boxShadow", "background", "borderRadius", "padding"]) {
41
+ assert.equal(branch.includes(chrome), false, `embedded branch must not carry ${chrome}`);
42
+ }
43
+ }
44
+ });
@@ -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 = [];