@modusensus/dsh-mneme 0.7.13 → 0.7.14

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 CHANGED
@@ -241,7 +241,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
241
241
 
242
242
  | 版本 | 亮点 |
243
243
  |------|------|
244
- | **v0.7.13** | 编码记忆蒸馏(`codingRetrospect` 默认关):完整转录(用户+助手思考/回复+工具调用/结果+代码执行)提炼原子记忆,新增 `rejected_solution`/`pitfall`/`constraint` 三类型,编码任务 `codingBoostFactor` 加权(cap 5)+ 智能调速器(蒸馏全局串行队列 + 429 指数退避自动重试)+ 原子记忆语义保留(宁可拆多条不合并丢细节,`distillMaxChars` 默认 24000)+ 修复 v0.7.12 CI 回归(恢复 c8 覆盖率);616 测试全绿 |
244
+ | **v0.7.14** | 安全修复(CWE-200):蒸馏不再把助手 `reasoning` 私有推理块送进蒸馏上下文(只采公开 `text`,防止记忆沉淀私有思考链);617 测试全绿 |
245
+ | **v0.7.13** | 编码记忆蒸馏(`codingRetrospect` 默认关):完整转录(用户+助手回答+工具调用/结果+代码执行)提炼原子记忆,新增 `rejected_solution`/`pitfall`/`constraint` 三类型,编码任务 `codingBoostFactor` 加权(cap 5)+ 智能调速器(蒸馏全局串行队列 + 429 指数退避自动重试)+ 原子记忆语义保留(宁可拆多条不合并丢细节,`distillMaxChars` 默认 24000)+ 修复 v0.7.12 CI 回归(恢复 c8 覆盖率);616 测试全绿 |
245
246
  | **v0.7.12** | 独立外部 API(插件自带 HTTP 服务 `127.0.0.1:8790` Bearer 鉴权,生态集成)+ 零依赖 CLI `dsh-mneme`(status/list/search/get/add/delete/config)+ 轻量模式 lightMode(简化预设、面板一键切换)+ 设置面板新卡片(运行模式/外部访问 API);612 测试全绿 |
246
247
  | **v0.7.11** | 记忆库面板改版:按月分页+无限滚动(`limit=500` 静默丢失→100 条/页分页,total 常显)、搜索全局化(关键词/语义走服务端命中全库)、30s 静默刷新 + 状态子页(记忆/实体/向量索引/LLM 消耗四卡)+ 删除两步确认 + 重要性过滤芯片 + 内联 Lucide 图标;bundle 默认开启实体抽取;Issue #72 最大化窗口图谱节点裁剪修复(力导向全程 viewBox 用户单位)+ Issue #59 autoSummarize 从不执行修复(snapshotEvents 垫片);双语 README;595 测试全绿 |
247
248
  | **v0.7.10** | Web 面板体验升级:记忆类型色点体系(筛选/时间树/详情三处贯穿)+ 图谱画布平移/滚轮缩放/重置视图(补齐 `cursor: grab` 暗示却缺失的交互)+ 设置页 Claude 风格分区重排(编号规则行、悬停删除、口语化文案)+ 侧边栏入口同标签冲突修复(可见性过滤 + 渲染验证 + 多候选重试)+ 详情 meta 精排(来源截断、相对时间)+ 新增只读 `GET /api/dsh-mneme/entities` 实体清单端点(19→20 条路由);815 测试全绿 |
package/lib/summarize.js CHANGED
@@ -8,7 +8,7 @@ const SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内
8
8
  // 编码记忆蒸馏 prompt(codingRetrospect 开启时启用):在通用记忆之外,额外提取
9
9
  // 三类编码专属记忆,专治重复踩坑 / 遗忘被否决方案 / 丢失工程约束。字段仍沿用
10
10
  // title/content 单列结构(store 无结构化字段),信息浓缩进 content。
11
- const CODING_SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容(含用户输入、助手思考/回答、工具调用与结果),提炼值得跨会话记住的原子记忆。
11
+ const CODING_SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容(含用户输入、助手回答、工具调用与结果),提炼值得跨会话记住的原子记忆。
12
12
  原子记忆原则:每条记忆只装一个独立事实/偏好/决策,短小、自带完整上下文(把数字、报错信息、命令、路径、结论等原始细节保留在 content 里,不要抽象概括);宁可拆成多条也绝不合并丢细节。信息量一般提 2-4 条,信息密集的对话可提 4-8 条。
13
13
  只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history|rejected_solution|pitfall|constraint","title":"简短标题","content":"保留原始细节的一句话","importance":1-5}。
14
14
  若对话涉及编码/调试,可额外提取编码类记忆:
@@ -91,18 +91,22 @@ function toProtocolChunk(chunk) {
91
91
  // check below.
92
92
  //
93
93
  // codingRetrospect: the distill context is the FULL turn transcript —
94
- // user prompts plus assistant thinking/replies, tool calls + results and code
94
+ // user prompts plus assistant public replies, tool calls + results and code
95
95
  // dispatch output — so the summarizer can see tool errors and extract pitfall
96
96
  // root causes, not just what the user typed. The same filtering stays: only
97
97
  // source.kind === "user" prompts enter (plugin/machine content is excluded).
98
98
  // The result is a single text transcript passed to the LLM as one user message
99
99
  // (SUMMARY_PROMPT already says "根据下面的会话内容").
100
+ //
101
+ // Privacy: assistant `reasoning` (private thought) blocks are deliberately NOT
102
+ // collected — distilled memories must never sink private reasoning chains.
103
+ // Only public text blocks (type "text") reach the summarizer.
100
104
  function collectMessages(session, maxChars = 8000) {
101
105
  // DSH 0.1.2-rc.1 起 Session 改用 snapshotEvents(),兼容旧版 .events
102
106
  const events = session.snapshotEvents?.() ?? session.events ?? [];
103
107
  const lines = [];
104
108
  // 兼容严格形状 [{type:"text",text}] 与宽松形状 ["字符串", ...](lib-smoke 用例
105
- // 直接传字符串数组)。text 之外按需抽 thinking/reasoning 块。
109
+ // 直接传字符串数组)。只取公开文本块;reasoning 私有推理块不进蒸馏上下文。
106
110
  const textOf = (content) => {
107
111
  if (typeof content === "string") return content;
108
112
  if (!Array.isArray(content)) return "";
@@ -127,11 +131,7 @@ function collectMessages(session, maxChars = 8000) {
127
131
  const blocks = Array.isArray(msg?.content) ? msg.content : [];
128
132
  const text = textOf(blocks);
129
133
  if (text.trim()) lines.push(`助手:${text}`);
130
- const thinking = blocks
131
- .map((b) => (typeof b === "string" ? "" : (b && b.type === "reasoning" && typeof b.text === "string" ? b.text : "")))
132
- .filter((s) => s)
133
- .join("\n");
134
- if (thinking.trim()) lines.push(`助手思考:${thinking}`);
134
+ // 私有推理块(reasoning)刻意不采集:蒸馏记忆不得沉淀模型私有思考链。
135
135
  break;
136
136
  }
137
137
  case "tool/call": {
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.13",
4
+ "version": "0.7.14",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -0,0 +1,42 @@
1
+ // 发布前校验 src/ 与 lib/ 一致性。
2
+ //
3
+ // npm 包实际加载 lib/(package main → lib/index.js),而 root 层 `npm publish`
4
+ // 不触发 dsh-mneme/ 的 prepack → sync 同步。历史教训:PR #60 只改了 src 忘了同步
5
+ // lib,v0.7.8 发出去的 npm 包跑的还是旧代码(issue #65)。此脚本让这类漂移在发布时
6
+ // 直接 fail,而不是带着旧产物上线。
7
+ //
8
+ // 用法:node scripts/check-sync.js (root package.json 的 prepack 钩子自动调用)
9
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
10
+ import { join, relative } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const root = join(fileURLToPath(new URL("..", import.meta.url))); // dsh-mneme/
14
+ const srcDir = join(root, "src");
15
+ const libDir = join(root, "lib");
16
+
17
+ function walk(dir) {
18
+ const out = [];
19
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
20
+ const full = join(dir, entry.name);
21
+ if (entry.isDirectory()) out.push(...walk(full));
22
+ else if (entry.isFile()) out.push(full);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ const bad = [];
28
+ for (const file of walk(srcDir)) {
29
+ const rel = relative(srcDir, file);
30
+ const dest = join(libDir, rel);
31
+ if (!existsSync(dest)) {
32
+ bad.push(`missing lib/${rel}`);
33
+ } else if (!readFileSync(file).equals(readFileSync(dest))) {
34
+ bad.push(`differ lib/${rel}`);
35
+ }
36
+ }
37
+ if (bad.length) {
38
+ console.error(`✗ src/ 与 lib/ 不一致(${bad.length} 处)——npm 包实际加载 lib/,请先 npm run sync 并提交:`);
39
+ for (const line of bad) console.error(` ${line}`);
40
+ process.exit(1);
41
+ }
42
+ console.log(`✓ src/ 与 lib/ 一致(${walk(srcDir).length} 个文件)`);
package/src/summarize.js CHANGED
@@ -8,7 +8,7 @@ const SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内
8
8
  // 编码记忆蒸馏 prompt(codingRetrospect 开启时启用):在通用记忆之外,额外提取
9
9
  // 三类编码专属记忆,专治重复踩坑 / 遗忘被否决方案 / 丢失工程约束。字段仍沿用
10
10
  // title/content 单列结构(store 无结构化字段),信息浓缩进 content。
11
- const CODING_SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容(含用户输入、助手思考/回答、工具调用与结果),提炼值得跨会话记住的原子记忆。
11
+ const CODING_SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容(含用户输入、助手回答、工具调用与结果),提炼值得跨会话记住的原子记忆。
12
12
  原子记忆原则:每条记忆只装一个独立事实/偏好/决策,短小、自带完整上下文(把数字、报错信息、命令、路径、结论等原始细节保留在 content 里,不要抽象概括);宁可拆成多条也绝不合并丢细节。信息量一般提 2-4 条,信息密集的对话可提 4-8 条。
13
13
  只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history|rejected_solution|pitfall|constraint","title":"简短标题","content":"保留原始细节的一句话","importance":1-5}。
14
14
  若对话涉及编码/调试,可额外提取编码类记忆:
@@ -91,18 +91,22 @@ function toProtocolChunk(chunk) {
91
91
  // check below.
92
92
  //
93
93
  // codingRetrospect: the distill context is the FULL turn transcript —
94
- // user prompts plus assistant thinking/replies, tool calls + results and code
94
+ // user prompts plus assistant public replies, tool calls + results and code
95
95
  // dispatch output — so the summarizer can see tool errors and extract pitfall
96
96
  // root causes, not just what the user typed. The same filtering stays: only
97
97
  // source.kind === "user" prompts enter (plugin/machine content is excluded).
98
98
  // The result is a single text transcript passed to the LLM as one user message
99
99
  // (SUMMARY_PROMPT already says "根据下面的会话内容").
100
+ //
101
+ // Privacy: assistant `reasoning` (private thought) blocks are deliberately NOT
102
+ // collected — distilled memories must never sink private reasoning chains.
103
+ // Only public text blocks (type "text") reach the summarizer.
100
104
  function collectMessages(session, maxChars = 8000) {
101
105
  // DSH 0.1.2-rc.1 起 Session 改用 snapshotEvents(),兼容旧版 .events
102
106
  const events = session.snapshotEvents?.() ?? session.events ?? [];
103
107
  const lines = [];
104
108
  // 兼容严格形状 [{type:"text",text}] 与宽松形状 ["字符串", ...](lib-smoke 用例
105
- // 直接传字符串数组)。text 之外按需抽 thinking/reasoning 块。
109
+ // 直接传字符串数组)。只取公开文本块;reasoning 私有推理块不进蒸馏上下文。
106
110
  const textOf = (content) => {
107
111
  if (typeof content === "string") return content;
108
112
  if (!Array.isArray(content)) return "";
@@ -127,11 +131,7 @@ function collectMessages(session, maxChars = 8000) {
127
131
  const blocks = Array.isArray(msg?.content) ? msg.content : [];
128
132
  const text = textOf(blocks);
129
133
  if (text.trim()) lines.push(`助手:${text}`);
130
- const thinking = blocks
131
- .map((b) => (typeof b === "string" ? "" : (b && b.type === "reasoning" && typeof b.text === "string" ? b.text : "")))
132
- .filter((s) => s)
133
- .join("\n");
134
- if (thinking.trim()) lines.push(`助手思考:${thinking}`);
134
+ // 私有推理块(reasoning)刻意不采集:蒸馏记忆不得沉淀模型私有思考链。
135
135
  break;
136
136
  }
137
137
  case "tool/call": {
@@ -114,7 +114,9 @@ test("peer-C: 多进程并发原子递增——8 进程×10 次 incrementGenerat
114
114
  store.close();
115
115
  }
116
116
  } finally {
117
- rmSync(dir, { recursive: true, force: true });
117
+ // Windows:8 SQLite 子进程退出后 -shm/-wal 句柄延迟释放,删目录偶发 EPERM;
118
+ // maxRetries 让 rm 自动重试(Node 文档:EBUSY/EPERM 等会重试),不影响并发逻辑。
119
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
118
120
  }
119
121
  });
120
122
 
@@ -293,6 +293,34 @@ test("distills full transcript (tool calls, results, code output) with atomic-me
293
293
  assert.ok(calls[0].messages[0].content[0].text.includes("原子记忆"));
294
294
  });
295
295
 
296
+ test("distill excludes private assistant reasoning blocks from the transcript", async () => {
297
+ const { events, calls } = setup();
298
+ const handler = events.find((e) => e.name === "session/event").fn;
299
+ const session = {
300
+ id: "s12",
301
+ requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }),
302
+ events: [
303
+ userMessage("解释一下这段代码"),
304
+ {
305
+ type: "assistant/message",
306
+ data: {
307
+ message: {
308
+ content: [
309
+ { type: "reasoning", text: "私有推理:内部权衡过程不该进记忆" },
310
+ { type: "text", text: "这段代码有死循环,第 3 行 while 条件恒真。" }
311
+ ]
312
+ }
313
+ }
314
+ },
315
+ { seq: 3, type: "turn/end" }
316
+ ]
317
+ };
318
+ await handler(session, { seq: 3, type: "turn/end" });
319
+ const transcript = JSON.stringify(calls[0].messages);
320
+ assert.ok(transcript.includes("有死循环,第 3 行"), "public assistant text still distills");
321
+ assert.ok(!transcript.includes("私有推理"), "private reasoning must never enter the distill context");
322
+ });
323
+
296
324
  test("codingRetrospect stores coding memory types in the coding memory type set", async () => {
297
325
  const { events, store, calls } = setup({ codingRetrospect: true }, {
298
326
  stream() {