@modusensus/dsh-mneme 0.8.2 → 0.8.4
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 +718 -633
- package/bin/cli.mjs +0 -0
- package/bin/dsh-mneme-mcp.mjs +502 -0
- package/cordis.patch.yml +21 -21
- package/lib/api-standalone.js +495 -316
- package/lib/api.js +1382 -1316
- package/lib/bootstrap.js +211 -0
- package/lib/client.js +4475 -4362
- package/lib/commands.js +95 -95
- package/lib/config.js +533 -500
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +658 -658
- package/lib/dream/narratives.js +44 -0
- package/lib/dream/sleep.js +700 -700
- package/lib/dream.js +1319 -1216
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +249 -249
- package/lib/heat.js +142 -136
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +515 -513
- package/lib/inject.js +299 -253
- package/lib/lang.js +202 -179
- package/lib/local-embedder.js +320 -308
- package/lib/mirror.js +198 -198
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +103 -0
- package/lib/reranker.js +269 -260
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +235 -235
- package/lib/runtime/loader.js +249 -249
- package/lib/runtime/model-fetch.js +361 -0
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +191 -191
- package/lib/scope.js +118 -118
- package/lib/search/adaptive.js +22 -22
- package/lib/search/bm25.js +96 -96
- package/lib/service.js +2171 -2071
- package/lib/settings.js +430 -419
- package/lib/store.js +2448 -2367
- package/lib/summarize.js +575 -449
- package/lib/tools.js +574 -574
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/package.json +109 -108
- package/runtime-manifest.json +596 -596
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +199 -199
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +217 -217
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +61 -61
- package/src/api-standalone.js +495 -316
- package/src/api.js +1382 -1316
- package/src/bootstrap.js +211 -0
- package/src/commands.js +95 -95
- package/src/config.js +533 -500
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +658 -658
- package/src/dream/narratives.js +44 -0
- package/src/dream/sleep.js +700 -700
- package/src/dream.js +1319 -1216
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +249 -249
- package/src/heat.js +142 -136
- package/src/hot-memory.js +46 -46
- package/src/index.js +515 -513
- package/src/inject.js +299 -253
- package/src/lang.js +202 -179
- package/src/local-embedder.js +320 -308
- package/src/mirror.js +198 -198
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +103 -0
- package/src/reranker.js +269 -260
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +235 -235
- package/src/runtime/loader.js +249 -249
- package/src/runtime/model-fetch.js +361 -0
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +191 -191
- package/src/scope.js +118 -118
- package/src/search/adaptive.js +22 -22
- package/src/search/bm25.js +96 -96
- package/src/service.js +2171 -2071
- package/src/settings.js +430 -419
- package/src/store.js +2448 -2367
- package/src/summarize.js +575 -449
- package/src/tools.js +574 -574
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1578 -1481
- package/test/audit.test.js +448 -448
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -0
- package/test/client.test.js +799 -736
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream.test.js +1257 -1254
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +547 -547
- package/test/entity-recall.test.js +179 -0
- package/test/epistemic.test.js +298 -298
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-api.test.js +175 -175
- package/test/heat.test.js +175 -148
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +89 -0
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +61 -0
- package/test/inject-rotation.test.js +161 -0
- package/test/inject.test.js +249 -249
- package/test/lang.test.js +87 -87
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +309 -309
- package/test/llm-message-source.test.js +177 -177
- package/test/local-embedder.test.js +249 -229
- package/test/mcp-stdio.test.js +385 -0
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -0
- package/test/peer-blockers.test.js +193 -193
- package/test/policy-epoch.test.js +259 -259
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +746 -746
- package/test/recall-evals.test.js +235 -235
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +117 -0
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -0
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +237 -237
- package/test/runtime-manifest.test.js +158 -158
- package/test/runtime-model-fetch.test.js +348 -0
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +214 -214
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +231 -231
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +405 -405
- package/test/settings.test.js +261 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -125
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +373 -373
- package/test/standalone-api.test.js +544 -359
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +383 -383
- package/test/stress.test.js +209 -209
- package/test/summarize.test.js +884 -575
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +341 -341
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
package/test/reflection.test.js
CHANGED
|
@@ -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
|
+
});
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { backfillMissingEmbeddings } from "../src/index.js";
|
|
4
|
-
import { createStore } from "../src/store.js";
|
|
5
|
-
import { createService } from "../src/service.js";
|
|
6
|
-
import { createVectorIndex } from "../src/vector-index.js";
|
|
7
|
-
|
|
8
|
-
// Issue #128:boot 回填曾被模型指纹门短路——任意一条嵌入成功写入 vector_meta
|
|
9
|
-
// 指纹后,指纹匹配的每次启动都在门上直接 return,写入时嵌入失败(embedder 未
|
|
10
|
-
// 就绪 / 服务商限流)的活跃行从此永远补不上(报告实测 88 条活跃记忆长期缺失,
|
|
11
|
-
// /vector-reindex 的配额还被归档死行吃掉)。
|
|
12
|
-
|
|
13
|
-
test("Issue #128: fingerprint match no longer blocks backfill; archived rows are skipped", async () => {
|
|
14
|
-
const store = createStore(":memory:");
|
|
15
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
16
|
-
const active = service.saveWithDedupe({ type: "preference", title: "活跃", content: "内容" }).memory;
|
|
17
|
-
const dead = service.saveWithDedupe({ type: "preference", title: "死行", content: "内容" }).memory;
|
|
18
|
-
store.setArchived(dead.id, true);
|
|
19
|
-
|
|
20
|
-
// 指纹已与 embedder 一致 —— 旧的早退条件在此成立,旧实现会整场不回填。
|
|
21
|
-
const vectorIndex = createVectorIndex({ store });
|
|
22
|
-
vectorIndex.markModel("mock-model#abc", 3);
|
|
23
|
-
const embedded = [];
|
|
24
|
-
const embedder = {
|
|
25
|
-
embedSingle: async (text) => { embedded.push(text); return [1, 0, 0]; },
|
|
26
|
-
modelHash: "mock-model#abc",
|
|
27
|
-
dimension: 3
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const logs = [];
|
|
31
|
-
const indexed = await backfillMissingEmbeddings({
|
|
32
|
-
store, embedder, vectorIndex,
|
|
33
|
-
logger: { info: (m) => logs.push(m) },
|
|
34
|
-
rateLimitMs: 0
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
assert.equal(indexed, 1, "the active row is backfilled despite the matching fingerprint");
|
|
38
|
-
assert.equal(store.getEmbeddings([active.id]).get(active.id)?.length, 3, "active row now carries a vector");
|
|
39
|
-
assert.equal(store.getEmbeddings([dead.id]).get(dead.id), undefined, "archived row is skipped");
|
|
40
|
-
assert.equal(vectorIndex.modelHash(), "mock-model#abc", "fingerprint unchanged (markModel idempotent)");
|
|
41
|
-
assert.ok(logs.some((m) => m.includes("backfilled 1")), "boot log records the backfill");
|
|
42
|
-
store.close();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("Issue #128: backfill is bounded by maxTotal; the rest stays queued", async () => {
|
|
46
|
-
const store = createStore(":memory:");
|
|
47
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
48
|
-
for (let i = 0; i < 5; i++) {
|
|
49
|
-
service.saveWithDedupe({ type: "preference", title: `记忆${i}`, content: `内容${i}` });
|
|
50
|
-
}
|
|
51
|
-
const embedder = { embedSingle: async () => [1, 0, 0], modelHash: "h#1", dimension: 3 };
|
|
52
|
-
const vectorIndex = createVectorIndex({ store });
|
|
53
|
-
|
|
54
|
-
const indexed = await backfillMissingEmbeddings({
|
|
55
|
-
store, embedder, vectorIndex,
|
|
56
|
-
logger: { info() {} },
|
|
57
|
-
maxTotal: 4, batchSize: 2, rateLimitMs: 0
|
|
58
|
-
});
|
|
59
|
-
assert.equal(indexed, 4, "stops at the maxTotal bound");
|
|
60
|
-
assert.equal(store.needsEmbedding(10).length, 1, "remaining rows stay queued for the next boot");
|
|
61
|
-
store.close();
|
|
62
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { backfillMissingEmbeddings } from "../src/index.js";
|
|
4
|
+
import { createStore } from "../src/store.js";
|
|
5
|
+
import { createService } from "../src/service.js";
|
|
6
|
+
import { createVectorIndex } from "../src/vector-index.js";
|
|
7
|
+
|
|
8
|
+
// Issue #128:boot 回填曾被模型指纹门短路——任意一条嵌入成功写入 vector_meta
|
|
9
|
+
// 指纹后,指纹匹配的每次启动都在门上直接 return,写入时嵌入失败(embedder 未
|
|
10
|
+
// 就绪 / 服务商限流)的活跃行从此永远补不上(报告实测 88 条活跃记忆长期缺失,
|
|
11
|
+
// /vector-reindex 的配额还被归档死行吃掉)。
|
|
12
|
+
|
|
13
|
+
test("Issue #128: fingerprint match no longer blocks backfill; archived rows are skipped", async () => {
|
|
14
|
+
const store = createStore(":memory:");
|
|
15
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
16
|
+
const active = service.saveWithDedupe({ type: "preference", title: "活跃", content: "内容" }).memory;
|
|
17
|
+
const dead = service.saveWithDedupe({ type: "preference", title: "死行", content: "内容" }).memory;
|
|
18
|
+
store.setArchived(dead.id, true);
|
|
19
|
+
|
|
20
|
+
// 指纹已与 embedder 一致 —— 旧的早退条件在此成立,旧实现会整场不回填。
|
|
21
|
+
const vectorIndex = createVectorIndex({ store });
|
|
22
|
+
vectorIndex.markModel("mock-model#abc", 3);
|
|
23
|
+
const embedded = [];
|
|
24
|
+
const embedder = {
|
|
25
|
+
embedSingle: async (text) => { embedded.push(text); return [1, 0, 0]; },
|
|
26
|
+
modelHash: "mock-model#abc",
|
|
27
|
+
dimension: 3
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const logs = [];
|
|
31
|
+
const indexed = await backfillMissingEmbeddings({
|
|
32
|
+
store, embedder, vectorIndex,
|
|
33
|
+
logger: { info: (m) => logs.push(m) },
|
|
34
|
+
rateLimitMs: 0
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
assert.equal(indexed, 1, "the active row is backfilled despite the matching fingerprint");
|
|
38
|
+
assert.equal(store.getEmbeddings([active.id]).get(active.id)?.length, 3, "active row now carries a vector");
|
|
39
|
+
assert.equal(store.getEmbeddings([dead.id]).get(dead.id), undefined, "archived row is skipped");
|
|
40
|
+
assert.equal(vectorIndex.modelHash(), "mock-model#abc", "fingerprint unchanged (markModel idempotent)");
|
|
41
|
+
assert.ok(logs.some((m) => m.includes("backfilled 1")), "boot log records the backfill");
|
|
42
|
+
store.close();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("Issue #128: backfill is bounded by maxTotal; the rest stays queued", async () => {
|
|
46
|
+
const store = createStore(":memory:");
|
|
47
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
48
|
+
for (let i = 0; i < 5; i++) {
|
|
49
|
+
service.saveWithDedupe({ type: "preference", title: `记忆${i}`, content: `内容${i}` });
|
|
50
|
+
}
|
|
51
|
+
const embedder = { embedSingle: async () => [1, 0, 0], modelHash: "h#1", dimension: 3 };
|
|
52
|
+
const vectorIndex = createVectorIndex({ store });
|
|
53
|
+
|
|
54
|
+
const indexed = await backfillMissingEmbeddings({
|
|
55
|
+
store, embedder, vectorIndex,
|
|
56
|
+
logger: { info() {} },
|
|
57
|
+
maxTotal: 4, batchSize: 2, rateLimitMs: 0
|
|
58
|
+
});
|
|
59
|
+
assert.equal(indexed, 4, "stops at the maxTotal bound");
|
|
60
|
+
assert.equal(store.needsEmbedding(10).length, 1, "remaining rows stay queued for the next boot");
|
|
61
|
+
store.close();
|
|
62
|
+
});
|