From 0949c439123852668776f6b028f38059c6531446 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 11 Oct 2026 19:54:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(inject):=20=E6=B3=A8=E5=85=A5=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E6=94=B6=E7=BC=96=20history=20=E4=B8=8E=20pa?= =?UTF-8?q?ttern=E2=80=94=E2=80=94=E5=AD=98=E6=B4=BB=E5=8D=B4=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E6=B3=A8=E5=85=A5=E7=9A=84=2023%=EF=BC=88#369?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - INJECT_TYPES 原先不收 history:蒸馏白名单(src/summarize.js 的 VALID)产得出, 注入面却一次不给看。本机活跃库 378/1820(20.8%)受此影响,而它们在 recall_runs 的 auto 模式里被按需检索过 328 次——是被真实使用的数据,只是进不了注入池。 - pattern 一并收编:40 条全为手工写入(source=manual),且已被 dreamMergeGuard 列为 长保留受保护类型。巩固层当资产护住、注入面无视,是同一条口径的两头不一致。 - 候选池 1362 → 1753;重要度闸门(>=3,summary/preference 例外)对新收编类型一视同仁, 低于闸门照旧出局。top_k=5 下粗算多占约 1 席。 - test/service.test.js 两处「history 排除」的旧行为锁按新口径翻成「收编生效」——白名单 日后若被收窄会当场红,而不是静默把两成条目关回不可注入;另加三条候选池用例 (history 进池 / 闸门不豁免 / pattern 进池)。 - src 变更已 sync;测试 1609 → 1612。 --- README.md | 6 +++--- dsh-mneme/README.md | 6 +++--- dsh-mneme/lib/service.js | 10 +++++++++- dsh-mneme/src/service.js | 10 +++++++++- dsh-mneme/test/inject-recall.test.js | 26 ++++++++++++++++++++++++++ dsh-mneme/test/service.test.js | 9 +++++++-- 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f9624880..0cc4306b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ license CI node - tests + tests coverage Awesome

@@ -207,7 +207,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790 ```bash cd dsh-mneme && npm install -npm test # 1609 个测试 +npm test # 1612 个测试 npm run stress # 三轴线压测 npm run sync # src → lib 同步 ``` @@ -414,7 +414,7 @@ Community projects that wire mneme into more platforms — not part of this repo ```bash cd dsh-mneme && npm install -npm test # 1609 tests +npm test # 1612 tests npm run stress # three-axis stress test npm run sync # src → lib sync ``` diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index 5edf195a..048cfe2f 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) -[![tests](https://img.shields.io/badge/tests-1609%20passed-success)](https://github.com/slow-stack/mneme) +[![tests](https://img.shields.io/badge/tests-1612%20passed-success)](https://github.com/slow-stack/mneme) [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions) [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org) [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme) @@ -613,7 +613,7 @@ src/ ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列) └── index.js # 插件接线 lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖) -test/ # 1609 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) +test/ # 1612 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单 ``` @@ -622,7 +622,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压 ```bash cd dsh-mneme npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试) -npm test # 运行 1609 个测试 +npm test # 运行 1612 个测试 npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM) npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行) ``` diff --git a/dsh-mneme/lib/service.js b/dsh-mneme/lib/service.js index c2ba694c..cca03dff 100644 --- a/dsh-mneme/lib/service.js +++ b/dsh-mneme/lib/service.js @@ -13,7 +13,15 @@ import { createBM25Index } from "./search/bm25.js"; import { adaptiveThreshold } from "./search/adaptive.js"; import { anchorSeeds, normalizePath, cascadeDepths, HOP_QUOTA } from "./graph/anchoring.js"; -const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint"]); +// Issue #369:蒸馏白名单(src/summarize.js 的 VALID)产得出 history,注入白名单 +// 原先不收——本机活跃库实测 378/1820(20.8%)因此永不可自动注入,而它们在同一库 +// 的 recall_runs `auto` 模式里被按需检索过 328 次:不是垃圾数据,只是进不了注入池。 +// 收编后候选池 1362 → 1713(+25.8%),按 top_k=5 粗算约多占 1 席;重要度闸门对 +// history 一视同仁(见 injectCandidates 的 importance >= threshold),低于闸门照旧出局。 +// pattern 一并收编:它只由手工写入(实测 40 条全 source=manual),却已被 dreamMergeGuard +// 列为长保留受保护类型(docs/CONFIGURATION.md)——巩固层当资产护住、注入面一次不给看, +// 是同一条口径的两头不一致。 +const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint", "history", "pattern"]); // document 摘要行(#230):仅 documentMemoryEnabled 开启时进注入候选。独立 // 集合而非常改共享 Set——INJECT_TYPES 本身是「类型可注入」的静态事实,flag diff --git a/dsh-mneme/src/service.js b/dsh-mneme/src/service.js index c2ba694c..cca03dff 100644 --- a/dsh-mneme/src/service.js +++ b/dsh-mneme/src/service.js @@ -13,7 +13,15 @@ import { createBM25Index } from "./search/bm25.js"; import { adaptiveThreshold } from "./search/adaptive.js"; import { anchorSeeds, normalizePath, cascadeDepths, HOP_QUOTA } from "./graph/anchoring.js"; -const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint"]); +// Issue #369:蒸馏白名单(src/summarize.js 的 VALID)产得出 history,注入白名单 +// 原先不收——本机活跃库实测 378/1820(20.8%)因此永不可自动注入,而它们在同一库 +// 的 recall_runs `auto` 模式里被按需检索过 328 次:不是垃圾数据,只是进不了注入池。 +// 收编后候选池 1362 → 1713(+25.8%),按 top_k=5 粗算约多占 1 席;重要度闸门对 +// history 一视同仁(见 injectCandidates 的 importance >= threshold),低于闸门照旧出局。 +// pattern 一并收编:它只由手工写入(实测 40 条全 source=manual),却已被 dreamMergeGuard +// 列为长保留受保护类型(docs/CONFIGURATION.md)——巩固层当资产护住、注入面一次不给看, +// 是同一条口径的两头不一致。 +const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint", "history", "pattern"]); // document 摘要行(#230):仅 documentMemoryEnabled 开启时进注入候选。独立 // 集合而非常改共享 Set——INJECT_TYPES 本身是「类型可注入」的静态事实,flag diff --git a/dsh-mneme/test/inject-recall.test.js b/dsh-mneme/test/inject-recall.test.js index 7d3ca45d..423cabc1 100644 --- a/dsh-mneme/test/inject-recall.test.js +++ b/dsh-mneme/test/inject-recall.test.js @@ -103,3 +103,29 @@ test("slotFillRate = 注入条数 / (轮数 × 槽位数)", () => { assert.equal(stats.injection.slotFillRate, 3 / 4); store.close(); }); + +// #369:history 曾被注入白名单挡在门外(蒸馏产得出、注入收不进)。这两条断言分 +// 别锁「收编生效」与「重要度闸门对 history 不豁免」——后者一旦失效,短流水账会 +// 直接挤掉决策类条目,那才是这个改动真正的风险面。 +test("#369: history 进注入候选池", () => { + const { service } = setup(); + service.saveWithDedupe({ type: "history", title: "切到暗色主题", content: "用户本次会话把界面切成暗色主题", importance: 4 }); + const titles = service.injectCandidates({ maxItems: 5 }).map((m) => m.title); + assert.ok(titles.includes("切到暗色主题"), "importance>=3 的 history 应进注入候选"); +}); + +test("#369: 低于重要度闸门的 history 不得例外放行", () => { + const { service } = setup(); + service.saveWithDedupe({ type: "history", title: "打开过一次终端", content: "用户中途打开过一次终端", importance: 2 }); + const titles = service.injectCandidates({ maxItems: 5 }).map((m) => m.title); + assert.ok(!titles.includes("打开过一次终端"), "history 不是 summary/preference,闸门照样生效"); +}); + +// pattern 只由手工写入,却已被 dreamMergeGuard 列为长保留受保护类型——巩固层当资产 +// 护住、注入面一次不给看是同一条口径的两头不一致。这条锁它进池。 +test("#369: 手写的 pattern 进注入候选池", () => { + const { service } = setup(); + service.saveWithDedupe({ type: "pattern", title: "重复字符串先抽共用模块", content: "多处重复的截断逻辑先收拢成共用模块,再改调用方", importance: 5 }); + const titles = service.injectCandidates({ maxItems: 5 }).map((m) => m.title); + assert.ok(titles.includes("重复字符串先抽共用模块"), "importance>=3 的 pattern 应进注入候选"); +}); diff --git a/dsh-mneme/test/service.test.js b/dsh-mneme/test/service.test.js index 440ffb3d..6809e177 100644 --- a/dsh-mneme/test/service.test.js +++ b/dsh-mneme/test/service.test.js @@ -53,7 +53,10 @@ test("injectCandidates returns preferences + high-importance items within limit" const types = candidates.map((c) => c.type); assert.ok(types.includes("preference"), "preferences included"); assert.ok(types.includes("project"), "high-importance project included"); - assert.ok(!types.includes("history"), "history excluded by default"); + // #369:history 曾被注入白名单挡在门外,现按「注入侧收编」进池(理由与实测数字 + // 见 src/service.js 的 INJECT_TYPES 注释)。这里从「锁排除」改成「锁收编」—— + // 白名单日后若被收窄,这条会红,而不是静默把两成条目关回不可注入。 + assert.ok(types.includes("history"), "history included since #369"); assert.ok(candidates.length <= 5, "respects maxItems"); }); @@ -220,7 +223,9 @@ test("Bug4: injectCandidates with an empty query keeps the legacy rule-based sel assert.deepEqual(noQuery.map((c) => c.id), legacy.map((c) => c.id), "empty query behaves identically to the old logic"); assert.deepEqual(blankQuery.map((c) => c.id), legacy.map((c) => c.id), "whitespace-only query degrades too"); assert.equal(legacy[0].type, "summary", "summary still leads"); - assert.ok(!legacy.some((c) => c.type === "history"), "history still excluded"); + // #369 收编后 history 可进池;本用例的重心是「空查询与旧规则路逐字节同序」, + // 这条只确认收编没有撼动优先级分层——summary 仍打头。 + assert.ok(legacy.some((c) => c.type === "history"), "history included since #369"); }); test("Bug4: injectCandidates ignores the query when hybridInject is disabled", () => { From 61698c47182b4902739d1f908405936a0def5127 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 11 Oct 2026 22:07:43 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(inject):=20=E8=AF=AD=E4=B9=89=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=9B=9E=E5=A1=AB=E5=88=86=E6=94=AF=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E9=87=8D=E8=A6=81=E5=BA=A6=E9=97=A8=E6=A7=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit on #404:injectCandidates 的 lastSemanticRecall 回填分支独漏 importance 门槛,而向量路(同函数上方)与 BM25 路(:1655 注释「门控与向量路径 一致(含 importance 阈值)」)都带着它。缺口在 #369 收编 history 后被放大—— 库里低于闸门的行(本机 history 378 条中 27 条 i<=2)只要被一次检索灌进缓存, 就能绕开门槛占注入位。 - 回填谓词与另两路对齐(summary/preference 仍豁免); - 新用例一正一反:正向锁「i4 的 history 经缓存进池」,反向锁「i2 的不得借缓存 绕行」,避免断言退化成常量真。撤掉门槛后正是反向那条红(已本机验证)。 --- README.md | 6 +++--- dsh-mneme/README.md | 6 +++--- dsh-mneme/lib/service.js | 7 ++++++- dsh-mneme/src/service.js | 7 ++++++- dsh-mneme/test/inject-recall.test.js | 13 +++++++++++++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0cc4306b..5f1c9d85 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ license CI node - tests + tests coverage Awesome

@@ -207,7 +207,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790 ```bash cd dsh-mneme && npm install -npm test # 1612 个测试 +npm test # 1613 个测试 npm run stress # 三轴线压测 npm run sync # src → lib 同步 ``` @@ -414,7 +414,7 @@ Community projects that wire mneme into more platforms — not part of this repo ```bash cd dsh-mneme && npm install -npm test # 1612 tests +npm test # 1613 tests npm run stress # three-axis stress test npm run sync # src → lib sync ``` diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index 048cfe2f..c32a783b 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) -[![tests](https://img.shields.io/badge/tests-1612%20passed-success)](https://github.com/slow-stack/mneme) +[![tests](https://img.shields.io/badge/tests-1613%20passed-success)](https://github.com/slow-stack/mneme) [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions) [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org) [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme) @@ -613,7 +613,7 @@ src/ ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列) └── index.js # 插件接线 lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖) -test/ # 1612 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) +test/ # 1613 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单 ``` @@ -622,7 +622,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压 ```bash cd dsh-mneme npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试) -npm test # 运行 1612 个测试 +npm test # 运行 1613 个测试 npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM) npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行) ``` diff --git a/dsh-mneme/lib/service.js b/dsh-mneme/lib/service.js index cca03dff..17526a95 100644 --- a/dsh-mneme/lib/service.js +++ b/dsh-mneme/lib/service.js @@ -1642,9 +1642,14 @@ export function createService({ store, mirror, config, onWrite, logger, document } catch { /* vector unavailable: fall through to the recall cache */ } } if (!semanticItems.length && lastSemanticRecall?.query === q && lastSemanticRecall.items?.length) { + // CodeRabbit on #404:这条回填分支此前独漏重要度门槛——向量路(上方)与 + // BM25 路(下方 :1655 注释「门控与向量路径一致(含 importance 阈值)」) + // 都带着它。#369 收编 history 后缺口会被放大:库里低于闸门的行(实测 + // history 378 条中有 27 条 i<=2)走一次检索进缓存就能绕开门槛占注入位。 for (const m of lastSemanticRecall.items) { if (m && !m.archived && injectTypes.has(m.type) && !m.forgotten && - (m.source !== "narrative" || allowNarrative) && codingGate(m)) semanticItems.push(m); + (m.source !== "narrative" || allowNarrative) && codingGate(m) && + (m.type === "summary" || m.type === "preference" || m.importance >= threshold)) semanticItems.push(m); } } // Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。 diff --git a/dsh-mneme/src/service.js b/dsh-mneme/src/service.js index cca03dff..17526a95 100644 --- a/dsh-mneme/src/service.js +++ b/dsh-mneme/src/service.js @@ -1642,9 +1642,14 @@ export function createService({ store, mirror, config, onWrite, logger, document } catch { /* vector unavailable: fall through to the recall cache */ } } if (!semanticItems.length && lastSemanticRecall?.query === q && lastSemanticRecall.items?.length) { + // CodeRabbit on #404:这条回填分支此前独漏重要度门槛——向量路(上方)与 + // BM25 路(下方 :1655 注释「门控与向量路径一致(含 importance 阈值)」) + // 都带着它。#369 收编 history 后缺口会被放大:库里低于闸门的行(实测 + // history 378 条中有 27 条 i<=2)走一次检索进缓存就能绕开门槛占注入位。 for (const m of lastSemanticRecall.items) { if (m && !m.archived && injectTypes.has(m.type) && !m.forgotten && - (m.source !== "narrative" || allowNarrative) && codingGate(m)) semanticItems.push(m); + (m.source !== "narrative" || allowNarrative) && codingGate(m) && + (m.type === "summary" || m.type === "preference" || m.importance >= threshold)) semanticItems.push(m); } } // Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。 diff --git a/dsh-mneme/test/inject-recall.test.js b/dsh-mneme/test/inject-recall.test.js index 423cabc1..cb36d319 100644 --- a/dsh-mneme/test/inject-recall.test.js +++ b/dsh-mneme/test/inject-recall.test.js @@ -129,3 +129,16 @@ test("#369: 手写的 pattern 进注入候选池", () => { const titles = service.injectCandidates({ maxItems: 5 }).map((m) => m.title); assert.ok(titles.includes("重复字符串先抽共用模块"), "importance>=3 的 pattern 应进注入候选"); }); + +// CodeRabbit on PR #404:语义缓存回填分支曾独漏重要度门槛(向量路与 BM25 路都带)。 +// 一正一反:反向锁「低于闸门的行走缓存也不许进池」,正向锁「达标的照常进池」, +// 免得断言退化成常量真;缓存没被灌进来时正向那条会先红,用例不会空压。 +test("#369: 语义缓存回填同样受重要度闸门约束", async () => { + const { service } = setup(); + const low = service.saveWithDedupe({ type: "history", title: "终端开了一次", content: "用户中途打开过一次终端做检查", importance: 2 }); + const high = service.saveWithDedupe({ type: "history", title: "终端方案定案", content: "用户终端接入方案已定案并验证", importance: 4 }); + await service.searchMemories("终端", { mode: "keyword", useRerank: false, recordRecall: false }); + const ids = service.injectCandidates({ query: "终端", maxItems: 5 }).map((m) => m.id); + assert.ok(ids.includes(high.memory.id), "i4 的 history 应经缓存回填进池(正向对照)"); + assert.equal(ids.includes(low.memory.id), false, "低于闸门的行不得借语义缓存绕开门槛"); +});