@modusensus/dsh-mneme 0.6.9 → 0.6.11
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/LICENSE +21 -21
- package/README.md +469 -219
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +15 -15
- package/{dsh-mneme/src → lib}/api.js +783 -783
- package/{dsh-mneme/lib → lib}/client.js +1754 -1757
- package/{dsh-mneme/src → lib}/commands.js +64 -64
- package/{dsh-mneme/lib → lib}/config.js +298 -298
- package/{dsh-mneme/lib → lib}/dream/clustering.js +118 -118
- package/{dsh-mneme/lib → lib}/dream/decisions.js +488 -488
- package/{dsh-mneme/lib → lib}/dream/sleep.js +561 -561
- package/{dsh-mneme/src → lib}/dream/tag-extractor.js +156 -156
- package/{dsh-mneme/lib → lib}/dream.js +958 -958
- package/{dsh-mneme/src → lib}/embedding.js +154 -154
- package/{dsh-mneme/src → lib}/entities/extractor.js +242 -242
- package/{dsh-mneme/lib → lib}/hot-memory.js +53 -53
- package/{dsh-mneme/lib → lib}/index.js +361 -361
- package/{dsh-mneme/src → lib}/inject.js +208 -208
- package/{dsh-mneme/lib → lib}/local-embedder.js +282 -282
- package/{dsh-mneme/lib → lib}/mirror.js +170 -170
- package/{dsh-mneme/lib → lib}/parser/tag.js +59 -59
- package/{dsh-mneme/lib → lib}/parser/wiki-link.js +38 -38
- package/{dsh-mneme/src → lib}/quality-filter.js +123 -123
- package/{dsh-mneme/lib → lib}/reranker.js +218 -218
- package/{dsh-mneme/src → lib}/search/adaptive.js +22 -22
- package/{dsh-mneme/src → lib}/search/bm25.js +96 -96
- package/{dsh-mneme/src → lib}/search/tag-boost.js +61 -61
- package/{dsh-mneme/src → lib}/service.js +1726 -1726
- package/{dsh-mneme/lib → lib}/settings.js +172 -172
- package/{dsh-mneme/src → lib}/store.js +2238 -2238
- package/{dsh-mneme/src → lib}/summarize.js +236 -236
- package/{dsh-mneme/src → lib}/tools.js +452 -290
- package/{dsh-mneme/lib → lib}/vector-index.js +116 -116
- package/package.json +40 -18
- package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +201 -201
- package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +133 -133
- package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +166 -166
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +218 -218
- package/{dsh-mneme/scripts → scripts}/stress-dsh.js +255 -255
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +52 -52
- package/{dsh-mneme/lib → src}/api.js +783 -783
- package/{dsh-mneme/lib → src}/commands.js +64 -64
- package/{dsh-mneme/src → src}/config.js +298 -298
- package/{dsh-mneme/src → src}/dream/clustering.js +118 -118
- package/{dsh-mneme/src → src}/dream/decisions.js +488 -488
- package/{dsh-mneme/src → src}/dream/sleep.js +561 -561
- package/{dsh-mneme/lib → src}/dream/tag-extractor.js +156 -156
- package/{dsh-mneme/src → src}/dream.js +958 -958
- package/{dsh-mneme/lib → src}/embedding.js +154 -154
- package/{dsh-mneme/lib → src}/entities/extractor.js +242 -242
- package/{dsh-mneme/src → src}/hot-memory.js +53 -53
- package/{dsh-mneme/src → src}/index.js +361 -361
- package/{dsh-mneme/lib → src}/inject.js +208 -208
- package/{dsh-mneme/src → src}/local-embedder.js +282 -282
- package/{dsh-mneme/src → src}/mirror.js +170 -170
- package/{dsh-mneme/src → src}/parser/tag.js +59 -59
- package/{dsh-mneme/src → src}/parser/wiki-link.js +38 -38
- package/{dsh-mneme/lib → src}/quality-filter.js +123 -123
- package/{dsh-mneme/src → src}/reranker.js +218 -218
- package/{dsh-mneme/lib → src}/search/adaptive.js +22 -22
- package/{dsh-mneme/lib → src}/search/bm25.js +96 -96
- package/{dsh-mneme/lib → src}/search/tag-boost.js +61 -61
- package/{dsh-mneme/lib → src}/service.js +1726 -1726
- package/{dsh-mneme/src → src}/settings.js +172 -172
- package/{dsh-mneme/lib → src}/store.js +2238 -2238
- package/{dsh-mneme/lib → src}/summarize.js +236 -236
- package/{dsh-mneme/lib → src}/tools.js +452 -290
- package/{dsh-mneme/src → src}/vector-index.js +116 -116
- package/{dsh-mneme/test → test}/api.test.js +594 -594
- package/{dsh-mneme/test → test}/audit.test.js +448 -448
- package/{dsh-mneme/test → test}/benchmark.test.js +35 -35
- package/{dsh-mneme/test → test}/boundary-v0625.test.js +82 -82
- package/{dsh-mneme/test → test}/client.test.js +368 -368
- package/{dsh-mneme/test → test}/clustering.test.js +100 -100
- package/{dsh-mneme/test → test}/commands.test.js +69 -69
- package/{dsh-mneme/test → test}/config.test.js +50 -50
- package/{dsh-mneme/test → test}/conflict-freeze.test.js +290 -290
- package/{dsh-mneme/test → test}/directory.test.js +134 -134
- package/{dsh-mneme/test → test}/dream.test.js +1060 -1060
- package/{dsh-mneme/test → test}/entities.test.js +522 -522
- package/{dsh-mneme/test → test}/epistemic.test.js +298 -298
- package/{dsh-mneme/test → test}/fnew-0112.test.js +311 -311
- package/{dsh-mneme/test → test}/fnew-03.test.js +422 -422
- package/{dsh-mneme/test → test}/graph-api.test.js +175 -175
- package/{dsh-mneme/test → test}/helpers/dream-mock.js +82 -82
- package/{dsh-mneme/test → test}/hot-memory.test.js +174 -174
- package/{dsh-mneme/test → test}/inject.test.js +103 -103
- package/{dsh-mneme/test → test}/llm-audit.test.js +279 -279
- package/{dsh-mneme/test → test}/local-embedder.test.js +227 -227
- package/{dsh-mneme/test → test}/mirror-dirty.test.js +424 -424
- package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +187 -187
- package/{dsh-mneme/test → test}/mirror-generation.test.js +499 -499
- package/{dsh-mneme/test → test}/mirror.test.js +249 -249
- package/{dsh-mneme/test → test}/normalize-decisions.test.js +120 -120
- package/{dsh-mneme/test → test}/peer-blockers.test.js +190 -190
- package/{dsh-mneme/test → test}/policy-epoch.test.js +259 -259
- package/{dsh-mneme/test → test}/provenance.test.js +103 -103
- package/{dsh-mneme/test → test}/quality-filter.test.js +118 -118
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +199 -199
- package/{dsh-mneme/test → test}/recall-evals.test.js +235 -235
- package/{dsh-mneme/test → test}/recall-layer.test.js +315 -315
- package/{dsh-mneme/test → test}/receipt-chain.test.js +451 -451
- package/{dsh-mneme/test → test}/reflection.test.js +226 -226
- package/{dsh-mneme/test → test}/reranker.test.js +240 -240
- package/{dsh-mneme/test → test}/search-fusion.test.js +90 -90
- package/{dsh-mneme/test → test}/semantic.test.js +124 -124
- package/{dsh-mneme/test → test}/service-search.test.js +199 -199
- package/{dsh-mneme/test → test}/service.test.js +435 -435
- package/{dsh-mneme/test → test}/settings.test.js +118 -118
- package/{dsh-mneme/test → test}/sleep.test.js +365 -365
- package/{dsh-mneme/test → test}/store.test.js +436 -436
- package/{dsh-mneme/test → test}/stress.test.js +209 -209
- package/{dsh-mneme/test → test}/summarize.test.js +191 -191
- package/{dsh-mneme/test → test}/tag-boost.test.js +125 -125
- package/{dsh-mneme/test → test}/tag.test.js +312 -312
- package/{dsh-mneme/test → test}/tools.test.js +541 -285
- package/{dsh-mneme/test → test}/vector-index.test.js +221 -221
- package/{dsh-mneme/test → test}/wiki-link.test.js +332 -332
- package/.github/workflows/test.yml +0 -32
- package/.release-notes-v0.6.9.md +0 -13
- package/CHANGELOG.md +0 -89
- package/SECURITY.md +0 -544
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -248
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -465
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -80
- package//346/250/252/345/271/205.png +0 -0
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { validateDecisions, applyDecisions } from "../src/dream/decisions.js";
|
|
4
|
-
import { createStore } from "../src/store.js";
|
|
5
|
-
import { createService } from "../src/service.js";
|
|
6
|
-
|
|
7
|
-
// snapshot helper: memories eligible for consolidation (non-archived, non-summary)
|
|
8
|
-
function makeSnapshot(store) {
|
|
9
|
-
const snap = new Map();
|
|
10
|
-
for (const m of store.all()) {
|
|
11
|
-
if (!m.archived && m.type !== "summary") snap.set(m.id, m);
|
|
12
|
-
}
|
|
13
|
-
return snap;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function setup() {
|
|
17
|
-
const store = createStore(":memory:");
|
|
18
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
19
|
-
return { store, service };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Insert a memory and backdate created_at to `hoursAgo`. */
|
|
23
|
-
function seedMemory(service, store, fields, hoursAgo = 48) {
|
|
24
|
-
const created = service.saveWithDedupe(fields);
|
|
25
|
-
const mem = created.memory;
|
|
26
|
-
if (hoursAgo) {
|
|
27
|
-
const old = new Date(Date.now() - hoursAgo * 3600000).toISOString();
|
|
28
|
-
store.db.prepare("UPDATE memories SET created_at = ? WHERE id = ?").run(old, mem.id);
|
|
29
|
-
}
|
|
30
|
-
return store.getById(mem.id);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// --- update validation ---------------------------------------------------
|
|
34
|
-
|
|
35
|
-
test("update validation rejects multiple ids", () => {
|
|
36
|
-
const { store } = setup();
|
|
37
|
-
const snap = makeSnapshot(store);
|
|
38
|
-
const { ok, errors } = validateDecisions(
|
|
39
|
-
[{ action: "update", ids: ["a", "b"], content: "new" }], snap
|
|
40
|
-
);
|
|
41
|
-
assert.equal(ok, false);
|
|
42
|
-
assert.ok(errors.some((e) => e.includes("exactly one id")));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("update validation rejects no actual change", () => {
|
|
46
|
-
const { store, service } = setup();
|
|
47
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
48
|
-
const snap = makeSnapshot(store);
|
|
49
|
-
const { ok, errors } = validateDecisions(
|
|
50
|
-
[{ action: "update", ids: [mem.id], content: "喜欢 Python" }], snap
|
|
51
|
-
);
|
|
52
|
-
assert.equal(ok, false);
|
|
53
|
-
assert.ok(errors.some((e) => e.includes("change at least one field")));
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("update validation rejects summary type", () => {
|
|
57
|
-
const { store, service } = setup();
|
|
58
|
-
const mem = seedMemory(service, store, { type: "summary", title: "总览", content: "overview" });
|
|
59
|
-
const snap = makeSnapshot(store);
|
|
60
|
-
// summary is excluded from snapshot, so also test direct: simulate by forcing it in
|
|
61
|
-
snap.set(mem.id, mem);
|
|
62
|
-
const { ok, errors } = validateDecisions(
|
|
63
|
-
[{ action: "update", ids: [mem.id], content: "new overview" }], snap
|
|
64
|
-
);
|
|
65
|
-
assert.equal(ok, false);
|
|
66
|
-
assert.ok(errors.some((e) => e.includes("summary") || e.includes("not eligible")));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("update validation rejects too-young memory (< minAgeHours)", () => {
|
|
70
|
-
const { store, service } = setup();
|
|
71
|
-
const mem = seedMemory(service, store, { type: "preference", title: "新", content: "新记忆" }, 1); // 1 hour old
|
|
72
|
-
const snap = makeSnapshot(store);
|
|
73
|
-
const { ok, errors } = validateDecisions(
|
|
74
|
-
[{ action: "update", ids: [mem.id], content: "修正" }], snap, { minAgeHours: 24 }
|
|
75
|
-
);
|
|
76
|
-
assert.equal(ok, false);
|
|
77
|
-
assert.ok(errors.some((e) => e.includes("too young")));
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("update validation respects configurable minAgeHours", () => {
|
|
81
|
-
const { store, service } = setup();
|
|
82
|
-
const mem = seedMemory(service, store, { type: "preference", title: "新", content: "新记忆" }, 1);
|
|
83
|
-
const snap = makeSnapshot(store);
|
|
84
|
-
// minAgeHours=0 disables the age guard -> update of a 1h-old memory passes
|
|
85
|
-
const { ok, errors } = validateDecisions(
|
|
86
|
-
[{ action: "update", ids: [mem.id], content: "修正" }], snap, { minAgeHours: 0 }
|
|
87
|
-
);
|
|
88
|
-
assert.equal(ok, true, errors.join("; "));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("update validation rejects > maxUpdatePerRun updates", () => {
|
|
92
|
-
const { store, service } = setup();
|
|
93
|
-
const a = seedMemory(service, store, { type: "preference", title: "A", content: "a" });
|
|
94
|
-
const b = seedMemory(service, store, { type: "preference", title: "B", content: "b" });
|
|
95
|
-
const c = seedMemory(service, store, { type: "preference", title: "C", content: "c" });
|
|
96
|
-
const snap = makeSnapshot(store);
|
|
97
|
-
const { ok, errors } = validateDecisions([
|
|
98
|
-
{ action: "update", ids: [a.id], content: "a2" },
|
|
99
|
-
{ action: "update", ids: [b.id], content: "b2" },
|
|
100
|
-
{ action: "update", ids: [c.id], content: "c2" }
|
|
101
|
-
], snap, { maxUpdatePerRun: 2 });
|
|
102
|
-
assert.equal(ok, false);
|
|
103
|
-
assert.ok(errors.some((e) => e.includes("too many update")));
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("update validation passes a single valid update", () => {
|
|
107
|
-
const { store, service } = setup();
|
|
108
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
109
|
-
const snap = makeSnapshot(store);
|
|
110
|
-
const { ok, errors } = validateDecisions(
|
|
111
|
-
[{ action: "update", ids: [mem.id], content: "喜欢 Rust" }], snap
|
|
112
|
-
);
|
|
113
|
-
assert.equal(ok, true, errors.join("; "));
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// --- update application --------------------------------------------------
|
|
117
|
-
|
|
118
|
-
test("update application changes the memory content", () => {
|
|
119
|
-
const { store, service } = setup();
|
|
120
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
121
|
-
const snap = makeSnapshot(store);
|
|
122
|
-
const decisions = [{ action: "update", ids: [mem.id], content: "喜欢 Rust" }];
|
|
123
|
-
assert.equal(validateDecisions(decisions, snap).ok, true);
|
|
124
|
-
const { applied } = applyDecisions(decisions, service);
|
|
125
|
-
assert.equal(applied, 1);
|
|
126
|
-
const updated = store.getById(mem.id);
|
|
127
|
-
assert.equal(updated.content, "喜欢 Rust");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("update application is idempotent on replay", () => {
|
|
131
|
-
const { store, service } = setup();
|
|
132
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
133
|
-
const snap = makeSnapshot(store);
|
|
134
|
-
const decisions = [{ action: "update", ids: [mem.id], content: "喜欢 Rust" }];
|
|
135
|
-
applyDecisions(decisions, service);
|
|
136
|
-
const appliedAgain = applyDecisions(decisions, service);
|
|
137
|
-
assert.equal(appliedAgain.applied, 0, "replay of an already-applied update should be a no-op");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("update application preserves unspecified fields", () => {
|
|
141
|
-
const { store, service } = setup();
|
|
142
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python", importance: 3 });
|
|
143
|
-
const snap = makeSnapshot(store);
|
|
144
|
-
const decisions = [{ action: "update", ids: [mem.id], content: "喜欢 Rust" }];
|
|
145
|
-
applyDecisions(decisions, service);
|
|
146
|
-
const updated = store.getById(mem.id);
|
|
147
|
-
assert.equal(updated.content, "喜欢 Rust");
|
|
148
|
-
assert.equal(updated.title, "语言", "title preserved");
|
|
149
|
-
assert.equal(updated.importance, 3, "importance preserved");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// --- failure tracking ----------------------------------------------------
|
|
153
|
-
|
|
154
|
-
test("user correction records a failure_memories row", () => {
|
|
155
|
-
const store = createStore(":memory:");
|
|
156
|
-
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
157
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
158
|
-
service.update(mem.id, { content: "喜欢 Rust" });
|
|
159
|
-
const failures = store.listFailures();
|
|
160
|
-
assert.equal(failures.length, 1);
|
|
161
|
-
assert.equal(failures[0].failure_type, "user_correction");
|
|
162
|
-
assert.equal(failures[0].memory_id, mem.id);
|
|
163
|
-
assert.equal(failures[0].actual, "喜欢 Python");
|
|
164
|
-
assert.equal(failures[0].expected, "喜欢 Rust");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("user correction records failure when only title changes", () => {
|
|
168
|
-
const store = createStore(":memory:");
|
|
169
|
-
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
170
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
171
|
-
service.update(mem.id, { title: "编程语言" }); // title change, content same
|
|
172
|
-
const failures = store.listFailures();
|
|
173
|
-
assert.equal(failures.length, 1, "title-only change should still record a failure");
|
|
174
|
-
assert.equal(failures[0].failure_type, "user_correction");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("user correction records query context when provided", () => {
|
|
178
|
-
const store = createStore(":memory:");
|
|
179
|
-
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
180
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
181
|
-
service.update(mem.id, { content: "喜欢 Rust" }, { query: "用户说:不对,我喜欢 Rust" });
|
|
182
|
-
const failures = store.listFailures();
|
|
183
|
-
assert.equal(failures.length, 1);
|
|
184
|
-
assert.equal(failures[0].query, "用户说:不对,我喜欢 Rust");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("user correction skips failure when nothing meaningful changes", () => {
|
|
188
|
-
const store = createStore(":memory:");
|
|
189
|
-
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
190
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python", importance: 3 });
|
|
191
|
-
service.update(mem.id, { title: "语言", content: "喜欢 Python", importance: 3 }); // identical
|
|
192
|
-
assert.equal(store.listFailures().length, 0, "no meaningful change -> no failure");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("deleteOldFailures removes rows older than the cutoff", () => {
|
|
196
|
-
const store = createStore(":memory:");
|
|
197
|
-
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
198
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
199
|
-
service.update(mem.id, { content: "喜欢 Rust" });
|
|
200
|
-
assert.equal(store.listFailures().length, 1);
|
|
201
|
-
// cutoff in the future: everything is older, so all rows get deleted
|
|
202
|
-
const removed = store.deleteOldFailures(new Date(Date.now() + 86400000).toISOString());
|
|
203
|
-
assert.equal(removed, 1);
|
|
204
|
-
assert.equal(store.listFailures().length, 0);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("failure tracking disabled by config", () => {
|
|
208
|
-
const store = createStore(":memory:");
|
|
209
|
-
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: false } });
|
|
210
|
-
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
211
|
-
service.update(mem.id, { content: "喜欢 Rust" });
|
|
212
|
-
assert.equal(store.listFailures().length, 0);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("listFailures and getFailureStats work", () => {
|
|
216
|
-
const store = createStore(":memory:");
|
|
217
|
-
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
218
|
-
const a = seedMemory(service, store, { type: "preference", title: "A", content: "x1" });
|
|
219
|
-
const b = seedMemory(service, store, { type: "preference", title: "B", content: "y1" });
|
|
220
|
-
service.update(a.id, { content: "x2" });
|
|
221
|
-
service.update(b.id, { content: "y2" });
|
|
222
|
-
const failures = store.listFailures();
|
|
223
|
-
assert.equal(failures.length, 2);
|
|
224
|
-
const stats = store.getFailureStats();
|
|
225
|
-
assert.equal(stats.user_correction, 2);
|
|
226
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { validateDecisions, applyDecisions } from "../src/dream/decisions.js";
|
|
4
|
+
import { createStore } from "../src/store.js";
|
|
5
|
+
import { createService } from "../src/service.js";
|
|
6
|
+
|
|
7
|
+
// snapshot helper: memories eligible for consolidation (non-archived, non-summary)
|
|
8
|
+
function makeSnapshot(store) {
|
|
9
|
+
const snap = new Map();
|
|
10
|
+
for (const m of store.all()) {
|
|
11
|
+
if (!m.archived && m.type !== "summary") snap.set(m.id, m);
|
|
12
|
+
}
|
|
13
|
+
return snap;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function setup() {
|
|
17
|
+
const store = createStore(":memory:");
|
|
18
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
19
|
+
return { store, service };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Insert a memory and backdate created_at to `hoursAgo`. */
|
|
23
|
+
function seedMemory(service, store, fields, hoursAgo = 48) {
|
|
24
|
+
const created = service.saveWithDedupe(fields);
|
|
25
|
+
const mem = created.memory;
|
|
26
|
+
if (hoursAgo) {
|
|
27
|
+
const old = new Date(Date.now() - hoursAgo * 3600000).toISOString();
|
|
28
|
+
store.db.prepare("UPDATE memories SET created_at = ? WHERE id = ?").run(old, mem.id);
|
|
29
|
+
}
|
|
30
|
+
return store.getById(mem.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- update validation ---------------------------------------------------
|
|
34
|
+
|
|
35
|
+
test("update validation rejects multiple ids", () => {
|
|
36
|
+
const { store } = setup();
|
|
37
|
+
const snap = makeSnapshot(store);
|
|
38
|
+
const { ok, errors } = validateDecisions(
|
|
39
|
+
[{ action: "update", ids: ["a", "b"], content: "new" }], snap
|
|
40
|
+
);
|
|
41
|
+
assert.equal(ok, false);
|
|
42
|
+
assert.ok(errors.some((e) => e.includes("exactly one id")));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("update validation rejects no actual change", () => {
|
|
46
|
+
const { store, service } = setup();
|
|
47
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
48
|
+
const snap = makeSnapshot(store);
|
|
49
|
+
const { ok, errors } = validateDecisions(
|
|
50
|
+
[{ action: "update", ids: [mem.id], content: "喜欢 Python" }], snap
|
|
51
|
+
);
|
|
52
|
+
assert.equal(ok, false);
|
|
53
|
+
assert.ok(errors.some((e) => e.includes("change at least one field")));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("update validation rejects summary type", () => {
|
|
57
|
+
const { store, service } = setup();
|
|
58
|
+
const mem = seedMemory(service, store, { type: "summary", title: "总览", content: "overview" });
|
|
59
|
+
const snap = makeSnapshot(store);
|
|
60
|
+
// summary is excluded from snapshot, so also test direct: simulate by forcing it in
|
|
61
|
+
snap.set(mem.id, mem);
|
|
62
|
+
const { ok, errors } = validateDecisions(
|
|
63
|
+
[{ action: "update", ids: [mem.id], content: "new overview" }], snap
|
|
64
|
+
);
|
|
65
|
+
assert.equal(ok, false);
|
|
66
|
+
assert.ok(errors.some((e) => e.includes("summary") || e.includes("not eligible")));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("update validation rejects too-young memory (< minAgeHours)", () => {
|
|
70
|
+
const { store, service } = setup();
|
|
71
|
+
const mem = seedMemory(service, store, { type: "preference", title: "新", content: "新记忆" }, 1); // 1 hour old
|
|
72
|
+
const snap = makeSnapshot(store);
|
|
73
|
+
const { ok, errors } = validateDecisions(
|
|
74
|
+
[{ action: "update", ids: [mem.id], content: "修正" }], snap, { minAgeHours: 24 }
|
|
75
|
+
);
|
|
76
|
+
assert.equal(ok, false);
|
|
77
|
+
assert.ok(errors.some((e) => e.includes("too young")));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("update validation respects configurable minAgeHours", () => {
|
|
81
|
+
const { store, service } = setup();
|
|
82
|
+
const mem = seedMemory(service, store, { type: "preference", title: "新", content: "新记忆" }, 1);
|
|
83
|
+
const snap = makeSnapshot(store);
|
|
84
|
+
// minAgeHours=0 disables the age guard -> update of a 1h-old memory passes
|
|
85
|
+
const { ok, errors } = validateDecisions(
|
|
86
|
+
[{ action: "update", ids: [mem.id], content: "修正" }], snap, { minAgeHours: 0 }
|
|
87
|
+
);
|
|
88
|
+
assert.equal(ok, true, errors.join("; "));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("update validation rejects > maxUpdatePerRun updates", () => {
|
|
92
|
+
const { store, service } = setup();
|
|
93
|
+
const a = seedMemory(service, store, { type: "preference", title: "A", content: "a" });
|
|
94
|
+
const b = seedMemory(service, store, { type: "preference", title: "B", content: "b" });
|
|
95
|
+
const c = seedMemory(service, store, { type: "preference", title: "C", content: "c" });
|
|
96
|
+
const snap = makeSnapshot(store);
|
|
97
|
+
const { ok, errors } = validateDecisions([
|
|
98
|
+
{ action: "update", ids: [a.id], content: "a2" },
|
|
99
|
+
{ action: "update", ids: [b.id], content: "b2" },
|
|
100
|
+
{ action: "update", ids: [c.id], content: "c2" }
|
|
101
|
+
], snap, { maxUpdatePerRun: 2 });
|
|
102
|
+
assert.equal(ok, false);
|
|
103
|
+
assert.ok(errors.some((e) => e.includes("too many update")));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("update validation passes a single valid update", () => {
|
|
107
|
+
const { store, service } = setup();
|
|
108
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
109
|
+
const snap = makeSnapshot(store);
|
|
110
|
+
const { ok, errors } = validateDecisions(
|
|
111
|
+
[{ action: "update", ids: [mem.id], content: "喜欢 Rust" }], snap
|
|
112
|
+
);
|
|
113
|
+
assert.equal(ok, true, errors.join("; "));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// --- update application --------------------------------------------------
|
|
117
|
+
|
|
118
|
+
test("update application changes the memory content", () => {
|
|
119
|
+
const { store, service } = setup();
|
|
120
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
121
|
+
const snap = makeSnapshot(store);
|
|
122
|
+
const decisions = [{ action: "update", ids: [mem.id], content: "喜欢 Rust" }];
|
|
123
|
+
assert.equal(validateDecisions(decisions, snap).ok, true);
|
|
124
|
+
const { applied } = applyDecisions(decisions, service);
|
|
125
|
+
assert.equal(applied, 1);
|
|
126
|
+
const updated = store.getById(mem.id);
|
|
127
|
+
assert.equal(updated.content, "喜欢 Rust");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("update application is idempotent on replay", () => {
|
|
131
|
+
const { store, service } = setup();
|
|
132
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
133
|
+
const snap = makeSnapshot(store);
|
|
134
|
+
const decisions = [{ action: "update", ids: [mem.id], content: "喜欢 Rust" }];
|
|
135
|
+
applyDecisions(decisions, service);
|
|
136
|
+
const appliedAgain = applyDecisions(decisions, service);
|
|
137
|
+
assert.equal(appliedAgain.applied, 0, "replay of an already-applied update should be a no-op");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("update application preserves unspecified fields", () => {
|
|
141
|
+
const { store, service } = setup();
|
|
142
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python", importance: 3 });
|
|
143
|
+
const snap = makeSnapshot(store);
|
|
144
|
+
const decisions = [{ action: "update", ids: [mem.id], content: "喜欢 Rust" }];
|
|
145
|
+
applyDecisions(decisions, service);
|
|
146
|
+
const updated = store.getById(mem.id);
|
|
147
|
+
assert.equal(updated.content, "喜欢 Rust");
|
|
148
|
+
assert.equal(updated.title, "语言", "title preserved");
|
|
149
|
+
assert.equal(updated.importance, 3, "importance preserved");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// --- failure tracking ----------------------------------------------------
|
|
153
|
+
|
|
154
|
+
test("user correction records a failure_memories row", () => {
|
|
155
|
+
const store = createStore(":memory:");
|
|
156
|
+
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
157
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
158
|
+
service.update(mem.id, { content: "喜欢 Rust" });
|
|
159
|
+
const failures = store.listFailures();
|
|
160
|
+
assert.equal(failures.length, 1);
|
|
161
|
+
assert.equal(failures[0].failure_type, "user_correction");
|
|
162
|
+
assert.equal(failures[0].memory_id, mem.id);
|
|
163
|
+
assert.equal(failures[0].actual, "喜欢 Python");
|
|
164
|
+
assert.equal(failures[0].expected, "喜欢 Rust");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("user correction records failure when only title changes", () => {
|
|
168
|
+
const store = createStore(":memory:");
|
|
169
|
+
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
170
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
171
|
+
service.update(mem.id, { title: "编程语言" }); // title change, content same
|
|
172
|
+
const failures = store.listFailures();
|
|
173
|
+
assert.equal(failures.length, 1, "title-only change should still record a failure");
|
|
174
|
+
assert.equal(failures[0].failure_type, "user_correction");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("user correction records query context when provided", () => {
|
|
178
|
+
const store = createStore(":memory:");
|
|
179
|
+
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
180
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
181
|
+
service.update(mem.id, { content: "喜欢 Rust" }, { query: "用户说:不对,我喜欢 Rust" });
|
|
182
|
+
const failures = store.listFailures();
|
|
183
|
+
assert.equal(failures.length, 1);
|
|
184
|
+
assert.equal(failures[0].query, "用户说:不对,我喜欢 Rust");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("user correction skips failure when nothing meaningful changes", () => {
|
|
188
|
+
const store = createStore(":memory:");
|
|
189
|
+
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
190
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python", importance: 3 });
|
|
191
|
+
service.update(mem.id, { title: "语言", content: "喜欢 Python", importance: 3 }); // identical
|
|
192
|
+
assert.equal(store.listFailures().length, 0, "no meaningful change -> no failure");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("deleteOldFailures removes rows older than the cutoff", () => {
|
|
196
|
+
const store = createStore(":memory:");
|
|
197
|
+
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
198
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
199
|
+
service.update(mem.id, { content: "喜欢 Rust" });
|
|
200
|
+
assert.equal(store.listFailures().length, 1);
|
|
201
|
+
// cutoff in the future: everything is older, so all rows get deleted
|
|
202
|
+
const removed = store.deleteOldFailures(new Date(Date.now() + 86400000).toISOString());
|
|
203
|
+
assert.equal(removed, 1);
|
|
204
|
+
assert.equal(store.listFailures().length, 0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("failure tracking disabled by config", () => {
|
|
208
|
+
const store = createStore(":memory:");
|
|
209
|
+
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: false } });
|
|
210
|
+
const mem = seedMemory(service, store, { type: "preference", title: "语言", content: "喜欢 Python" });
|
|
211
|
+
service.update(mem.id, { content: "喜欢 Rust" });
|
|
212
|
+
assert.equal(store.listFailures().length, 0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("listFailures and getFailureStats work", () => {
|
|
216
|
+
const store = createStore(":memory:");
|
|
217
|
+
const service = createService({ store, mirror: null, config: { reflectionFailureTracking: true } });
|
|
218
|
+
const a = seedMemory(service, store, { type: "preference", title: "A", content: "x1" });
|
|
219
|
+
const b = seedMemory(service, store, { type: "preference", title: "B", content: "y1" });
|
|
220
|
+
service.update(a.id, { content: "x2" });
|
|
221
|
+
service.update(b.id, { content: "y2" });
|
|
222
|
+
const failures = store.listFailures();
|
|
223
|
+
assert.equal(failures.length, 2);
|
|
224
|
+
const stats = store.getFailureStats();
|
|
225
|
+
assert.equal(stats.user_correction, 2);
|
|
226
|
+
});
|