@modusensus/dsh-mneme 0.6.11 → 0.7.1
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/test.yml +32 -0
- package/CHANGELOG.md +89 -0
- package/CONTRIBUTING.md +245 -0
- package/README.md +152 -398
- package/SECURITY.md +544 -0
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +247 -0
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +145 -0
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +56 -0
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +77 -0
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +64 -0
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +90 -0
- package/dsh-mneme/CHANGELOG.md +295 -0
- package/dsh-mneme/LICENSE +21 -0
- package/dsh-mneme/README.md +484 -0
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +183 -0
- package/dsh-mneme/docs/ENTITIES.md +245 -0
- package/dsh-mneme/docs/LOCAL_MODEL.md +141 -0
- package/dsh-mneme/docs/MIGRATION.md +127 -0
- package/dsh-mneme/docs/SEMANTIC.md +256 -0
- package/dsh-mneme/docs/SLEEP.md +163 -0
- package/{src → dsh-mneme/lib}/api.js +10 -4
- package/{lib → dsh-mneme/lib}/client.js +10 -2
- package/{lib → dsh-mneme/lib}/config.js +17 -0
- package/{lib → dsh-mneme/lib}/dream/sleep.js +13 -1
- package/{lib → dsh-mneme/lib}/dream.js +7 -2
- package/dsh-mneme/lib/heat.js +136 -0
- package/{lib → dsh-mneme/lib}/index.js +14 -6
- package/{src → dsh-mneme/lib}/service.js +100 -11
- package/{lib → dsh-mneme/lib}/settings.js +15 -6
- package/{lib → dsh-mneme/lib}/store.js +22 -0
- package/dsh-mneme/package-lock.json +1936 -0
- package/dsh-mneme/package.json +80 -0
- package/{lib → dsh-mneme/src}/api.js +10 -4
- package/{src → dsh-mneme/src}/config.js +17 -0
- package/{src → dsh-mneme/src}/dream/sleep.js +13 -1
- package/{src → dsh-mneme/src}/dream.js +7 -2
- package/dsh-mneme/src/heat.js +136 -0
- package/{src → dsh-mneme/src}/index.js +14 -6
- package/{lib → dsh-mneme/src}/service.js +100 -11
- package/{src → dsh-mneme/src}/settings.js +15 -6
- package/{src → dsh-mneme/src}/store.js +22 -0
- package/{test → dsh-mneme/test}/api.test.js +10 -3
- package/{test → dsh-mneme/test}/graph-api.test.js +34 -0
- package/dsh-mneme/test/heat.test.js +148 -0
- package/{test → dsh-mneme/test}/recall-layer.test.js +26 -5
- package/dsh-mneme/test/recall-runs.test.js +93 -0
- package/{test → dsh-mneme/test}/settings.test.js +6 -2
- package/dsh-mneme/test/sleep-heat.test.js +112 -0
- package/{test → dsh-mneme/test}/sleep.test.js +12 -4
- package/{test → dsh-mneme/test}/tag.test.js +114 -0
- package/dsh-mneme/test/updated-at-semantics.test.js +113 -0
- package/package.json +18 -40
- package/v0.7-plan-2026-08-21.md +75 -0
- package//346/250/252/345/271/205.png +0 -0
- /package/{cordis.patch.yml → dsh-mneme/cordis.patch.yml} +0 -0
- /package/{lib → dsh-mneme/lib}/commands.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/clustering.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/decisions.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/tag-extractor.js +0 -0
- /package/{lib → dsh-mneme/lib}/embedding.js +0 -0
- /package/{lib → dsh-mneme/lib}/entities/extractor.js +0 -0
- /package/{lib → dsh-mneme/lib}/hot-memory.js +0 -0
- /package/{lib → dsh-mneme/lib}/inject.js +0 -0
- /package/{lib → dsh-mneme/lib}/local-embedder.js +0 -0
- /package/{lib → dsh-mneme/lib}/mirror.js +0 -0
- /package/{lib → dsh-mneme/lib}/parser/tag.js +0 -0
- /package/{lib → dsh-mneme/lib}/parser/wiki-link.js +0 -0
- /package/{lib → dsh-mneme/lib}/quality-filter.js +0 -0
- /package/{lib → dsh-mneme/lib}/reranker.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/adaptive.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/bm25.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/tag-boost.js +0 -0
- /package/{lib → dsh-mneme/lib}/summarize.js +0 -0
- /package/{lib → dsh-mneme/lib}/tools.js +0 -0
- /package/{lib → dsh-mneme/lib}/vector-index.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-embed.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-recall.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-rerank.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/e2e-dsh.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/stress-dsh.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/sync-lib.js +0 -0
- /package/{src → dsh-mneme/src}/commands.js +0 -0
- /package/{src → dsh-mneme/src}/dream/clustering.js +0 -0
- /package/{src → dsh-mneme/src}/dream/decisions.js +0 -0
- /package/{src → dsh-mneme/src}/dream/tag-extractor.js +0 -0
- /package/{src → dsh-mneme/src}/embedding.js +0 -0
- /package/{src → dsh-mneme/src}/entities/extractor.js +0 -0
- /package/{src → dsh-mneme/src}/hot-memory.js +0 -0
- /package/{src → dsh-mneme/src}/inject.js +0 -0
- /package/{src → dsh-mneme/src}/local-embedder.js +0 -0
- /package/{src → dsh-mneme/src}/mirror.js +0 -0
- /package/{src → dsh-mneme/src}/parser/tag.js +0 -0
- /package/{src → dsh-mneme/src}/parser/wiki-link.js +0 -0
- /package/{src → dsh-mneme/src}/quality-filter.js +0 -0
- /package/{src → dsh-mneme/src}/reranker.js +0 -0
- /package/{src → dsh-mneme/src}/search/adaptive.js +0 -0
- /package/{src → dsh-mneme/src}/search/bm25.js +0 -0
- /package/{src → dsh-mneme/src}/search/tag-boost.js +0 -0
- /package/{src → dsh-mneme/src}/summarize.js +0 -0
- /package/{src → dsh-mneme/src}/tools.js +0 -0
- /package/{src → dsh-mneme/src}/vector-index.js +0 -0
- /package/{test → dsh-mneme/test}/audit.test.js +0 -0
- /package/{test → dsh-mneme/test}/benchmark.test.js +0 -0
- /package/{test → dsh-mneme/test}/boundary-v0625.test.js +0 -0
- /package/{test → dsh-mneme/test}/client.test.js +0 -0
- /package/{test → dsh-mneme/test}/clustering.test.js +0 -0
- /package/{test → dsh-mneme/test}/commands.test.js +0 -0
- /package/{test → dsh-mneme/test}/config.test.js +0 -0
- /package/{test → dsh-mneme/test}/conflict-freeze.test.js +0 -0
- /package/{test → dsh-mneme/test}/directory.test.js +0 -0
- /package/{test → dsh-mneme/test}/dream.test.js +0 -0
- /package/{test → dsh-mneme/test}/entities.test.js +0 -0
- /package/{test → dsh-mneme/test}/epistemic.test.js +0 -0
- /package/{test → dsh-mneme/test}/fnew-0112.test.js +0 -0
- /package/{test → dsh-mneme/test}/fnew-03.test.js +0 -0
- /package/{test → dsh-mneme/test}/helpers/dream-mock.js +0 -0
- /package/{test → dsh-mneme/test}/hot-memory.test.js +0 -0
- /package/{test → dsh-mneme/test}/inject.test.js +0 -0
- /package/{test → dsh-mneme/test}/llm-audit.test.js +0 -0
- /package/{test → dsh-mneme/test}/local-embedder.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-dirty.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-edit-digest.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-generation.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror.test.js +0 -0
- /package/{test → dsh-mneme/test}/normalize-decisions.test.js +0 -0
- /package/{test → dsh-mneme/test}/peer-blockers.test.js +0 -0
- /package/{test → dsh-mneme/test}/policy-epoch.test.js +0 -0
- /package/{test → dsh-mneme/test}/provenance.test.js +0 -0
- /package/{test → dsh-mneme/test}/quality-filter.test.js +0 -0
- /package/{test → dsh-mneme/test}/reasoning-effort.test.js +0 -0
- /package/{test → dsh-mneme/test}/recall-evals.test.js +0 -0
- /package/{test → dsh-mneme/test}/receipt-chain.test.js +0 -0
- /package/{test → dsh-mneme/test}/reflection.test.js +0 -0
- /package/{test → dsh-mneme/test}/reranker.test.js +0 -0
- /package/{test → dsh-mneme/test}/search-fusion.test.js +0 -0
- /package/{test → dsh-mneme/test}/semantic.test.js +0 -0
- /package/{test → dsh-mneme/test}/service-search.test.js +0 -0
- /package/{test → dsh-mneme/test}/service.test.js +0 -0
- /package/{test → dsh-mneme/test}/store.test.js +0 -0
- /package/{test → dsh-mneme/test}/stress.test.js +0 -0
- /package/{test → dsh-mneme/test}/summarize.test.js +0 -0
- /package/{test → dsh-mneme/test}/tag-boost.test.js +0 -0
- /package/{test → dsh-mneme/test}/tools.test.js +0 -0
- /package/{test → dsh-mneme/test}/vector-index.test.js +0 -0
- /package/{test → dsh-mneme/test}/wiki-link.test.js +0 -0
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { createStore } from "../src/store.js";
|
|
13
13
|
import { createService } from "../src/service.js";
|
|
14
14
|
import { createMirror } from "../src/mirror.js";
|
|
15
|
+
import { createSettings } from "../src/settings.js";
|
|
15
16
|
import { parseTags, sanitizeTags, MAX_TAG_LENGTH } from "../src/parser/tag.js";
|
|
16
17
|
import { runAutoTag } from "../src/dream/tag-extractor.js";
|
|
17
18
|
import { createDreamScheduler } from "../src/dream.js";
|
|
@@ -310,3 +311,116 @@ test("service.setMemoryTags respects manualTagEnabled=false gate", () => {
|
|
|
310
311
|
assert.equal(r.ok, false);
|
|
311
312
|
assert.match(r.error, /manualTagEnabled/);
|
|
312
313
|
});
|
|
314
|
+
|
|
315
|
+
// ============================================================ issue #31 fixes
|
|
316
|
+
|
|
317
|
+
test("memory_save with tags bridges into the entity_attrs tag store (issue #31)", () => {
|
|
318
|
+
const { store, service } = makeService();
|
|
319
|
+
const { memory } = service.saveWithDedupe({
|
|
320
|
+
type: "project", title: "桥接", content: "c", tags: ["linux", "考研"]
|
|
321
|
+
});
|
|
322
|
+
assert.deepEqual(store.getMemoryTags(memory.id), ["linux", "考研"], "tool-passed tags land in entity_attrs");
|
|
323
|
+
assert.deepEqual(store.getMemoryTagsMap([memory.id]).get(memory.id), ["linux", "考研"], "tag map sees them");
|
|
324
|
+
const dir = store.getDirectory();
|
|
325
|
+
assert.deepEqual(dir.groups.map((g) => g.tag).sort(), ["linux", "考研"], "directory groups by tool-passed tags");
|
|
326
|
+
assert.equal(dir.untagged.length, 0, "not dumped in the untagged bucket");
|
|
327
|
+
assert.ok(store.findMemoriesByTags(["linux"]).some((m) => m.id === memory.id), "tag: recall finds it");
|
|
328
|
+
store.close();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("saveWithDedupe merge with tags bridges into the tag store too (issue #31)", () => {
|
|
332
|
+
const { store, service } = makeService();
|
|
333
|
+
const { memory } = service.saveWithDedupe({
|
|
334
|
+
type: "project", title: "同题", content: "第一段", tags: ["linux"]
|
|
335
|
+
});
|
|
336
|
+
assert.deepEqual(store.getMemoryTags(memory.id), ["linux"]);
|
|
337
|
+
const { action, memory: merged } = service.saveWithDedupe({
|
|
338
|
+
type: "project", title: "同题", content: "第二段", tags: ["bash"]
|
|
339
|
+
});
|
|
340
|
+
assert.equal(action, "merged");
|
|
341
|
+
assert.deepEqual(store.getMemoryTags(merged.id), ["bash"], "merge overwrites tags and bridges them");
|
|
342
|
+
store.close();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("saveWithDedupe merge with empty tags clears stale entity_attrs tags (issue #31)", () => {
|
|
346
|
+
const { store, service } = makeService();
|
|
347
|
+
const { memory } = service.saveWithDedupe({ type: "project", title: "清空", content: "第一段", tags: ["old"] });
|
|
348
|
+
assert.deepEqual(store.getMemoryTags(memory.id), ["old"]);
|
|
349
|
+
const { action, memory: merged } = service.saveWithDedupe({
|
|
350
|
+
type: "project", title: "清空", content: "第二段", tags: []
|
|
351
|
+
});
|
|
352
|
+
assert.equal(action, "merged");
|
|
353
|
+
assert.deepEqual(store.getMemoryTags(merged.id), [], "empty tags on merge invalidate the stale live row");
|
|
354
|
+
const dir = store.getDirectory();
|
|
355
|
+
assert.equal(dir.groups.length, 0, "directory no longer shows the stale tag");
|
|
356
|
+
assert.equal(dir.untagged.length, 1, "memory back to untagged");
|
|
357
|
+
store.close();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("memory_update (service.update) with tags bridges and clearing moves to untagged (issue #31)", () => {
|
|
361
|
+
const { store, service } = makeService();
|
|
362
|
+
const { memory } = service.saveWithDedupe({ type: "project", title: "更新", content: "c", tags: ["old"] });
|
|
363
|
+
const updated = service.update(memory.id, { tags: ["new", "考研"] });
|
|
364
|
+
assert.deepEqual(store.getMemoryTags(updated.id), ["new", "考研"], "update bridges new tags");
|
|
365
|
+
assert.ok(store.getDirectory().groups.some((g) => g.tag === "new"), "directory reflects update tags");
|
|
366
|
+
service.update(updated.id, { tags: [] });
|
|
367
|
+
assert.deepEqual(store.getMemoryTags(updated.id), [], "clearing removes the live row");
|
|
368
|
+
const dir = store.getDirectory();
|
|
369
|
+
assert.equal(dir.groups.length, 0, "no tag groups after clearing");
|
|
370
|
+
assert.equal(dir.untagged.length, 1, "memory back to untagged");
|
|
371
|
+
store.close();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("setMemoryTags keeps the memories.tags column in sync with entity_attrs (issue #31)", () => {
|
|
375
|
+
const { store, service } = makeService();
|
|
376
|
+
const { memory } = service.saveWithDedupe({ type: "project", title: "列同步", content: "c" });
|
|
377
|
+
service.setMemoryTags(memory.id, ["linux"]);
|
|
378
|
+
assert.deepEqual(store.getById(memory.id).tags, ["linux"], "column mirrors the live tag row");
|
|
379
|
+
service.setMemoryTags(memory.id, []);
|
|
380
|
+
assert.deepEqual(store.getById(memory.id).tags, [], "clearing also clears the column");
|
|
381
|
+
store.close();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("runDream auto-tag respects the settings toggle over plugin config (issue #31)", async () => {
|
|
385
|
+
const store = createStore(":memory:");
|
|
386
|
+
const settings = createSettings(store.db);
|
|
387
|
+
settings.setAutoTagConfig({ autoTagEnabled: true });
|
|
388
|
+
// plugin config says auto-tagging is OFF; the settings toggle flips it on.
|
|
389
|
+
const service = createService({ store, mirror: null, config: { autoTagEnabled: false }, settings });
|
|
390
|
+
const a = service.saveWithDedupe({ type: "preference", title: "旧1", content: "第一段" }).memory;
|
|
391
|
+
const b = service.saveWithDedupe({ type: "preference", title: "旧2", content: "第二段" }).memory;
|
|
392
|
+
const keep = (id) => JSON.stringify([{ action: "keep", ids: [id] }]);
|
|
393
|
+
let calls = 0;
|
|
394
|
+
const ctx = {
|
|
395
|
+
llm: {
|
|
396
|
+
stream: async function* () {
|
|
397
|
+
calls++;
|
|
398
|
+
if (calls === 1) yield { type: "text-delta", text: keep(a.id) }; // consolidation
|
|
399
|
+
else if (calls === 2) yield { type: "text-delta", text: JSON.stringify([{ id: a.id, tags: ["内核"] }]) }; // auto-tag
|
|
400
|
+
else yield { type: "text-delta", text: "总览" }; // summary
|
|
401
|
+
yield { type: "finish", reason: { kind: "ok" } };
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
logger: { warn: () => {}, info: () => {} }
|
|
405
|
+
};
|
|
406
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
407
|
+
const result = await dream.runDream(ctx, service, {
|
|
408
|
+
dreamProvider: "deepseek", dreamModel: "deepseek-chat", autoTagEnabled: false
|
|
409
|
+
}, settings);
|
|
410
|
+
assert.equal(result.ok, true);
|
|
411
|
+
assert.deepEqual(store.getMemoryTags(a.id), ["内核"], "settings toggle enabled auto-tag despite config.autoTagEnabled=false");
|
|
412
|
+
store.close();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("service.setMemoryTags manual gate follows the settings toggle (issue #31)", () => {
|
|
416
|
+
const store = createStore(":memory:");
|
|
417
|
+
const settings = createSettings(store.db);
|
|
418
|
+
settings.setAutoTagConfig({ manualTagEnabled: false });
|
|
419
|
+
const service = createService({ store, mirror: null, config: {}, settings });
|
|
420
|
+
const a = service.saveWithDedupe({ type: "preference", title: "Alpha", content: "正文" }).memory;
|
|
421
|
+
const r = service.setMemoryTags(a.id, ["linux"]);
|
|
422
|
+
assert.equal(r.ok, false, "settings gate blocks manual tagging");
|
|
423
|
+
assert.match(r.error, /manualTagEnabled/);
|
|
424
|
+
assert.equal(service.manualTagEnabled(), false, "gate flag reflects the settings override");
|
|
425
|
+
store.close();
|
|
426
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { runSleep } from "../src/dream/sleep.js";
|
|
4
|
+
import { createStore } from "../src/store.js";
|
|
5
|
+
import { createService } from "../src/service.js";
|
|
6
|
+
|
|
7
|
+
// v0.7.0 语义修正(待办④ updated_at 不算访问):
|
|
8
|
+
// - merge/update 刷 updated_at 绝不当访问,last_accessed_at 与 updated_at 正交;
|
|
9
|
+
// - sleep 降级 ref 只用 last_accessed_at ?? created_at,杜绝 autoDream 合并
|
|
10
|
+
// 动作把"刚更新过内容"伪装成"刚被召回";
|
|
11
|
+
// - 触达数据采集(touchRecalled)由 heatEnabled 控制,不再依赖 sleepModeEnabled。
|
|
12
|
+
|
|
13
|
+
const DAY = 86400000;
|
|
14
|
+
|
|
15
|
+
function setup(config = {}) {
|
|
16
|
+
const store = createStore(":memory:");
|
|
17
|
+
const service = createService({ store, mirror: null, config });
|
|
18
|
+
return { store, service };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function saveMemory(service, title, content, type = "project") {
|
|
22
|
+
return service.saveWithDedupe({ type, title, content, importance: 3 }).memory;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("store.update bumps updated_at but never last_accessed_at", () => {
|
|
26
|
+
const { store, service } = setup();
|
|
27
|
+
const mem = saveMemory(service, "项目A", "状态:进行中");
|
|
28
|
+
assert.equal(mem.last_accessed_at, undefined, "saved memory has no last_accessed_at");
|
|
29
|
+
|
|
30
|
+
const touched = service.touchLastAccess(mem.id, new Date(Date.now() - 5 * DAY).toISOString());
|
|
31
|
+
assert.equal(touched, true, "explicit touch succeeds");
|
|
32
|
+
const afterTouch = store.getById(mem.id);
|
|
33
|
+
assert.ok(afterTouch.last_accessed_at, "last_accessed_at set after touch");
|
|
34
|
+
|
|
35
|
+
const updatedAtBefore = afterTouch.updated_at;
|
|
36
|
+
store.update(mem.id, { content: "状态:已交付" });
|
|
37
|
+
const afterUpdate = store.getById(mem.id);
|
|
38
|
+
assert.notEqual(afterUpdate.updated_at, updatedAtBefore, "updated_at changed by update");
|
|
39
|
+
assert.equal(
|
|
40
|
+
afterUpdate.last_accessed_at,
|
|
41
|
+
afterTouch.last_accessed_at,
|
|
42
|
+
"last_accessed_at untouched by update (content change ≠ access)"
|
|
43
|
+
);
|
|
44
|
+
store.close();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("search bumps last_accessed_at by default (heatEnabled on) and is gated off by heatEnabled=false", async () => {
|
|
48
|
+
// Default config: heatEnabled 默认开 → last_accessed_at 被采集(与 sleep 开关解耦)。
|
|
49
|
+
const a = setup();
|
|
50
|
+
saveMemory(a.service, "量子计算", "入门");
|
|
51
|
+
await a.service.searchMemories("量子", { mode: "keyword" });
|
|
52
|
+
const rowA = a.store.getById(a.service.all()[0].id);
|
|
53
|
+
assert.ok(rowA.last_accessed_at, "touch runs with heatEnabled default on even when sleepModeEnabled off");
|
|
54
|
+
a.store.close();
|
|
55
|
+
|
|
56
|
+
// heatEnabled=false → 热路径零写入。
|
|
57
|
+
const b = setup({ heatEnabled: false });
|
|
58
|
+
saveMemory(b.service, "量子计算", "入门");
|
|
59
|
+
await b.service.searchMemories("量子", { mode: "keyword" });
|
|
60
|
+
const rowB = b.store.getById(b.service.all()[0].id);
|
|
61
|
+
assert.equal(rowB.last_accessed_at, undefined, "no touch when heatEnabled=false");
|
|
62
|
+
b.store.close();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("sleep demotion anchors on last_accessed_at — a merge-bumped updated_at does not reset the fresh clock", async () => {
|
|
66
|
+
const { store, service } = setup();
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const idle = saveMemory(service, "冷查记忆A", "很久没被召回");
|
|
69
|
+
// 模拟一次 autoDream 合并:updated_at 刷成"现在",但真实访问在 100 天前。
|
|
70
|
+
store.update(idle.id, { content: "合并带来的内容更新" });
|
|
71
|
+
const wait = saveMemory(service, "冷查记忆B", "40 天未访问");
|
|
72
|
+
|
|
73
|
+
service.touchLastAccess(idle.id, new Date(now - 100 * DAY).toISOString());
|
|
74
|
+
service.touchLastAccess(wait.id, new Date(now - 40 * DAY).toISOString());
|
|
75
|
+
|
|
76
|
+
// runSleep 的最低配置:demotion 阶段不需要 LLM/语义,其余阶段会 skip。
|
|
77
|
+
const ctx = {
|
|
78
|
+
logger: { warn: () => {}, info: () => {} },
|
|
79
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "sleep-model" }) },
|
|
80
|
+
llm: {
|
|
81
|
+
async *stream() {
|
|
82
|
+
yield { type: "text-delta", index: 0, text: "[]" };
|
|
83
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const config = {
|
|
88
|
+
sleepModeEnabled: true,
|
|
89
|
+
sleepConflictStrictness: "normal",
|
|
90
|
+
sleepArchiveDays: 30,
|
|
91
|
+
sleepCompressDays: 90,
|
|
92
|
+
sleepPatternMinMemories: 100,
|
|
93
|
+
sleepMaxPatternPerRun: 3,
|
|
94
|
+
// v0.7.0 热联合判定:project λ=0.0008 默认太慢衰减,40/100 天热值都高于
|
|
95
|
+
// 阈值、被热闸保护——本测试只验证"updated_at 不算访问"的 ref 语义,故把
|
|
96
|
+
// λ 调快到 0.02 打开降级路径(默认保守语义由 sleep-heat.test.js 覆盖)。
|
|
97
|
+
heatTypeDecay: { project: 0.02 }
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const result = await runSleep(ctx, service, config, { warn: () => {}, info: () => {} }, null, null);
|
|
101
|
+
|
|
102
|
+
const demotion = result.phases?.["demotion"];
|
|
103
|
+
assert.ok(demotion, "demotion phase ran");
|
|
104
|
+
// A:last_accessed 100 天前(即使 updated_at 是"现在")→ 越过 90 天归档线。
|
|
105
|
+
assert.ok(demotion.archived.includes(idle.id), "A archived by last_accessed_at (updated_at ignored)");
|
|
106
|
+
const afterA = store.getById(idle.id);
|
|
107
|
+
assert.equal(afterA.archived, true, "A is archived in store");
|
|
108
|
+
// B:last_accessed 40 天前 → 进入 30 天压缩窗口,被降级为摘要。
|
|
109
|
+
assert.ok(demotion.demoted.includes(wait.id), "B demoted in 30d compress window");
|
|
110
|
+
const afterB = store.getById(wait.id);
|
|
111
|
+
assert.ok(afterB._full_content, "B full body parked in _full_content");
|
|
112
|
+
store.close();
|
|
113
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"version": "0.7.1",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -12,23 +12,16 @@
|
|
|
12
12
|
"url": "https://github.com/modusensus/dsh-mneme/issues"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
|
-
"main": "lib/index.js",
|
|
15
|
+
"main": "./dsh-mneme/lib/index.js",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"default": "./lib/index.js"
|
|
18
|
+
"default": "./dsh-mneme/lib/index.js"
|
|
19
19
|
},
|
|
20
20
|
"./client": {
|
|
21
|
-
"default": "./lib/client.js"
|
|
21
|
+
"default": "./dsh-mneme/lib/client.js"
|
|
22
22
|
},
|
|
23
|
-
"./package.json": "./package.json"
|
|
23
|
+
"./package.json": "./dsh-mneme/package.json"
|
|
24
24
|
},
|
|
25
|
-
"files": [
|
|
26
|
-
"lib",
|
|
27
|
-
"src",
|
|
28
|
-
"scripts",
|
|
29
|
-
"test",
|
|
30
|
-
"cordis.patch.yml"
|
|
31
|
-
],
|
|
32
25
|
"dsh": {
|
|
33
26
|
"client": {
|
|
34
27
|
"inject": [
|
|
@@ -40,9 +33,20 @@
|
|
|
40
33
|
"platform": "web"
|
|
41
34
|
},
|
|
42
35
|
"bundle": {
|
|
43
|
-
"patch": "./cordis.patch.yml"
|
|
36
|
+
"patch": "./dsh-mneme/cordis.patch.yml"
|
|
37
|
+
},
|
|
38
|
+
"marketplace": {
|
|
39
|
+
"profiles": [
|
|
40
|
+
"web"
|
|
41
|
+
],
|
|
42
|
+
"requiresBuildApproval": false,
|
|
43
|
+
"requiresRestart": true,
|
|
44
|
+
"manualSteps": false
|
|
44
45
|
}
|
|
45
46
|
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"prepublishOnly": "node -e \"const fs=require('fs');const v=JSON.parse(fs.readFileSync('./dsh-mneme/package.json','utf8')).version;const p=JSON.parse(fs.readFileSync('./package.json','utf8'));p.version=v;fs.writeFileSync('./package.json',JSON.stringify(p,null,2)+'\\n')\""
|
|
49
|
+
},
|
|
46
50
|
"peerDependencies": {
|
|
47
51
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
52
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
@@ -50,31 +54,5 @@
|
|
|
50
54
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
51
55
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
52
56
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
58
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
61
|
-
"c8": "^12.0.0"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"sync": "node scripts/sync-lib.js",
|
|
65
|
-
"prepack": "npm run sync",
|
|
66
|
-
"test": "node --test test/*.test.js",
|
|
67
|
-
"test:coverage": "c8 node --test test/*.test.js",
|
|
68
|
-
"e2e": "node scripts/e2e-dsh.js",
|
|
69
|
-
"stress": "node scripts/stress-dsh.js"
|
|
70
|
-
},
|
|
71
|
-
"dependencies": {
|
|
72
|
-
"@huggingface/transformers": "^4.2.0"
|
|
73
|
-
},
|
|
74
|
-
"overrides": {
|
|
75
|
-
"adm-zip": "0.6.0"
|
|
76
|
-
},
|
|
77
|
-
"c8": {
|
|
78
|
-
"reporter": ["text", "lcov"]
|
|
79
57
|
}
|
|
80
58
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# dsh-mneme v0.7 版本策划书 + 分工安排(2026-08-21)
|
|
2
|
+
|
|
3
|
+
> 状态:**待桉桉过目** → 通过后**晚上错峰执行**(兼顾火山 coding plan ~1200 次/5 小时额度,分阶段推进,不在同一时段烧光)。
|
|
4
|
+
> 当前版本:v0.6.6(710 全绿)。本策划基于 README 路线图 + Discussion #21 + 社区 heptaspirit 调研(`docs/AGENT_MEMORY_RESEARCH.md`,已归档为 v0.7 参考输入)。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、版本定位(调整:v0.7 不着急,先做 v0.6.7)
|
|
9
|
+
|
|
10
|
+
**近期先做:v0.6.7 —— Issue #20 面板删除按钮**(兑现对社区 lqs50 的承诺,快速小版本)。
|
|
11
|
+
|
|
12
|
+
**v0.7 主题:自进化记忆(heat 热度 + 兴趣漂移)—— 桉桉 2026-08-21 拍板:不着急,列为远期**,先把 v0.6.x 的欠账收干净再说。
|
|
13
|
+
|
|
14
|
+
一句话:v0.7 让记忆库不只是"存得准、召得回",还能**根据访问热度自我衰减、识别兴趣漂移、降级冷记忆**,进化为会"进化"的记忆体。核心来自 #21(简化幂律 + type 差异化半衰期),社区调研补足数据前提与图谱增强。
|
|
15
|
+
|
|
16
|
+
**复用承接(不引入新依赖/新概念):** 全部落在现有 `recall_runs` 表 / `TYPE_DECAY` / sleep 批处理 / `entity_attrs` valid_from-until / api.js 的 ego-BFS 上。图数据库(Neo4j/Graphiti)明确不做——SQLite 递归 CTE 实测够用。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 二、范围拆分
|
|
21
|
+
|
|
22
|
+
### v0.6.7 —— Issue #20 记忆面板删除按钮(近期,优先做)
|
|
23
|
+
| # | 功能 | 说明 | 承接机制 |
|
|
24
|
+
|---|------|------|----------|
|
|
25
|
+
| ⭐1 | **面板删除按钮** | 记忆库界面**直接删除自己不需要的记忆**(社区 lqs50 提,issue #20 仍 OPEN)。后端 storage/`memory_delete` 工具已有 → **补一个面板 HTTP DELETE 端点 + 前端「选中删除」按钮**(永久删除带二次确认;可挂到目录 view / 详情页)| store.deleteMemory + api.js + 面板 |
|
|
26
|
+
- **验收:** 全绿;面板可选中删除记忆(含目录视图入口);**收掉 issue #20** 并回帖致谢 lqs50。
|
|
27
|
+
- **依赖关系:** 独立小版本,不需要等 v0.7。夜间一个窗口即可完成。
|
|
28
|
+
|
|
29
|
+
### v0.7.0 —— heat 热度 + 兴趣漂移核心(远期,⏸ 不着急)
|
|
30
|
+
| # | 功能 | 说明 | 承接机制 |
|
|
31
|
+
|---|------|------|----------|
|
|
32
|
+
| 1 | **heat 模型** | 简化幂律衰减 + `TYPE_DECAY`(type 差异化半衰期);**公式预留信号接口**(新列/metadata,以后加多信号是增量不重构)| #21 已定方向 |
|
|
33
|
+
| 2 | **recall_runs 数据前提** | 记录**默认开、消费默认关**;candidates 打 **`injected` 标记**(区分"被召回"vs"被注入上下文"两个强度);**90 天滚动清理**防膨胀 | recall_runs / touchRecalled |
|
|
34
|
+
| 3 | **sleep 降级联合判定** | 现状 30/90 天硬阈值不看 importance/type,高价值 preference 也会被降级 → 改为 `heat<阈值 && importance<5 && type 非免疫`,双保护,仍以时间为主 | sleep 批处理 / archived |
|
|
35
|
+
| 4 | **updated_at 不算访问** | 合并/更新刷 updated_at 不应计为访问,防 autoDream 合并动作干扰新鲜度 | 时间戳语义修正 |
|
|
36
|
+
| 5 | **heat 投影实体层** | 实体热 = 关联记忆 heat 聚合;ego-graph 前端节点大小/明暗随热变化,兴趣漂移**在图上可见**(成本≈0,heat 本就 v0.7 改)| entities 表 + 前端 |
|
|
37
|
+
- **验收:** 全绿(目标 710 → 740+);heat 全部 opt-in(`heatEnabled` 默认保守开启、不破坏现有)。
|
|
38
|
+
- **状态:** ⏸ 桉桉拍板不着急,先做完 v0.6.x 收尾再启动。
|
|
39
|
+
|
|
40
|
+
### v0.7.1 —— 图谱数据积累 + 检索/关系增强(次轮)
|
|
41
|
+
| # | 功能 | 说明 | 承接机制 |
|
|
42
|
+
|---|------|------|----------|
|
|
43
|
+
| 6 | **实体提取移入 sleep 批量** | 写入路径实时抽实体成本高 → 挪进 sleep 批量,**写入路径零 LLM 成本**;代价是图谱滞后一个 sleep 周期(长期关系网无感)| sleep LLM 阶段模式 |
|
|
44
|
+
| 7 | **sleep 批量实体消歧** | extractor 目前不合并同义词,全称/简称成孤立节点 → 仿 autoDream 的 winner/loser + 审计,或规则兜底(小写/去空格)| findEntityByName |
|
|
45
|
+
| 8 | **1-hop 邻域回流检索** | `entity:X` 顺带召回相邻实体记忆——把图谱从"展示"变成"召回信号"(差异化价值)| api.js ego-BFS |
|
|
46
|
+
| 9 | **关系时效** | `entity_relations` 补 `valid_until` + `superseded` 标记(不删只标,保留历史回溯),换方案后旧关系不一直挂着 | entity_attrs 的 valid_from/until 模式 |
|
|
47
|
+
- **验收:** 全绿;图谱数据能攒起来,前端可见热投影。
|
|
48
|
+
|
|
49
|
+
### 远期 / 等 DSH(不在本次做)
|
|
50
|
+
- 跨 workspace 记忆共享(README v0.7.0+ 标注,**等 DSH 支持**);多模态记忆(本地化定位冲突,排除);参数化记忆 MemoryLLM(模型层路线,基本放弃);图数据库/PPR/社区子图(CTE 够用,暂缓)。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 三、分工安排(多模型协作,服从派发架构)
|
|
55
|
+
|
|
56
|
+
| 角色 | 模型 | 负责 | 量级 |
|
|
57
|
+
|------|------|------|------|
|
|
58
|
+
| **协调 + 方案 + 把关** | qwen3.7-max | 拆任务、定义 heat 公式/TYPE_DECAY 表格/schema、每步评审整合 | 中 |
|
|
59
|
+
| **核心代码 + 复杂测试** | kimi-k3 | heat 模型、sleep 降级判定、1-hop 回流、实体消歧、90 天清理等实现 + 全绿测试 | 高 |
|
|
60
|
+
| **轻量校验/快检** | glm-4.5-air | 小步 diff 校验、配置项联动、回归抽查 | 低 |
|
|
61
|
+
| **文档同步** | qwen3.6-plus | CHANGELOG + README 双表(root+子目录、中英),ROADMAP 状态对 | 低 |
|
|
62
|
+
| **集成 + 发布** | 主模型(Ivresse) | 汇总、核验、**报桉桉批准后**发布;一次只发一个 sub-version | — |
|
|
63
|
+
|
|
64
|
+
> 派发铁律:发布/改动前先征得桉桉许可;复杂编程/测试派 kimi-k3;REANME/CHANGELOG 根+子目录中英都要同步,避免漏改。
|
|
65
|
+
|
|
66
|
+
## 四、执行节奏(错峰,尊重 ~1200 次/5 小时)
|
|
67
|
+
- **窗口**:每晚 7 点后 / 周末,一次只推一个 sub-step(先 0.7.0,休息/量量再用 0.7.1)。
|
|
68
|
+
- **每步收尾**:跑全绿 → 更新 CHANGELOG/README → 记 activeContext/progress → 停。
|
|
69
|
+
- **不集中突发**:同一窗口不做超长连续生成,做完即停,额度留着。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 五、待桉桉拍板(已定项已在文内标注)
|
|
74
|
+
✅ 已定:**Issue #20 → v0.6.7**(快速兑现);**v0.7.0 不着急**(远期,先收 v0.6.x 账)。
|
|
75
|
+
> 剩下细节到真正开工前再定即可:heat 默认开/关、v0.7.0 与 v0.7.1 节奏。
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|