diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index 09e109dd..8dc7b321 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- **知识库未启用时的空态文案指错了分组名(#668 尾巴)**。空态一直写「设置 → 记忆(mneme) → 功能 → 知识库」,但 v0.13 起 `group.ext` 是独立分组、标签是「扩展功能」(英文 `Extensions`),「知识库」只是组里的一项——照这句话去设置页找「知识库」分组会找不到。中英两版都改成「功能 → 扩展功能」并点名组内那一项,`test/client.test.js` 的文案锁同步跟上。 + - **跨类型同义双录:蒸馏提示词里「宁可拆成多条」把同一件事记了两遍,而三处去重各守一道类型闸(#687);实体表 75% 的实体只被提过一次、名字只按大小写敏感精确匹配(#688)**。两条 issue 报告的是同一个根病:**记忆的「类型」被当成了「对象身份」**。#687 上报者(xueyue33)给的分布很刺眼——他自己库里 `sim>=0.85` 的 81 对近重复中 **72 对跨类型**、`>=0.90` 的 7 对中 6 对跨类型;`sim>=0.88` 的 24 对里 21 对跨类型。四处夹缝逐个核过都成立:`src/lang.js` 的 `codingSummary` 明确让模型「宁可拆成多条」而 `pitfall`(现象+根因+规避)与 `constraint`(约束+来源)本就是同一结论的两种写法;`src/service.js:1480` 的 `findSessionDuplicate` 用 `store.list({ type: memory.type, source, limit: 200 })` 锁死同类型同会话;`src/dream/clustering.js:117` 的 `findPotentialConflicts` 首行 `if (memories[i].type !== memories[j].type) continue;`;dream 巩固提示词同样写着「仅合并同类型」。**我们不开常开类型闸**:本机活跃库 9,275 条带向量的记忆里跨类型 `cosine>=0.90` 共 45 对,逐对读正文后 **36 对(80%)确为同义,9 对合并会丢信息**(5 对时间序列/状态快照、2 对症状 vs 修复、1 对结论反转、1 对版本号不同)——也就是说「凡是跨类型高相似就合并」每 5 对要吞掉 1 对真互补的信息,而这类对只占活跃总量 0.97%,收益与风险不对称。落地的三层是**源头互斥 + 批内去重 + 存量一次性处置**:① `codingSummary` 提示词(中英双语)补一条互斥约束——同一结论只记一条,`pitfall` 与 `constraint` 不双写,时间序列/状态快照写进同一条;② 蒸馏落盘前做**批内**跨类型去重,只对 `pitfall↔constraint`、`history↔project` 两组生效(正是实测最集中的应用对:9 对 + 8 对),`planBatchCrossTypeDedupe(entries, { minSim })` 是纯函数——按相似度降序、每条最多被退休一次、不许链式藏行(A 取代 B 的同时 B 又取代过 C 会把 C 永久埋在链底);门槛 `BATCH_CROSS_TYPE_MIN_SIM = 0.85`(textSim,不是余弦,词面重合是这两组对的主特征)、`BATCH_CROSS_TYPE_MIN_CHARS = 24`、`BATCH_CROSS_TYPE_MIN_LEN_RATIO = 0.5`;③ 处置走 `store.supersedeChain` 只写 `superseded_by` / `superseded_at`,**保留正文更全的一条、被取代者正文零改写、不归档、不 merge**——判错时旧行仍在库里、显式搜索照旧读得到,且 `supersededIds()` 的 `JOIN ... AND w.archived=0` 让继任者一旦被归档旧行自动回到可见,等于自带回退。刻意**没有**复用 `src/dream/decisions.js` 的 `supersedeInfoFirstPair`:它走 `service.update` 会在正文尾部追加「已被取代」前缀,与「正文零改写」直接冲突。**默认开**(2026-11-13 拍板):这个开关存在的意义就是让新写入不再双录,默认关等于修了不生效,所以 `src/config.js` 给 `true`、设 `false` 是 kill switch,且关时审计行的 `batchDeduped` / `batchPairs` 两个键根本不出现——加开关之前的行为逐字节不变。两个键同时登记进 `settings.js` 的 `FEATURE_FLAG_BOOLEANS` 与 `FEATURE_FLAG_NUMBER_RANGES`,面板上能改。存量 17 对**不由代码处置**:只读脚本 `scripts/cross-type-dedupe-dryrun.mjs` 出清单(输出**禁止落在仓库内**,清单里含记忆原文),复核后由人决定、按纯 SQL 打链。 **#688 的修法只落在写入侧**。上报者库里 12,700+ 实体、38,500+ 关系、孤儿 41%、`mention_count=1` 占 72%;本机 live 库(只读打开)实测实体 **25,474**、关系 **59,715**、`mention_count=1` 占 **75%**、大小写/空白重复 262 组;而按 `entity_relations.memory_id` 真实连接口径,孤儿是 **17.9%(4,560 行)** 而不是 41%——口径不同,这里写清楚。存量**不做合并**:实体 id 是 `randomUUID()`,合并要重指 59,715 行关系且 `entity_relations` 没有唯一约束,重指过程一旦中断会造出重复边,收益(0.68% 的实体)与风险同样不对称。改的是查找键——`findEntityByName` 从 `WHERE name = ?` 改成 `lower(trim(name))` + `type` 的等价比较并为此建表达式索引(`DeepSeek-V3` 与 `DeepSeek-V4` 是两个不同对象,不剥版本号后缀;`createEntity` 落库只去首尾空白、**保留原大小写**用于展示)。类型维度不能省:实测去掉 type 会误并 95 组(`constraint` 与 `pitfall` 常同名)。另外 `entities.canonical_memory_id` 是 v0.7.1(`5b5a2aa4`)引入的预留列,**从没接通线**——`src/store.js` 里只有 DDL、行映射、`INSERT`、`UPDATE` 四处 plumbing,没有任何调用方写它或读它,25,474 行全 NULL;这次按幂等迁移删列(`PRAGMA table_info` 判断 + `ALTER TABLE ... DROP COLUMN`,宿主 SQLite 3.51.3 支持)。迁移成本实测:25,474 行旧 schema 表首次 `createStore` **396ms**(含建表达式索引),二次 24ms。 diff --git a/dsh-mneme/lib/client.js b/dsh-mneme/lib/client.js index 4a8b396f..9e94da75 100644 --- a/dsh-mneme/lib/client.js +++ b/dsh-mneme/lib/client.js @@ -403,7 +403,7 @@ window.__ModuleLoader__.load({ "kb.panel.open": "知识库", "kb.search": "检索", "kb.col.intro": "知识库是 mneme 插件的功能:把外部资料(本地文件 / 网页 / 仓库地志页)切块索引,检索与注入时一起用。", - "kb.empty.disabled": "「知识库」未启用:打开 设置 → 记忆(mneme) → 功能 → 知识库,打开开关后重启 DSH 生效。", + "kb.empty.disabled": "「知识库」未启用:打开 设置 → 记忆(mneme) → 功能 → 扩展功能,勾上「知识库」后重启 DSH 生效。", "kb.empty.unavailable": "知识库后端不可用:宿主里还没装配 kb 模块,重启 DSH 或升级插件后再看。", "kb.empty.collections": "还没有集合。左边先建一个集合(比如「项目文档」),再把本地文件、网页或地志页收进去;入库的正文会被切块索引。", "kb.empty.docs": "这个集合还没有文档:右边选来源类型,填路径或 URL 后点「入库」。", @@ -1583,7 +1583,7 @@ window.__ModuleLoader__.load({ "kb.panel.open": "Knowledge Base", "kb.search": "Search", "kb.col.intro": "The knowledge base is an mneme plugin feature: it chunks and indexes external material (local files / web pages / Repo Atlas pages) so recall and injection can use it.", - "kb.empty.disabled": "The knowledge base is disabled: open Settings → Memory (mneme) → Features → Knowledge base, turn it on, then restart DSH to apply.", + "kb.empty.disabled": "The knowledge base is disabled: open Settings → Memory (mneme) → Features → Extensions, turn on the knowledge base switch, then restart DSH to apply.", "kb.empty.unavailable": "The knowledge base backend is unavailable: the host has not wired the kb module yet — restart DSH or update the plugin and try again.", "kb.empty.collections": "No collections yet. Create one on the left (say \"Project docs\"), then add local files, web pages or atlas pages; ingested text gets chunked and indexed.", "kb.empty.docs": "This collection has no documents yet: pick a source type on the right, fill in a path or URL, then press \"Ingest\".", diff --git a/dsh-mneme/test/client.test.js b/dsh-mneme/test/client.test.js index 1c61fff0..664295c1 100644 --- a/dsh-mneme/test/client.test.js +++ b/dsh-mneme/test/client.test.js @@ -355,12 +355,12 @@ test("knowledge base page exposes the §3.6 routes and explains every empty stat // 文案本身锁一次:说清这是 mneme 的功能、开关在哪儿。 assert.ok( - clientSource.includes("设置 → 记忆(mneme) → 功能 → 知识库"), + clientSource.includes("设置 → 记忆(mneme) → 功能 → 扩展功能"), "the Chinese disabled copy must name the exact toggle path" ); assert.ok( - clientSource.includes("Settings → Memory (mneme) → Features → Knowledge base"), - "the English disabled copy must name the same path" + clientSource.includes("Settings → Memory (mneme) → Features → Extensions, turn on the knowledge base switch"), + "the English disabled copy must name the same path (group label is Extensions since v0.13)" ); });