@modusensus/dsh-mneme 0.6.11 → 0.7.0
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 +149 -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 +286 -0
- package/dsh-mneme/LICENSE +21 -0
- package/dsh-mneme/README.md +482 -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 +4 -1
- 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/dsh-mneme/lib/heat.js +136 -0
- package/{lib → dsh-mneme/lib}/index.js +7 -0
- package/{src → dsh-mneme/lib}/service.js +63 -8
- package/{lib → dsh-mneme/lib}/store.js +17 -0
- package/dsh-mneme/package-lock.json +1936 -0
- package/dsh-mneme/package.json +80 -0
- package/{lib → dsh-mneme/src}/api.js +4 -1
- package/{src → dsh-mneme/src}/config.js +17 -0
- package/{src → dsh-mneme/src}/dream/sleep.js +13 -1
- package/dsh-mneme/src/heat.js +136 -0
- package/{src → dsh-mneme/src}/index.js +7 -0
- package/{lib → dsh-mneme/src}/service.js +63 -8
- package/{src → dsh-mneme/src}/store.js +17 -0
- 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/dsh-mneme/test/sleep-heat.test.js +112 -0
- package/{test → dsh-mneme/test}/sleep.test.js +12 -4
- 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}/dream.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}/settings.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}/dream.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}/settings.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}/api.test.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}/settings.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}/tag.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
|
@@ -77,14 +77,23 @@ test("recorded candidates carry id/title/content/score/source and match the retu
|
|
|
77
77
|
assert.ok(cands.some((c) => c.id === b.id), "second memory recorded");
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
test("recordRecall defaults
|
|
80
|
+
test("recordRecall defaults on (v0.7.0) — recorder is called by default", async () => {
|
|
81
81
|
const { service } = setup();
|
|
82
82
|
let calls = 0;
|
|
83
83
|
service.setRecallRecorder(() => calls++);
|
|
84
84
|
saveMemory(service, null, { title: "量子计算", content: "入门" });
|
|
85
85
|
await service.searchMemories("量子", { mode: "keyword" });
|
|
86
|
+
assert.equal(calls, 1, "recorder called once by default (recall_runs 记录默认开)");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("recordRecall can be opted out via explicit false or config.recallRecordDefault=false", async () => {
|
|
90
|
+
const { service } = setup({ recallRecordDefault: false });
|
|
91
|
+
let calls = 0;
|
|
92
|
+
service.setRecallRecorder(() => calls++);
|
|
93
|
+
saveMemory(service, null, { title: "量子计算", content: "入门" });
|
|
94
|
+
await service.searchMemories("量子", { mode: "keyword" });
|
|
86
95
|
await service.searchMemories("量子", { mode: "keyword", recordRecall: false });
|
|
87
|
-
assert.equal(calls, 0, "no recorder call when
|
|
96
|
+
assert.equal(calls, 0, "no recorder call when default disabled or explicit false");
|
|
88
97
|
});
|
|
89
98
|
|
|
90
99
|
test("recordRecall=true with no recorder installed is safe and returns normally", async () => {
|
|
@@ -301,7 +310,7 @@ test("e2e: index.js wiring — recordRecall search lands a row listRecallRuns ca
|
|
|
301
310
|
assert.equal(store.getRecallRun(run.id).query, "量子", "row readable right away");
|
|
302
311
|
});
|
|
303
312
|
|
|
304
|
-
test("e2e:
|
|
313
|
+
test("e2e: recordRecall defaults on (v0.7.0) but is disabled by config.recallRecordDefault=false", async () => {
|
|
305
314
|
const store = createStore(":memory:");
|
|
306
315
|
const service = createService({ store, mirror: null, config: {} });
|
|
307
316
|
service.setRecallRecorder((recall) => store.saveRecallRun({
|
|
@@ -310,6 +319,18 @@ test("e2e: without recordRecall the recall_runs table gains no rows", async () =
|
|
|
310
319
|
}));
|
|
311
320
|
service.saveWithDedupe({ type: "preference", title: "量子计算入门", content: "叠加态" });
|
|
312
321
|
await service.searchMemories("量子", { mode: "keyword" });
|
|
313
|
-
|
|
314
|
-
|
|
322
|
+
assert.equal(store.listRecallRuns().length, 1, "default config records a row (记录默认开)");
|
|
323
|
+
|
|
324
|
+
const store2 = createStore(":memory:");
|
|
325
|
+
const service2 = createService({ store: store2, mirror: null, config: { recallRecordDefault: false } });
|
|
326
|
+
service2.setRecallRecorder((recall) => store2.saveRecallRun({
|
|
327
|
+
query: recall.query, mode: recall.mode, topK: recall.topK, threshold: recall.threshold ?? null,
|
|
328
|
+
candidates: recall.candidates ?? [], created_at: recall.createdAt
|
|
329
|
+
}));
|
|
330
|
+
service2.saveWithDedupe({ type: "preference", title: "量子计算入门", content: "叠加态" });
|
|
331
|
+
await service2.searchMemories("量子", { mode: "keyword" });
|
|
332
|
+
await service2.searchMemories("量子", { mode: "keyword", recordRecall: false });
|
|
333
|
+
assert.deepEqual(store2.listRecallRuns(), [], "no rows when recallRecordDefault=false or explicit false");
|
|
334
|
+
store.close();
|
|
335
|
+
store2.close();
|
|
315
336
|
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createStore } from "../src/store.js";
|
|
4
|
+
import { createService } from "../src/service.js";
|
|
5
|
+
|
|
6
|
+
// v0.7.0 recall-layer 强度标记 + 滚动清理:
|
|
7
|
+
// - searchMemories 记搜索帧:candidates 打 injected:false(被召回);
|
|
8
|
+
// - injectCandidates 记注入帧:mode="inject"、candidates 打 injected:true
|
|
9
|
+
// (被注入上下文)—— 两个消耗强度可分可查;
|
|
10
|
+
// - 两路都受 recallRecordDefault 门控,注入帧记账失败绝不阻断注入;
|
|
11
|
+
// - store.purgeRecallRunsOlderThan(days) 按 created_at 滚动清理防膨胀。
|
|
12
|
+
|
|
13
|
+
function setup(config = {}) {
|
|
14
|
+
const store = createStore(":memory:");
|
|
15
|
+
const service = createService({ store, mirror: null, config });
|
|
16
|
+
return { store, service };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function wireRecorder(service, seen) {
|
|
20
|
+
service.setRecallRecorder((r) => seen.push(r));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("search scene candidates carry injected:false", async () => {
|
|
24
|
+
const { service } = setup();
|
|
25
|
+
const seen = [];
|
|
26
|
+
wireRecorder(service, seen);
|
|
27
|
+
service.saveWithDedupe({ type: "preference", title: "量子计算", content: "入门", importance: 5 });
|
|
28
|
+
const rows = await service.searchMemories("量子", { mode: "keyword", recordRecall: true });
|
|
29
|
+
assert.equal(rows.length, 1);
|
|
30
|
+
assert.equal(seen[0].mode, "keyword");
|
|
31
|
+
assert.equal(seen[0].candidates[0].injected, false, "搜索帧是被召回的帧,标记 false");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("inject scene records mode=inject with injected:true on every candidate", async () => {
|
|
35
|
+
const { service } = setup();
|
|
36
|
+
const seen = [];
|
|
37
|
+
wireRecorder(service, seen);
|
|
38
|
+
const mem = service.saveWithDedupe({ type: "preference", title: "量子计算", content: "入门", importance: 5 }).memory;
|
|
39
|
+
const injected = service.injectCandidates({ query: "量子", maxItems: 3 });
|
|
40
|
+
assert.ok(injected.some((m) => m.id === mem.id), "注入选中的记忆在返回里");
|
|
41
|
+
assert.equal(seen.length, 1, "注入帧记了 1 条 recall_runs");
|
|
42
|
+
const rec = seen[0];
|
|
43
|
+
assert.equal(rec.mode, "inject", "注入帧 mode 为 inject");
|
|
44
|
+
assert.equal(rec.topK, injected.length);
|
|
45
|
+
assert.ok(rec.candidates.length >= 1 && rec.candidates.every((c) => c.injected === true), "注入候选全部 injected:true");
|
|
46
|
+
assert.ok(rec.candidates.every((c) => c.source === "inject"), "注入候选 source 标记 inject");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("inject recording is gated off by recallRecordDefault=false, search stays explicit", async () => {
|
|
50
|
+
const { service } = setup({ recallRecordDefault: false });
|
|
51
|
+
const seen = [];
|
|
52
|
+
wireRecorder(service, seen);
|
|
53
|
+
service.saveWithDedupe({ type: "preference", title: "量子计算", content: "入门", importance: 5 });
|
|
54
|
+
service.injectCandidates({ query: "量子", maxItems: 3 });
|
|
55
|
+
assert.equal(seen.length, 0, "recallRecordDefault=false 时注入帧不上报");
|
|
56
|
+
await service.searchMemories("量子", { mode: "keyword", recordRecall: true });
|
|
57
|
+
assert.equal(seen.length, 1, "显式 recordRecall:true 的搜索帧照常上报");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("injected flag survives the store round-trip", async () => {
|
|
61
|
+
const store = createStore(":memory:");
|
|
62
|
+
store.saveRecallRun({
|
|
63
|
+
query: "量子", mode: "inject", topK: 1, candidates: [{ id: "a", title: "量子", content: "入门", score: 0.8, source: "inject", injected: true }]
|
|
64
|
+
});
|
|
65
|
+
store.saveRecallRun({
|
|
66
|
+
query: "量子", mode: "keyword", topK: 1, candidates: [{ id: "b", title: "量子计算", content: "叠加态", score: 0.9, source: "keyword", injected: false }]
|
|
67
|
+
});
|
|
68
|
+
const runs = store.listRecallRuns();
|
|
69
|
+
assert.equal(runs.length, 2);
|
|
70
|
+
const inject = runs.find((r) => r.mode === "inject");
|
|
71
|
+
const search = runs.find((r) => r.mode === "keyword");
|
|
72
|
+
assert.equal(inject.candidates[0].injected, true, "注入帧 injected 存得回来");
|
|
73
|
+
assert.equal(search.candidates[0].injected, false, "搜索帧 injected 存得回来");
|
|
74
|
+
assert.equal(store.getRecallRun(search.id).candidates[0].injected, false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("purgeRecallRunsOlderThan drops only rows past the retention window", async () => {
|
|
78
|
+
const store = createStore(":memory:");
|
|
79
|
+
const old = store.saveRecallRun({
|
|
80
|
+
id: "old", query: "旧查询", mode: "keyword", topK: 5, candidates: [],
|
|
81
|
+
created_at: new Date(Date.now() - 200 * 86400000).toISOString()
|
|
82
|
+
});
|
|
83
|
+
const fresh = store.saveRecallRun({
|
|
84
|
+
id: "fresh", query: "新查询", mode: "inject", topK: 5, candidates: [],
|
|
85
|
+
created_at: new Date().toISOString()
|
|
86
|
+
});
|
|
87
|
+
const deleted = store.purgeRecallRunsOlderThan(90);
|
|
88
|
+
assert.equal(deleted, 1, "只有 200 天前的老行被清除");
|
|
89
|
+
const runs = store.listRecallRuns();
|
|
90
|
+
assert.deepEqual(runs.map((r) => r.id), ["fresh"], "新行保留");
|
|
91
|
+
assert.equal(store.getRecallRun(old.id), undefined, "旧行不可再读");
|
|
92
|
+
store.close();
|
|
93
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
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 待办③ sleep 降级热联合判定:
|
|
8
|
+
// 降级需同时满足 时间窗 + heat<sleepHeatThreshold + importance<5 三条件;
|
|
9
|
+
// λ=0 的免疫类型 heat 恒 1.0 天然豁免;importance≥5 紧要记忆无论多冷都保留。
|
|
10
|
+
// 默认 λ 下:history(0.006) 约 77 天后热值跌破 0.05,project(0.0008) 约 581 天,
|
|
11
|
+
// decision(0.002) 约 232 天 —— 整体取向保守,突出"冷但重要"与"热但低值"都不降级。
|
|
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", importance = 3) {
|
|
22
|
+
return service.saveWithDedupe({ type, title, content, importance }).memory;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sleepConfig(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
sleepModeEnabled: true,
|
|
28
|
+
sleepConflictStrictness: "normal",
|
|
29
|
+
sleepArchiveDays: 30, // 30-90 天窗口 → 压缩为摘要
|
|
30
|
+
sleepCompressDays: 90, // >=90 天 → 直接归档
|
|
31
|
+
sleepPatternMinMemories: 100, // 记忆数不足 → pattern 阶段跳过
|
|
32
|
+
sleepMaxPatternPerRun: 3,
|
|
33
|
+
...overrides
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mockCtx() {
|
|
38
|
+
return {
|
|
39
|
+
logger: { warn: () => {}, info: () => {} },
|
|
40
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "sleep-model" }) },
|
|
41
|
+
llm: {
|
|
42
|
+
async *stream() {
|
|
43
|
+
yield { type: "text-delta", index: 0, text: "[]" };
|
|
44
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function run(memories, service, config) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
for (const [mem, daysAgo] of memories) {
|
|
53
|
+
service.touchLastAccess(mem.id, new Date(now - daysAgo * DAY).toISOString());
|
|
54
|
+
}
|
|
55
|
+
const result = await runSleep(mockCtx(), service, config, { warn: () => {}, info: () => {} }, null, null);
|
|
56
|
+
return result.phases?.["demotion"];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("cold low-importance history past the compress tier archives (heat gate open)", async () => {
|
|
60
|
+
const { store, service } = setup();
|
|
61
|
+
const mem = saveMemory(service, "久远的会话记录", "早期讨论内容", "history", 2);
|
|
62
|
+
const demotion = await run([[mem, 120]], service, sleepConfig());
|
|
63
|
+
assert.ok(demotion, "demotion phase ran");
|
|
64
|
+
assert.ok(demotion.archived.includes(mem.id), "120 天 ref + 热值≈0.03<0.05 + importance 2 → 归档");
|
|
65
|
+
assert.equal(store.getById(mem.id).archived, true);
|
|
66
|
+
store.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("importance 5 protects a memory even when the heat is icy", async () => {
|
|
70
|
+
const { store, service } = setup();
|
|
71
|
+
const mem = saveMemory(service, "关键决策", "不可丢失的重要结论", "decision", 5);
|
|
72
|
+
const demotion = await run([[mem, 400]], service, sleepConfig());
|
|
73
|
+
assert.ok(demotion, "demotion phase ran");
|
|
74
|
+
// decision λ=0.002, 400 天热值≈0.03<0.05,但 importance=5 → 保护,绝不降级。
|
|
75
|
+
assert.ok(!demotion.archived.includes(mem.id), "紧要记忆不被归档");
|
|
76
|
+
assert.ok(!demotion.demoted.includes(mem.id), "紧要记忆不被压缩");
|
|
77
|
+
assert.equal(store.getById(mem.id).archived, false);
|
|
78
|
+
store.close();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("immune preference (λ=0, heat=1.0) is never demoted even when ancient", async () => {
|
|
82
|
+
const { store, service } = setup();
|
|
83
|
+
const mem = saveMemory(service, "用户偏好", "喜欢简洁的总结", "preference", 3);
|
|
84
|
+
const demotion = await run([[mem, 400]], service, sleepConfig());
|
|
85
|
+
assert.ok(!demotion.archived.includes(mem.id), "免疫类型不归档");
|
|
86
|
+
assert.ok(!demotion.demoted.includes(mem.id), "免疫类型不压缩");
|
|
87
|
+
assert.equal(store.getById(mem.id).archived, false);
|
|
88
|
+
store.close();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("slow-decay project stays heat-protected at the compress tier (λ=0.0008)", async () => {
|
|
92
|
+
const { store, service } = setup();
|
|
93
|
+
const mem = saveMemory(service, "项目A", "慢衰减的进行中项目", "project", 2);
|
|
94
|
+
const demotion = await run([[mem, 100]], service, sleepConfig());
|
|
95
|
+
// 100 天已越过 90 天归档窗,但 project 热值≈0.28>0.05 → heat 闸拦下。
|
|
96
|
+
assert.ok(!demotion.archived.includes(mem.id), "慢衰减类型热值仍高 → 不归档");
|
|
97
|
+
assert.equal(store.getById(mem.id).archived, false);
|
|
98
|
+
store.close();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("demote tier (30-90d) fires when the type's λ is cold enough", async () => {
|
|
102
|
+
const { store, service } = setup();
|
|
103
|
+
const mem = saveMemory(service, "中期会话摘要", "可压缩的历史碎片", "history", 2);
|
|
104
|
+
// 调高 history 的 λ 到 0.05 → 40 天热值≈0.009<0.05,解锁 30-90 天压缩窗口。
|
|
105
|
+
const config = sleepConfig({ heatTypeDecay: { history: 0.05 } });
|
|
106
|
+
const demotion = await run([[mem, 40]], service, config);
|
|
107
|
+
assert.ok(!demotion.archived.includes(mem.id), "40 天未到归档线");
|
|
108
|
+
assert.ok(demotion.demoted.includes(mem.id), "热值足够冷 + importance 2 → 压缩为摘要");
|
|
109
|
+
const after = store.getById(mem.id);
|
|
110
|
+
assert.ok(after._full_content, "全文停放在 _full_content");
|
|
111
|
+
store.close();
|
|
112
|
+
});
|
|
@@ -45,6 +45,14 @@ function baseConfig(overrides = {}) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// v0.7.0 热联合判定:慢衰减类型(project λ=0.0008)默认受热闸保护——40 天热值
|
|
49
|
+
// ≈0.5、100 天 ≈0.28,始终高于 sleepHeatThreshold(0.05),demotion 不触发。
|
|
50
|
+
// 降级语义测试把 project 的 λ 调快到 0.02(40 天热值≈0.03),复现"时间窗冷态
|
|
51
|
+
// 即降级"的旧路径;默认保守语义由 sleep-heat.test.js 单独覆盖。
|
|
52
|
+
function demotionConfig(overrides = {}) {
|
|
53
|
+
return baseConfig({ heatTypeDecay: { project: 0.02 }, ...overrides });
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
function setup() {
|
|
49
57
|
const store = createStore(":memory:");
|
|
50
58
|
const service = createService({ store, mirror: null, config: {} });
|
|
@@ -173,7 +181,7 @@ test("sleep: demotion shrinks cold memory to summary, keeps _full_content", asyn
|
|
|
173
181
|
const m = makeMemory(service, "cold", "原内容".repeat(60), "project");
|
|
174
182
|
service.touchLastAccess(m.id, new Date(now - 40 * 86400000).toISOString());
|
|
175
183
|
const ctx = mockCtx(() => "[]");
|
|
176
|
-
const result = await runSleep(ctx, service,
|
|
184
|
+
const result = await runSleep(ctx, service, demotionConfig(), ctx.logger, null, null);
|
|
177
185
|
const after = service.getById(m.id);
|
|
178
186
|
assert.equal(result.status, "ok");
|
|
179
187
|
assert.ok(after._full_content && after._full_content.length > 0, "full body preserved");
|
|
@@ -188,7 +196,7 @@ test("sleep: demotion fully archives memory past sleepCompressDays", async () =>
|
|
|
188
196
|
const m = makeMemory(service, "ancient", "很老的记忆", "project");
|
|
189
197
|
service.touchLastAccess(m.id, new Date(now - 100 * 86400000).toISOString());
|
|
190
198
|
const ctx = mockCtx(() => "[]");
|
|
191
|
-
const result = await runSleep(ctx, service,
|
|
199
|
+
const result = await runSleep(ctx, service, demotionConfig(), ctx.logger, null, null);
|
|
192
200
|
const after = service.getById(m.id);
|
|
193
201
|
assert.equal(result.status, "ok");
|
|
194
202
|
assert.equal(after.archived, true, "past compress days → archived");
|
|
@@ -275,7 +283,7 @@ test("sleep: a failing phase does not block the others (fail-safe)", async () =>
|
|
|
275
283
|
const m = makeMemory(service, "cold", "内容".repeat(60), "project");
|
|
276
284
|
service.touchLastAccess(m.id, new Date(now - 40 * 86400000).toISOString());
|
|
277
285
|
const ctx = mockCtx(() => "[]");
|
|
278
|
-
const result = await runSleep(ctx, service,
|
|
286
|
+
const result = await runSleep(ctx, service, demotionConfig(), ctx.logger, null, null);
|
|
279
287
|
assert.equal(result.phases.conflicts.status, "skipped", "conflicts phase degraded gracefully (no usable vectors)");
|
|
280
288
|
assert.equal(result.phases.demotion.status, "ok", "demotion still ran");
|
|
281
289
|
assert.equal(result.status, "ok", "overall run still ok despite conflicts degrading");
|
|
@@ -289,7 +297,7 @@ test("sleep: no LLM route skips LLM phases but demotion still runs", async () =>
|
|
|
289
297
|
const m = makeMemory(service, "cold", "内容".repeat(60), "project");
|
|
290
298
|
service.touchLastAccess(m.id, new Date(now - 40 * 86400000).toISOString());
|
|
291
299
|
const ctx = mockCtx(() => "[]", null); // currentSelection() → null, no route
|
|
292
|
-
const result = await runSleep(ctx, service,
|
|
300
|
+
const result = await runSleep(ctx, service, demotionConfig(), ctx.logger, null, null);
|
|
293
301
|
assert.equal(result.phases.conflicts.status, "skipped", "no llm route → conflicts skipped");
|
|
294
302
|
assert.equal(result.phases.patterns.status, "skipped", "no llm route → patterns skipped");
|
|
295
303
|
assert.equal(result.phases.demotion.status, "ok", "demotion is LLM-free and runs");
|
|
@@ -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.0",
|
|
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
|