oh-memos-mcp 3.1.9 → 3.1.10

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/CHANGELOG.md CHANGED
@@ -1,11 +1,18 @@
1
- # Changelog
2
-
3
- All notable changes to `oh-memos-mcp` are documented here.
4
-
5
- This project follows [Semantic Versioning](https://semver.org/).
6
-
1
+ # Changelog
2
+
3
+ All notable changes to `oh-memos-mcp` are documented here.
4
+
5
+ This project follows [Semantic Versioning](https://semver.org/).
6
+
7
7
  ---
8
8
 
9
+ ## [3.1.10] - 2026-10-18
10
+
11
+ 配套 API / Docker 镜像版本为 3.1.10。MCP server 代码无改动。
12
+
13
+ - 仓库迁回 `lsg1103275794/oh-memos`:`package.json` 的 repository / bugs / homepage 与 README 链接同步更新。
14
+ - 配套 API 导入时不再加载 torch / transformers / sklearn,内存降低约 2/3;Docker 镜像不再安装 torch。MCP 调用方式不变。
15
+
9
16
  ## [3.1.9] - 2026-09-16
10
17
 
11
18
  配套 API / Docker 镜像版本为 3.1.9。保存先确认原文与向量已持久化,再返回记忆 ID;
@@ -22,713 +29,713 @@ LLM 解析由 API 在后台自动完成,即使模型调用耗时数分钟,
22
29
  原文、ID 和已有解析结果均保留。
23
30
 
24
31
  ## [3.1.8] - 2026-08-27
25
-
26
- 仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
27
-
28
- ### 🏷️ `memos_get` 报 root 无权限,而 root 从未被传入
29
-
30
- `memos_get` 打自己项目的 cube 时回:
31
-
32
- ```
33
- ❌ [API_ERROR] Get failed: User 'root' does not have access to cube
34
- 'jincaizhaopin_cube'. Please register the cube first or request access.
35
- ```
36
-
37
- 但 `MEMOS_USER=dev_user`,root 不在任何调用参数里。
38
-
39
- root 来自后端的默认回退:`start_api.py` 的 `get_memory(mem_cube_id, memory_id,
40
- user_id=None)` 把 `user_id` 声明为可选,缺失时 `core.py` 的 `get()` 回退到
41
- `self.user_id`(MOS 实例自己的用户,即 root)。而 `ensureCubeRegistered` 注册的是
42
- `MEMOS_USER`,于是 root 对该 cube 无授权,`_validate_cube_access` 抛 ValueError,
43
- 被 ValueError handler 映射成 400。
44
-
45
- **`handleMemosGet` 是唯一漏传 `user_id` 的调用点** —— save / list / search /
46
- stats / calendar / siblings 全都传了,admin 的 delete 路径也拼了查询串。
47
-
48
- `apiCallWithRetry` 的 400 分支救不回来:它把 400 当「cube 未加载」,重注册后重试
49
- —— 但重注册仍按 `MEMOS_USER` 注册,重试请求仍然不带 `user_id`,第二次照样落到 root。
50
-
51
- 实测对照(同一个不存在的 id,`jincaizhaopin_cube`):
52
-
53
- ```
54
- 不带 user_id -> User 'root' does not have access to cube 'jincaizhaopin_cube'
55
- 带 user_id=dev_user -> Memory with ID ... not found ← 正常语义
56
- ```
57
-
58
- 为什么以前没人踩到:cube 的**归属**决定是否触发。root 名下的 cube(`dev_cube`、
59
- `claude_cube`、`memos_cube` 等)不受影响;只有 owner 是 `MEMOS_USER` 的项目 cube
60
- 才会报错 —— 也就是每一个用 `project_path` 自动派生出来的 cube。
61
-
62
- 同源缺陷也修了 `wiki-import.ts` 的 `getStoredMemory`。那里更隐蔽:该消息既不含
63
- `not found` 也不是 404,会被当成真实故障,把整页 import 判成失败而不是「记忆不存在」。
64
-
65
- `apiErrorResponse` 另加了对 `does not have access` 的识别。该消息同时含 `user` 和
66
- cube 字样,却不匹配任何既有分支,此前只能给通用的「查 health / 看日志」。新提示先
67
- 指向 `user_id`,因为最常见的成因是调用方漏传它,而不是用户或 cube 不存在。
68
-
69
- 回归测试 `get-user-id-param.test.ts` 的 fetch stub **按 `user_id` 是否存在分流**
70
- 应答,复刻上面的实测对照。无条件回 not-found 的 stub 守不住这个缺陷 —— 变异验证
71
- (把 params 改回 `{}`)时三条断言必须全红,写测试时实际拿它验过。
72
-
73
- ## [3.1.7] - 2026-08-27
74
-
75
- 仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
76
-
77
- ### 🏷️ `open` 报出的画布名,`list` 找不到
78
-
79
- goal 足够长时,`actionOpen` 报出的名字比磁盘上真实存在的多一个字符:
80
-
81
- ```
82
- open reports: 000-verify-3-1-6-canvas-delete-and-ref-escaping-over-live-mcp (61)
83
- list shows : 000-verify-3-1-6-canvas-delete-and-ref-escaping-over-live-mc (60)
84
- on disk : ...-live-mc.mmd
85
- ```
86
-
87
- `actionOpen` 先 `slugify(goal)` 拿到最多 `SLUG_MAX`(60)字符,再拼 `${prefix}-`,
88
- 总长可达 64。`saveCanvas` → `canvasPath` 在落盘路上又 slugify 一次,砍回 60。
89
- **前缀那 4 个字符的预算从来没被算进去。**
90
-
91
- `canvasPath` 里那处「slug 与原名不同就拒绝」的检查本该拦住它,但它只在名字
92
- 「像路径」(含 `/`、`\` 或 `..`)时抛错 —— 截断不像路径,于是静默放过。
93
-
94
- 影响是**报错的句柄,不是损坏的数据**:前缀仍让每个画布互不相同,
95
- 两个前 60 字符相同的长 goal 得到 `000-`/`001-` 两个独立文件,各自节点都在;
96
- `update`/`delete` 传 `open` 报的长名字也照样能用,因为 `canvasPath` 的截断是幂等的。
97
- 坏的只有一件事:调用方拿到的名字,`list` 永远不会显示,磁盘上也不存在。
98
-
99
- 修法是把名字组装收进 `canvas-format.ts` 的新函数 `canvasName(prefix, goal)` ——
100
- 让它紧挨着自己必须遵守的那个上限,而不是让 handler 去做预算算术:
101
-
102
- ```ts
103
- export function canvasName(prefix: string, goal: string): string {
104
- const head = `${prefix}-`;
105
- const slug = slugify(goal, SLUG_MAX - head.length);
106
- return slug ? `${head}${slug}` : `${head}task`;
107
- }
108
- ```
109
-
110
- `slugify` 因此多一个可选的 `maxLength`,**向内钳制到 `SLUG_MAX`** ——
111
- 调用方能收紧上限,不能放宽它,否则「每个画布文件名都装得进 `SLUG_MAX`」
112
- 这个不变量就成了各调用点的自选项。截断后仍走一次尾部连字符清理,
113
- 所以 `000-abc-.mmd` 这种名字不会出现。
114
-
115
- ### 🧪 测试
116
-
117
- canvas-format 单测 41 → 50,canvas e2e 34 → 40 项。
118
-
119
- 核心断言是**不动点**而不是长度:`slugify(canvasName(p, g)) === canvasName(p, g)`,
120
- 跨 3 个前缀 × 6 种 goal(超长、纯 CJK、尾部连字符诱饵、前 60 字符相同的两个变体)。
121
- 长度只是症状 —— `canvasPath` 会再 slugify 一次,任何不是不动点的名字都是
122
- 「告诉了调用方、但文件系统从没见过」的名字。
123
-
124
- e2e 补的 5 项跨 open/list/show/delete 与磁盘:`open` 报的名字必须等于
125
- `list` 显示的名字、等于磁盘上存在的文件。反向验证还原实现后 3 项变红
126
- (长度 64、磁盘上无此名、`list` 显示另一个名字)。
127
-
128
- 另两项(`reported name still routes to its canvas` / `... to delete`)
129
- 在修复前后都通过 —— `canvasPath` 截断幂等使然。**保留但改名如实**:
130
- 它们守的是「如果有人改用『让 `canvasPath` 对有损名字抛错』来修这个缺陷,
131
- 这条回归会被抓到」,不是这次的不变量。
132
-
133
- 门禁:vitest 520 passed、tsc、canvas e2e 40 项、pack 契约 104 文件、
134
- schema budget +0.8%、semantic 快照 17 工具、protocol legacy + v2、
135
- lite smoke、host-env smoke、spread smoke。
136
-
137
- ---
138
-
139
- ## [3.1.6] - 2026-08-26
140
-
141
- 仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
142
-
143
- ### 🔤 `memos_canvas` 的 ref 里,引号读不回来
144
-
145
- `ref` 走 JSON 转义而非标签转义,因为 Windows 路径的反斜杠必须逐字返回,
146
- 否则锚点失效。但 `renderRef` 在 `JSON.stringify` 之后又替换了一次裸引号:
147
-
148
- ```js
149
- JSON.stringify(ref).slice(1, -1).replace(/"/g, "\\u0022") // 旧
150
- ```
151
-
152
- `JSON.stringify` 已经把 `"` 写成 `\"`。那个 `replace` 只匹配裸引号,
153
- **JSON 留下的反斜杠还在前面**,于是落盘成 `\\u0022`;`parseRef` 走 `JSON.parse`,
154
- `\\` 解成一个字面反斜杠,取回 `"` 而不是 `"`。少匹配了一个反斜杠。
155
-
156
- 现在匹配 `\"` 整体:`.replace(/\\"/g, "\\u0022")`。引号仍须离开标签 ——
157
- `"` 会提前终止它所在的 Mermaid `["..."]` —— 但这次是**替换**已转义的序列,
158
- 不是在它上面再叠一层。
159
-
160
- 反斜杠一侧一直是对的:`\\` 就是 JSON 在磁盘上的正确表示,`parseRef` 会解回单反斜杠。
161
- 只有引号这一条路径坏了。
162
-
163
- ### 🗑️ `memos_canvas` 新增 `delete` 动作
164
-
165
- 此前只有 open/update/show/list,清理必须手动删 `.mmd`。而画布的定位是**短期**状态,
166
- 注定会产生废弃文件 —— 加之全局规则禁止手工改动记忆文件,等于没有合法的清理路径。
167
-
168
- `action="delete"` 在有未完成节点(doing/todo/blocked)时**拒绝**并给出逃生口,
169
- 全 done 或空画布直接删:
170
-
171
- ```
172
- ❌ Canvas '000-x' still has 1 unfinished node (0 doing · 0 todo · 1 blocked)
173
- Review it first: memos_canvas(action="show", name="000-x")
174
- Delete anyway: memos_canvas(action="delete", name="000-x", confirm=true)
175
- ```
176
-
177
- **没有套用 `MEMOS_ENABLE_DELETE`**。那道门是保护长期记忆的:走 API、有 embedding 成本、
178
- 且 Lite 模式下整个禁用。画布是本地文件、不走 API,而 Lite 模式下它恰恰是少数可用功能之一 ——
179
- 套上那道门等于在最需要清理的模式里砍掉清理能力。`confirm` 这种「拒绝并给出下一步」
180
- 与 `MAX_NODES`、非法 status 是同一个既有模式。
181
-
182
- 删除是硬删,不做回收站:画布存在的意义就是任务收尾后被丢弃,
183
- 回收站只会攒下它本该清掉的文件。`deleteCanvas` 复用 `canvasPath`,遍历防护自动继承。
184
-
185
- ### 🔢 前缀水位线:`delete` 破坏了「前缀永不复用」
186
-
187
- 加上 delete 之后有两个 e2e 断言失败。**不是断言写错** —— `nextPrefix` 用现存文件算
188
- `max+1`,在只增不减的世界里「前缀永不复用」自动成立,delete 打破了这个不变量:
189
- 删掉最高前缀的画布后,那个前缀会被重新发放。
190
-
191
- 后果正是 `nextPrefix` 自己注释里警告过的:前缀嵌在它铸出的每个节点 id 里,
192
- commit message 或记忆正文引用的 `001-N3` 会变得指向两个不同画布的节点。
193
-
194
- 修法是 `{cube}/canvas/.prefix-hwm` 水位线文件,`saveCanvas` 与 `deleteCanvas` 都抬升它,
195
- 只增不减。delete 侧必须抬升:文件是该前缀唯一的记录,删掉后若水位线仍低于它,
196
- 就会被重新发放。旧 cube 没有这个文件时(`readPrefixHwm` 返回 -1),
197
- 从现存文件恢复下限,所以升级不会因为水位线缺失而复用前缀。
198
-
199
- 水位线损坏或不可读时退回扫描文件,与 `parseCanvas` 一致 ——
200
- 画布是恢复态,读取时机正是刚丢失上下文的那一刻,抛异常是最坏的选择。
201
-
202
- ### 🧪 测试
203
-
204
- canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e 18 → 34 项。
205
- 两个修复都做了反向验证,还原实现后对应用例确实变红:转义 3 项、水位线 1 项(含 delete 侧那次抬升)。
206
-
207
- e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实升级路径 ——
208
- 它跨 store 与 handler,单测碰不到。
209
-
210
- 顺带改掉一个自己写坏的用例:名字叫 "does not free the prefix it used",
211
- 断言却在验证前缀被释放了。名字与断言矛盾的测试比没有测试更坏。
212
-
213
- `memos_canvas` 的 schema 改了(action enum 加 `delete`,新增 `confirm`),
214
- 两处冻结的哈希同步重算:`schema-semantic-baseline.json` 与
215
- `protocol-contract.test.ts` 的 `EXPECTED_SCHEMA_HASHES`。
216
-
217
- 门禁:vitest 511 passed、tsc、canvas e2e 34 项、pack 契约 104 文件、
218
- schema budget +0.8%(限 5%)、semantic 快照 17 工具、protocol v2、lite smoke。
219
-
220
- ---
221
-
222
- ## [3.1.5] - 2026-08-26
223
-
224
- 3.1.1~3.1.4 都是「仅 MCP server」的版本,Python 包与容器镜像停在 3.1.0。
225
- **3.1.5 两侧同时有改动**,三个版本串(npm 包、`pyproject.toml`、`src/oh_memos/__init__.py`)
226
- 因此重新对齐到 3.1.5。Python/容器侧的内容见仓库根的 `docs/CHANGELOG.md`。
227
-
228
- ### 🧾 `memos_get` 的 not-found 文案指向真正的成因
229
-
230
- 旧文案让用户「核对 id 是否正确(从 `memos_search` 结果复制)」,但实测中最常见的成因恰恰是
231
- **从搜索结果复制来的 id 过一会儿就失效**:`POST /search` 返回的是 `WorkingMemory` 层的副本 id,
232
- 该层按 `user_name` 只保留 20 条(后端 `manager.py` 的 `memory_size["WorkingMemory"]`),
233
- 每次检索都重写并淘汰最旧的。实测 `oh_memos_cube` 的那 20 条只覆盖 22 分钟。
234
-
235
- 因此新文案把 `memos_list_v2` 指出来是有实质区别的:它返回 `LongTermMemory` 的 id,
236
- 那一层不淘汰(实测同一批 id 隔天仍可取回)。措辞上不把淘汰断言成事实 ——
237
- Lite 模式(本地 JSONL)没有 `WorkingMemory` 层,那里的 not-found 就是单纯的 id 不存在。
238
-
239
- `notFoundText` 同时改为导出,便于测试直接断言文案而不经由 handler。
240
-
241
- ### 🌐 API 客户端保留 400 响应体
242
-
243
- `apiCallWithRetry` 此前把非 2xx 一律丢弃响应体,于是后端在 400 里给出的
244
- 校验错误说明拿不到,工具层只能报一个无信息的失败。现在 400 单独放行并解析 JSON,
245
- 后端的错误描述可以原样呈现给调用方。
246
-
247
- ### 🔒 分层过滤的强制机制
248
-
249
- 同一个缺陷已复发 7 次:新增一条检索路径,忘了把 `WorkingMemory` 短期副本滤掉,
250
- 于是把一个几分钟后就失效的 id 交给了 agent。每次都靠 review 抓到,这次改成结构性拦截。
251
-
252
- **为什么普通单测拦不住**:实测把所有层级过滤切断后,319 项 vitest 依然全绿。
253
- 单测问的是"过滤函数算得对不对",而缺陷形态是"过滤函数没被调用" —— 两个问题不同。
254
-
255
- 新增 `src/memory-tier-boundary.ts`(清单)与同名 `.test.ts`(守卫,18 项)。
256
- 清单把 `handlers/index.ts` 的 28 条分派路由逐条分类并附实测证据,守卫扫描源码与分派表做双向比对。
257
- 四条 fail-closed 棘轮,均以变异测试验证过确实会红:
258
-
259
- - 分派表新增路由却没在清单里分类 → 失败;清单有而分派表已无(陈旧条目)→ 也失败
260
- - 静默新增已知缺口 → 失败(`KNOWN_GAPS` 为冻结集合,计数上限硬编码)
261
- - 修好缺口却忘了从 `KNOWN_GAPS` 移除 → 失败,否则集合会膨胀成一张无人清理的豁免名单
262
- - `CYPHER_PROJECTION_BASELINE` 按严格相等断言,修好了不调基线同样失败 —— 棘轮只能往紧的方向走
263
-
264
- **覆盖了第二种复发形态**:过滤接上了,但构造 metadata 时漏掉 `memory_type`,
265
- 判定函数恒读到 `undefined` 并判为可见。`search.ts` 曾因此让四个调用点的过滤同时空转,
266
- 而没有任何测试失败。守卫现在会检查投影记忆的 Cypher `RETURN` 是否带出该字段。
267
-
268
- **已记录但尚未修的缺口**(`memos_think`、`memos_graph` 的三条路由):
269
- `think.ts` 上朴素过滤会把语义召回从 15 条砍到 5 条,正确解法是按 `metadata.key`
270
- 换成持久层孪生节点的 id,而不是加过滤。不变式的准确表述是"永不把易失 id 交给 agent",
271
- "过滤掉 `WorkingMemory`" 只是其中一种(有损的)满足方式。
272
-
273
- 门禁:vitest 491 通过(新增 18),`tsc --noEmit` 无输出。
274
-
275
- ## [3.1.4] - 2026-08-24
276
-
277
- ### 🐛 修复:同源节点列表混入 scheduler 短期副本
278
-
279
- 3.1.3 上线后实测:同源列表里每个 `key` **成对出现** —— 一条被切成 6 段的记忆列出了 12 条同源。
280
-
281
- 后端对每条抽取结果写两个节点:一个 `WorkingMemory` 短期副本 + 一个 `LongTermMemory`
282
- (少数为 `UserMemory`)持久节点,`key` 与 `created_at` 逐字相同。`findSiblings` 没有滤层级,
283
- 于是短期副本一并列出。
284
-
285
- **这不是新决策,是 3.1.0 已经做过的决定**(`memory-tier.ts`)。`memos_search` 与
286
- `memos_list_v2` 早已在滤,`findSiblings` 是 3.1.2 新增的路径 —— 又漏了一处。
287
- 同一形态在本项目已出现五次:**新写的检索路径必须继承既有的分层过滤决定。**
288
-
289
- 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 时不滤,与 `filterEphemeralTier` 语义一致。
290
- 缺 `memory_type` 视为可见 —— Lite 的 JSONL 不写该字段。
291
-
292
- #### 测试
293
-
294
- 新增 5 项分层断言(共 48 项),三个变异全部被捕获:不滤层级(回到本次缺陷)、
295
- 忽略逃生开关、忽略 limit。其中「limit 在滤层级之后生效」用交错排列的候选集断言 ——
296
- 顺序反了会让 limit 被随即隐藏的副本吃掉,只返回一半。
297
-
298
- 门禁:vitest 447 passed、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
299
- protocol v2、lite smoke、host-env smoke。
300
-
301
- ## [3.1.3] - 2026-08-24
302
-
303
- ### 🐛 修复:同源碎片区块从不出现 —— `sources` 有两种线上形态
304
-
305
- 3.1.2 上线后实测:原文能正确返回,但「同源碎片」区块**从不出现**。根因是同一个字段
306
- 在两个端点上形态不同:
307
-
308
- | 端点 | `sources[0]` 的形态 |
309
- |---|---|
310
- | `GET /memories/{cube}/{id}`(单条) | **对象** `{type, role, chat_time, content}` |
311
- | `GET /memories`(列表) | **JSON 字符串** `'{"type":...,"content":"..."}'` |
312
-
313
- `verbatimOf` 初版只处理对象形态。单条取回走对象 → 原文正常;同源查找的候选集来自
314
- 列表端点 → 全部返回 null → 永远匹配不到。**原文层可用,配对层静默失效。**
315
-
316
- 单测没抓住的原因很直接:fixture 只造了对象形态,两种形态里只测了一种。
317
-
318
- 修法:抽出 `contentOf(entry)` 同时接受对象与 JSON 字符串。非 JSON 字符串返回 null
319
- 而**不猜它是裸原文** —— 否则任意字符串都会被当原文,把无关记忆归成同源。
320
- 坏 JSON 按无 content 处理不抛异常:这是展示层,脏数据不该让 `memos_get` 失败。
321
-
322
- #### 测试
323
-
324
- 新增 6 项断言与一个 list 形态 fixture(共 43 项)。三个变异全部被捕获,
325
- 其中「不处理字符串形态」即回到本次修复前的行为,另两个覆盖「非 JSON 当裸原文」
326
- 与「坏 JSON 抛异常」。跨形态指纹一致性单独断言 —— 否则单条与列表永远配不上。
327
-
328
- 门禁:vitest 442 passed、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
329
- protocol v2、lite smoke、host-env smoke。
330
-
331
- ## [3.1.2] - 2026-08-23
332
-
333
- ### 📄 `memos_get` 返回原文,不再只返回 LLM 概括
334
-
335
- 开启 LLM 抽取(后端 `MOS_TYPED_SAVE_FAST=false`)时,后端把一次写入拆成多条细粒度记忆:
336
- 每条的 `memory` 字段是 LLM 概括,而**逐字原文完整保留在 `metadata.sources[0].content`**。
337
- 实测一条 1056 字的写入被拆成 5 条,每条 `memory` 约 244 字,5 条共享同一份原文。
338
- `memos_get` 此前只读 `memory`,于是完整上下文取不回来。
339
-
340
- 这不是取舍问题 —— 库里本来就是两份数据,各有各的用途:
341
-
342
- | 层 | 内容 | 用途 |
343
- |---|---|---|
344
- | `memory` | LLM 概括(细粒度) | 向量化输入、建边对象 → 决定图谱节点与联想质量 |
345
- | `metadata.sources[].content` | 逐字原文 | `memos_get` 的完整上下文来源 |
346
-
347
- **新输出结构**(有原文时):原文作为 `### Content` 正文;LLM 概括降级到
348
- `### 抽取概括` 区块并保留 —— 它是向量化与建边的实际输入,看得见才能判断图谱节点切得对不对;
349
- `### 同源碎片` 列出由同一次写入抽出的其他记忆(id + key),从而能看出 LLM 的切分方式。
350
- 无原文时(fast-path 写入)输出与此前完全一致。
351
-
352
- 同源判定用原文全文,不用 `session_id` —— 同一会话里的无关写入会被错误归组。
353
-
354
- 新增 `src/verbatim-source.ts`:`verbatimOf` / `sourceFingerprint` / `findSiblings` /
355
- `renderVerbatimSections`,四个纯函数,handler 与测试调用同一份实现。
356
- Full 与 Lite 两条路径都已接线(Lite 的 JSONL 无同源概念,列表恒空)。
357
- 同源查询失败时返回空列表,不影响原文返回。
358
-
359
- #### 测试
360
-
361
- 新增 `src/verbatim-source.test.ts`(37 项)。**变异验证抓出三个断言缺乏判别力**,
362
- 每一个都是「测试写了但等于没写」的形态:
363
-
364
- - 接线守卫数两条路径**合计**调用次数 `>= 2`,而 Lite 路径自身就有 2 次,
365
- 砍掉 Full 路径那次仍然通过。改为按路径分别断言。
366
- - 「原文不截断」用 30 字测试原文,而变异截断到 200 字 —— 改不动它。
367
- 测试原文改为 600+ 字并加尾部标记。**这条正是本次要修的缺陷,却最后才抓住。**
368
- - `"not-an-array"[0]` 是字符 `"n"`、无 `.content`,宽松实现也返回 null。
369
- 改用伪装成数组的对象才有判别力。
370
-
371
- 顺带删除 `sourceFingerprint` 的长度前缀:变异验证时构造不出它能防住的碰撞,
372
- 无测试支撑的防御代码比没有更糟。
373
-
374
- 12 个变异被捕获(W1「Full 路径不读原文」即回到本次修复前的行为)。
375
- 门禁:vitest 436/436、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
376
- protocol v2、lite smoke。
377
-
378
- ## [3.1.1] - 2026-08-22
379
-
380
- ### 🐛 修复:`memos_context_resume` 每条记忆成对出现
381
-
382
- 3.1.0 装好后 `memos_search` 与 `memos_list_v2` 都已正确隐藏 `WorkingMemory` 短期副本,
383
- 但 `memos_context_resume` 仍成对显示(10 条 = 5 对,内容逐字相同、UUID 不同)。
384
-
385
- **根因不止是漏接一条路径。** temporal 查询的 Cypher 不返回 `memory_type`,
386
- 构造出的 metadata 只有 `relativity`/`temporal_rank`/`source` —— 于是分层判定永远
387
- 读到 `undefined` 并判为可见,**下游任何过滤对 temporal 记忆都是空转**。
388
- 受影响的是四个调用点,不只是 `memos_context_resume`:
389
-
390
- | 路径 | 3.1.0 的状态 |
391
- |---|---|
392
- | `memos_context_resume` | 成对显示(用户实测发现) |
393
- | `memos_search` temporal intent(两处) | 有过滤代码,但读不到字段 |
394
- | `memos_think` | 同上 |
395
-
396
- 后三处一直漏着且不易察觉 —— 过滤代码存在且看起来合理,只是作用对象缺字段。
397
-
398
- 修法:在 Cypher 里排除 `WorkingMemory` 并返回 `memory_type`。这是唯一对四处都成立的做法,
399
- 且 `LIMIT` 在过滤之后 —— 要 N 条就得 N 条真记忆,无需超额取数。
400
- `memos_context_resume` 的 API 回退路径由服务端施加 limit,无法先过滤,故超额取 3 倍。
401
-
402
- 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 仍然有效:为真时不加排除条件,
403
- 但仍返回 `memory_type` —— 开关只关过滤,不关可观测性。
404
-
405
- #### 测试
406
-
407
- 新增 `src/context-resume.test.ts`(22 项),**9 个变异全部被捕获**,
408
- 其中 W1「API 路径不过滤」即回到本次报告的原缺陷。
409
-
410
- 新增 `npm run test:host-env-smoke`:用 MCP host 配置里的**原样环境**驱动 server,
411
- 且不继承外层环境变量。已有的 spread smoke 自带硬编码 Neo4j 凭据兜底,
412
- 因此「host 读不到凭据」这一失败形态它测不出来 —— 实测该形态下检索照常返回记忆、
413
- 但联想标注为 0,属静默降级。
414
-
415
- 门禁:vitest 416/416、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
416
- protocol v2、lite smoke、spread smoke、host-env smoke。
417
-
418
- ## [3.1.0] - 2026-08-22
419
-
420
- ### 🧠 检索排序:衰减、强化、分档去重与图扩散联想
421
-
422
- 本版把检索侧的排序从「纯相似度」改为「相似度 + 时间衰减 + 访问强化」,并加入按类型分档的
423
- 近重复折叠与一跳图扩散联想。
424
-
425
- #### 记忆层级:`WorkingMemory` 默认隐藏
426
-
427
- 后端 scheduler 会为每条持久记忆写一份 `WorkingMemory` 短期副本,该层按用户仅保留最近 20 条、
428
- 每次检索都会重写淘汰。把这些 id 交给 agent 意味着它拿到的引用几分钟后就失效。
429
- 新增 `src/memory-tier.ts`,`memos_search` 与 `memos_list_v2` 默认滤掉该层;
430
- 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 可关闭过滤。缺 `memory_type` 视为可见
431
- (Lite 的 JSONL 不写该字段)。
432
-
433
- #### 衰减与强化
434
-
435
- 排序分数加入按 `created_at` 的时间衰减与按访问次数的强化项,两者都可用环境变量关掉,
436
- 默认开启。访问计数由 `src/access-tracker.ts` 本地维护,不写回后端。
437
-
438
- #### 近重复折叠(按类型分档)
439
-
440
- 同类型内容高度重合的记忆折叠为一条并标注折叠数。分档按 `memory_type` ——
441
- 不同类型的相似文本(一条 `DECISION` 与一条 `BUGFIX`)不应互相折叠。
442
-
443
- #### 一跳图扩散联想(`MEMOS_SPREAD_ACTIVATION`,默认关闭)
444
-
445
- 命中记忆沿知识图谱做一跳扩散,把强关联但关键词不匹配的记忆带进结果并标注来源。
446
- 默认关闭,因为它需要可用的 Neo4j 凭据;读不到凭据时静默返回 0 条联想。
447
-
448
- #### 检索期注解
449
-
450
- 结果里标注衰减后分数、访问次数与折叠数,使排序结果可解释而非黑箱。
451
-
452
- #### graph schema 四处缺陷修复
453
-
454
- `memos_graph` 的 schema 模式修正四处:节点计数口径、边类型去重、孤立节点统计与采样上限。
455
-
456
- ## [3.0.1] - 2026-08-19
457
-
458
- ### 🩹 修复 3.0.0 打包出的多余依赖与失效仓库链接
459
-
460
- 3.0.0 的 npm 包声明了两个与运行时无关的依赖,并且 npm 页面上的仓库链接全部指向一个不存在的仓库。
461
- 功能没有受影响,但每个用户都要多下载约 17 MB,且 issue/homepage 链接是死链。
462
-
463
- #### 修复
464
-
465
- - **移除 `ci@^2.3.0` 与 `npm@^11.19.0`**。两者都不被 `dist/` 里的任何代码 import,属纯安装期膨胀:
466
- `npm` 是完整的 npm CLI(16 MB),`ci` 是无关的第三方工具(17 KB)。安装体积从约 30 MB 回到约 13 MB。
467
- 根因是发布前工作区里未提交的依赖漂移被打进了 tarball;仓库提交本身一直只有三个依赖。
468
- - **`repository` / `bugs` / `homepage` 从 `github.com/xigou/oh-memos` 改为 `github.com/lsg1103275794/oh-memos`**。
469
- 前者返回 404,导致 npm 页面上的仓库、issue 和主页链接全部失效。`mcp-server-node/README.md`
470
- 里的 clone 命令与仓库链接同步修正。
471
-
472
- #### 不变
473
-
474
- 运行时行为、工具名、输入 schema、协议协商和持久化格式与 3.0.0 完全一致,仅包元数据与依赖清单变化。
475
- 从 3.0.0 升级只需重新安装,无需任何迁移。
476
-
477
- ## [3.0.0] - 2026-08-19
478
-
479
- ### Breaking
480
-
481
- - **Node.js 20 is now the minimum runtime.** Node 18 users must stay on
482
- `oh-memos-mcp@2` (`npx -y oh-memos-mcp@2`) until they can upgrade their
483
- runtime. The entry point exits with an actionable message on Node 18 instead
484
- of failing with an opaque module error.
485
- - The MCP runtime now uses the role-split `@modelcontextprotocol/server@2` and
486
- Zod 4 instead of the monolithic v1 SDK and Zod 3.
487
-
488
- ### Added
489
-
490
- - **Dual-era stdio serving.** The same package accepts legacy 2025-era
491
- initialize clients and clients pinned to MCP `2026-07-28` through
492
- `serveStdio(..., { legacy: "serve" })`.
493
- - Stringified `tools/call` arguments are normalized before era classification
494
- and schema validation, preserving the existing client compatibility path.
495
- - Protocol, lifecycle, semantic-schema and package-boundary release gates now
496
- cover legacy/auto/modern clients, Full/Lite providers, 16/17 tools, large
497
- requests, probe fallback and graceful process shutdown.
498
-
499
- ### Migration
500
-
501
- - Install with `npx -y oh-memos-mcp` (npm `latest`) or pin `oh-memos-mcp@3.0.0`.
502
- - No memory, cube or Lite JSONL migration is required. Existing Full and Lite
503
- data formats, tool names and input schemas remain unchanged, so Agent
504
- configurations only need the package reference updated.
505
- - Clients that still negotiate the 2025-era wire keep working unchanged; no
506
- client-side protocol migration is required to adopt 3.0.
507
-
508
- ### Rollback
509
-
510
- - Pin Agent configurations to `npx -y oh-memos-mcp@2.1.0`. No data rollback is
511
- necessary because this release does not change persisted formats.
512
- - `2.x` remains installable from npm; this release does not unpublish or
513
- deprecate it.
514
-
515
- ## [2.1.0] - 2026-08-16
516
-
517
- ### Fixed
518
-
519
- - **Auto-discovered `.env` no longer overrides launcher environment.** A `.env`
520
- found in the working directory or package root now only fills missing values,
521
- so MCP client `env` blocks and shell exports such as `MEMOS_PROVIDER=local`
522
- stay in effect. A file selected explicitly through `MEMOS_ENV_FILE` or
523
- `--memos-env-file` remains authoritative.
524
-
525
- ### Added
526
-
527
- - **Local Lite provider.** `MEMOS_PROVIDER=local` (implied by `MEMOS_MODE=lite`)
528
- serves `memos_save`, `memos_get`, `memos_list_v2`, `memos_search`, and
529
- `memos_context_resume` from a per-cube `memories.jsonl` with deterministic
530
- lexical ranking, durable appends, and a cross-process lock. Graph, Think,
531
- Wiki, and admin tools report `LOCAL_PROVIDER_UNSUPPORTED` rather than
532
- pretending a graph backend exists.
533
-
534
- - **Skill candidate lifecycle.** `memos_review_skill_candidate` records
535
- approve/reject with reviewer audit metadata, and
536
- `memos_install_skill_candidate` installs only approved candidates into
537
- `.claude/skills/<slug>/SKILL.md` without overwriting, following symlinks, or
538
- executing anything.
539
-
540
- - **Explainable graph provenance.** `memos_graph(mode="related"|"path"|"impact")`
541
- now reports normalized evidence categories (`EXTRACTED`, `INFERRED`,
542
- `AMBIGUOUS`, or `UNKNOWN`) and includes confidence, evidence references,
543
- source file/location, extractor version and verification time when those
544
- fields exist. Legacy graph data without provenance remains readable and is
545
- reported as `UNKNOWN` rather than assigned invented evidence.
546
-
547
- - **`memos_graph(mode="import")` Graphify boundary.** Accepts up to 5 MB of
548
- Graphify/NetworkX node-link JSON through `graph_json`, validates `nodes` plus
549
- `links` (or the `edges` alias), and produces a deterministic import plan with
550
- portable stable Code Graph ids. Duplicate ids, dangling edges, unsafe source
551
- paths, invalid confidence values and oversized graphs are rejected before
552
- any persistence boundary. This mode is intentionally **dry-run only**: it
553
- never writes to Neo4j, Qdrant or a memory cube.
554
-
555
- - **`memos_canvas`** — a symbolic task canvas: short-term task state that survives
556
- context compaction. One Mermaid file per task under
557
- `{MEMOS_CUBES_DIR}/{cube_id}/canvas/`, whose nodes carry a greppable id
558
- (`000-N1`) and an optional `ref` anchoring them to evidence:
559
- `mem:<memory_id>` (a memory in the graph), `file:<path>` (any file, including
560
- the large tool results the harness already offloads), or `note:<text>`.
561
-
562
- `action`: `open` (needs `goal`) · `update` (appends a node; `node_id` edits one
563
- in place) · `show` · `list`.
564
-
565
- Canvases are deliberately **not** written to Neo4j or Qdrant. A canvas changes
566
- several times an hour, and paying an embedding round trip for a `doing→done`
567
- flip would be absurd. Durable facts still go through `memos_save`; the canvas
568
- points at them with a `mem:` ref.
569
-
570
- This is **not** a token-saving feature. The design it borrows from
571
- ([TencentDB-Agent-Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory))
572
- achieves its reduction by intercepting and replacing tool output before the
573
- model sees it, which a Claude Code hook cannot do — `PreToolUse` can rewrite a
574
- tool's *input* via `updatedInput`, but no hook can rewrite its *output*. What
575
- this offers instead is task state that survives compaction, and a path from a
576
- summary back to the evidence behind it.
577
-
578
- Node ids and canvas prefixes are allocated as max+1 and **never reuse a gap**:
579
- a deleted `000-N2` may still be cited from a commit message, a memory body or
580
- another node's ref, and handing that id to a new node would silently repoint
581
- every one of those citations.
582
-
583
- - `memos_context_resume` now lists **unfinished** canvases first — headlines and
584
- counts only, a few dozen tokens — so the first thing visible after a compaction
585
- is the open work rather than a memory feed. Injecting canvas bodies here would
586
- rebuild the very context bloat the canvas exists to avoid; the model opens what
587
- it needs with `show`.
588
-
589
- - **Test infrastructure.** This package previously had none: no test script, no
590
- `*.test.ts`. Adds vitest and `npm test`, with 65 unit tests covering the
591
- parse/render round trip, node-id allocation, Mermaid label injection, path
592
- traversal refusal and atomic writes. `scripts/canvas-e2e.mjs` adds 18 checks
593
- driven over real MCP stdio rather than mocks.
594
-
595
- - `MEMOS_ENV_FILE` env var and `--memos-env-file` flag, to point the server at an
596
- explicit `.env`. Previously the file was located only by guessing from position
597
- (cwd, two levels above the package, dotenv's upward search) — which works from
598
- a checkout and never works under `npx`, where the package root sits in the npm
599
- cache and every candidate misses, loading not one variable. A path that does
600
- not exist now warns on stderr rather than falling through silently.
601
-
602
- ### Fixed
603
-
604
- - `toLocalPath()` rewrote Windows cube paths to `/mnt/...` unconditionally. That
605
- mapping only resolves inside WSL; under native Windows Node the result is not
606
- absolute, so it resolved against the current drive and every cube write landed
607
- in a phantom tree while the API kept reading the real path. Registration
608
- reported success, then `/search` and `/memories` failed 400 — the loaded cube
609
- had no memory backend.
610
-
611
- ### Security
612
-
613
- - `canvasPath` is the one place caller text becomes a filesystem path. It uses a
614
- **whitelist** (`[a-z0-9-]`) rather than a blacklist of dangerous characters,
615
- because a blacklist is always missing an entry. `.` is outside the whitelist,
616
- so `..` cannot survive it — traversal is structurally impossible rather than
617
- merely checked for — and a post-resolve containment check backs that up
618
- independently. `cube_id` is treated as untrusted too, since it is derived from
619
- a caller-supplied `project_path`.
620
-
621
- ### Documentation
622
-
623
- - The `alwaysAllow` example carried two defects. `memos_search` appeared twice,
624
- and several entries were **call forms** — `memos_admin(action=list_cubes)` and
625
- the like. `alwaysAllow` matches tool names, so those entries matched nothing:
626
- a reader would believe those calls were pre-approved and still be prompted for
627
- every one. Replaced with bare tool names, which auto-approve every action of
628
- the tool.
629
- - `memos_delete` was dropped from that example, which also set
630
- `MEMOS_ENABLE_DELETE: "true"` — together they auto-approve deleting memories
631
- with no prompt. Enabling that is a decision worth making deliberately.
632
- - The `.env` note claimed the working directory's file loads "automatically with
633
- highest priority". Under `npx` no file loads at all. Documented `MEMOS_ENV_FILE`
634
- and what actually happens.
635
-
636
- ### Notes
637
-
638
- - Tool surface grew 12488 B → 13680 B (+9.5%), past the +5% drift budget, and the
639
- baseline was re-frozen. This is the cost of a new tool rather than description
640
- drift; the description was trimmed from 1333 B to 1192 B first, moving detail
641
- into `show`'s output (the same move `memos_suggest` made with the memory-type
642
- decision tree).
643
- - `tsconfig.json` now excludes `src/**/*.test.ts`. Without it the test files
644
- compile into `dist` and ship with `npm publish`.
645
-
646
- ---
647
-
648
- ## [2.0.1] — 2026-08-02
649
-
650
- ### Fixed
651
-
652
- - MCP `serverInfo` was hardcoded in `server.ts` and reported
653
- `{name: "memos-memory", version: "1.0.1"}` — a name matching neither the
654
- package (`oh-memos-mcp`) nor the conventional server key (`oh-memos`), and a
655
- version that 2.0.0 shipped stale. Clients display this during the handshake.
656
- It is now read from `package.json` at startup, so it cannot drift again.
657
-
658
- ---
659
-
660
- ## [2.0.0] — 2026-08-02
661
-
662
- ### ⚠️ Breaking — tool surface consolidated from 18 to 10
663
-
664
- Eleven tools were merged into three dispatching tools. **There is no compatibility
665
- shim**: the MCP SDK only dispatches registered tools, so calling a removed name now
666
- returns `Unknown tool`.
667
-
668
- If your MCP client config pins tool names (e.g. Claude Code's `alwaysAllow` list),
669
- you must update it before upgrading.
670
-
671
- #### Migration
672
-
673
- | 1.x tool | 2.0 replacement |
674
- |----------|-----------------|
675
- | `memos_search_context` | `memos_search` — pass the `context` array (recent turns) |
676
- | `memos_get_graph` | `memos_graph(mode="related")` |
677
- | `memos_trace_path` | `memos_graph(mode="path")` |
678
- | `memos_impact` | `memos_graph(mode="impact")` |
679
- | `memos_export_schema` | `memos_graph(mode="schema")` |
680
- | `memos_list_cubes` | `memos_admin(action="list_cubes")` |
681
- | `memos_register_cube` | `memos_admin(action="register_cube")` |
682
- | `memos_create_user` | `memos_admin(action="create_user")` |
683
- | `memos_validate_cubes` | `memos_admin(action="validate_cubes")` |
684
- | `memos_get_stats` | `memos_admin(action="stats")` |
685
- | `memos_calendar` | `memos_admin(action="calendar")` |
686
-
687
- Unchanged: `memos_context_resume`, `memos_search`, `memos_save`, `memos_list_v2`,
688
- `memos_get`, `memos_suggest`, `memos_delete`.
689
-
690
- `alwaysAllow` for 2.0:
691
-
692
- ```json
693
- "alwaysAllow": [
694
- "memos_context_resume", "memos_search", "memos_save",
695
- "memos_list_v2", "memos_get", "memos_suggest",
696
- "memos_think", "memos_graph", "memos_admin", "memos_export_wiki"
697
- ]
698
- ```
699
-
700
- ### Added
701
-
702
- - **`memos_think`** — evidence pack for a question. Runs semantic retrieval plus a
703
- recent-72h temporal pass, deduplicates, and returns numbered evidence with graph
704
- relationships between items, contradiction/evolution candidates, staleness
705
- candidates, and gap analysis. Synthesis is deliberately left to the calling model;
706
- the server emits no prose. Results can be persisted back as `SYNTHESIS`.
707
- - **`memos_export_wiki`** — export a cube as an interlinked markdown wiki: one page
708
- per memory (YAML frontmatter, `[[wikilink]]` relations), plus `index.md` and a
709
- mermaid `graph.md`. Only files carrying the generator marker are ever replaced;
710
- foreign files in the output directory are preserved. Defaults to
711
- `<project_path>/docs/memory-wiki`.
712
- - **`SYNTHESIS`** memory type, for answers synthesized from retrieved evidence.
713
-
714
- ### Changed
715
-
716
- - Tool-surface cost dropped ~51%: `tools/list` payload 22.9 KB → 11.2 KB
717
- (≈5850 → ≈2856 tokens). Shared `project_path` / `cube_id` parameter descriptions
718
- were deduplicated; the full cube-routing rules are now stated once, on
719
- `memos_save` and `memos_search`.
720
- - Runtime error `suggestions` now name the 2.0 call forms, so a model reading a
721
- failure message is no longer told to call a tool that does not exist.
722
-
723
- ### Fixed
724
-
725
- - `memos_admin`'s `cube_id` no longer carries a `MEMOS_DEFAULT_CUBE` default.
726
- Previously, calling `register_cube` without `cube_id` would silently register the
727
- default cube instead of failing.
728
-
729
- ---
730
-
731
- ## [1.0.0] — 2026-03-04
732
-
733
- Initial npm release. 18 tools, Node-only MCP server for oh-memos — no Python
734
- required, runs via `npx`.
32
+
33
+ 仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
34
+
35
+ ### 🏷️ `memos_get` 报 root 无权限,而 root 从未被传入
36
+
37
+ `memos_get` 打自己项目的 cube 时回:
38
+
39
+ ```
40
+ ❌ [API_ERROR] Get failed: User 'root' does not have access to cube
41
+ 'jincaizhaopin_cube'. Please register the cube first or request access.
42
+ ```
43
+
44
+ 但 `MEMOS_USER=dev_user`,root 不在任何调用参数里。
45
+
46
+ root 来自后端的默认回退:`start_api.py` 的 `get_memory(mem_cube_id, memory_id,
47
+ user_id=None)` 把 `user_id` 声明为可选,缺失时 `core.py` 的 `get()` 回退到
48
+ `self.user_id`(MOS 实例自己的用户,即 root)。而 `ensureCubeRegistered` 注册的是
49
+ `MEMOS_USER`,于是 root 对该 cube 无授权,`_validate_cube_access` 抛 ValueError,
50
+ 被 ValueError handler 映射成 400。
51
+
52
+ **`handleMemosGet` 是唯一漏传 `user_id` 的调用点** —— save / list / search /
53
+ stats / calendar / siblings 全都传了,admin 的 delete 路径也拼了查询串。
54
+
55
+ `apiCallWithRetry` 的 400 分支救不回来:它把 400 当「cube 未加载」,重注册后重试
56
+ —— 但重注册仍按 `MEMOS_USER` 注册,重试请求仍然不带 `user_id`,第二次照样落到 root。
57
+
58
+ 实测对照(同一个不存在的 id,`jincaizhaopin_cube`):
59
+
60
+ ```
61
+ 不带 user_id -> User 'root' does not have access to cube 'jincaizhaopin_cube'
62
+ 带 user_id=dev_user -> Memory with ID ... not found ← 正常语义
63
+ ```
64
+
65
+ 为什么以前没人踩到:cube 的**归属**决定是否触发。root 名下的 cube(`dev_cube`、
66
+ `claude_cube`、`memos_cube` 等)不受影响;只有 owner 是 `MEMOS_USER` 的项目 cube
67
+ 才会报错 —— 也就是每一个用 `project_path` 自动派生出来的 cube。
68
+
69
+ 同源缺陷也修了 `wiki-import.ts` 的 `getStoredMemory`。那里更隐蔽:该消息既不含
70
+ `not found` 也不是 404,会被当成真实故障,把整页 import 判成失败而不是「记忆不存在」。
71
+
72
+ `apiErrorResponse` 另加了对 `does not have access` 的识别。该消息同时含 `user` 和
73
+ cube 字样,却不匹配任何既有分支,此前只能给通用的「查 health / 看日志」。新提示先
74
+ 指向 `user_id`,因为最常见的成因是调用方漏传它,而不是用户或 cube 不存在。
75
+
76
+ 回归测试 `get-user-id-param.test.ts` 的 fetch stub **按 `user_id` 是否存在分流**
77
+ 应答,复刻上面的实测对照。无条件回 not-found 的 stub 守不住这个缺陷 —— 变异验证
78
+ (把 params 改回 `{}`)时三条断言必须全红,写测试时实际拿它验过。
79
+
80
+ ## [3.1.7] - 2026-08-27
81
+
82
+ 仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
83
+
84
+ ### 🏷️ `open` 报出的画布名,`list` 找不到
85
+
86
+ goal 足够长时,`actionOpen` 报出的名字比磁盘上真实存在的多一个字符:
87
+
88
+ ```
89
+ open reports: 000-verify-3-1-6-canvas-delete-and-ref-escaping-over-live-mcp (61)
90
+ list shows : 000-verify-3-1-6-canvas-delete-and-ref-escaping-over-live-mc (60)
91
+ on disk : ...-live-mc.mmd
92
+ ```
93
+
94
+ `actionOpen` 先 `slugify(goal)` 拿到最多 `SLUG_MAX`(60)字符,再拼 `${prefix}-`,
95
+ 总长可达 64。`saveCanvas` → `canvasPath` 在落盘路上又 slugify 一次,砍回 60。
96
+ **前缀那 4 个字符的预算从来没被算进去。**
97
+
98
+ `canvasPath` 里那处「slug 与原名不同就拒绝」的检查本该拦住它,但它只在名字
99
+ 「像路径」(含 `/`、`\` 或 `..`)时抛错 —— 截断不像路径,于是静默放过。
100
+
101
+ 影响是**报错的句柄,不是损坏的数据**:前缀仍让每个画布互不相同,
102
+ 两个前 60 字符相同的长 goal 得到 `000-`/`001-` 两个独立文件,各自节点都在;
103
+ `update`/`delete` 传 `open` 报的长名字也照样能用,因为 `canvasPath` 的截断是幂等的。
104
+ 坏的只有一件事:调用方拿到的名字,`list` 永远不会显示,磁盘上也不存在。
105
+
106
+ 修法是把名字组装收进 `canvas-format.ts` 的新函数 `canvasName(prefix, goal)` ——
107
+ 让它紧挨着自己必须遵守的那个上限,而不是让 handler 去做预算算术:
108
+
109
+ ```ts
110
+ export function canvasName(prefix: string, goal: string): string {
111
+ const head = `${prefix}-`;
112
+ const slug = slugify(goal, SLUG_MAX - head.length);
113
+ return slug ? `${head}${slug}` : `${head}task`;
114
+ }
115
+ ```
116
+
117
+ `slugify` 因此多一个可选的 `maxLength`,**向内钳制到 `SLUG_MAX`** ——
118
+ 调用方能收紧上限,不能放宽它,否则「每个画布文件名都装得进 `SLUG_MAX`」
119
+ 这个不变量就成了各调用点的自选项。截断后仍走一次尾部连字符清理,
120
+ 所以 `000-abc-.mmd` 这种名字不会出现。
121
+
122
+ ### 🧪 测试
123
+
124
+ canvas-format 单测 41 → 50,canvas e2e 34 → 40 项。
125
+
126
+ 核心断言是**不动点**而不是长度:`slugify(canvasName(p, g)) === canvasName(p, g)`,
127
+ 跨 3 个前缀 × 6 种 goal(超长、纯 CJK、尾部连字符诱饵、前 60 字符相同的两个变体)。
128
+ 长度只是症状 —— `canvasPath` 会再 slugify 一次,任何不是不动点的名字都是
129
+ 「告诉了调用方、但文件系统从没见过」的名字。
130
+
131
+ e2e 补的 5 项跨 open/list/show/delete 与磁盘:`open` 报的名字必须等于
132
+ `list` 显示的名字、等于磁盘上存在的文件。反向验证还原实现后 3 项变红
133
+ (长度 64、磁盘上无此名、`list` 显示另一个名字)。
134
+
135
+ 另两项(`reported name still routes to its canvas` / `... to delete`)
136
+ 在修复前后都通过 —— `canvasPath` 截断幂等使然。**保留但改名如实**:
137
+ 它们守的是「如果有人改用『让 `canvasPath` 对有损名字抛错』来修这个缺陷,
138
+ 这条回归会被抓到」,不是这次的不变量。
139
+
140
+ 门禁:vitest 520 passed、tsc、canvas e2e 40 项、pack 契约 104 文件、
141
+ schema budget +0.8%、semantic 快照 17 工具、protocol legacy + v2、
142
+ lite smoke、host-env smoke、spread smoke。
143
+
144
+ ---
145
+
146
+ ## [3.1.6] - 2026-08-26
147
+
148
+ 仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
149
+
150
+ ### 🔤 `memos_canvas` 的 ref 里,引号读不回来
151
+
152
+ `ref` 走 JSON 转义而非标签转义,因为 Windows 路径的反斜杠必须逐字返回,
153
+ 否则锚点失效。但 `renderRef` 在 `JSON.stringify` 之后又替换了一次裸引号:
154
+
155
+ ```js
156
+ JSON.stringify(ref).slice(1, -1).replace(/"/g, "\\u0022") // 旧
157
+ ```
158
+
159
+ `JSON.stringify` 已经把 `"` 写成 `\"`。那个 `replace` 只匹配裸引号,
160
+ **JSON 留下的反斜杠还在前面**,于是落盘成 `\\u0022`;`parseRef` 走 `JSON.parse`,
161
+ `\\` 解成一个字面反斜杠,取回 `"` 而不是 `"`。少匹配了一个反斜杠。
162
+
163
+ 现在匹配 `\"` 整体:`.replace(/\\"/g, "\\u0022")`。引号仍须离开标签 ——
164
+ `"` 会提前终止它所在的 Mermaid `["..."]` —— 但这次是**替换**已转义的序列,
165
+ 不是在它上面再叠一层。
166
+
167
+ 反斜杠一侧一直是对的:`\\` 就是 JSON 在磁盘上的正确表示,`parseRef` 会解回单反斜杠。
168
+ 只有引号这一条路径坏了。
169
+
170
+ ### 🗑️ `memos_canvas` 新增 `delete` 动作
171
+
172
+ 此前只有 open/update/show/list,清理必须手动删 `.mmd`。而画布的定位是**短期**状态,
173
+ 注定会产生废弃文件 —— 加之全局规则禁止手工改动记忆文件,等于没有合法的清理路径。
174
+
175
+ `action="delete"` 在有未完成节点(doing/todo/blocked)时**拒绝**并给出逃生口,
176
+ 全 done 或空画布直接删:
177
+
178
+ ```
179
+ ❌ Canvas '000-x' still has 1 unfinished node (0 doing · 0 todo · 1 blocked)
180
+ Review it first: memos_canvas(action="show", name="000-x")
181
+ Delete anyway: memos_canvas(action="delete", name="000-x", confirm=true)
182
+ ```
183
+
184
+ **没有套用 `MEMOS_ENABLE_DELETE`**。那道门是保护长期记忆的:走 API、有 embedding 成本、
185
+ 且 Lite 模式下整个禁用。画布是本地文件、不走 API,而 Lite 模式下它恰恰是少数可用功能之一 ——
186
+ 套上那道门等于在最需要清理的模式里砍掉清理能力。`confirm` 这种「拒绝并给出下一步」
187
+ 与 `MAX_NODES`、非法 status 是同一个既有模式。
188
+
189
+ 删除是硬删,不做回收站:画布存在的意义就是任务收尾后被丢弃,
190
+ 回收站只会攒下它本该清掉的文件。`deleteCanvas` 复用 `canvasPath`,遍历防护自动继承。
191
+
192
+ ### 🔢 前缀水位线:`delete` 破坏了「前缀永不复用」
193
+
194
+ 加上 delete 之后有两个 e2e 断言失败。**不是断言写错** —— `nextPrefix` 用现存文件算
195
+ `max+1`,在只增不减的世界里「前缀永不复用」自动成立,delete 打破了这个不变量:
196
+ 删掉最高前缀的画布后,那个前缀会被重新发放。
197
+
198
+ 后果正是 `nextPrefix` 自己注释里警告过的:前缀嵌在它铸出的每个节点 id 里,
199
+ commit message 或记忆正文引用的 `001-N3` 会变得指向两个不同画布的节点。
200
+
201
+ 修法是 `{cube}/canvas/.prefix-hwm` 水位线文件,`saveCanvas` 与 `deleteCanvas` 都抬升它,
202
+ 只增不减。delete 侧必须抬升:文件是该前缀唯一的记录,删掉后若水位线仍低于它,
203
+ 就会被重新发放。旧 cube 没有这个文件时(`readPrefixHwm` 返回 -1),
204
+ 从现存文件恢复下限,所以升级不会因为水位线缺失而复用前缀。
205
+
206
+ 水位线损坏或不可读时退回扫描文件,与 `parseCanvas` 一致 ——
207
+ 画布是恢复态,读取时机正是刚丢失上下文的那一刻,抛异常是最坏的选择。
208
+
209
+ ### 🧪 测试
210
+
211
+ canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e 18 → 34 项。
212
+ 两个修复都做了反向验证,还原实现后对应用例确实变红:转义 3 项、水位线 1 项(含 delete 侧那次抬升)。
213
+
214
+ e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实升级路径 ——
215
+ 它跨 store 与 handler,单测碰不到。
216
+
217
+ 顺带改掉一个自己写坏的用例:名字叫 "does not free the prefix it used",
218
+ 断言却在验证前缀被释放了。名字与断言矛盾的测试比没有测试更坏。
219
+
220
+ `memos_canvas` 的 schema 改了(action enum 加 `delete`,新增 `confirm`),
221
+ 两处冻结的哈希同步重算:`schema-semantic-baseline.json` 与
222
+ `protocol-contract.test.ts` 的 `EXPECTED_SCHEMA_HASHES`。
223
+
224
+ 门禁:vitest 511 passed、tsc、canvas e2e 34 项、pack 契约 104 文件、
225
+ schema budget +0.8%(限 5%)、semantic 快照 17 工具、protocol v2、lite smoke。
226
+
227
+ ---
228
+
229
+ ## [3.1.5] - 2026-08-26
230
+
231
+ 3.1.1~3.1.4 都是「仅 MCP server」的版本,Python 包与容器镜像停在 3.1.0。
232
+ **3.1.5 两侧同时有改动**,三个版本串(npm 包、`pyproject.toml`、`src/oh_memos/__init__.py`)
233
+ 因此重新对齐到 3.1.5。Python/容器侧的内容见仓库根的 `docs/CHANGELOG.md`。
234
+
235
+ ### 🧾 `memos_get` 的 not-found 文案指向真正的成因
236
+
237
+ 旧文案让用户「核对 id 是否正确(从 `memos_search` 结果复制)」,但实测中最常见的成因恰恰是
238
+ **从搜索结果复制来的 id 过一会儿就失效**:`POST /search` 返回的是 `WorkingMemory` 层的副本 id,
239
+ 该层按 `user_name` 只保留 20 条(后端 `manager.py` 的 `memory_size["WorkingMemory"]`),
240
+ 每次检索都重写并淘汰最旧的。实测 `oh_memos_cube` 的那 20 条只覆盖 22 分钟。
241
+
242
+ 因此新文案把 `memos_list_v2` 指出来是有实质区别的:它返回 `LongTermMemory` 的 id,
243
+ 那一层不淘汰(实测同一批 id 隔天仍可取回)。措辞上不把淘汰断言成事实 ——
244
+ Lite 模式(本地 JSONL)没有 `WorkingMemory` 层,那里的 not-found 就是单纯的 id 不存在。
245
+
246
+ `notFoundText` 同时改为导出,便于测试直接断言文案而不经由 handler。
247
+
248
+ ### 🌐 API 客户端保留 400 响应体
249
+
250
+ `apiCallWithRetry` 此前把非 2xx 一律丢弃响应体,于是后端在 400 里给出的
251
+ 校验错误说明拿不到,工具层只能报一个无信息的失败。现在 400 单独放行并解析 JSON,
252
+ 后端的错误描述可以原样呈现给调用方。
253
+
254
+ ### 🔒 分层过滤的强制机制
255
+
256
+ 同一个缺陷已复发 7 次:新增一条检索路径,忘了把 `WorkingMemory` 短期副本滤掉,
257
+ 于是把一个几分钟后就失效的 id 交给了 agent。每次都靠 review 抓到,这次改成结构性拦截。
258
+
259
+ **为什么普通单测拦不住**:实测把所有层级过滤切断后,319 项 vitest 依然全绿。
260
+ 单测问的是"过滤函数算得对不对",而缺陷形态是"过滤函数没被调用" —— 两个问题不同。
261
+
262
+ 新增 `src/memory-tier-boundary.ts`(清单)与同名 `.test.ts`(守卫,18 项)。
263
+ 清单把 `handlers/index.ts` 的 28 条分派路由逐条分类并附实测证据,守卫扫描源码与分派表做双向比对。
264
+ 四条 fail-closed 棘轮,均以变异测试验证过确实会红:
265
+
266
+ - 分派表新增路由却没在清单里分类 → 失败;清单有而分派表已无(陈旧条目)→ 也失败
267
+ - 静默新增已知缺口 → 失败(`KNOWN_GAPS` 为冻结集合,计数上限硬编码)
268
+ - 修好缺口却忘了从 `KNOWN_GAPS` 移除 → 失败,否则集合会膨胀成一张无人清理的豁免名单
269
+ - `CYPHER_PROJECTION_BASELINE` 按严格相等断言,修好了不调基线同样失败 —— 棘轮只能往紧的方向走
270
+
271
+ **覆盖了第二种复发形态**:过滤接上了,但构造 metadata 时漏掉 `memory_type`,
272
+ 判定函数恒读到 `undefined` 并判为可见。`search.ts` 曾因此让四个调用点的过滤同时空转,
273
+ 而没有任何测试失败。守卫现在会检查投影记忆的 Cypher `RETURN` 是否带出该字段。
274
+
275
+ **已记录但尚未修的缺口**(`memos_think`、`memos_graph` 的三条路由):
276
+ `think.ts` 上朴素过滤会把语义召回从 15 条砍到 5 条,正确解法是按 `metadata.key`
277
+ 换成持久层孪生节点的 id,而不是加过滤。不变式的准确表述是"永不把易失 id 交给 agent",
278
+ "过滤掉 `WorkingMemory`" 只是其中一种(有损的)满足方式。
279
+
280
+ 门禁:vitest 491 通过(新增 18),`tsc --noEmit` 无输出。
281
+
282
+ ## [3.1.4] - 2026-08-24
283
+
284
+ ### 🐛 修复:同源节点列表混入 scheduler 短期副本
285
+
286
+ 3.1.3 上线后实测:同源列表里每个 `key` **成对出现** —— 一条被切成 6 段的记忆列出了 12 条同源。
287
+
288
+ 后端对每条抽取结果写两个节点:一个 `WorkingMemory` 短期副本 + 一个 `LongTermMemory`
289
+ (少数为 `UserMemory`)持久节点,`key` 与 `created_at` 逐字相同。`findSiblings` 没有滤层级,
290
+ 于是短期副本一并列出。
291
+
292
+ **这不是新决策,是 3.1.0 已经做过的决定**(`memory-tier.ts`)。`memos_search` 与
293
+ `memos_list_v2` 早已在滤,`findSiblings` 是 3.1.2 新增的路径 —— 又漏了一处。
294
+ 同一形态在本项目已出现五次:**新写的检索路径必须继承既有的分层过滤决定。**
295
+
296
+ 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 时不滤,与 `filterEphemeralTier` 语义一致。
297
+ 缺 `memory_type` 视为可见 —— Lite 的 JSONL 不写该字段。
298
+
299
+ #### 测试
300
+
301
+ 新增 5 项分层断言(共 48 项),三个变异全部被捕获:不滤层级(回到本次缺陷)、
302
+ 忽略逃生开关、忽略 limit。其中「limit 在滤层级之后生效」用交错排列的候选集断言 ——
303
+ 顺序反了会让 limit 被随即隐藏的副本吃掉,只返回一半。
304
+
305
+ 门禁:vitest 447 passed、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
306
+ protocol v2、lite smoke、host-env smoke。
307
+
308
+ ## [3.1.3] - 2026-08-24
309
+
310
+ ### 🐛 修复:同源碎片区块从不出现 —— `sources` 有两种线上形态
311
+
312
+ 3.1.2 上线后实测:原文能正确返回,但「同源碎片」区块**从不出现**。根因是同一个字段
313
+ 在两个端点上形态不同:
314
+
315
+ | 端点 | `sources[0]` 的形态 |
316
+ |---|---|
317
+ | `GET /memories/{cube}/{id}`(单条) | **对象** `{type, role, chat_time, content}` |
318
+ | `GET /memories`(列表) | **JSON 字符串** `'{"type":...,"content":"..."}'` |
319
+
320
+ `verbatimOf` 初版只处理对象形态。单条取回走对象 → 原文正常;同源查找的候选集来自
321
+ 列表端点 → 全部返回 null → 永远匹配不到。**原文层可用,配对层静默失效。**
322
+
323
+ 单测没抓住的原因很直接:fixture 只造了对象形态,两种形态里只测了一种。
324
+
325
+ 修法:抽出 `contentOf(entry)` 同时接受对象与 JSON 字符串。非 JSON 字符串返回 null
326
+ 而**不猜它是裸原文** —— 否则任意字符串都会被当原文,把无关记忆归成同源。
327
+ 坏 JSON 按无 content 处理不抛异常:这是展示层,脏数据不该让 `memos_get` 失败。
328
+
329
+ #### 测试
330
+
331
+ 新增 6 项断言与一个 list 形态 fixture(共 43 项)。三个变异全部被捕获,
332
+ 其中「不处理字符串形态」即回到本次修复前的行为,另两个覆盖「非 JSON 当裸原文」
333
+ 与「坏 JSON 抛异常」。跨形态指纹一致性单独断言 —— 否则单条与列表永远配不上。
334
+
335
+ 门禁:vitest 442 passed、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
336
+ protocol v2、lite smoke、host-env smoke。
337
+
338
+ ## [3.1.2] - 2026-08-23
339
+
340
+ ### 📄 `memos_get` 返回原文,不再只返回 LLM 概括
341
+
342
+ 开启 LLM 抽取(后端 `MOS_TYPED_SAVE_FAST=false`)时,后端把一次写入拆成多条细粒度记忆:
343
+ 每条的 `memory` 字段是 LLM 概括,而**逐字原文完整保留在 `metadata.sources[0].content`**。
344
+ 实测一条 1056 字的写入被拆成 5 条,每条 `memory` 约 244 字,5 条共享同一份原文。
345
+ `memos_get` 此前只读 `memory`,于是完整上下文取不回来。
346
+
347
+ 这不是取舍问题 —— 库里本来就是两份数据,各有各的用途:
348
+
349
+ | 层 | 内容 | 用途 |
350
+ |---|---|---|
351
+ | `memory` | LLM 概括(细粒度) | 向量化输入、建边对象 → 决定图谱节点与联想质量 |
352
+ | `metadata.sources[].content` | 逐字原文 | `memos_get` 的完整上下文来源 |
353
+
354
+ **新输出结构**(有原文时):原文作为 `### Content` 正文;LLM 概括降级到
355
+ `### 抽取概括` 区块并保留 —— 它是向量化与建边的实际输入,看得见才能判断图谱节点切得对不对;
356
+ `### 同源碎片` 列出由同一次写入抽出的其他记忆(id + key),从而能看出 LLM 的切分方式。
357
+ 无原文时(fast-path 写入)输出与此前完全一致。
358
+
359
+ 同源判定用原文全文,不用 `session_id` —— 同一会话里的无关写入会被错误归组。
360
+
361
+ 新增 `src/verbatim-source.ts`:`verbatimOf` / `sourceFingerprint` / `findSiblings` /
362
+ `renderVerbatimSections`,四个纯函数,handler 与测试调用同一份实现。
363
+ Full 与 Lite 两条路径都已接线(Lite 的 JSONL 无同源概念,列表恒空)。
364
+ 同源查询失败时返回空列表,不影响原文返回。
365
+
366
+ #### 测试
367
+
368
+ 新增 `src/verbatim-source.test.ts`(37 项)。**变异验证抓出三个断言缺乏判别力**,
369
+ 每一个都是「测试写了但等于没写」的形态:
370
+
371
+ - 接线守卫数两条路径**合计**调用次数 `>= 2`,而 Lite 路径自身就有 2 次,
372
+ 砍掉 Full 路径那次仍然通过。改为按路径分别断言。
373
+ - 「原文不截断」用 30 字测试原文,而变异截断到 200 字 —— 改不动它。
374
+ 测试原文改为 600+ 字并加尾部标记。**这条正是本次要修的缺陷,却最后才抓住。**
375
+ - `"not-an-array"[0]` 是字符 `"n"`、无 `.content`,宽松实现也返回 null。
376
+ 改用伪装成数组的对象才有判别力。
377
+
378
+ 顺带删除 `sourceFingerprint` 的长度前缀:变异验证时构造不出它能防住的碰撞,
379
+ 无测试支撑的防御代码比没有更糟。
380
+
381
+ 12 个变异被捕获(W1「Full 路径不读原文」即回到本次修复前的行为)。
382
+ 门禁:vitest 436/436、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
383
+ protocol v2、lite smoke。
384
+
385
+ ## [3.1.1] - 2026-08-22
386
+
387
+ ### 🐛 修复:`memos_context_resume` 每条记忆成对出现
388
+
389
+ 3.1.0 装好后 `memos_search` 与 `memos_list_v2` 都已正确隐藏 `WorkingMemory` 短期副本,
390
+ 但 `memos_context_resume` 仍成对显示(10 条 = 5 对,内容逐字相同、UUID 不同)。
391
+
392
+ **根因不止是漏接一条路径。** temporal 查询的 Cypher 不返回 `memory_type`,
393
+ 构造出的 metadata 只有 `relativity`/`temporal_rank`/`source` —— 于是分层判定永远
394
+ 读到 `undefined` 并判为可见,**下游任何过滤对 temporal 记忆都是空转**。
395
+ 受影响的是四个调用点,不只是 `memos_context_resume`:
396
+
397
+ | 路径 | 3.1.0 的状态 |
398
+ |---|---|
399
+ | `memos_context_resume` | 成对显示(用户实测发现) |
400
+ | `memos_search` temporal intent(两处) | 有过滤代码,但读不到字段 |
401
+ | `memos_think` | 同上 |
402
+
403
+ 后三处一直漏着且不易察觉 —— 过滤代码存在且看起来合理,只是作用对象缺字段。
404
+
405
+ 修法:在 Cypher 里排除 `WorkingMemory` 并返回 `memory_type`。这是唯一对四处都成立的做法,
406
+ 且 `LIMIT` 在过滤之后 —— 要 N 条就得 N 条真记忆,无需超额取数。
407
+ `memos_context_resume` 的 API 回退路径由服务端施加 limit,无法先过滤,故超额取 3 倍。
408
+
409
+ 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 仍然有效:为真时不加排除条件,
410
+ 但仍返回 `memory_type` —— 开关只关过滤,不关可观测性。
411
+
412
+ #### 测试
413
+
414
+ 新增 `src/context-resume.test.ts`(22 项),**9 个变异全部被捕获**,
415
+ 其中 W1「API 路径不过滤」即回到本次报告的原缺陷。
416
+
417
+ 新增 `npm run test:host-env-smoke`:用 MCP host 配置里的**原样环境**驱动 server,
418
+ 且不继承外层环境变量。已有的 spread smoke 自带硬编码 Neo4j 凭据兜底,
419
+ 因此「host 读不到凭据」这一失败形态它测不出来 —— 实测该形态下检索照常返回记忆、
420
+ 但联想标注为 0,属静默降级。
421
+
422
+ 门禁:vitest 416/416、tsc、pack 契约、schema budget +0.0%、semantic 快照 17 工具、
423
+ protocol v2、lite smoke、spread smoke、host-env smoke。
424
+
425
+ ## [3.1.0] - 2026-08-22
426
+
427
+ ### 🧠 检索排序:衰减、强化、分档去重与图扩散联想
428
+
429
+ 本版把检索侧的排序从「纯相似度」改为「相似度 + 时间衰减 + 访问强化」,并加入按类型分档的
430
+ 近重复折叠与一跳图扩散联想。
431
+
432
+ #### 记忆层级:`WorkingMemory` 默认隐藏
433
+
434
+ 后端 scheduler 会为每条持久记忆写一份 `WorkingMemory` 短期副本,该层按用户仅保留最近 20 条、
435
+ 每次检索都会重写淘汰。把这些 id 交给 agent 意味着它拿到的引用几分钟后就失效。
436
+ 新增 `src/memory-tier.ts`,`memos_search` 与 `memos_list_v2` 默认滤掉该层;
437
+ 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 可关闭过滤。缺 `memory_type` 视为可见
438
+ (Lite 的 JSONL 不写该字段)。
439
+
440
+ #### 衰减与强化
441
+
442
+ 排序分数加入按 `created_at` 的时间衰减与按访问次数的强化项,两者都可用环境变量关掉,
443
+ 默认开启。访问计数由 `src/access-tracker.ts` 本地维护,不写回后端。
444
+
445
+ #### 近重复折叠(按类型分档)
446
+
447
+ 同类型内容高度重合的记忆折叠为一条并标注折叠数。分档按 `memory_type` ——
448
+ 不同类型的相似文本(一条 `DECISION` 与一条 `BUGFIX`)不应互相折叠。
449
+
450
+ #### 一跳图扩散联想(`MEMOS_SPREAD_ACTIVATION`,默认关闭)
451
+
452
+ 命中记忆沿知识图谱做一跳扩散,把强关联但关键词不匹配的记忆带进结果并标注来源。
453
+ 默认关闭,因为它需要可用的 Neo4j 凭据;读不到凭据时静默返回 0 条联想。
454
+
455
+ #### 检索期注解
456
+
457
+ 结果里标注衰减后分数、访问次数与折叠数,使排序结果可解释而非黑箱。
458
+
459
+ #### graph schema 四处缺陷修复
460
+
461
+ `memos_graph` 的 schema 模式修正四处:节点计数口径、边类型去重、孤立节点统计与采样上限。
462
+
463
+ ## [3.0.1] - 2026-08-19
464
+
465
+ ### 🩹 修复 3.0.0 打包出的多余依赖与失效仓库链接
466
+
467
+ 3.0.0 的 npm 包声明了两个与运行时无关的依赖,并且 npm 页面上的仓库链接全部指向一个不存在的仓库。
468
+ 功能没有受影响,但每个用户都要多下载约 17 MB,且 issue/homepage 链接是死链。
469
+
470
+ #### 修复
471
+
472
+ - **移除 `ci@^2.3.0` 与 `npm@^11.19.0`**。两者都不被 `dist/` 里的任何代码 import,属纯安装期膨胀:
473
+ `npm` 是完整的 npm CLI(16 MB),`ci` 是无关的第三方工具(17 KB)。安装体积从约 30 MB 回到约 13 MB。
474
+ 根因是发布前工作区里未提交的依赖漂移被打进了 tarball;仓库提交本身一直只有三个依赖。
475
+ - **`repository` / `bugs` / `homepage` 从 `github.com/xigou/oh-memos` 改为 `github.com/lsg1103275794/oh-memos`**。
476
+ 前者返回 404,导致 npm 页面上的仓库、issue 和主页链接全部失效。`mcp-server-node/README.md`
477
+ 里的 clone 命令与仓库链接同步修正。
478
+
479
+ #### 不变
480
+
481
+ 运行时行为、工具名、输入 schema、协议协商和持久化格式与 3.0.0 完全一致,仅包元数据与依赖清单变化。
482
+ 从 3.0.0 升级只需重新安装,无需任何迁移。
483
+
484
+ ## [3.0.0] - 2026-08-19
485
+
486
+ ### Breaking
487
+
488
+ - **Node.js 20 is now the minimum runtime.** Node 18 users must stay on
489
+ `oh-memos-mcp@2` (`npx -y oh-memos-mcp@2`) until they can upgrade their
490
+ runtime. The entry point exits with an actionable message on Node 18 instead
491
+ of failing with an opaque module error.
492
+ - The MCP runtime now uses the role-split `@modelcontextprotocol/server@2` and
493
+ Zod 4 instead of the monolithic v1 SDK and Zod 3.
494
+
495
+ ### Added
496
+
497
+ - **Dual-era stdio serving.** The same package accepts legacy 2025-era
498
+ initialize clients and clients pinned to MCP `2026-07-28` through
499
+ `serveStdio(..., { legacy: "serve" })`.
500
+ - Stringified `tools/call` arguments are normalized before era classification
501
+ and schema validation, preserving the existing client compatibility path.
502
+ - Protocol, lifecycle, semantic-schema and package-boundary release gates now
503
+ cover legacy/auto/modern clients, Full/Lite providers, 16/17 tools, large
504
+ requests, probe fallback and graceful process shutdown.
505
+
506
+ ### Migration
507
+
508
+ - Install with `npx -y oh-memos-mcp` (npm `latest`) or pin `oh-memos-mcp@3.0.0`.
509
+ - No memory, cube or Lite JSONL migration is required. Existing Full and Lite
510
+ data formats, tool names and input schemas remain unchanged, so Agent
511
+ configurations only need the package reference updated.
512
+ - Clients that still negotiate the 2025-era wire keep working unchanged; no
513
+ client-side protocol migration is required to adopt 3.0.
514
+
515
+ ### Rollback
516
+
517
+ - Pin Agent configurations to `npx -y oh-memos-mcp@2.1.0`. No data rollback is
518
+ necessary because this release does not change persisted formats.
519
+ - `2.x` remains installable from npm; this release does not unpublish or
520
+ deprecate it.
521
+
522
+ ## [2.1.0] - 2026-08-16
523
+
524
+ ### Fixed
525
+
526
+ - **Auto-discovered `.env` no longer overrides launcher environment.** A `.env`
527
+ found in the working directory or package root now only fills missing values,
528
+ so MCP client `env` blocks and shell exports such as `MEMOS_PROVIDER=local`
529
+ stay in effect. A file selected explicitly through `MEMOS_ENV_FILE` or
530
+ `--memos-env-file` remains authoritative.
531
+
532
+ ### Added
533
+
534
+ - **Local Lite provider.** `MEMOS_PROVIDER=local` (implied by `MEMOS_MODE=lite`)
535
+ serves `memos_save`, `memos_get`, `memos_list_v2`, `memos_search`, and
536
+ `memos_context_resume` from a per-cube `memories.jsonl` with deterministic
537
+ lexical ranking, durable appends, and a cross-process lock. Graph, Think,
538
+ Wiki, and admin tools report `LOCAL_PROVIDER_UNSUPPORTED` rather than
539
+ pretending a graph backend exists.
540
+
541
+ - **Skill candidate lifecycle.** `memos_review_skill_candidate` records
542
+ approve/reject with reviewer audit metadata, and
543
+ `memos_install_skill_candidate` installs only approved candidates into
544
+ `.claude/skills/<slug>/SKILL.md` without overwriting, following symlinks, or
545
+ executing anything.
546
+
547
+ - **Explainable graph provenance.** `memos_graph(mode="related"|"path"|"impact")`
548
+ now reports normalized evidence categories (`EXTRACTED`, `INFERRED`,
549
+ `AMBIGUOUS`, or `UNKNOWN`) and includes confidence, evidence references,
550
+ source file/location, extractor version and verification time when those
551
+ fields exist. Legacy graph data without provenance remains readable and is
552
+ reported as `UNKNOWN` rather than assigned invented evidence.
553
+
554
+ - **`memos_graph(mode="import")` Graphify boundary.** Accepts up to 5 MB of
555
+ Graphify/NetworkX node-link JSON through `graph_json`, validates `nodes` plus
556
+ `links` (or the `edges` alias), and produces a deterministic import plan with
557
+ portable stable Code Graph ids. Duplicate ids, dangling edges, unsafe source
558
+ paths, invalid confidence values and oversized graphs are rejected before
559
+ any persistence boundary. This mode is intentionally **dry-run only**: it
560
+ never writes to Neo4j, Qdrant or a memory cube.
561
+
562
+ - **`memos_canvas`** — a symbolic task canvas: short-term task state that survives
563
+ context compaction. One Mermaid file per task under
564
+ `{MEMOS_CUBES_DIR}/{cube_id}/canvas/`, whose nodes carry a greppable id
565
+ (`000-N1`) and an optional `ref` anchoring them to evidence:
566
+ `mem:<memory_id>` (a memory in the graph), `file:<path>` (any file, including
567
+ the large tool results the harness already offloads), or `note:<text>`.
568
+
569
+ `action`: `open` (needs `goal`) · `update` (appends a node; `node_id` edits one
570
+ in place) · `show` · `list`.
571
+
572
+ Canvases are deliberately **not** written to Neo4j or Qdrant. A canvas changes
573
+ several times an hour, and paying an embedding round trip for a `doing→done`
574
+ flip would be absurd. Durable facts still go through `memos_save`; the canvas
575
+ points at them with a `mem:` ref.
576
+
577
+ This is **not** a token-saving feature. The design it borrows from
578
+ ([TencentDB-Agent-Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory))
579
+ achieves its reduction by intercepting and replacing tool output before the
580
+ model sees it, which a Claude Code hook cannot do — `PreToolUse` can rewrite a
581
+ tool's *input* via `updatedInput`, but no hook can rewrite its *output*. What
582
+ this offers instead is task state that survives compaction, and a path from a
583
+ summary back to the evidence behind it.
584
+
585
+ Node ids and canvas prefixes are allocated as max+1 and **never reuse a gap**:
586
+ a deleted `000-N2` may still be cited from a commit message, a memory body or
587
+ another node's ref, and handing that id to a new node would silently repoint
588
+ every one of those citations.
589
+
590
+ - `memos_context_resume` now lists **unfinished** canvases first — headlines and
591
+ counts only, a few dozen tokens — so the first thing visible after a compaction
592
+ is the open work rather than a memory feed. Injecting canvas bodies here would
593
+ rebuild the very context bloat the canvas exists to avoid; the model opens what
594
+ it needs with `show`.
595
+
596
+ - **Test infrastructure.** This package previously had none: no test script, no
597
+ `*.test.ts`. Adds vitest and `npm test`, with 65 unit tests covering the
598
+ parse/render round trip, node-id allocation, Mermaid label injection, path
599
+ traversal refusal and atomic writes. `scripts/canvas-e2e.mjs` adds 18 checks
600
+ driven over real MCP stdio rather than mocks.
601
+
602
+ - `MEMOS_ENV_FILE` env var and `--memos-env-file` flag, to point the server at an
603
+ explicit `.env`. Previously the file was located only by guessing from position
604
+ (cwd, two levels above the package, dotenv's upward search) — which works from
605
+ a checkout and never works under `npx`, where the package root sits in the npm
606
+ cache and every candidate misses, loading not one variable. A path that does
607
+ not exist now warns on stderr rather than falling through silently.
608
+
609
+ ### Fixed
610
+
611
+ - `toLocalPath()` rewrote Windows cube paths to `/mnt/...` unconditionally. That
612
+ mapping only resolves inside WSL; under native Windows Node the result is not
613
+ absolute, so it resolved against the current drive and every cube write landed
614
+ in a phantom tree while the API kept reading the real path. Registration
615
+ reported success, then `/search` and `/memories` failed 400 — the loaded cube
616
+ had no memory backend.
617
+
618
+ ### Security
619
+
620
+ - `canvasPath` is the one place caller text becomes a filesystem path. It uses a
621
+ **whitelist** (`[a-z0-9-]`) rather than a blacklist of dangerous characters,
622
+ because a blacklist is always missing an entry. `.` is outside the whitelist,
623
+ so `..` cannot survive it — traversal is structurally impossible rather than
624
+ merely checked for — and a post-resolve containment check backs that up
625
+ independently. `cube_id` is treated as untrusted too, since it is derived from
626
+ a caller-supplied `project_path`.
627
+
628
+ ### Documentation
629
+
630
+ - The `alwaysAllow` example carried two defects. `memos_search` appeared twice,
631
+ and several entries were **call forms** — `memos_admin(action=list_cubes)` and
632
+ the like. `alwaysAllow` matches tool names, so those entries matched nothing:
633
+ a reader would believe those calls were pre-approved and still be prompted for
634
+ every one. Replaced with bare tool names, which auto-approve every action of
635
+ the tool.
636
+ - `memos_delete` was dropped from that example, which also set
637
+ `MEMOS_ENABLE_DELETE: "true"` — together they auto-approve deleting memories
638
+ with no prompt. Enabling that is a decision worth making deliberately.
639
+ - The `.env` note claimed the working directory's file loads "automatically with
640
+ highest priority". Under `npx` no file loads at all. Documented `MEMOS_ENV_FILE`
641
+ and what actually happens.
642
+
643
+ ### Notes
644
+
645
+ - Tool surface grew 12488 B → 13680 B (+9.5%), past the +5% drift budget, and the
646
+ baseline was re-frozen. This is the cost of a new tool rather than description
647
+ drift; the description was trimmed from 1333 B to 1192 B first, moving detail
648
+ into `show`'s output (the same move `memos_suggest` made with the memory-type
649
+ decision tree).
650
+ - `tsconfig.json` now excludes `src/**/*.test.ts`. Without it the test files
651
+ compile into `dist` and ship with `npm publish`.
652
+
653
+ ---
654
+
655
+ ## [2.0.1] — 2026-08-02
656
+
657
+ ### Fixed
658
+
659
+ - MCP `serverInfo` was hardcoded in `server.ts` and reported
660
+ `{name: "memos-memory", version: "1.0.1"}` — a name matching neither the
661
+ package (`oh-memos-mcp`) nor the conventional server key (`oh-memos`), and a
662
+ version that 2.0.0 shipped stale. Clients display this during the handshake.
663
+ It is now read from `package.json` at startup, so it cannot drift again.
664
+
665
+ ---
666
+
667
+ ## [2.0.0] — 2026-08-02
668
+
669
+ ### ⚠️ Breaking — tool surface consolidated from 18 to 10
670
+
671
+ Eleven tools were merged into three dispatching tools. **There is no compatibility
672
+ shim**: the MCP SDK only dispatches registered tools, so calling a removed name now
673
+ returns `Unknown tool`.
674
+
675
+ If your MCP client config pins tool names (e.g. Claude Code's `alwaysAllow` list),
676
+ you must update it before upgrading.
677
+
678
+ #### Migration
679
+
680
+ | 1.x tool | 2.0 replacement |
681
+ |----------|-----------------|
682
+ | `memos_search_context` | `memos_search` — pass the `context` array (recent turns) |
683
+ | `memos_get_graph` | `memos_graph(mode="related")` |
684
+ | `memos_trace_path` | `memos_graph(mode="path")` |
685
+ | `memos_impact` | `memos_graph(mode="impact")` |
686
+ | `memos_export_schema` | `memos_graph(mode="schema")` |
687
+ | `memos_list_cubes` | `memos_admin(action="list_cubes")` |
688
+ | `memos_register_cube` | `memos_admin(action="register_cube")` |
689
+ | `memos_create_user` | `memos_admin(action="create_user")` |
690
+ | `memos_validate_cubes` | `memos_admin(action="validate_cubes")` |
691
+ | `memos_get_stats` | `memos_admin(action="stats")` |
692
+ | `memos_calendar` | `memos_admin(action="calendar")` |
693
+
694
+ Unchanged: `memos_context_resume`, `memos_search`, `memos_save`, `memos_list_v2`,
695
+ `memos_get`, `memos_suggest`, `memos_delete`.
696
+
697
+ `alwaysAllow` for 2.0:
698
+
699
+ ```json
700
+ "alwaysAllow": [
701
+ "memos_context_resume", "memos_search", "memos_save",
702
+ "memos_list_v2", "memos_get", "memos_suggest",
703
+ "memos_think", "memos_graph", "memos_admin", "memos_export_wiki"
704
+ ]
705
+ ```
706
+
707
+ ### Added
708
+
709
+ - **`memos_think`** — evidence pack for a question. Runs semantic retrieval plus a
710
+ recent-72h temporal pass, deduplicates, and returns numbered evidence with graph
711
+ relationships between items, contradiction/evolution candidates, staleness
712
+ candidates, and gap analysis. Synthesis is deliberately left to the calling model;
713
+ the server emits no prose. Results can be persisted back as `SYNTHESIS`.
714
+ - **`memos_export_wiki`** — export a cube as an interlinked markdown wiki: one page
715
+ per memory (YAML frontmatter, `[[wikilink]]` relations), plus `index.md` and a
716
+ mermaid `graph.md`. Only files carrying the generator marker are ever replaced;
717
+ foreign files in the output directory are preserved. Defaults to
718
+ `<project_path>/docs/memory-wiki`.
719
+ - **`SYNTHESIS`** memory type, for answers synthesized from retrieved evidence.
720
+
721
+ ### Changed
722
+
723
+ - Tool-surface cost dropped ~51%: `tools/list` payload 22.9 KB → 11.2 KB
724
+ (≈5850 → ≈2856 tokens). Shared `project_path` / `cube_id` parameter descriptions
725
+ were deduplicated; the full cube-routing rules are now stated once, on
726
+ `memos_save` and `memos_search`.
727
+ - Runtime error `suggestions` now name the 2.0 call forms, so a model reading a
728
+ failure message is no longer told to call a tool that does not exist.
729
+
730
+ ### Fixed
731
+
732
+ - `memos_admin`'s `cube_id` no longer carries a `MEMOS_DEFAULT_CUBE` default.
733
+ Previously, calling `register_cube` without `cube_id` would silently register the
734
+ default cube instead of failing.
735
+
736
+ ---
737
+
738
+ ## [1.0.0] — 2026-03-04
739
+
740
+ Initial npm release. 18 tools, Node-only MCP server for oh-memos — no Python
741
+ required, runs via `npx`.