@modusensus/dsh-mneme 0.7.31 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -27
- package/lib/api.js +139 -6
- package/lib/client.js +369 -32
- package/lib/commands.js +36 -5
- package/lib/config.js +101 -14
- package/lib/dream/decisions.js +147 -14
- package/lib/dream/sleep.js +84 -34
- package/lib/dream.js +250 -60
- package/lib/embedding.js +22 -2
- package/lib/index.js +68 -37
- package/lib/inject.js +54 -13
- package/lib/lang.js +168 -0
- package/lib/local-embedder.js +23 -9
- package/lib/mirror.js +17 -14
- package/lib/reranker.js +23 -8
- package/lib/runtime/adopt.js +176 -0
- package/lib/runtime/closure.js +172 -0
- package/lib/runtime/download.js +247 -0
- package/lib/runtime/layout.js +235 -0
- package/lib/runtime/loader.js +249 -0
- package/lib/runtime/provision.js +237 -0
- package/lib/runtime/tarball.js +130 -0
- package/lib/runtime/verify.js +191 -0
- package/lib/scope.js +83 -0
- package/lib/service.js +374 -55
- package/lib/settings.js +43 -2
- package/lib/store.js +126 -13
- package/lib/summarize.js +108 -35
- package/lib/tools.js +205 -12
- package/package.json +7 -2
- package/runtime-manifest.json +596 -0
- package/scripts/build-runtime-manifest.mjs +217 -0
- package/scripts/mneme-runtime.mjs +277 -0
- package/src/api.js +139 -6
- package/src/commands.js +36 -5
- package/src/config.js +101 -14
- package/src/dream/decisions.js +147 -14
- package/src/dream/sleep.js +84 -34
- package/src/dream.js +250 -60
- package/src/embedding.js +22 -2
- package/src/index.js +68 -37
- package/src/inject.js +54 -13
- package/src/lang.js +168 -0
- package/src/local-embedder.js +23 -9
- package/src/mirror.js +17 -14
- package/src/reranker.js +23 -8
- package/src/runtime/adopt.js +176 -0
- package/src/runtime/closure.js +172 -0
- package/src/runtime/download.js +247 -0
- package/src/runtime/layout.js +235 -0
- package/src/runtime/loader.js +249 -0
- package/src/runtime/provision.js +237 -0
- package/src/runtime/tarball.js +130 -0
- package/src/runtime/verify.js +191 -0
- package/src/scope.js +83 -0
- package/src/service.js +374 -55
- package/src/settings.js +43 -2
- package/src/store.js +126 -13
- package/src/summarize.js +108 -35
- package/src/tools.js +205 -12
- package/test/api.test.js +175 -9
- package/test/client.test.js +152 -10
- package/test/commands.test.js +40 -3
- package/test/config.test.js +17 -1
- package/test/dream-candidate-hybrid.test.js +281 -0
- package/test/dream.test.js +172 -0
- package/test/embedding-ready.test.js +196 -0
- package/test/helpers/runtime-source.js +60 -0
- package/test/helpers/tar-builder.js +80 -0
- package/test/inject.test.js +130 -1
- package/test/lang.test.js +87 -0
- package/test/quality-filter.test.js +68 -0
- package/test/reasoning-effort.test.js +106 -15
- package/test/reindex-backfill.test.js +62 -0
- package/test/runtime-adopt.test.js +206 -0
- package/test/runtime-cli.test.js +89 -0
- package/test/runtime-closure.test.js +193 -0
- package/test/runtime-download.test.js +302 -0
- package/test/runtime-layout.test.js +163 -0
- package/test/runtime-loader.test.js +237 -0
- package/test/runtime-manifest.test.js +158 -0
- package/test/runtime-provision.test.js +101 -0
- package/test/runtime-tarball.test.js +82 -0
- package/test/runtime-verify.test.js +214 -0
- package/test/scope-retrieval.test.js +197 -0
- package/test/scope-storage.test.js +247 -0
- package/test/scope-strict.test.js +165 -0
- package/test/service.test.js +76 -0
- package/test/sleep-action-set.test.js +422 -0
- package/test/store.test.js +14 -0
- package/test/summarize.test.js +224 -0
- package/test/tools.test.js +30 -5
- package/test/webServer-optional.test.js +34 -1
package/test/inject.test.js
CHANGED
|
@@ -105,12 +105,15 @@ test("Bug6: injected block stays within the ~1500 char budget, later entries col
|
|
|
105
105
|
// DSH ≥0.1.2-rc removed Session.events; events are only reachable via
|
|
106
106
|
// snapshotEvents(). Regression for #59: the hot block must still render from
|
|
107
107
|
// a session object that has no .events property.
|
|
108
|
+
// 注意:assistant/message 的消息体在 data.message 下(见 Issue #129),夹具必须用这个
|
|
109
|
+
// 真实形状 —— 原先的夹具写的是 data.content,与当时有 bug 的实现是同一个错误假设,
|
|
110
|
+
// 所以它永远抓不到这个回归。
|
|
108
111
|
test("extracts hot rounds from snapshotEvents() when Session.events is absent", () => {
|
|
109
112
|
const { contexts } = setup();
|
|
110
113
|
const session = {
|
|
111
114
|
snapshotEvents: () => [
|
|
112
115
|
{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "怎么修图谱面板" }] } },
|
|
113
|
-
{ type: "assistant/message", data: { content: [{ type: "text", text: "用 viewBox 单位跑模拟" }] } }
|
|
116
|
+
{ type: "assistant/message", data: { message: { content: [{ type: "text", text: "用 viewBox 单位跑模拟" }] } } }
|
|
114
117
|
]
|
|
115
118
|
};
|
|
116
119
|
const text = contexts[0].text({ agent: { session } });
|
|
@@ -118,3 +121,129 @@ test("extracts hot rounds from snapshotEvents() when Session.events is absent",
|
|
|
118
121
|
assert.ok(text.includes("怎么修图谱面板"), "round query present");
|
|
119
122
|
assert.ok(text.includes("用 viewBox 单位跑模拟"), "round response present");
|
|
120
123
|
});
|
|
124
|
+
|
|
125
|
+
// Issue #129 的复现:assistant 轮的正文取自 data.message.content。修复前 textOf 只读
|
|
126
|
+
// data.content,于是每一轮的 response 都落成空串 —— 表现为「N 轮 A 全空」。
|
|
127
|
+
test("Issue #129: response 取自 data.message.content,N 轮不再出现空 A", () => {
|
|
128
|
+
const { contexts } = setup();
|
|
129
|
+
const session = {
|
|
130
|
+
snapshotEvents: () => [
|
|
131
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "第一个问题" }] } },
|
|
132
|
+
{ type: "assistant/message", data: { message: { content: [{ type: "text", text: "第一个回答" }] } } },
|
|
133
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "第二个问题" }] } },
|
|
134
|
+
{ type: "assistant/message", data: { message: { content: [{ type: "text", text: "第二个回答" }] } } }
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
const text = contexts[0].text({ agent: { session } });
|
|
138
|
+
for (const s of ["第一个问题", "第一个回答", "第二个问题", "第二个回答"]) {
|
|
139
|
+
assert.ok(text.includes(s), `hot block 应包含 ${s}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 修复用的是 ?? 兜底而非破坏性替换:扁平的 data.content 形状(user 消息一贯如此,
|
|
144
|
+
// 历史事件也可能如此)必须继续可用。
|
|
145
|
+
test("Issue #129: 扁平的 data.content 形状仍被兼容", () => {
|
|
146
|
+
const { contexts } = setup();
|
|
147
|
+
const session = {
|
|
148
|
+
snapshotEvents: () => [
|
|
149
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "问题" }] } },
|
|
150
|
+
{ type: "assistant/message", data: { content: [{ type: "text", text: "扁平形状的回答" }] } }
|
|
151
|
+
]
|
|
152
|
+
};
|
|
153
|
+
const text = contexts[0].text({ agent: { session } });
|
|
154
|
+
assert.ok(text.includes("问题"), "query present");
|
|
155
|
+
assert.ok(text.includes("扁平形状的回答"), "flat content shape still supported");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ---- issue #162: 注入边界花括号转义(v0.7.4 #40 修复被 v0.7.11 误删后恢复) ----
|
|
159
|
+
|
|
160
|
+
test("issue #162: {{...}} in memory content is escaped at the injection boundary", () => {
|
|
161
|
+
const { contexts, service } = setup();
|
|
162
|
+
service.saveWithDedupe({ type: "preference", title: "模板记忆", content: "{{hl|highlight}} 和 {{挖空}} {{关键词}}", importance: 5 });
|
|
163
|
+
const text = contexts[0].text({});
|
|
164
|
+
assert.ok(text.includes("模板记忆"), "memory still rendered by title");
|
|
165
|
+
assert.ok(text.includes("{\\{hl|highlight}\\}"), "ASCII template braces escaped");
|
|
166
|
+
assert.ok(text.includes("{\\{挖空}\\}"), "CJK template braces escaped");
|
|
167
|
+
assert.ok(!text.includes("{{"), "no raw {{ survives into the prompt");
|
|
168
|
+
assert.ok(!text.includes("}}"), "no raw }} survives into the prompt");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("issue #162: odd brace runs like {{{a}}} are escaped too", () => {
|
|
172
|
+
const { contexts, service } = setup();
|
|
173
|
+
service.saveWithDedupe({ type: "preference", title: "三层模板", content: "{{{a}}} 和 }}} 结尾", importance: 5 });
|
|
174
|
+
const text = contexts[0].text({});
|
|
175
|
+
assert.ok(!text.includes("{{"), "no raw {{ for odd brace runs");
|
|
176
|
+
assert.ok(!text.includes("}}"), "no raw }} for odd brace runs");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("issue #162: {{...}} in user profile/rules is escaped", () => {
|
|
180
|
+
const { contexts, settings } = setup();
|
|
181
|
+
settings.setProfile("我叫{{名字}},前端开发者");
|
|
182
|
+
settings.setRules(["回答时用 {{hl|term}}", "以 }} 结尾的规则"]);
|
|
183
|
+
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
184
|
+
const text = settingsCtx.text({});
|
|
185
|
+
assert.ok(text.includes("{\\{名字}\\}"), "profile braces escaped");
|
|
186
|
+
assert.ok(text.includes("{\\{hl|term}\\}"), "rule braces escaped");
|
|
187
|
+
assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
|
|
188
|
+
assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("issue #162: hot-context rounds with {{...}} are escaped", () => {
|
|
192
|
+
const { contexts } = setup();
|
|
193
|
+
const text = contexts[0].text({
|
|
194
|
+
agent: {
|
|
195
|
+
session: {
|
|
196
|
+
id: "s1",
|
|
197
|
+
events: [
|
|
198
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: ["用 {{hl|你好}} 测试"] } },
|
|
199
|
+
{ type: "assistant/message", data: { source: { kind: "assistant" }, content: ["返回 {{答案}}"] } }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
assert.ok(text.includes("[短期上下文]"), "hot context rendered");
|
|
205
|
+
assert.ok(!text.includes("{{"), "no raw {{ in hot context");
|
|
206
|
+
assert.ok(!text.includes("}}"), "no raw }} in hot context");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("issue #162: reasoning parts are excluded from hot memory (docker --format case)", () => {
|
|
210
|
+
const { contexts } = setup();
|
|
211
|
+
// issue #162 的触发源:assistant 的 reasoning part 是 { type: "reasoning", text }
|
|
212
|
+
// 结构,内容里出现 docker --format "{{.Server.Version}}" 时,若被注入 hot memory
|
|
213
|
+
// 会让 DSH interpolate() 扫到非法变量名 throw → 整轮永久失败。textOf 必须跳过它。
|
|
214
|
+
const text = contexts[0].text({
|
|
215
|
+
agent: {
|
|
216
|
+
session: {
|
|
217
|
+
id: "s1",
|
|
218
|
+
events: [
|
|
219
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: ["检查 docker 版本"] } },
|
|
220
|
+
{
|
|
221
|
+
type: "assistant/message",
|
|
222
|
+
data: {
|
|
223
|
+
source: { kind: "assistant" },
|
|
224
|
+
content: [
|
|
225
|
+
{ type: "reasoning", text: "运行 docker version --format \"{{.Server.Version}}\" 查看版本" },
|
|
226
|
+
{ type: "text", text: "已检查,Docker 版本 27.1.1。" }
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// reasoning 不应进入 hot memory
|
|
235
|
+
assert.ok(!text.includes("Server.Version"), "reasoning text excluded from hot memory");
|
|
236
|
+
// 正文保留
|
|
237
|
+
assert.ok(text.includes("27.1.1"), "assistant text part still injected");
|
|
238
|
+
// 整个注入块没有任何原始 {{ (双保险:即使将来 reasoning 被引入,也会被转义)
|
|
239
|
+
assert.ok(!text.includes("{{"), "no raw {{ anywhere in injected context");
|
|
240
|
+
assert.ok(!text.includes("}}"), "no raw }} anywhere in injected context");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("issue #162: escapePromptVariables=false passes {{...}} through verbatim", () => {
|
|
244
|
+
const { contexts, service } = setup({ escapePromptVariables: false });
|
|
245
|
+
service.saveWithDedupe({ type: "preference", title: "模板", content: "{{挖空}} 与 {{hl|term}}", importance: 5 });
|
|
246
|
+
const text = contexts[0].text({});
|
|
247
|
+
assert.ok(text.includes("{{挖空}}"), "raw braces preserved when escaping disabled");
|
|
248
|
+
assert.ok(text.includes("{{hl|term}}"), "raw ASCII braces preserved when escaping disabled");
|
|
249
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { STR, langOf } from "../src/lang.js";
|
|
5
|
+
import { createMirror, parseHumanEdits, renderMirrorText } from "../src/mirror.js";
|
|
6
|
+
import { createStore } from "../src/store.js";
|
|
7
|
+
import { createService } from "../src/service.js";
|
|
8
|
+
import { createInjector } from "../src/inject.js";
|
|
9
|
+
import { createSettings } from "../src/settings.js";
|
|
10
|
+
|
|
11
|
+
test("langOf resolves the per-instance config language", () => {
|
|
12
|
+
assert.equal(langOf({}), "zh");
|
|
13
|
+
assert.equal(langOf({ language: "zh" }), "zh");
|
|
14
|
+
assert.equal(langOf({ language: "en" }), "en");
|
|
15
|
+
assert.equal(langOf({ language: "fr" }), "zh");
|
|
16
|
+
assert.equal(langOf(null), "zh");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("prompt table resolves by explicit language key", () => {
|
|
20
|
+
assert.ok(STR.prompts.summary.zh.includes("记忆库提炼助手"));
|
|
21
|
+
assert.ok(STR.prompts.summary.en.includes("memory curation assistant"));
|
|
22
|
+
assert.ok(STR.prompts.consolidation.zh.includes("记忆库整理助手"));
|
|
23
|
+
assert.ok(STR.prompts.consolidation.en.includes("consolidation assistant"));
|
|
24
|
+
assert.ok(STR.prompts.conflict.zh.includes("冲突仲裁"));
|
|
25
|
+
assert.ok(STR.prompts.conflict.en.includes("conflict arbiter"));
|
|
26
|
+
assert.ok(STR.prompts.pattern.zh.includes("模式发现助手"));
|
|
27
|
+
assert.ok(STR.prompts.pattern.en.includes("pattern-discovery assistant"));
|
|
28
|
+
// 冻结模式后缀:纯提示文本,可直接拼接在巩固 prompt 后。
|
|
29
|
+
assert.ok(STR.prompts.freezeSuffix.zh.startsWith("\n\n当前为「冲突冻结」模式"));
|
|
30
|
+
assert.ok(STR.prompts.freezeSuffix.en.startsWith("\n\nConflict-freeze mode is active"));
|
|
31
|
+
// 无 JS 语法残留:不携带 const 声明、模板字面量包裹或拼接片段。
|
|
32
|
+
for (const [key, pair] of Object.entries(STR.prompts)) {
|
|
33
|
+
for (const [lang, text] of Object.entries(pair)) {
|
|
34
|
+
assert.ok(!text.startsWith("const ") && !text.startsWith("? ")
|
|
35
|
+
&& !text.endsWith("`;") && !text.endsWith("`"),
|
|
36
|
+
`${key}.${lang} must not carry JS syntax`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("two injectors with different language configs render independently", () => {
|
|
42
|
+
function setup(language) {
|
|
43
|
+
const store = createStore(":memory:");
|
|
44
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
45
|
+
const settings = createSettings(store.db);
|
|
46
|
+
const contexts = [];
|
|
47
|
+
const ctx = { systemPrompt: { context(def) { contexts.push(def); } } };
|
|
48
|
+
createInjector(ctx, service, settings, { maxInjectedItems: 3, importanceThreshold: 3, language });
|
|
49
|
+
service.saveWithDedupe({ type: "preference", title: "lang", content: "value", importance: 5 });
|
|
50
|
+
return { store, contexts };
|
|
51
|
+
}
|
|
52
|
+
const en = setup("en");
|
|
53
|
+
const zh = setup("zh");
|
|
54
|
+
try {
|
|
55
|
+
assert.ok(en.contexts[0].text({}).includes("[Memory]"), "en instance renders English header");
|
|
56
|
+
assert.ok(zh.contexts[0].text({}).includes("[记忆库]"), "zh instance renders Chinese header");
|
|
57
|
+
} finally {
|
|
58
|
+
en.store.close();
|
|
59
|
+
zh.store.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("mirror render follows the instance language; parser accepts both", () => {
|
|
64
|
+
const memory = { id: "m1", type: "preference", title: "t", content: "c", importance: 5, tags: [], updated_at: "2026-01-01" };
|
|
65
|
+
const enText = renderMirrorText("preference", [memory], "en");
|
|
66
|
+
const zhText = renderMirrorText("preference", [memory], "zh");
|
|
67
|
+
assert.ok(enText.includes("- **Type**: preference"));
|
|
68
|
+
assert.ok(zhText.includes("- **类型**: preference"));
|
|
69
|
+
const edits = parseHumanEdits(enText + "\n" + zhText);
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
edits.map((e) => e.id),
|
|
72
|
+
["m1", "m1"]
|
|
73
|
+
);
|
|
74
|
+
assert.equal(edits[1].content, "c");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("dream summary keeps one row across a language switch (stable source identity)", () => {
|
|
78
|
+
const store = createStore(":memory:");
|
|
79
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
80
|
+
service.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "zh overview", importance: 5, source: "dream", _overwrite: true });
|
|
81
|
+
service.saveWithDedupe({ type: "summary", title: "Memory library overview", content: "en overview", importance: 5, source: "dream", _overwrite: true });
|
|
82
|
+
const summaries = store.all().filter((m) => m.type === "summary" && !m.archived);
|
|
83
|
+
assert.equal(summaries.length, 1);
|
|
84
|
+
assert.equal(summaries[0].title, "Memory library overview");
|
|
85
|
+
assert.equal(summaries[0].content, "en overview");
|
|
86
|
+
store.close();
|
|
87
|
+
});
|
|
@@ -116,3 +116,71 @@ test("exported helpers behave (meta regex, similarity, dedup ratio)", () => {
|
|
|
116
116
|
assert.ok(dedupRatio("哈哈哈哈哈哈") < 0.3, "repetitive filler has low dedup ratio");
|
|
117
117
|
assert.ok(dedupRatio("一句信息量足够的话") > 0.3);
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
// --- Issue #135 附属发现 1:importance ≥ exemptImportance 只降权,不静默归档 ----
|
|
121
|
+
|
|
122
|
+
test("Issue #135: importance-5 memory below archive threshold is demoted, not archived", () => {
|
|
123
|
+
const { store, service } = setup();
|
|
124
|
+
const { memory } = service.saveWithDedupe({
|
|
125
|
+
type: "preference", title: "关键决策", content: "短", importance: 5
|
|
126
|
+
});
|
|
127
|
+
const got = store.getById(memory.id);
|
|
128
|
+
assert.equal(got.archived, false, "importance 5 >= exemptImportance 4 → kept active");
|
|
129
|
+
assert.ok(got.quality_score < 30, `score still persisted, got ${got.quality_score}`);
|
|
130
|
+
assert.ok(got.tags.includes("low_quality"), "signal tag still written (verdict observable)");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("Issue #135: importance-4 is exempt by default; importance-3 is still archived", () => {
|
|
134
|
+
const { store, service } = setup();
|
|
135
|
+
const four = service.saveWithDedupe({ type: "preference", title: "决策甲", content: "短", importance: 4 }).memory;
|
|
136
|
+
const three = service.saveWithDedupe({ type: "preference", title: "决策乙", content: "短", importance: 3 }).memory;
|
|
137
|
+
assert.equal(store.getById(four.id).archived, false, "importance 4 exempt by default");
|
|
138
|
+
assert.equal(store.getById(three.id).archived, true, "importance 3 below the floor still archived");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("Issue #135: exemptImportance=1 disables auto-archive; =5 narrows the floor", () => {
|
|
142
|
+
const one = setup({ memoryQualityFilter: { enabled: true, exemptImportance: 1 } });
|
|
143
|
+
const threeLow = one.service.saveWithDedupe({ type: "preference", title: "语言", content: "短", importance: 3 }).memory;
|
|
144
|
+
assert.equal(one.store.getById(threeLow.id).archived, false, "exemptImportance 1 exempts everything");
|
|
145
|
+
|
|
146
|
+
const five = setup({ memoryQualityFilter: { enabled: true, exemptImportance: 5 } });
|
|
147
|
+
const four = five.service.saveWithDedupe({ type: "preference", title: "决策甲", content: "短", importance: 4 }).memory;
|
|
148
|
+
assert.equal(five.store.getById(four.id).archived, true, "importance 4 no longer exempt at floor 5");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// --- Issue #135 附属发现 2:update 的 tags 整组替换不再抹掉系统信号标签 --------
|
|
152
|
+
|
|
153
|
+
test("Issue #135: update with tags preserves the filter's signal tags", () => {
|
|
154
|
+
const { store, service } = setup();
|
|
155
|
+
const { memory } = service.saveWithDedupe({ type: "preference", title: "语言", content: "短" });
|
|
156
|
+
assert.ok(store.getById(memory.id).tags.includes("low_quality"), "precondition: filter tagged the row");
|
|
157
|
+
|
|
158
|
+
service.update(memory.id, { tags: ["用户标签"], content: "补充后的完整内容,不再是短文本了" });
|
|
159
|
+
const after = store.getById(memory.id);
|
|
160
|
+
assert.ok(after.tags.includes("用户标签"), "user tags applied");
|
|
161
|
+
assert.ok(after.tags.includes("low_quality"), "low_quality preserved through the update");
|
|
162
|
+
assert.ok(after.tags.includes("short_content"), "short_content preserved too");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("Issue #135: update without tags leaves the row untouched (legacy path)", () => {
|
|
166
|
+
const { store, service } = setup();
|
|
167
|
+
const { memory } = service.saveWithDedupe({ type: "preference", title: "语言", content: "短" });
|
|
168
|
+
const before = store.getById(memory.id).tags;
|
|
169
|
+
service.update(memory.id, { content: "只改内容,不带 tags 字段" });
|
|
170
|
+
assert.deepEqual(store.getById(memory.id).tags, before, "no tags in patch → tags untouched");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("Issue #135 review: self_referential is a signal tag and survives a tagged update", () => {
|
|
174
|
+
const { store, service } = setup();
|
|
175
|
+
// type 自指(title 提到自身类型标签「偏好」)→ quality-filter.js 写
|
|
176
|
+
// self_referential(−15);加短内容一起跌破归档线。SIGNAL_TAGS 清单必须
|
|
177
|
+
// 覆盖它,否则带 tags 的 update 恰好把这个标签抹掉(审计线索缺口)。
|
|
178
|
+
const { memory } = service.saveWithDedupe({ type: "preference", title: "偏好记录", content: "短" });
|
|
179
|
+
const tagged = store.getById(memory.id);
|
|
180
|
+
assert.ok(tagged.tags.includes("self_referential"), "precondition: self_referential written");
|
|
181
|
+
|
|
182
|
+
service.update(memory.id, { tags: ["用户标签"], content: "补充后的完整内容" });
|
|
183
|
+
const after = store.getById(memory.id);
|
|
184
|
+
assert.ok(after.tags.includes("self_referential"), "self_referential preserved through the update");
|
|
185
|
+
assert.ok(after.tags.includes("用户标签"), "user tags still applied");
|
|
186
|
+
});
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
// Regression for issue #9:
|
|
1
|
+
// Regression for issue #9, extended by issue #135 (建议 3/4/5):
|
|
2
2
|
// - B: dreamMaxTokens cap widened (min 256, max 131072) so large memory
|
|
3
|
-
// libraries no longer starve the consolidation output.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// libraries no longer starve the consolidation output. #135 raises the
|
|
4
|
+
// DEFAULT to 131072 too — 32768 was exactly what thinking-model reasoning
|
|
5
|
+
// burnt, leaving an empty body.
|
|
6
|
+
// - A: dreamReasoningEffort / sleepReasoningEffort pass-through. Explicit
|
|
7
|
+
// 'none' omits the reasoningEffort field entirely (provider default
|
|
8
|
+
// applies); low/medium/high are forwarded verbatim on every dream /
|
|
9
|
+
// sleep LLM call. #135: UNSET now resolves to the LOWEST effort the model
|
|
10
|
+
// declares (the old default 'none' let the harness substitute the model's
|
|
11
|
+
// defaultEffort, burning the budget on reasoning). Asserted by capturing
|
|
12
|
+
// the options each llm.stream() sees.
|
|
8
13
|
import test from "node:test";
|
|
9
14
|
import assert from "node:assert/strict";
|
|
10
15
|
import { Config } from "../src/config.js";
|
|
@@ -25,9 +30,9 @@ const embedder = {
|
|
|
25
30
|
|
|
26
31
|
// ---------------------------------------------------------------- config schema
|
|
27
32
|
|
|
28
|
-
test("issue#
|
|
29
|
-
assert.equal(Config({}).dreamMaxTokens,
|
|
30
|
-
assert.equal(Config({ dreamMaxTokens: 131072 }).dreamMaxTokens, 131072, "
|
|
33
|
+
test("issue#135: dreamMaxTokens defaults to 131072 and accepts values up to 131072", () => {
|
|
34
|
+
assert.equal(Config({}).dreamMaxTokens, 131072, "old default 32768 was exactly what thinking-model reasoning burnt (Issue #135 建议 3)");
|
|
35
|
+
assert.equal(Config({ dreamMaxTokens: 131072 }).dreamMaxTokens, 131072, "upper bound accepted");
|
|
31
36
|
assert.equal(Config({ dreamMaxTokens: 65536 }).dreamMaxTokens, 65536, "intermediate value accepted");
|
|
32
37
|
});
|
|
33
38
|
|
|
@@ -39,11 +44,12 @@ test("issue#9: dreamMaxTokens clamps to [256, 131072], out-of-range values are r
|
|
|
39
44
|
assert.throws(() => Config({ dreamMaxTokens: 0 }), "zero rejected");
|
|
40
45
|
});
|
|
41
46
|
|
|
42
|
-
test("issue#
|
|
47
|
+
test("issue#135: reasoningEffort unset = undefined (auto-lowest at resolve time); explicit values preserved", () => {
|
|
43
48
|
const cfg = Config({});
|
|
44
|
-
assert.equal(cfg.dreamReasoningEffort, "none");
|
|
45
|
-
assert.equal(cfg.sleepReasoningEffort,
|
|
46
|
-
assert.equal(Config({ dreamReasoningEffort: "
|
|
49
|
+
assert.equal(cfg.dreamReasoningEffort, undefined, "unset must be distinguishable from explicit 'none'");
|
|
50
|
+
assert.equal(cfg.sleepReasoningEffort, undefined);
|
|
51
|
+
assert.equal(Config({ dreamReasoningEffort: "none" }).dreamReasoningEffort, "none", "explicit 'none' kept");
|
|
52
|
+
assert.equal(Config({ dreamReasoningEffort: "off" }).dreamReasoningEffort, "off", "'off' now a valid explicit choice");
|
|
47
53
|
assert.equal(Config({ sleepReasoningEffort: "medium" }).sleepReasoningEffort, "medium");
|
|
48
54
|
assert.throws(() => Config({ dreamReasoningEffort: "bogus" }), "invalid effort rejected");
|
|
49
55
|
assert.throws(() => Config({ sleepReasoningEffort: "ultra" }), "invalid effort rejected");
|
|
@@ -71,7 +77,7 @@ function dreamCtx({ onConsolidation, summaryText = "记忆库总览:用户偏
|
|
|
71
77
|
};
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
test("issue#
|
|
80
|
+
test("issue#135: dream omits reasoningEffort when unset without a capability API and still consolidates (applied>0)", async () => {
|
|
75
81
|
const store = createStore(":memory:");
|
|
76
82
|
const service = createService({ store, mirror: null, config: {} });
|
|
77
83
|
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
@@ -89,7 +95,64 @@ test("issue#9: dream omits reasoningEffort under default 'none' and still consol
|
|
|
89
95
|
assert.ok(result.applied > 0, "end-to-end dream run still lands changes");
|
|
90
96
|
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
91
97
|
for (const options of captured) {
|
|
92
|
-
assert.equal("reasoningEffort" in options, false, `
|
|
98
|
+
assert.equal("reasoningEffort" in options, false, `unset without capability API must not forward reasoningEffort (${options.purpose})`);
|
|
99
|
+
}
|
|
100
|
+
store.close();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("issue#135: unset effort resolves to the LOWEST supported effort, never the poison default", async () => {
|
|
104
|
+
const store = createStore(":memory:");
|
|
105
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
106
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
107
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
108
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
109
|
+
const captured = [];
|
|
110
|
+
const ctx = dreamCtx({
|
|
111
|
+
captured,
|
|
112
|
+
onConsolidation: () => JSON.stringify([
|
|
113
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
114
|
+
])
|
|
115
|
+
});
|
|
116
|
+
// v4-flash 系能力报告:声明 off/low/high/max、defaultEffort=high。旧默认
|
|
117
|
+
// (省略字段)让 harness 顶上 high → 推理烧光预算返回空体;现在未配置应
|
|
118
|
+
// 自动取最低档 off,而不是服务商默认。
|
|
119
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
120
|
+
provider,
|
|
121
|
+
model,
|
|
122
|
+
reasoning: { efforts: [{ id: "high" }, { id: "off" }, { id: "low" }, { id: "max" }], defaultEffort: "high" }
|
|
123
|
+
});
|
|
124
|
+
const result = await dream.runDream(ctx, service, {});
|
|
125
|
+
assert.equal(result.ok, true);
|
|
126
|
+
assert.ok(result.applied > 0, "consolidation lands changes");
|
|
127
|
+
assert.equal(captured.length, 2, "consolidation + summary both hit the LLM");
|
|
128
|
+
for (const options of captured) {
|
|
129
|
+
assert.equal(options.reasoningEffort, "off", `unset resolves to the lowest declared effort on ${options.purpose}`);
|
|
130
|
+
}
|
|
131
|
+
store.close();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("issue#135: explicit 'none' still omits the field even when the capability API is available", async () => {
|
|
135
|
+
const store = createStore(":memory:");
|
|
136
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
137
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
138
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
139
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
140
|
+
const captured = [];
|
|
141
|
+
const ctx = dreamCtx({
|
|
142
|
+
captured,
|
|
143
|
+
onConsolidation: () => JSON.stringify([
|
|
144
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "插件总览", content: "合并内容", importance: 4 }
|
|
145
|
+
])
|
|
146
|
+
});
|
|
147
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
148
|
+
provider,
|
|
149
|
+
model,
|
|
150
|
+
reasoning: { efforts: [{ id: "off" }, { id: "low" }], defaultEffort: "high" }
|
|
151
|
+
});
|
|
152
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "none" });
|
|
153
|
+
assert.equal(result.ok, true);
|
|
154
|
+
for (const options of captured) {
|
|
155
|
+
assert.equal("reasoningEffort" in options, false, `explicit 'none' is a user decision to omit (${options.purpose})`);
|
|
93
156
|
}
|
|
94
157
|
store.close();
|
|
95
158
|
});
|
|
@@ -301,6 +364,34 @@ test("issue#9: sleep forwards sleepReasoningEffort on its LLM passes", async ()
|
|
|
301
364
|
store.close();
|
|
302
365
|
});
|
|
303
366
|
|
|
367
|
+
test("issue#135: sleep unset effort also resolves to the lowest supported", async () => {
|
|
368
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
369
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
370
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
371
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
372
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
373
|
+
const captured = [];
|
|
374
|
+
const ctx = sleepCtx(
|
|
375
|
+
(userText) => userText.startsWith("候选冲突")
|
|
376
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
377
|
+
: "[]",
|
|
378
|
+
{ provider: "mock", model: "sleep-model" },
|
|
379
|
+
captured
|
|
380
|
+
);
|
|
381
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
382
|
+
provider,
|
|
383
|
+
model,
|
|
384
|
+
reasoning: { efforts: [{ id: "low" }, { id: "high" }], defaultEffort: "high" }
|
|
385
|
+
});
|
|
386
|
+
const result = await runSleep(ctx, service, baseConfig(), ctx.logger, { embedder, vectorIndex }, null);
|
|
387
|
+
assert.equal(result.status, "ok");
|
|
388
|
+
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
389
|
+
for (const options of captured) {
|
|
390
|
+
assert.equal(options.reasoningEffort, "low", `unset sleep effort resolves to the lowest declared (${options.purpose})`);
|
|
391
|
+
}
|
|
392
|
+
store.close();
|
|
393
|
+
});
|
|
394
|
+
|
|
304
395
|
// ------------------------------------------------------------------ stream-level rejection
|
|
305
396
|
// dsh-llm rc.1 converts adapter-stage failures (including the provider's
|
|
306
397
|
// UNSUPPORTED_REASONING_EFFORT throw from resolveCallWithInfo) into a terminal
|
|
@@ -0,0 +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
|
+
});
|