@modusensus/dsh-mneme 0.7.5 → 0.7.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/.github/workflows/publish.yml +58 -0
- package/CHANGELOG.md +2 -0
- package/README.md +18 -13
- package/SECURITY.md +3 -3
- package/dsh-mneme/CHANGELOG.md +28 -0
- package/dsh-mneme/README.md +11 -8
- package/dsh-mneme/lib/config.js +9 -0
- package/dsh-mneme/lib/dream/sleep.js +121 -0
- package/dsh-mneme/lib/service.js +41 -0
- package/dsh-mneme/lib/store.js +79 -0
- package/dsh-mneme/lib/tools.js +24 -18
- package/dsh-mneme/package-lock.json +2 -2
- package/dsh-mneme/package.json +1 -1
- package/dsh-mneme/scripts/sync-lib.js +3 -3
- package/dsh-mneme/src/client.js +2050 -0
- package/dsh-mneme/src/config.js +9 -0
- package/dsh-mneme/src/dream/sleep.js +121 -0
- package/dsh-mneme/src/service.js +41 -0
- package/dsh-mneme/src/store.js +79 -0
- package/dsh-mneme/src/tools.js +24 -18
- package/dsh-mneme/test/client.test.js +10 -5
- package/dsh-mneme/test/sleep.test.js +159 -0
- package/dsh-mneme/test/tools.test.js +133 -0
- package/package.json +1 -1
|
@@ -398,6 +398,139 @@ test("memory_delete on missing id returns deleted:false", async () => {
|
|
|
398
398
|
assert.equal(result.deleted, false);
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
+
// --- issue #48: id resolution via unique prefix (delete/update/forget/archive) --
|
|
402
|
+
|
|
403
|
+
test("memory_delete accepts a unique id prefix", async () => {
|
|
404
|
+
const id = "a1b2c3d4-0000-0000-0000-000000000001";
|
|
405
|
+
const { registered, store } = setup();
|
|
406
|
+
store.save({ id, type: "decision", title: "t", content: "c" });
|
|
407
|
+
const del = registered.find((t) => t.name === "memory_delete");
|
|
408
|
+
const result = await del.execute({ id: "a1b2c3d4" });
|
|
409
|
+
assert.equal(result.deleted, true);
|
|
410
|
+
assert.equal(store.getById(id), undefined, "entry is gone");
|
|
411
|
+
assert.equal(store.count(), 0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("memory_delete rejects an ambiguous id prefix without deleting", async () => {
|
|
415
|
+
const { registered, store } = setup();
|
|
416
|
+
store.save({ id: "deadbeef-0000-0000-0000-000000000001", type: "decision", title: "t1", content: "c" });
|
|
417
|
+
store.save({ id: "deadbeef-0000-0000-0000-000000000002", type: "decision", title: "t2", content: "c" });
|
|
418
|
+
const del = registered.find((t) => t.name === "memory_delete");
|
|
419
|
+
await assert.rejects(() => del.execute({ id: "deadbeef" }), /matches 2 entries.*refusing to guess/s);
|
|
420
|
+
assert.equal(store.count(), 2, "ambiguous prefix removes nothing");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("memory_delete with a non-matching id prefix returns deleted:false and leaves data", async () => {
|
|
424
|
+
const { registered, store } = setup();
|
|
425
|
+
store.save({ id: "a1b2c3d4-0000-0000-0000-000000000001", type: "decision", title: "t", content: "c" });
|
|
426
|
+
const del = registered.find((t) => t.name === "memory_delete");
|
|
427
|
+
const result = await del.execute({ id: "ffffffff" });
|
|
428
|
+
assert.equal(result.deleted, false);
|
|
429
|
+
assert.equal(store.count(), 1, "nothing removed when the prefix matches nothing");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("memory_update accepts a unique id prefix", async () => {
|
|
433
|
+
const { registered, service, store } = setup();
|
|
434
|
+
const { memory } = service.saveWithDedupe({ type: "decision", title: "t", content: "c" });
|
|
435
|
+
const update = registered.find((t) => t.name === "memory_update");
|
|
436
|
+
const result = await update.execute({ id: memory.id.slice(0, 8), content: "updated via prefix" });
|
|
437
|
+
assert.equal(result.memory.id, memory.id, "resolved to the full id");
|
|
438
|
+
assert.equal(store.getById(memory.id).content, "updated via prefix");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("memory_update rejects an ambiguous id prefix", async () => {
|
|
442
|
+
const { registered, store } = setup();
|
|
443
|
+
store.save({ id: "cafe1234-0000-0000-0000-000000000001", type: "decision", title: "t1", content: "c" });
|
|
444
|
+
store.save({ id: "cafe1234-0000-0000-0000-000000000002", type: "decision", title: "t2", content: "c" });
|
|
445
|
+
const update = registered.find((t) => t.name === "memory_update");
|
|
446
|
+
await assert.rejects(() => update.execute({ id: "cafe1234", content: "x" }), /matches 2 entries.*refusing to guess/s);
|
|
447
|
+
assert.equal(store.getById("cafe1234-0000-0000-0000-000000000001").content, "c");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("memory_forget accepts a unique id prefix and restores it", async () => {
|
|
451
|
+
const id = "facade01-0000-0000-0000-000000000001";
|
|
452
|
+
const { registered, store } = setup();
|
|
453
|
+
store.save({ id, type: "project", title: "t", content: "c", importance: 5 });
|
|
454
|
+
const forget = registered.find((t) => t.name === "memory_forget");
|
|
455
|
+
const result = await forget.execute({ id: "facade01" });
|
|
456
|
+
assert.equal(result.memory.id, id, "resolved to the full id");
|
|
457
|
+
assert.equal(result.memory.forgotten, true);
|
|
458
|
+
assert.equal(store.getById(id).forgotten, true);
|
|
459
|
+
const restored = await forget.execute({ id: "facade01", forgotten: false });
|
|
460
|
+
assert.equal(restored.memory.forgotten, false);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("memory_archive accepts a unique id prefix and restores it", async () => {
|
|
464
|
+
const id = "arcade01-0000-0000-0000-000000000001";
|
|
465
|
+
const { registered, service, store } = setup();
|
|
466
|
+
store.save({ id, type: "project", title: "t", content: "c" });
|
|
467
|
+
const archive = registered.find((t) => t.name === "memory_archive");
|
|
468
|
+
const result = await archive.execute({ id: "arcade01" });
|
|
469
|
+
assert.equal(result.memory.id, id, "resolved to the full id");
|
|
470
|
+
assert.equal(result.memory.archived, true);
|
|
471
|
+
assert.ok(!service.list({ type: "project" }).some((x) => x.id === id), "archived hidden from default list");
|
|
472
|
+
const restored = await archive.execute({ id: "arcade01", archived: false });
|
|
473
|
+
assert.equal(restored.memory.archived, false);
|
|
474
|
+
assert.ok(service.list({ type: "project" }).some((x) => x.id === id), "restored entry visible again");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Exact id match must win even when the same string is also a prefix of
|
|
478
|
+
// another (custom/unequal-length ids): a full id is never "ambiguous" with
|
|
479
|
+
// a longer one that merely starts with it.
|
|
480
|
+
test("memory_delete prefers an exact id match over an identical-prefix collision", async () => {
|
|
481
|
+
const { registered, store } = setup();
|
|
482
|
+
store.save({ id: "abc", type: "decision", title: "short", content: "c" });
|
|
483
|
+
store.save({ id: "abc-def-000000000000000000000000001", type: "decision", title: "long", content: "c" });
|
|
484
|
+
const del = registered.find((t) => t.name === "memory_delete");
|
|
485
|
+
const result = await del.execute({ id: "abc" });
|
|
486
|
+
assert.equal(result.deleted, true);
|
|
487
|
+
assert.equal(store.getById("abc"), undefined, "exact-match entry removed");
|
|
488
|
+
assert.equal(store.count(), 1, "the longer id sharing the prefix stays");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// A wildcard-only id ("%", "_") strips to an empty prefix; it must never
|
|
492
|
+
// degrade into a `LIKE '%'` table-wide match that deletes a lone memory.
|
|
493
|
+
test("memory_delete with a wildcard-only id matches nothing and deletes nothing", async () => {
|
|
494
|
+
const { registered, store } = setup();
|
|
495
|
+
store.save({ id: "a1b2c3d4-0000-0000-0000-000000000001", type: "decision", title: "t", content: "c" });
|
|
496
|
+
const del = registered.find((t) => t.name === "memory_delete");
|
|
497
|
+
for (const wildcard of ["%", "_", "%_", "%%", "___"]) {
|
|
498
|
+
const result = await del.execute({ id: wildcard });
|
|
499
|
+
assert.equal(result.deleted, false, `id "${wildcard}" must not match the whole table`);
|
|
500
|
+
}
|
|
501
|
+
assert.equal(store.count(), 1, "nothing removed by wildcard-only ids");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Hand-copied ids can carry surrounding whitespace; resolve against the
|
|
505
|
+
// trimmed form so a padded id still reaches the right memory.
|
|
506
|
+
test("memory_delete trims surrounding whitespace from the id", async () => {
|
|
507
|
+
const id = "a1b2c3d4-0000-0000-0000-000000000001";
|
|
508
|
+
const { registered, store } = setup();
|
|
509
|
+
store.save({ id, type: "decision", title: "t", content: "c" });
|
|
510
|
+
const del = registered.find((t) => t.name === "memory_delete");
|
|
511
|
+
const result = await del.execute({ id: ` ${id} ` });
|
|
512
|
+
assert.equal(result.deleted, true);
|
|
513
|
+
assert.equal(store.count(), 0);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// issue #48 observability: an unmatched id on memory_delete is logged (warn)
|
|
517
|
+
// instead of silently vanishing — the logger is optional, but when present the
|
|
518
|
+
// service layer records the silent-miss case.
|
|
519
|
+
test("memory_delete logs a warn when the id matches nothing", async () => {
|
|
520
|
+
const warns = [];
|
|
521
|
+
const store = createStore(":memory:");
|
|
522
|
+
const service = createService({ store, mirror: null, config: {}, logger: { warn: (msg) => warns.push(msg) } });
|
|
523
|
+
store.save({ id: "a1b2c3d4-0000-0000-0000-000000000001", type: "decision", title: "t", content: "c" });
|
|
524
|
+
const registered = [];
|
|
525
|
+
const ctx = { tools: { register(def) { registered.push(def); return () => {}; } } };
|
|
526
|
+
createTools(ctx, service, {}, undefined);
|
|
527
|
+
const del = registered.find((t) => t.name === "memory_delete");
|
|
528
|
+
const result = await del.execute({ id: "ffffffff" });
|
|
529
|
+
assert.equal(result.deleted, false);
|
|
530
|
+
assert.equal(warns.length, 1, "one warn for the silent-miss case");
|
|
531
|
+
assert.match(warns[0], /matched nothing/);
|
|
532
|
+
});
|
|
533
|
+
|
|
401
534
|
test("memory_delete by query deletes the best match (no id round-trip)", async () => {
|
|
402
535
|
const { registered, service, store } = setup();
|
|
403
536
|
const { memory } = service.saveWithDedupe({ type: "preference", title: "喜欢 Rust", content: "用户偏好 Rust 优先于 Go", importance: 4 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Structured memory engine for DeepSeek Harness. Offline semantic search, entity-attribute-timeline, autoDream self-consolidation, and human-editable Markdown storage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|