From 45f20e811c5d44d3dd7b79febe4d0c580a1d11c2 Mon Sep 17 00:00:00 2001 From: modusensus Date: Wed, 30 Sep 2026 20:07:35 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(graph):=20=E8=A2=AB=E5=8A=A8=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=20Passive=20Confirmation=EF=BC=88issue=20#24=20?= =?UTF-8?q?=E5=9D=974=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把「关系是用出来的」闭环落到触达路径:正常触达(记忆被召回/注入)= 对该 记忆挂联关系边的被动确认——bumpRelationWeight 抬一格(只加不减、封顶 1.0)。 独立于 heat 闸门:graphPassiveConfirm 开时即使 heatEnabled=false 也照常 bump(touchLastAccess 的热度消费仍随 heat 关停,语义不变)。默认关=零行为 变化。自激回路由 bump 封顶天然遏制,复核 UI 留后续块——「仅异常路径暴露 给用户复核」的确认记录侧在此落地。 - store.getRelationsByMemory:按记忆反查关联边(复用 idx_relations_memory) - service.touchRecalled:heatOn 与 passive 拆成独立闸门,passive 开时 bump - config:graphPassiveConfirm(默认 false,全走 feature_flags、lightMode 关) 测试:5 组 graph-passive-confirm 新增全过;heat/weight 回归绿 --- dsh-mneme/lib/config.js | 6 ++ dsh-mneme/lib/service.js | 18 ++++- dsh-mneme/lib/store.js | 14 ++++ dsh-mneme/src/config.js | 6 ++ dsh-mneme/src/service.js | 18 ++++- dsh-mneme/src/store.js | 14 ++++ dsh-mneme/test/graph-passive-confirm.test.js | 81 ++++++++++++++++++++ 7 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 dsh-mneme/test/graph-passive-confirm.test.js diff --git a/dsh-mneme/lib/config.js b/dsh-mneme/lib/config.js index 29fc11c..4d4bd12 100644 --- a/dsh-mneme/lib/config.js +++ b/dsh-mneme/lib/config.js @@ -469,6 +469,10 @@ export const Config = z.object({ graphWeightEnabled: z.boolean().default(false), // 单次有效触达的抬升幅度。 graphWeightDelta: z.number().min(0).max(1).default(0.1), + // 被动确认(issue #24 块4):heat 触达(记忆被召回/注入=被用)即对该记忆 + // 挂联的关系边做被动确认——bumpRelationWeight 抬一格。默认关=零行为变化; + // 开=正常触达演化边权,「关系是用出来的」闭环落地(异常路径复核留后续块)。 + graphPassiveConfirm: z.boolean().default(false), // --- sleep mode: idle-triggered deep maintenance (v0.4.0) --------------- // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight) @@ -720,6 +724,8 @@ const LIGHT_MODE_OFF = [ "entityRecallEnabled", // 轻量模式不开边权重演化(#24 块2,依赖实体抽取产出;抽取关则无 v08 边可抬升)。 "graphWeightEnabled", + // 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。 + "graphPassiveConfirm", // 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。 "dreamNarrativeEnabled", // 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。 diff --git a/dsh-mneme/lib/service.js b/dsh-mneme/lib/service.js index 4324ad4..77c5a5c 100644 --- a/dsh-mneme/lib/service.js +++ b/dsh-mneme/lib/service.js @@ -530,11 +530,25 @@ export function createService({ store, mirror, config, onWrite, logger, document * on the hot recall path). A touch failure must never break search/inject. */ function touchRecalled(memories) { - if (config?.heatEnabled === false || !Array.isArray(memories) || memories.length === 0) return; + if (!Array.isArray(memories) || memories.length === 0) return; + // heatEnabled=false 只关 touchLastAccess 的热度消费(既有语义),但 #24 + // 块4 被动确认独立于 heat——graphPassiveConfirm 开时照常 bump 关联边。 + // 两条路径都 best-effort,失败绝不阻断检索/注入。 + const heatOn = config?.heatEnabled !== false; + const passive = config?.graphPassiveConfirm === true; + const delta = config?.graphWeightDelta ?? 0.1; for (const m of memories) { if (!m?.id) continue; try { - store.touchLastAccess(m.id); + if (heatOn) store.touchLastAccess(m.id); + if (passive) { + // 正常触达=对该记忆挂联关系边的被动确认;bump 只加不减封顶 1.0, + // 自激回路由封顶天然遏制。「仅异常路径暴露给用户复核」的确认记录 + // 侧在此落地,复核 UI 留后续块。 + for (const rel of store.getRelationsByMemory(m.id)) { + store.bumpRelationWeight(rel.id, delta); + } + } } catch { /* touch is best effort */ } } } diff --git a/dsh-mneme/lib/store.js b/dsh-mneme/lib/store.js index 66a792f..34eb7c8 100644 --- a/dsh-mneme/lib/store.js +++ b/dsh-mneme/lib/store.js @@ -2500,6 +2500,19 @@ export function createStore(path) { ).all(entityId, entityId).map(toRelation); } + /** + * 按记忆反查关联边(issue #24 块4 被动确认):一条记忆被召回/注入(触达) + * 时,挂在同一记忆上的关系行即「这条关系被用过了」的证据——正常触达视为 + * 被动确认,调用方据此 bumpRelationWeight。返回 toRelation 行(含 weight/ + * source)。索引 idx_relations_memory 已建(#219)。 + */ + function getRelationsByMemory(memoryId) { + if (!memoryId) return []; + return db.prepare( + "SELECT * FROM entity_relations WHERE memory_id = ?" + ).all(memoryId).map(toRelation); + } + /** All entities (optionally name-filtered, newest first). Used by sleep phase 4 * orphan detection: an entity with zero relations is a candidate for relation * completion. */ @@ -2884,6 +2897,7 @@ export function createStore(path) { bumpRelationWeight, migrateAttrsToMemory, getRelations, + getRelationsByMemory, setMirrorState, getMirrorState, markMirrorDirty, diff --git a/dsh-mneme/src/config.js b/dsh-mneme/src/config.js index 29fc11c..4d4bd12 100644 --- a/dsh-mneme/src/config.js +++ b/dsh-mneme/src/config.js @@ -469,6 +469,10 @@ export const Config = z.object({ graphWeightEnabled: z.boolean().default(false), // 单次有效触达的抬升幅度。 graphWeightDelta: z.number().min(0).max(1).default(0.1), + // 被动确认(issue #24 块4):heat 触达(记忆被召回/注入=被用)即对该记忆 + // 挂联的关系边做被动确认——bumpRelationWeight 抬一格。默认关=零行为变化; + // 开=正常触达演化边权,「关系是用出来的」闭环落地(异常路径复核留后续块)。 + graphPassiveConfirm: z.boolean().default(false), // --- sleep mode: idle-triggered deep maintenance (v0.4.0) --------------- // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight) @@ -720,6 +724,8 @@ const LIGHT_MODE_OFF = [ "entityRecallEnabled", // 轻量模式不开边权重演化(#24 块2,依赖实体抽取产出;抽取关则无 v08 边可抬升)。 "graphWeightEnabled", + // 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。 + "graphPassiveConfirm", // 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。 "dreamNarrativeEnabled", // 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。 diff --git a/dsh-mneme/src/service.js b/dsh-mneme/src/service.js index 4324ad4..77c5a5c 100644 --- a/dsh-mneme/src/service.js +++ b/dsh-mneme/src/service.js @@ -530,11 +530,25 @@ export function createService({ store, mirror, config, onWrite, logger, document * on the hot recall path). A touch failure must never break search/inject. */ function touchRecalled(memories) { - if (config?.heatEnabled === false || !Array.isArray(memories) || memories.length === 0) return; + if (!Array.isArray(memories) || memories.length === 0) return; + // heatEnabled=false 只关 touchLastAccess 的热度消费(既有语义),但 #24 + // 块4 被动确认独立于 heat——graphPassiveConfirm 开时照常 bump 关联边。 + // 两条路径都 best-effort,失败绝不阻断检索/注入。 + const heatOn = config?.heatEnabled !== false; + const passive = config?.graphPassiveConfirm === true; + const delta = config?.graphWeightDelta ?? 0.1; for (const m of memories) { if (!m?.id) continue; try { - store.touchLastAccess(m.id); + if (heatOn) store.touchLastAccess(m.id); + if (passive) { + // 正常触达=对该记忆挂联关系边的被动确认;bump 只加不减封顶 1.0, + // 自激回路由封顶天然遏制。「仅异常路径暴露给用户复核」的确认记录 + // 侧在此落地,复核 UI 留后续块。 + for (const rel of store.getRelationsByMemory(m.id)) { + store.bumpRelationWeight(rel.id, delta); + } + } } catch { /* touch is best effort */ } } } diff --git a/dsh-mneme/src/store.js b/dsh-mneme/src/store.js index 66a792f..34eb7c8 100644 --- a/dsh-mneme/src/store.js +++ b/dsh-mneme/src/store.js @@ -2500,6 +2500,19 @@ export function createStore(path) { ).all(entityId, entityId).map(toRelation); } + /** + * 按记忆反查关联边(issue #24 块4 被动确认):一条记忆被召回/注入(触达) + * 时,挂在同一记忆上的关系行即「这条关系被用过了」的证据——正常触达视为 + * 被动确认,调用方据此 bumpRelationWeight。返回 toRelation 行(含 weight/ + * source)。索引 idx_relations_memory 已建(#219)。 + */ + function getRelationsByMemory(memoryId) { + if (!memoryId) return []; + return db.prepare( + "SELECT * FROM entity_relations WHERE memory_id = ?" + ).all(memoryId).map(toRelation); + } + /** All entities (optionally name-filtered, newest first). Used by sleep phase 4 * orphan detection: an entity with zero relations is a candidate for relation * completion. */ @@ -2884,6 +2897,7 @@ export function createStore(path) { bumpRelationWeight, migrateAttrsToMemory, getRelations, + getRelationsByMemory, setMirrorState, getMirrorState, markMirrorDirty, diff --git a/dsh-mneme/test/graph-passive-confirm.test.js b/dsh-mneme/test/graph-passive-confirm.test.js new file mode 100644 index 0000000..4d7b7ae --- /dev/null +++ b/dsh-mneme/test/graph-passive-confirm.test.js @@ -0,0 +1,81 @@ +// issue #24 块4 被动确认(graphPassiveConfirm)测试。 +// 覆盖:默认关=零行为变化 / 开启=命中记忆的关联边 bump / heat 关但 passive +// 开仍 bump(独立闸门)/ bump 只加不减封顶 / 无关联边的记忆触达安全降级。 +import test from "node:test"; +import assert from "node:assert/strict"; +import { createStore } from "../src/store.js"; +import { createService } from "../src/service.js"; + +function setup(config = {}) { + const store = createStore(":memory:"); + const service = createService({ store, mirror: null, config }); + return { store, service }; +} + +function putMem(store, service, { title, content, importance = 3 }) { + return service.saveWithDedupe({ type: "project", title, content, importance }).memory; +} + +function linkRelToMem(store, { fromName, toName, memoryId, source = "llm" }) { + const a = store.findEntityByName(fromName) ?? store.createEntity({ name: fromName, type: "technology" }); + const b = store.findEntityByName(toName) ?? store.createEntity({ name: toName, type: "technology" }); + return store.saveRelation({ from_entity: a.id, to_entity: b.id, relation_type: "uses", memory_id: memoryId, source }); +} + +/** 跑一次检索(内部 touchRecalled 触发触达),返回 rows。 */ +async function searchOnce(service, query) { + return service.searchMemories(query, { mode: "auto" }); +} + +test("graphPassiveConfirm=false (default): touched memory does NOT bump relation weight", async () => { + const { store, service } = setup(); + const mem = putMem(store, service, { title: "数据库迁移", content: "主库切换完成" }); + const rel = linkRelToMem(store, { fromName: "A", toName: "B", memoryId: mem.id, source: "llm" }); + assert.equal(rel.weight, 0.4); + await searchOnce(service, "数据库迁移"); + // 默认关:检索触达不 bump(weight 保持 0.4)。 + assert.equal(store.getRelationsByMemory(mem.id)[0].weight, 0.4, "weight unchanged by default"); + store.close(); +}); + +test("graphPassiveConfirm=true: touched memory bumps its linked relation weight", async () => { + const { store, service } = setup({ graphPassiveConfirm: true, graphWeightDelta: 0.1 }); + const mem = putMem(store, service, { title: "数据库迁移", content: "主库切换完成" }); + const rel = linkRelToMem(store, { fromName: "A", toName: "B", memoryId: mem.id, source: "llm" }); + assert.equal(rel.weight, 0.4); + const rows = await searchOnce(service, "数据库迁移"); + assert.ok(rows.some((r) => r.id === mem.id), "memory surfaced"); + assert.ok(store.getRelationsByMemory(mem.id)[0].weight > 0.4, "weight bumped after touch"); + store.close(); +}); + +test("heatEnabled=false does NOT gate passive confirm (independent gates)", async () => { + const { store, service } = setup({ heatEnabled: false, graphPassiveConfirm: true, graphWeightDelta: 0.1 }); + const mem = putMem(store, service, { title: "数据库迁移", content: "主库切换完成" }); + linkRelToMem(store, { fromName: "A", toName: "B", memoryId: mem.id, source: "llm" }); + await searchOnce(service, "数据库迁移"); + assert.ok(store.getRelationsByMemory(mem.id)[0].weight > 0.4, "bumped even with heat off"); + // 但 touchLastAccess 不写(heat 关的既有语义保留:last_accessed_at 仍为空)。 + assert.equal(store.getById(mem.id).last_accessed_at, undefined); + store.close(); +}); + +test("repeated touches cap at 1.0 (never exceeds)", () => { + const { store } = setup(); + const a = store.createEntity({ name: "A", type: "technology" }); + const b = store.createEntity({ name: "B", type: "technology" }); + const rel = store.saveRelation({ from_entity: a.id, to_entity: b.id, relation_type: "x", source: "llm" }); // 0.4 + for (let i = 0; i < 5; i++) { + store.bumpRelationWeight(rel.id, 0.3); // 0.4→0.7→1.0→1.0… + } + assert.equal(store.getRelations(a.id)[0].weight, 1.0, "capped at 1.0"); + store.close(); +}); + +test("memory without relations survives touch (no crash)", async () => { + const { store, service } = setup({ graphPassiveConfirm: true }); + const mem = putMem(store, service, { title: "孤立条", content: "无关联边" }); + await searchOnce(service, "孤立条"); + assert.ok(store.getById(mem.id), "memory still present"); + store.close(); +}); \ No newline at end of file From 91f96d331c323c20b6662e1179ec5c1cbb7e1a9f Mon Sep 17 00:00:00 2001 From: modusensus Date: Thu, 1 Oct 2026 03:10:37 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(graph):=20#24=20=E5=9D=974=20=E5=A4=8D?= =?UTF-8?q?=E5=AE=A1=E4=BF=AE=E6=AD=A3=E2=80=94=E2=80=94=E8=A2=AB=E5=8A=A8?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E6=8E=A5=E4=B8=8A=E5=9D=972=20=E6=80=BB?= =?UTF-8?q?=E9=97=B8=20graphWeightEnabled=EF=BC=88=E5=8D=95=E5=BC=80?= =?UTF-8?q?=E9=80=9A=E9=81=93=E9=94=AE=E4=B8=8D=E5=BE=97=E7=BB=95=E8=BF=87?= =?UTF-8?q?=E6=BC=94=E5=8C=96=E9=BB=98=E8=AE=A4=E5=85=B3=EF=BC=89=E3=80=81?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E6=8E=A5=E7=BA=BF(+3=20=E9=94=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsh-mneme/CHANGELOG.md | 6 ++++++ dsh-mneme/lib/service.js | 5 ++++- dsh-mneme/lib/settings.js | 11 ++++++++++- dsh-mneme/src/service.js | 5 ++++- dsh-mneme/src/settings.js | 11 ++++++++++- dsh-mneme/test/api.test.js | 5 +++-- dsh-mneme/test/graph-passive-confirm.test.js | 15 +++++++++++++-- 7 files changed, 50 insertions(+), 8 deletions(-) diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index dc1b24b..5cf923d 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +## 🆕 新增 + +- **被动确认 Passive Confirmation(issue #24 块4)**:把「人工认可/删除/忽略」的确认门槛收敛成例外管理——正常触达(记忆被召回/注入)即视为对挂联关系边的被动确认,`store.getRelationsByMemory` 反查关联边后 `bumpRelationWeight` 抬一格(只加不减、封顶 1.0,自激回路由封顶遏制)。`graphPassiveConfirm` 与 heat 是独立闸门(`heatEnabled=false` 时仍可演化边权),且受块2 总闸 `graphWeightEnabled` 约束——两键同开才生效,单开通道键绕不过「演化默认关」。仅异常路径暴露给用户复核的复核 UI 留后续块。新增键注册进 feature_flags 白名单;lightMode 强制关。 + ## [0.8.11] - 2026-09-30 ## 🐛 修复 diff --git a/dsh-mneme/lib/service.js b/dsh-mneme/lib/service.js index 77c5a5c..43d2fa4 100644 --- a/dsh-mneme/lib/service.js +++ b/dsh-mneme/lib/service.js @@ -533,9 +533,12 @@ export function createService({ store, mirror, config, onWrite, logger, document if (!Array.isArray(memories) || memories.length === 0) return; // heatEnabled=false 只关 touchLastAccess 的热度消费(既有语义),但 #24 // 块4 被动确认独立于 heat——graphPassiveConfirm 开时照常 bump 关联边。 + // graphWeightEnabled 是块2 立的演化总闸:它关着时任何触达都不该改边权,被动 + // 确认只是它底下的一个通道(后续块还会接别的确认通道),两键同开才 bump, + // 否则单开通道键就等于绕过「演化默认关」的承诺。 // 两条路径都 best-effort,失败绝不阻断检索/注入。 const heatOn = config?.heatEnabled !== false; - const passive = config?.graphPassiveConfirm === true; + const passive = config?.graphPassiveConfirm === true && config?.graphWeightEnabled === true; const delta = config?.graphWeightDelta ?? 0.1; for (const m of memories) { if (!m?.id) continue; diff --git a/dsh-mneme/lib/settings.js b/dsh-mneme/lib/settings.js index 1d9591b..67822f0 100644 --- a/dsh-mneme/lib/settings.js +++ b/dsh-mneme/lib/settings.js @@ -58,6 +58,13 @@ const FEATURE_FLAG_BOOLEANS = [ // Issue #219:图召回轴——查询命中实体名时把挂联记忆并入检索融合池 // (默认关;依赖实体抽取产出,lightMode 强制关闭)。 "entityRecallEnabled", + // Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关; + // 依赖实体抽取产出,lightMode 强制关闭)。本块与树内 #341/#343 的键一并 + // 登记,保证这条分支上出现在 config 里的键都进了白名单。 + "graphWeightEnabled", + // Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格 + // (默认关;需总闸 graphWeightEnabled 同开)。 + "graphPassiveConfirm", // Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source= // narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。 "dreamNarrativeEnabled", @@ -151,7 +158,9 @@ const FEATURE_FLAG_NUMBER_RANGES = { // Issue #127:vector 去重档的并入阈值。 summarizeDedupeMinSim: [0.5, 0.99], // Issue #125:hybrid 判"高相似"的阈值。 - dreamCandidateMinSim: [0.5, 0.99] + dreamCandidateMinSim: [0.5, 0.99], + // Issue #24 块2:单次有效触达的边权抬升幅度(与 config.js 的闭区间对齐)。 + graphWeightDelta: [0, 1] }; // 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符, // 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。 diff --git a/dsh-mneme/src/service.js b/dsh-mneme/src/service.js index 77c5a5c..43d2fa4 100644 --- a/dsh-mneme/src/service.js +++ b/dsh-mneme/src/service.js @@ -533,9 +533,12 @@ export function createService({ store, mirror, config, onWrite, logger, document if (!Array.isArray(memories) || memories.length === 0) return; // heatEnabled=false 只关 touchLastAccess 的热度消费(既有语义),但 #24 // 块4 被动确认独立于 heat——graphPassiveConfirm 开时照常 bump 关联边。 + // graphWeightEnabled 是块2 立的演化总闸:它关着时任何触达都不该改边权,被动 + // 确认只是它底下的一个通道(后续块还会接别的确认通道),两键同开才 bump, + // 否则单开通道键就等于绕过「演化默认关」的承诺。 // 两条路径都 best-effort,失败绝不阻断检索/注入。 const heatOn = config?.heatEnabled !== false; - const passive = config?.graphPassiveConfirm === true; + const passive = config?.graphPassiveConfirm === true && config?.graphWeightEnabled === true; const delta = config?.graphWeightDelta ?? 0.1; for (const m of memories) { if (!m?.id) continue; diff --git a/dsh-mneme/src/settings.js b/dsh-mneme/src/settings.js index 1d9591b..67822f0 100644 --- a/dsh-mneme/src/settings.js +++ b/dsh-mneme/src/settings.js @@ -58,6 +58,13 @@ const FEATURE_FLAG_BOOLEANS = [ // Issue #219:图召回轴——查询命中实体名时把挂联记忆并入检索融合池 // (默认关;依赖实体抽取产出,lightMode 强制关闭)。 "entityRecallEnabled", + // Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关; + // 依赖实体抽取产出,lightMode 强制关闭)。本块与树内 #341/#343 的键一并 + // 登记,保证这条分支上出现在 config 里的键都进了白名单。 + "graphWeightEnabled", + // Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格 + // (默认关;需总闸 graphWeightEnabled 同开)。 + "graphPassiveConfirm", // Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source= // narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。 "dreamNarrativeEnabled", @@ -151,7 +158,9 @@ const FEATURE_FLAG_NUMBER_RANGES = { // Issue #127:vector 去重档的并入阈值。 summarizeDedupeMinSim: [0.5, 0.99], // Issue #125:hybrid 判"高相似"的阈值。 - dreamCandidateMinSim: [0.5, 0.99] + dreamCandidateMinSim: [0.5, 0.99], + // Issue #24 块2:单次有效触达的边权抬升幅度(与 config.js 的闭区间对齐)。 + graphWeightDelta: [0, 1] }; // 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符, // 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。 diff --git a/dsh-mneme/test/api.test.js b/dsh-mneme/test/api.test.js index 4f96982..fb66a91 100644 --- a/dsh-mneme/test/api.test.js +++ b/dsh-mneme/test/api.test.js @@ -669,8 +669,9 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d // issue #239 第 4 项镜像到巩固新增 dreamPeakHours/dreamPeakMaxDeferMinutes, // issue #292 新增 autoDreamFailureBackoff、 // issue #254 新增 writeAdmission.enabled/writeAdmission.enforce, - // issue #34 恢复(#333)新增 injectTimePrefix) - assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1 + 2 + 1); + // issue #34 恢复(#333)新增 injectTimePrefix、 + // issue #24 块2 新增 graphWeightEnabled/graphWeightDelta、块4 新增 graphPassiveConfirm) + assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1 + 2 + 1 + 3); assert.equal(data.effective.dreamSkipInvalid, true); assert.equal(data.effective.allowCrossTypeMerge, false); assert.equal(data.effective.dreamMinIntervalMinutes, 0); diff --git a/dsh-mneme/test/graph-passive-confirm.test.js b/dsh-mneme/test/graph-passive-confirm.test.js index 4d7b7ae..fe3b3e2 100644 --- a/dsh-mneme/test/graph-passive-confirm.test.js +++ b/dsh-mneme/test/graph-passive-confirm.test.js @@ -39,7 +39,7 @@ test("graphPassiveConfirm=false (default): touched memory does NOT bump relation }); test("graphPassiveConfirm=true: touched memory bumps its linked relation weight", async () => { - const { store, service } = setup({ graphPassiveConfirm: true, graphWeightDelta: 0.1 }); + const { store, service } = setup({ graphWeightEnabled: true, graphPassiveConfirm: true, graphWeightDelta: 0.1 }); const mem = putMem(store, service, { title: "数据库迁移", content: "主库切换完成" }); const rel = linkRelToMem(store, { fromName: "A", toName: "B", memoryId: mem.id, source: "llm" }); assert.equal(rel.weight, 0.4); @@ -50,7 +50,7 @@ test("graphPassiveConfirm=true: touched memory bumps its linked relation weight" }); test("heatEnabled=false does NOT gate passive confirm (independent gates)", async () => { - const { store, service } = setup({ heatEnabled: false, graphPassiveConfirm: true, graphWeightDelta: 0.1 }); + const { store, service } = setup({ heatEnabled: false, graphWeightEnabled: true, graphPassiveConfirm: true, graphWeightDelta: 0.1 }); const mem = putMem(store, service, { title: "数据库迁移", content: "主库切换完成" }); linkRelToMem(store, { fromName: "A", toName: "B", memoryId: mem.id, source: "llm" }); await searchOnce(service, "数据库迁移"); @@ -72,6 +72,17 @@ test("repeated touches cap at 1.0 (never exceeds)", () => { store.close(); }); +// 总闸(块2 的 graphWeightEnabled)关着时被动确认不许生效——单开通道键就能演化 +// 边权的话,块2 立的「演化默认关」总闸形同虚设。 +test("graphWeightEnabled=false gates passive confirm (master switch)", async () => { + const { store, service } = setup({ graphPassiveConfirm: true, graphWeightDelta: 0.1 }); + const mem = putMem(store, service, { title: "数据库迁移", content: "主库切换完成" }); + linkRelToMem(store, { fromName: "A", toName: "B", memoryId: mem.id, source: "llm" }); + await searchOnce(service, "数据库迁移"); + assert.equal(store.getRelationsByMemory(mem.id)[0].weight, 0.4, "master switch off → weight untouched"); + store.close(); +}); + test("memory without relations survives touch (no crash)", async () => { const { store, service } = setup({ graphPassiveConfirm: true }); const mem = putMem(store, service, { title: "孤立条", content: "无关联边" }); From a798f17adce9298c985d39080aeb69b5d91c5920 Mon Sep 17 00:00:00 2001 From: modusensus Date: Thu, 1 Oct 2026 13:31:25 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20base=20=E6=8D=A2=E5=88=B0=20main?= =?UTF-8?q?=20=E5=90=8E=E9=87=8D=E8=B7=91=E6=A3=80=E6=9F=A5=E7=9F=A9?= =?UTF-8?q?=E9=98=B5=EF=BC=88retarget=20=E4=B8=8D=E8=A7=A6=E5=8F=91=20sync?= =?UTF-8?q?hronize=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit