@modusensus/dsh-mneme 0.7.3 → 0.7.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/README.md +4 -2
- package/lib/client.js +5 -1
- package/lib/config.js +7 -0
- package/lib/inject.js +20 -3
- package/package.json +1 -1
- package/src/config.js +7 -0
- package/src/inject.js +20 -3
- package/test/config.test.js +7 -0
- package/test/inject.test.js +57 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
9
9
|
[](https://github.com/modusensus/dsh-mneme/actions)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
@@ -241,6 +241,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
241
241
|
|
|
242
242
|
| 版本 | 亮点 |
|
|
243
243
|
|------|------|
|
|
244
|
+
| **v0.7.4** | issue #40 修复:记忆内容含 `{{...}}` 模板语法时整轮崩溃(注入边界 run-based 花括号转义 `{{a}}`→`{\{a\}\}`,奇数连续如 `{{{a}}}` 也不残留字面 `{{`;新增 `escapePromptVariables` 配置默认开);issue #41 修复:记忆窗口关闭按钮与宿主窗口控制按钮重叠无法点击(顶栏左对齐,关闭按钮离开右上角宿主控制区);782 测试全绿 |
|
|
244
245
|
| **v0.7.3** | issue #38 新功能:左下角入口按钮可选开关 `showSidebarTrigger`(默认开)——与 dsh-cost-meter 等抢占 footer slot 的插件冲突时可在 Web 面板「设置」一键关闭,仅隐藏按钮、记忆库标签不受影响;776 测试全绿 |
|
|
245
246
|
| **v0.7.2** | issue #35 修复:目录页删除按钮改面板内联两步确认(不再依赖宿主 `window.confirm`)+ 删除失败可见报错;issue #34 新功能:opt-in `injectTimePrefix` 对话开始自动注入当前时间一次(默认关);770 测试全绿 |
|
|
246
247
|
| **v0.7.1** | issue #31 修复:memory_save/memory_update 的 tags 桥接进 entity_attrs 标签存储(目录/`tag:` 检索/tagBoost 立即可见,`tags: []` 清空移回 untagged)+ `store.setMemoryTags` 反向同步 `memories.tags` 列 + autoTag 面板开关成为运行时消费方(settings 覆盖 config);764 测试全绿 |
|
|
@@ -291,6 +292,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
291
292
|
| **v0.7.1** | ✅ 完成 | issue #31 修复 | memory_save/update tags 桥接 entity_attrs 标签存储 + 列反向同步 + autoTag 面板开关生效(settings 覆盖 config);764 测试全绿 |
|
|
292
293
|
| **v0.7.2** | ✅ 完成 | issue #34 + #35 修复 | 目录页删除按钮改内联两步确认 + 删除失败可见报错;opt-in `injectTimePrefix` 对话开始注入当前时间一次(默认关);770 测试全绿 |
|
|
293
294
|
| **v0.7.3** | ✅ 完成 | issue #38 新功能 | 左下角入口按钮可选开关 `showSidebarTrigger`(默认开,settings-over-config);Web 面板设置一键关闭,与 dsh-cost-meter 等 footer 插件冲突可隐藏按钮、记忆库标签不受影响;776 测试全绿 |
|
|
295
|
+
| **v0.7.4** | ✅ 完成 | issue #40 + #41 修复 | 注入边界 run-based 花括号转义(`{{a}}`→`{\{a\}\}`、奇数连续如 `{{{a}}}` 不残留字面,`escapePromptVariables` 默认开)+ 记忆窗口顶栏左对齐、关闭按钮避开宿主窗口控制按钮区;782 测试全绿 |
|
|
294
296
|
| **v0.8.0** | 🚧 计划中(9 月末) | 图谱增强 | 兴趣漂移可视化 + 跨 workspace 记忆共享 + 更多 heat 信号 |
|
|
295
297
|
|
|
296
298
|
> 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
|
|
@@ -466,7 +468,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
466
468
|
```bash
|
|
467
469
|
cd dsh-mneme
|
|
468
470
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
469
|
-
npm test # 运行
|
|
471
|
+
npm test # 运行 782 个测试
|
|
470
472
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
471
473
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
472
474
|
```
|
package/lib/client.js
CHANGED
|
@@ -472,7 +472,11 @@ window.__ModuleLoader__.load({
|
|
|
472
472
|
// there. This surface reuses the exact MemoryExplorer UI at full size —
|
|
473
473
|
// not a side drawer — so the library stays reachable from any state.
|
|
474
474
|
".mneme-overlay{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);animation:mneme-fadein .12s ease-out}",
|
|
475
|
-
|
|
475
|
+
// issue #41: overlay 顶栏右侧原本与宿主窗口标题栏的控制按钮(最小化/最大化/关闭)
|
|
476
|
+
// 落在同一区域——宿主控制按钮浮在 web 内容最上层,抢占了 overlay"关闭"按钮的
|
|
477
|
+
// 点击热区,导致记忆窗口无法关闭。改为左对齐(标题 + 关闭按钮并排),关闭按钮
|
|
478
|
+
// 离开右上角宿主控制按钮区,任何窗口尺寸/宿主下都可见可点。
|
|
479
|
+
".mneme-overlaybar{flex:none;display:flex;align-items:center;justify-content:flex-start;gap:12px;height:44px;padding:0 12px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
476
480
|
".mneme-overlaytitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
477
481
|
".mneme-overlaybody{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
478
482
|
"@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}"
|
package/lib/config.js
CHANGED
|
@@ -22,6 +22,13 @@ export const Config = z.object({
|
|
|
22
22
|
// "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
|
|
23
23
|
// 关闭时行为与之前完全一致。
|
|
24
24
|
injectTimePrefix: z.boolean().default(false),
|
|
25
|
+
// 花括号转义(issue #40,默认开):DSH 核心 interpolate() 会把 `{{name}}`
|
|
26
|
+
// 当 prompt 变量做严格校验(变量名须匹配 /^[a-z][a-z0-9_]*$/),记忆正文里
|
|
27
|
+
// 合法的模板语法(如 `{{hl|}}`、`{{挖空}}`、`{{关键词}}`)会因非法变量名
|
|
28
|
+
// 直接 throw、让整轮对话崩溃。开启后注入边界把 `{{`→`\{\{`、`}}`→`\}\}`,
|
|
29
|
+
// 文本不再含 `{{`/`}}` 子串,interpolate 不再扫描到,内容保持可读且幂等
|
|
30
|
+
// (不会二次转义)。关闭后按原样透传。
|
|
31
|
+
escapePromptVariables: z.boolean().default(true),
|
|
25
32
|
autoDream: z.boolean().default(true),
|
|
26
33
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
27
34
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
package/lib/inject.js
CHANGED
|
@@ -75,6 +75,23 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
75
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
76
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
77
|
|
|
78
|
+
// Prompt-variable escaping (issue #40): DSH's interpolate() treats `{{name}}`
|
|
79
|
+
// as a prompt variable and strictly validates the name against
|
|
80
|
+
// /^[a-z][a-z0-9_]*$/ — memory/profile content carrying legal template syntax
|
|
81
|
+
// (Obsidian-style `{{hl|}}`, `{{挖空}}`, `{{关键词}}`) would hit an illegal
|
|
82
|
+
// variable name and throw, crashing the whole turn. At the injection boundary
|
|
83
|
+
// we escape every run of 2+ consecutive braces, inserting a `\` between each
|
|
84
|
+
// pair, so no `{{`/`}}` substring survives into the prompt: `{{a}}` → `{\{a\}\}`,
|
|
85
|
+
// and odd runs like `{{{a}}}` (which pair-wise escaping would leave with a
|
|
86
|
+
// literal `{{`) are handled too. The text keeps its readable template form,
|
|
87
|
+
// the transform is idempotent (escaped braces are single + `\`, never two
|
|
88
|
+
// adjacent), and single braces pass through untouched — interpolate only
|
|
89
|
+
// scans `{{`. Off via config.escapePromptVariables=false (default true).
|
|
90
|
+
function escapePromptVars(text) {
|
|
91
|
+
if (config.escapePromptVariables === false) return String(text);
|
|
92
|
+
return String(text).replace(/[{}]{2,}/g, (run) => run.split("").join("\\"));
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
79
96
|
// current date/time is injected once per conversation — at the first prompt
|
|
80
97
|
// assembly of a new session — so the model can sense what time/day it is.
|
|
@@ -134,7 +151,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
134
151
|
for (const r of rounds) hot.add(r);
|
|
135
152
|
const body = hot.getContext();
|
|
136
153
|
if (!body) return "";
|
|
137
|
-
return `[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}
|
|
154
|
+
return escapePromptVars(`[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}`);
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
function render(candidates) {
|
|
@@ -158,7 +175,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
158
175
|
lines.push(`- [${m.type}] ${verified}${title}`);
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
|
-
return lines.join("\n");
|
|
178
|
+
return escapePromptVars(lines.join("\n"));
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
// Bug4: the system-prompt render is synchronous, so the semantic query vector
|
|
@@ -192,7 +209,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
192
209
|
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
193
210
|
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
194
211
|
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
195
|
-
return lines.join("\n");
|
|
212
|
+
return escapePromptVars(lines.join("\n"));
|
|
196
213
|
}
|
|
197
214
|
|
|
198
215
|
const disposers = [
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.4",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/config.js
CHANGED
|
@@ -22,6 +22,13 @@ export const Config = z.object({
|
|
|
22
22
|
// "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
|
|
23
23
|
// 关闭时行为与之前完全一致。
|
|
24
24
|
injectTimePrefix: z.boolean().default(false),
|
|
25
|
+
// 花括号转义(issue #40,默认开):DSH 核心 interpolate() 会把 `{{name}}`
|
|
26
|
+
// 当 prompt 变量做严格校验(变量名须匹配 /^[a-z][a-z0-9_]*$/),记忆正文里
|
|
27
|
+
// 合法的模板语法(如 `{{hl|}}`、`{{挖空}}`、`{{关键词}}`)会因非法变量名
|
|
28
|
+
// 直接 throw、让整轮对话崩溃。开启后注入边界把 `{{`→`\{\{`、`}}`→`\}\}`,
|
|
29
|
+
// 文本不再含 `{{`/`}}` 子串,interpolate 不再扫描到,内容保持可读且幂等
|
|
30
|
+
// (不会二次转义)。关闭后按原样透传。
|
|
31
|
+
escapePromptVariables: z.boolean().default(true),
|
|
25
32
|
autoDream: z.boolean().default(true),
|
|
26
33
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
27
34
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
package/src/inject.js
CHANGED
|
@@ -75,6 +75,23 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
75
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
76
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
77
|
|
|
78
|
+
// Prompt-variable escaping (issue #40): DSH's interpolate() treats `{{name}}`
|
|
79
|
+
// as a prompt variable and strictly validates the name against
|
|
80
|
+
// /^[a-z][a-z0-9_]*$/ — memory/profile content carrying legal template syntax
|
|
81
|
+
// (Obsidian-style `{{hl|}}`, `{{挖空}}`, `{{关键词}}`) would hit an illegal
|
|
82
|
+
// variable name and throw, crashing the whole turn. At the injection boundary
|
|
83
|
+
// we escape every run of 2+ consecutive braces, inserting a `\` between each
|
|
84
|
+
// pair, so no `{{`/`}}` substring survives into the prompt: `{{a}}` → `{\{a\}\}`,
|
|
85
|
+
// and odd runs like `{{{a}}}` (which pair-wise escaping would leave with a
|
|
86
|
+
// literal `{{`) are handled too. The text keeps its readable template form,
|
|
87
|
+
// the transform is idempotent (escaped braces are single + `\`, never two
|
|
88
|
+
// adjacent), and single braces pass through untouched — interpolate only
|
|
89
|
+
// scans `{{`. Off via config.escapePromptVariables=false (default true).
|
|
90
|
+
function escapePromptVars(text) {
|
|
91
|
+
if (config.escapePromptVariables === false) return String(text);
|
|
92
|
+
return String(text).replace(/[{}]{2,}/g, (run) => run.split("").join("\\"));
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
79
96
|
// current date/time is injected once per conversation — at the first prompt
|
|
80
97
|
// assembly of a new session — so the model can sense what time/day it is.
|
|
@@ -134,7 +151,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
134
151
|
for (const r of rounds) hot.add(r);
|
|
135
152
|
const body = hot.getContext();
|
|
136
153
|
if (!body) return "";
|
|
137
|
-
return `[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}
|
|
154
|
+
return escapePromptVars(`[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}`);
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
function render(candidates) {
|
|
@@ -158,7 +175,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
158
175
|
lines.push(`- [${m.type}] ${verified}${title}`);
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
|
-
return lines.join("\n");
|
|
178
|
+
return escapePromptVars(lines.join("\n"));
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
// Bug4: the system-prompt render is synchronous, so the semantic query vector
|
|
@@ -192,7 +209,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
192
209
|
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
193
210
|
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
194
211
|
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
195
|
-
return lines.join("\n");
|
|
212
|
+
return escapePromptVars(lines.join("\n"));
|
|
196
213
|
}
|
|
197
214
|
|
|
198
215
|
const disposers = [
|
package/test/config.test.js
CHANGED
|
@@ -62,3 +62,10 @@ test("showSidebarTrigger is on by default, disabled explicitly (issue #38)", ()
|
|
|
62
62
|
const off = Config({ showSidebarTrigger: false });
|
|
63
63
|
assert.equal(off.showSidebarTrigger, false, "explicit false hides the trigger");
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
test("escapePromptVariables is on by default, disabled explicitly (issue #40)", () => {
|
|
67
|
+
const cfg = Config({});
|
|
68
|
+
assert.equal(cfg.escapePromptVariables, true, "{{...}} escaping defaults to on (protects interpolate)");
|
|
69
|
+
const off = Config({ escapePromptVariables: false });
|
|
70
|
+
assert.equal(off.escapePromptVariables, false, "explicit false passes {{...}} through verbatim");
|
|
71
|
+
});
|
package/test/inject.test.js
CHANGED
|
@@ -129,3 +129,60 @@ test("injectTimePrefix: same session never re-injects, a new session injects aga
|
|
|
129
129
|
const third = contexts[0].text({ agent: { session: { id: "s2" } } });
|
|
130
130
|
assert.match(third, /^\[当前时间:/, "a new session injects the prefix again");
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
test("issue #40: {{...}} in memory content is escaped at the injection boundary", () => {
|
|
134
|
+
const { contexts, service } = setup();
|
|
135
|
+
service.saveWithDedupe({ type: "preference", title: "模板记忆", content: "{{hl|highlight}} 和 {{挖空}} {{关键词}}", importance: 5 });
|
|
136
|
+
const text = contexts[0].text({});
|
|
137
|
+
assert.ok(text.includes("模板记忆"), "memory still rendered by title");
|
|
138
|
+
assert.ok(text.includes("{\\{hl|highlight}\\}"), "ASCII template braces escaped");
|
|
139
|
+
assert.ok(text.includes("{\\{挖空}\\}"), "CJK template braces escaped");
|
|
140
|
+
assert.ok(!text.includes("{{"), "no raw {{ survives into the prompt");
|
|
141
|
+
assert.ok(!text.includes("}}"), "no raw }} survives into the prompt");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("issue #40: odd brace runs like {{{a}}} are escaped too", () => {
|
|
145
|
+
const { contexts, service } = setup();
|
|
146
|
+
service.saveWithDedupe({ type: "preference", title: "三层模板", content: "{{{a}}} 和 }}} 结尾", importance: 5 });
|
|
147
|
+
const text = contexts[0].text({});
|
|
148
|
+
assert.ok(!text.includes("{{"), "no raw {{ for odd brace runs");
|
|
149
|
+
assert.ok(!text.includes("}}"), "no raw }} for odd brace runs");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("issue #40: {{...}} in user profile/rules is escaped", () => {
|
|
153
|
+
const { contexts, settings } = setup();
|
|
154
|
+
settings.setProfile("我叫{{名字}},前端开发者");
|
|
155
|
+
settings.setRules(["回答时用 {{hl|term}}", "以 }} 结尾的规则"]);
|
|
156
|
+
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
157
|
+
const text = settingsCtx.text({});
|
|
158
|
+
assert.ok(text.includes("{\\{名字}\\}"), "profile braces escaped");
|
|
159
|
+
assert.ok(text.includes("{\\{hl|term}\\}"), "rule braces escaped");
|
|
160
|
+
assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
|
|
161
|
+
assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("issue #40: hot-context rounds with {{...}} are escaped", () => {
|
|
165
|
+
const { contexts } = setup();
|
|
166
|
+
const text = contexts[0].text({
|
|
167
|
+
agent: {
|
|
168
|
+
session: {
|
|
169
|
+
id: "s1",
|
|
170
|
+
events: [
|
|
171
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: ["用 {{hl|你好}} 测试"] } },
|
|
172
|
+
{ type: "assistant/message", data: { source: { kind: "assistant" }, content: ["返回 {{答案}}"] } }
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
assert.ok(text.includes("[短期上下文]"), "hot context rendered");
|
|
178
|
+
assert.ok(!text.includes("{{"), "no raw {{ in hot context");
|
|
179
|
+
assert.ok(!text.includes("}}"), "no raw }} in hot context");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("issue #40: escapePromptVariables=false passes {{...}} through verbatim", () => {
|
|
183
|
+
const { contexts, service } = setup({ escapePromptVariables: false });
|
|
184
|
+
service.saveWithDedupe({ type: "preference", title: "模板", content: "{{挖空}} 与 {{hl|term}}", importance: 5 });
|
|
185
|
+
const text = contexts[0].text({});
|
|
186
|
+
assert.ok(text.includes("{{挖空}}"), "raw braces preserved when escaping disabled");
|
|
187
|
+
assert.ok(text.includes("{{hl|term}}"), "raw ASCII braces preserved when escaping disabled");
|
|
188
|
+
});
|