@modusensus/dsh-mneme 0.7.10 → 0.7.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/README.en.md +368 -0
- package/README.md +298 -169
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/{dsh-mneme/lib → lib}/api.js +50 -295
- package/{dsh-mneme/lib → lib}/client.js +780 -936
- package/{dsh-mneme/src → lib}/config.js +6 -100
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → lib}/index.js +6 -34
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/lib → lib}/settings.js +0 -70
- package/{dsh-mneme/lib → lib}/store.js +35 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +41 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/{dsh-mneme/src → src}/api.js +50 -295
- package/{dsh-mneme/lib → src}/config.js +6 -100
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/src → src}/index.js +6 -34
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/src → src}/settings.js +0 -70
- package/{dsh-mneme/src → src}/store.js +35 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +1 -156
- package/{dsh-mneme/test → test}/client.test.js +10 -206
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +0 -43
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -293
- package/SECURITY.md +0 -718
- 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 -419
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -501
- 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/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2242
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
import { createInjector } from "../src/inject.js";
|
|
6
|
+
import { createSettings } from "../src/settings.js";
|
|
7
|
+
|
|
8
|
+
function setup(over = {}) {
|
|
9
|
+
const store = createStore(":memory:");
|
|
10
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
11
|
+
const settings = createSettings(store.db);
|
|
12
|
+
const contexts = [];
|
|
13
|
+
const disposers = [];
|
|
14
|
+
const ctx = {
|
|
15
|
+
systemPrompt: {
|
|
16
|
+
context(def) {
|
|
17
|
+
contexts.push(def);
|
|
18
|
+
const dispose = () => disposers.push(def.name);
|
|
19
|
+
return dispose;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
|
|
24
|
+
const injector = createInjector(ctx, service, settings, config);
|
|
25
|
+
return { store, service, contexts, injector, settings };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("registers memory and user-settings contexts", () => {
|
|
29
|
+
const { contexts } = setup();
|
|
30
|
+
assert.equal(contexts.length, 2);
|
|
31
|
+
assert.equal(contexts[0].name, "memory");
|
|
32
|
+
assert.equal(contexts[1].name, "user-settings");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("context text renders injected memories as markdown block", () => {
|
|
36
|
+
const { contexts, service } = setup();
|
|
37
|
+
service.saveWithDedupe({ type: "preference", title: "语言", content: "用户用中文交流", importance: 5 });
|
|
38
|
+
service.saveWithDedupe({ type: "project", title: "记忆插件", content: "SQLite+Markdown", importance: 4 });
|
|
39
|
+
const text = contexts[0].text({});
|
|
40
|
+
assert.ok(text.includes("[记忆库]"), "has header");
|
|
41
|
+
assert.ok(text.includes("语言"), "includes preference");
|
|
42
|
+
assert.ok(text.includes("记忆插件"), "includes high-importance project");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns empty text when nothing qualifies", () => {
|
|
46
|
+
const { contexts } = setup();
|
|
47
|
+
const text = contexts[0].text({});
|
|
48
|
+
assert.equal(text, "");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("renders summary block first when summary candidate exists", () => {
|
|
52
|
+
const { contexts, service } = setup();
|
|
53
|
+
service.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "这是总览摘要", importance: 5 });
|
|
54
|
+
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文", importance: 5 });
|
|
55
|
+
const text = contexts[0].text({});
|
|
56
|
+
const summaryIdx = text.indexOf("这是总览摘要");
|
|
57
|
+
const prefIdx = text.indexOf("语言");
|
|
58
|
+
assert.ok(summaryIdx !== -1, "summary present");
|
|
59
|
+
assert.ok(prefIdx !== -1, "preference present");
|
|
60
|
+
assert.ok(summaryIdx < prefIdx, "summary rendered first");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("user-settings context renders profile and rules, empty when unset", () => {
|
|
64
|
+
const { contexts, settings } = setup();
|
|
65
|
+
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
66
|
+
assert.ok(settingsCtx, "user-settings context registered");
|
|
67
|
+
assert.equal(settingsCtx.text({}), "", "empty when no profile/rules");
|
|
68
|
+
settings.setProfile("我叫小明,是一名前端开发者");
|
|
69
|
+
settings.setRules(["回答时先给结论", "使用简体中文"]);
|
|
70
|
+
const text = settingsCtx.text({});
|
|
71
|
+
assert.ok(text.includes("用户画像"), "has profile header");
|
|
72
|
+
assert.ok(text.includes("前端开发者"), "includes profile");
|
|
73
|
+
assert.ok(text.includes("先给结论"), "includes rule");
|
|
74
|
+
assert.ok(text.includes("简体中文"), "includes second rule");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("user-settings context precedes memory block (order 85 < 90)", () => {
|
|
78
|
+
const { contexts, settings } = setup();
|
|
79
|
+
settings.setProfile("画像");
|
|
80
|
+
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
81
|
+
assert.ok(settingsCtx.order < contexts.find((c) => c.name === "memory").order);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("Bug6: long content is truncated to ~300 chars with an ellipsis", () => {
|
|
85
|
+
const { contexts, service } = setup();
|
|
86
|
+
const longContent = "这是一段非常长的记忆正文".repeat(200); // ~2600 chars
|
|
87
|
+
service.saveWithDedupe({ type: "preference", title: "长记忆", content: longContent, importance: 5 });
|
|
88
|
+
const text = contexts[0].text({});
|
|
89
|
+
assert.ok(text.includes("长记忆"), "memory still rendered");
|
|
90
|
+
assert.ok(text.includes("…"), "ellipsis marks the truncation");
|
|
91
|
+
assert.ok(!text.includes(longContent.slice(300)), "full body not injected verbatim");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("Bug6: injected block stays within the ~1500 char budget, later entries collapse", () => {
|
|
95
|
+
const { contexts, service } = setup({ maxInjectedItems: 8 });
|
|
96
|
+
for (let i = 0; i < 8; i++) {
|
|
97
|
+
service.saveWithDedupe({ type: "preference", title: `长标题记忆${i}`, content: "这是一段".repeat(100), importance: 5 });
|
|
98
|
+
}
|
|
99
|
+
const text = contexts[0].text({});
|
|
100
|
+
assert.ok(text.length <= 1600, `memory block bounded near budget, got ${text.length} chars`);
|
|
101
|
+
// The first entries render full bodies; every entry is present by title.
|
|
102
|
+
for (let i = 0; i < 8; i++) assert.ok(text.includes(`长标题记忆${i}`), `entry ${i} present`);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// DSH ≥0.1.2-rc removed Session.events; events are only reachable via
|
|
106
|
+
// snapshotEvents(). Regression for #59: the hot block must still render from
|
|
107
|
+
// a session object that has no .events property.
|
|
108
|
+
test("extracts hot rounds from snapshotEvents() when Session.events is absent", () => {
|
|
109
|
+
const { contexts } = setup();
|
|
110
|
+
const session = {
|
|
111
|
+
snapshotEvents: () => [
|
|
112
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "怎么修图谱面板" }] } },
|
|
113
|
+
{ type: "assistant/message", data: { content: [{ type: "text", text: "用 viewBox 单位跑模拟" }] } }
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
const text = contexts[0].text({ agent: { session } });
|
|
117
|
+
assert.ok(text.includes("[短期上下文]"), "hot block rendered");
|
|
118
|
+
assert.ok(text.includes("怎么修图谱面板"), "round query present");
|
|
119
|
+
assert.ok(text.includes("用 viewBox 单位跑模拟"), "round response present");
|
|
120
|
+
});
|
|
@@ -50,10 +50,10 @@ test("autoDream writes llm_audit_logs rows for consolidation and summary", async
|
|
|
50
50
|
assert.equal(summarize.trigger_source, "autoDream");
|
|
51
51
|
assert.equal(consolidate.status, "success");
|
|
52
52
|
assert.equal(summarize.status, "success");
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
assert.equal(consolidate.model_id, "
|
|
56
|
-
assert.equal(summarize.model_id, "
|
|
53
|
+
// mockCtx resolves the route from agentDefaultModel (mock:stress-model), not
|
|
54
|
+
// the config fallback — assert the actually-used route.
|
|
55
|
+
assert.equal(consolidate.model_id, "mock:stress-model");
|
|
56
|
+
assert.equal(summarize.model_id, "mock:stress-model");
|
|
57
57
|
assert.ok(Array.isArray(consolidate.related_memory_ids) && consolidate.related_memory_ids.length === 2,
|
|
58
58
|
"consolidation audit records the snapshot ids");
|
|
59
59
|
assert.ok(consolidate.total_tokens >= 0 && summarize.total_tokens >= 0);
|
|
@@ -36,11 +36,6 @@ test("issue#9: reasoningEffort config defaults to none and rejects unknown value
|
|
|
36
36
|
assert.equal(cfg.sleepReasoningEffort, "none");
|
|
37
37
|
assert.equal(Config({ dreamReasoningEffort: "high" }).dreamReasoningEffort, "high");
|
|
38
38
|
assert.equal(Config({ sleepReasoningEffort: "medium" }).sleepReasoningEffort, "medium");
|
|
39
|
-
// v0.5.3: 'off' explicitly disables thinking — required for thinking-type
|
|
40
|
-
// models (deepseek-v4-flash) whose reasoning otherwise drains the whole
|
|
41
|
-
// token budget and returns an empty consolidation body.
|
|
42
|
-
assert.equal(Config({ dreamReasoningEffort: "off" }).dreamReasoningEffort, "off");
|
|
43
|
-
assert.equal(Config({ sleepReasoningEffort: "off" }).sleepReasoningEffort, "off");
|
|
44
39
|
assert.throws(() => Config({ dreamReasoningEffort: "bogus" }), "invalid effort rejected");
|
|
45
40
|
assert.throws(() => Config({ sleepReasoningEffort: "ultra" }), "invalid effort rejected");
|
|
46
41
|
});
|
|
@@ -112,28 +107,6 @@ test("issue#9: dream forwards dreamReasoningEffort on both LLM calls", async ()
|
|
|
112
107
|
store.close();
|
|
113
108
|
});
|
|
114
109
|
|
|
115
|
-
test("issue#9: dream forwards dreamReasoningEffort:off (thinking disabled) on both LLM calls", async () => {
|
|
116
|
-
const store = createStore(":memory:");
|
|
117
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
118
|
-
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
119
|
-
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
120
|
-
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
121
|
-
const captured = [];
|
|
122
|
-
const ctx = dreamCtx({
|
|
123
|
-
captured,
|
|
124
|
-
onConsolidation: () => JSON.stringify([
|
|
125
|
-
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
126
|
-
])
|
|
127
|
-
});
|
|
128
|
-
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "off" });
|
|
129
|
-
assert.equal(result.ok, true);
|
|
130
|
-
assert.equal(captured.length, 2);
|
|
131
|
-
for (const options of captured) {
|
|
132
|
-
assert.equal(options.reasoningEffort, "off", `reasoningEffort:"off" forwarded on ${options.purpose}`);
|
|
133
|
-
}
|
|
134
|
-
store.close();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
110
|
// ---------------------------------------------------------------- sleep passthrough
|
|
138
111
|
|
|
139
112
|
function sleepSetup() {
|
|
@@ -77,23 +77,14 @@ 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 to off — recorder is not called", 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" });
|
|
95
86
|
await service.searchMemories("量子", { mode: "keyword", recordRecall: false });
|
|
96
|
-
assert.equal(calls, 0, "no recorder call when
|
|
87
|
+
assert.equal(calls, 0, "no recorder call when recordRecall is unset or false");
|
|
97
88
|
});
|
|
98
89
|
|
|
99
90
|
test("recordRecall=true with no recorder installed is safe and returns normally", async () => {
|
|
@@ -310,7 +301,7 @@ test("e2e: index.js wiring — recordRecall search lands a row listRecallRuns ca
|
|
|
310
301
|
assert.equal(store.getRecallRun(run.id).query, "量子", "row readable right away");
|
|
311
302
|
});
|
|
312
303
|
|
|
313
|
-
test("e2e: recordRecall
|
|
304
|
+
test("e2e: without recordRecall the recall_runs table gains no rows", async () => {
|
|
314
305
|
const store = createStore(":memory:");
|
|
315
306
|
const service = createService({ store, mirror: null, config: {} });
|
|
316
307
|
service.setRecallRecorder((recall) => store.saveRecallRun({
|
|
@@ -319,18 +310,6 @@ test("e2e: recordRecall defaults on (v0.7.0) but is disabled by config.recallRec
|
|
|
319
310
|
}));
|
|
320
311
|
service.saveWithDedupe({ type: "preference", title: "量子计算入门", content: "叠加态" });
|
|
321
312
|
await service.searchMemories("量子", { mode: "keyword" });
|
|
322
|
-
|
|
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();
|
|
313
|
+
await service.searchMemories("量子", { mode: "keyword", recordRecall: false });
|
|
314
|
+
assert.deepEqual(store.listRecallRuns(), [], "no rows without recordRecall=true");
|
|
336
315
|
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
3
|
import { LocalReranker } from "../src/reranker.js";
|
|
6
4
|
|
|
7
5
|
/** Injected scorer: records (query, passage) calls, returns a fixed score. */
|
|
@@ -197,44 +195,3 @@ test("dispose releases the loaded pipeline", async () => {
|
|
|
197
195
|
assert.equal(r.pipeline, null);
|
|
198
196
|
assert.equal(extractor.disposed, true);
|
|
199
197
|
});
|
|
200
|
-
|
|
201
|
-
test("default pipeline loader mirrors cache_dir onto env.cacheDir (issue #13)", () => {
|
|
202
|
-
// The fix lives in the module's *default* loader — the dynamic-import of
|
|
203
|
-
// @huggingface/transformers that the in-process tests bypass by injecting
|
|
204
|
-
// engineFactory. So it is exercised in a child node process under the
|
|
205
|
-
// --experimental-test-module-mocks flag: the transformers module is mocked
|
|
206
|
-
// with an empty env, LocalReranker uses the real default loader, and we
|
|
207
|
-
// assert env.cacheDir picks up the constructor's cache_dir. A regression
|
|
208
|
-
// (loader no longer mirroring) fails the child and surfaces here as a
|
|
209
|
-
// non-zero exit.
|
|
210
|
-
const srcUrl = new URL("../src/reranker.js", import.meta.url).href;
|
|
211
|
-
const cacheDir = "/tmp/dsh-mneme-cache-mirror-test";
|
|
212
|
-
const script = `
|
|
213
|
-
import { test } from "node:test";
|
|
214
|
-
const cacheDir = ${JSON.stringify(cacheDir)};
|
|
215
|
-
test("cache_dir is mirrored onto env.cacheDir", async (t) => {
|
|
216
|
-
t.mock.module("@huggingface/transformers", {
|
|
217
|
-
namedExports: {
|
|
218
|
-
env: {},
|
|
219
|
-
pipeline: async () => ({ dispose: () => {} })
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
const { LocalReranker } = await import(${JSON.stringify(srcUrl)});
|
|
223
|
-
const r = new LocalReranker({ cacheDir, device: "cpu" });
|
|
224
|
-
await r.init();
|
|
225
|
-
const { env } = await import("@huggingface/transformers");
|
|
226
|
-
if (env.cacheDir !== cacheDir) {
|
|
227
|
-
throw new Error("env.cacheDir not mirrored from cache_dir: " + env.cacheDir);
|
|
228
|
-
}
|
|
229
|
-
console.log("CACHE_MIRROR_OK");
|
|
230
|
-
});
|
|
231
|
-
`;
|
|
232
|
-
const res = spawnSync(process.execPath, [
|
|
233
|
-
"--experimental-test-module-mocks",
|
|
234
|
-
"--input-type=module",
|
|
235
|
-
"-e",
|
|
236
|
-
script
|
|
237
|
-
], { encoding: "utf8", cwd: fileURLToPath(new URL("..", import.meta.url)) });
|
|
238
|
-
assert.equal(res.status, 0, `cache-dir mirror child failed:\n${res.stdout}\n${res.stderr}`);
|
|
239
|
-
assert.match(res.stdout, /CACHE_MIRROR_OK/);
|
|
240
|
-
});
|
|
@@ -85,31 +85,6 @@ test("hybrid blend honors configured vector/keyword weights", async () => {
|
|
|
85
85
|
assert.ok(Math.abs(t.score - 0.9) < 1e-9, `tuned blend ${t.score}`);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
test("hybrid fusion clamps blended scores into [0,1]", async () => {
|
|
89
|
-
// Weights summing above 1 (1.0 + 1.0) push a same-memory blend over 1.0:
|
|
90
|
-
// title hit (importance 5) = 1 * (0.5 + 0.5) = 1.0, vector cosine = 1.0 →
|
|
91
|
-
// raw blend 1.0*1 + 1.0*1 = 2.0. The fused score must be clamped so
|
|
92
|
-
// consumers never see a score outside [0,1].
|
|
93
|
-
const store = createStore(":memory:");
|
|
94
|
-
const service = createService({ store, mirror: null, config: {
|
|
95
|
-
hybridSearchVectorWeight: 1,
|
|
96
|
-
hybridSearchKeywordWeight: 1,
|
|
97
|
-
adaptiveThresholdEnabled: false,
|
|
98
|
-
searchSemanticDedup: false
|
|
99
|
-
} });
|
|
100
|
-
const vi = createVectorIndex({ store });
|
|
101
|
-
service.setEmbedder(embedder);
|
|
102
|
-
service.setVectorIndex(vi);
|
|
103
|
-
const m = service.saveWithDedupe({ type: "preference", title: "量子计算", content: "量子计算入门", importance: 5 });
|
|
104
|
-
vi.saveEmbedding(m.memory.id, [1, 0, 0]);
|
|
105
|
-
|
|
106
|
-
const rows = await service.searchMemories("量子计算", { mode: "hybrid", topK: 10 });
|
|
107
|
-
const hit = rows.find((r) => r.id === m.memory.id);
|
|
108
|
-
assert.ok(hit, "blended row is recalled");
|
|
109
|
-
assert.equal(hit.score, 1, `over-weight blend clamped to 1 (got ${hit.score})`);
|
|
110
|
-
assert.ok(hit.score >= 0 && hit.score <= 1);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
88
|
test("auto = keyword leads, vector fills the remaining slots", async () => {
|
|
114
89
|
const { service, vectorIndex } = setup();
|
|
115
90
|
const kw = service.saveWithDedupe({ type: "preference", title: "量子计算", content: "量子计算入门" });
|
|
@@ -76,10 +76,6 @@ test("toApiList maps store rows to wire DTOs", () => {
|
|
|
76
76
|
assert.equal(dto[0].title, "选型");
|
|
77
77
|
assert.equal(dto[0].content, "node:sqlite");
|
|
78
78
|
assert.equal(dto[0].importance, 3);
|
|
79
|
-
// session_id rides the DTO when present (session lifecycle needs it)
|
|
80
|
-
const withSession = service.saveWithDedupe({ type: "decision", title: "带会话", content: "y", session_id: "sess-x" });
|
|
81
|
-
const dto2 = service.toApiList([withSession.memory]);
|
|
82
|
-
assert.equal(dto2[0].session_id, "sess-x");
|
|
83
79
|
});
|
|
84
80
|
|
|
85
81
|
test("mergeHumanEdits skips edits without id and keeps applying the rest", () => {
|
|
@@ -331,105 +327,3 @@ test("Bug7: injection ranking re-weights by quality_score (degraded memories ran
|
|
|
331
327
|
assert.equal(candidates[0].title, "高质量", "100-quality preference leads the degraded one");
|
|
332
328
|
assert.equal(candidates[1].title, "元记忆");
|
|
333
329
|
});
|
|
334
|
-
|
|
335
|
-
// --- session lifecycle (v0.6.0): dispose/restore by session -----------------
|
|
336
|
-
// session_disposed_at is orthogonal to `archived`: dispose hides only entries
|
|
337
|
-
// born in the deleted session, and restore never resurrects user-archived ones.
|
|
338
|
-
|
|
339
|
-
test("disposeBySession marks only that session's memories", () => {
|
|
340
|
-
const store = createStore(":memory:");
|
|
341
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
342
|
-
const s1 = svc.saveWithDedupe({ type: "project", title: "会话A记忆", content: "x", session_id: "sess-1" });
|
|
343
|
-
svc.saveWithDedupe({ type: "project", title: "会话B记忆", content: "y", session_id: "sess-2" });
|
|
344
|
-
|
|
345
|
-
const res = svc.disposeBySession("sess-1");
|
|
346
|
-
assert.deepEqual(res, { disposed: 1 });
|
|
347
|
-
assert.ok(svc.getById(s1.memory.id).session_disposed_at, "session-1 memory marked disposed");
|
|
348
|
-
// disposed memories vanish from default list (like archived)
|
|
349
|
-
assert.ok(!svc.list({ type: "project" }).some((m) => m.id === s1.memory.id), "disposed hidden from default list");
|
|
350
|
-
const other = svc.list({ type: "project", includeDisposed: true });
|
|
351
|
-
assert.ok(other.some((m) => m.title === "会话B记忆" && !m.session_disposed_at), "other session untouched");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test("restoreBySession clears the dispose mark and makes entries visible again", () => {
|
|
355
|
-
const store = createStore(":memory:");
|
|
356
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
357
|
-
const s1 = svc.saveWithDedupe({ type: "project", title: "存档会话", content: "x", session_id: "sess-9" });
|
|
358
|
-
svc.disposeBySession("sess-9");
|
|
359
|
-
|
|
360
|
-
assert.ok(!svc.list({ type: "project" }).some((m) => m.id === s1.memory.id), "hidden while disposed");
|
|
361
|
-
const res = svc.restoreBySession("sess-9");
|
|
362
|
-
assert.deepEqual(res, { restored: 1 });
|
|
363
|
-
assert.equal(svc.getById(s1.memory.id).session_disposed_at, undefined, "dispose mark cleared");
|
|
364
|
-
assert.ok(svc.list({ type: "project" }).some((m) => m.id === s1.memory.id), "visible again");
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
test("disposeBySession is idempotent: re-disposing returns 0", () => {
|
|
368
|
-
const store = createStore(":memory:");
|
|
369
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
370
|
-
svc.saveWithDedupe({ type: "project", title: "重复销毁", content: "x", session_id: "sess-3" });
|
|
371
|
-
|
|
372
|
-
assert.deepEqual(svc.disposeBySession("sess-3"), { disposed: 1 });
|
|
373
|
-
assert.deepEqual(svc.disposeBySession("sess-3"), { disposed: 0 }, "no rows flipped the second time");
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("disposeBySession on an unknown session is a no-op", () => {
|
|
377
|
-
const store = createStore(":memory:");
|
|
378
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
379
|
-
assert.deepEqual(svc.disposeBySession("nope"), { disposed: 0 });
|
|
380
|
-
assert.deepEqual(svc.restoreBySession("nope"), { restored: 0 });
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test("legacy rows without session_id are never touched by session lifecycle", () => {
|
|
384
|
-
const store = createStore(":memory:");
|
|
385
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
386
|
-
const legacy = svc.saveWithDedupe({ type: "preference", title: "全局记忆", content: "no session", session_id: undefined });
|
|
387
|
-
|
|
388
|
-
svc.disposeBySession("sess-x");
|
|
389
|
-
assert.equal(svc.getById(legacy.memory.id).session_disposed_at, undefined, "global memory survives session dispose");
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
test("restoreBySession does NOT resurrect user-archived memories (orthogonal)", () => {
|
|
393
|
-
const store = createStore(":memory:");
|
|
394
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
395
|
-
// user manually archived this one (archived=true), then the session is disposed
|
|
396
|
-
const { memory: userArchived } = svc.saveWithDedupe({ type: "decision", title: "手动归档", content: "x", session_id: "sess-4" });
|
|
397
|
-
svc.setArchived(userArchived.id, true);
|
|
398
|
-
svc.disposeBySession("sess-4");
|
|
399
|
-
|
|
400
|
-
svc.restoreBySession("sess-4");
|
|
401
|
-
assert.equal(svc.getById(userArchived.id).archived, true, "user archive survives restore");
|
|
402
|
-
assert.equal(svc.getById(userArchived.id).session_disposed_at, undefined, "dispose mark cleared but archive kept");
|
|
403
|
-
assert.ok(!svc.list({ type: "decision" }).some((m) => m.id === userArchived.id), "still hidden by user archive");
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
test("listBySession returns only that session's memories via wire DTOs", () => {
|
|
407
|
-
const store = createStore(":memory:");
|
|
408
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
409
|
-
const { memory } = svc.saveWithDedupe({ type: "decision", title: "会话决策", content: "d", session_id: "sess-7" });
|
|
410
|
-
svc.saveWithDedupe({ type: "decision", title: "别会话", content: "e", session_id: "sess-8" });
|
|
411
|
-
|
|
412
|
-
const rows = svc.listBySession("sess-7");
|
|
413
|
-
assert.equal(rows.length, 1);
|
|
414
|
-
assert.equal(rows[0].id, memory.id);
|
|
415
|
-
assert.equal(rows[0].title, "会话决策");
|
|
416
|
-
assert.equal(rows[0].session_id, "sess-7");
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
test("listBySession DTO hides disposed by default and flags it when included", () => {
|
|
420
|
-
const store = createStore(":memory:");
|
|
421
|
-
const svc = createService({ store, mirror: null, config: {} });
|
|
422
|
-
const { memory } = svc.saveWithDedupe({ type: "decision", title: "会话决策", content: "d", session_id: "sess-9" });
|
|
423
|
-
svc.disposeBySession("sess-9");
|
|
424
|
-
|
|
425
|
-
assert.equal(svc.listBySession("sess-9").length, 0, "disposed hidden by default");
|
|
426
|
-
const visible = svc.listBySession("sess-9", { includeDisposed: true });
|
|
427
|
-
assert.equal(visible.length, 1);
|
|
428
|
-
assert.equal(visible[0].id, memory.id);
|
|
429
|
-
assert.equal(visible[0].disposed, true, "DTO carries disposed marker so restore is not blind");
|
|
430
|
-
|
|
431
|
-
svc.restoreBySession("sess-9");
|
|
432
|
-
const restored = svc.listBySession("sess-9");
|
|
433
|
-
assert.equal(restored.length, 1, "restore brings it back to default view");
|
|
434
|
-
assert.equal(restored[0].disposed, undefined, "marker cleared after restore");
|
|
435
|
-
});
|
|
@@ -99,46 +99,3 @@ test("vector config disabled value is stored as false", () => {
|
|
|
99
99
|
assert.equal(settings.getVectorConfig().enabled, false);
|
|
100
100
|
store.close();
|
|
101
101
|
});
|
|
102
|
-
|
|
103
|
-
test("autoTag config defaults to null (unset) and round-trips only explicit keys", () => {
|
|
104
|
-
const { store, settings } = setup();
|
|
105
|
-
// Issue #31: an unset key is null so runtime consumers fall back to the
|
|
106
|
-
// plugin config default — the settings layer must not invent `false`.
|
|
107
|
-
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: null, manualTagEnabled: null });
|
|
108
|
-
settings.setAutoTagConfig({ autoTagEnabled: true, manualTagEnabled: true });
|
|
109
|
-
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: true, manualTagEnabled: true });
|
|
110
|
-
// A partial update only touches the key it was given: manualTagEnabled was
|
|
111
|
-
// stored earlier so it keeps its value (true), it does not revert to null.
|
|
112
|
-
settings.setAutoTagConfig({ autoTagEnabled: false });
|
|
113
|
-
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: true });
|
|
114
|
-
store.close();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("autoTag config coerces non-boolean to false", () => {
|
|
118
|
-
const { store, settings } = setup();
|
|
119
|
-
settings.setAutoTagConfig({ autoTagEnabled: "yes", manualTagEnabled: 1 });
|
|
120
|
-
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: false });
|
|
121
|
-
store.close();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("ui config (sidebar trigger) defaults to null and round-trips only explicit keys", () => {
|
|
125
|
-
const { store, settings } = setup();
|
|
126
|
-
// Issue #38: same settings-over-config pattern as the tag toggles — an unset
|
|
127
|
-
// key is null so consumers fall back to the plugin-config default (visible).
|
|
128
|
-
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: null });
|
|
129
|
-
settings.setUiConfig({ showSidebarTrigger: false });
|
|
130
|
-
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: false });
|
|
131
|
-
settings.setUiConfig({ showSidebarTrigger: true });
|
|
132
|
-
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: true });
|
|
133
|
-
// A partial update with no showSidebarTrigger keeps the stored value.
|
|
134
|
-
settings.setUiConfig({});
|
|
135
|
-
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: true });
|
|
136
|
-
store.close();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("ui config coerces non-boolean to false", () => {
|
|
140
|
-
const { store, settings } = setup();
|
|
141
|
-
settings.setUiConfig({ showSidebarTrigger: "yes" });
|
|
142
|
-
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: false });
|
|
143
|
-
store.close();
|
|
144
|
-
});
|