Skip to content
2 changes: 2 additions & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
- **边权重演化 Weight Evolution(issue #24 块2)**:`entity_relations` 加 `weight`/`source` 两列(幂等迁移,存量行按 manual 1.0 认账)——建边来源决定初值(manual/confirmed 1.0、llm 0.4、tag 0.3),LLM 抽取自动建的边标 `source='llm'`;演化走 `bumpRelationWeight`(只加不减、封顶 1.0,单条 `UPDATE ... MIN(1.0, weight + ?)` 原子抬升,负/非有限增量在存储边界拒绝)。新增两键 `graphWeightEnabled`/`graphWeightDelta` 注册进 feature_flags 白名单,面板可启停;lightMode 强制关。触达侧接线见块4(被动确认)。
- **关联提示防幻觉 Injection Budget(issue #24 块3)**:图召回候选进注入时默认只参与排序、不改变注入块构成(`graphInjectHint` 关,保守档);开启后以固定句式「[检索线索]」标注、并在首个线索行前加引导语(链路信息非事实断言),条数由独立预算 `graphInjectBudget` 约束——既不受 document 预算约束、也不挤占 `maxItems` 槽位。线索行不进 pin 池(pin 会在保守档下把它放回块内,绕过开关)。新增两键注册进 feature_flags 白名单;lightMode 强制关。

- **被动确认 Passive Confirmation(issue #24 块4)**:把「人工认可/删除/忽略」的确认门槛收敛成例外管理——正常触达(记忆被召回/注入)即视为对挂联关系边的被动确认,`store.getRelationsByMemory` 反查关联边后 `bumpRelationWeight` 抬一格(只加不减、封顶 1.0,自激回路由封顶遏制)。`graphPassiveConfirm` 与 heat 是独立闸门(`heatEnabled=false` 时仍可演化边权),且受块2 总闸 `graphWeightEnabled` 约束——两键同开才生效,单开通道键绕不过「演化默认关」。仅异常路径暴露给用户复核的复核 UI 留后续块。新增键注册进 feature_flags 白名单;lightMode 强制关。

## 🧹 工程

- **基线持久化回归集 9 条,全量测试 1441 → 1450**:无种子对照组与播种组同库同写入(一个仍触发、一个被拦下——锁的正是这个差异,否则播种被忽略时两条断言同真同假)、阈值照常累积(+10 条仍触发)、run 自报基线优先于现场计算(运行后再长 15 条仍不触发)、成功轮落库 / 失败轮不落、sleep 行不进 auto 基线、半个基线被拒、老库补列幂等、非法种子(半个 / 负数 / 非整数 / 非对象)一律按「没有基线」处理、收尾取证失败既不反噬审计行也不写假基线、`index.js` 接线源码锁。**变异检验**:把播种改成忽略种子(`toBaseline(baselineSeed)` 恒为空)→ 2 条变红;把「成功轮落基线」分支短路 → 1 条变红。
Expand Down
6 changes: 6 additions & 0 deletions dsh-mneme/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),

// --- 图谱锚定层(issue #24 · 块1 Activation Anchoring)-------------------
// entityRecallEnabled 之上再开一层:以查询命中的实体为种子,沿实体关系表
Expand Down Expand Up @@ -743,6 +747,8 @@ const LIGHT_MODE_OFF = [
"graphWeightEnabled",
// 轻量模式不开关联提示标注(#24 块3,常驻注入文本多一段提示成本)。
"graphInjectHint",
// 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
"graphPassiveConfirm",
// 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
"dreamNarrativeEnabled",
// 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
Expand Down
21 changes: 19 additions & 2 deletions dsh-mneme/lib/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,11 +593,28 @@ 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 关联边。
// graphWeightEnabled 是块2 立的演化总闸:它关着时任何触达都不该改边权,被动
// 确认只是它底下的一个通道(后续块还会接别的确认通道),两键同开才 bump,
// 否则单开通道键就等于绕过「演化默认关」的承诺。
// 两条路径都 best-effort,失败绝不阻断检索/注入。
const heatOn = config?.heatEnabled !== false;
const passive = config?.graphPassiveConfirm === true && config?.graphWeightEnabled === 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 */ }
}
}
Expand Down
7 changes: 5 additions & 2 deletions dsh-mneme/lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,15 @@ const FEATURE_FLAG_BOOLEANS = [
// Issue #24 块1:图谱锚定层级联——命中实体作种子、沿关系表向邻居扩散
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
"graphAnchoringEnabled",
// Issue #24 块2:边权重演化——touch 门控把被寻回路径的边权重抬升
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
// Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关;
// 依赖实体抽取产出,lightMode 强制关闭)。块4 的被动确认是它底下的一个通道。
"graphWeightEnabled",
// Issue #24 块3:关联提示防幻觉——图召回线索行以「[检索线索]」标注进注入
// (默认关=线索只参与排序不进块;依赖实体抽取产出,lightMode 强制关闭)。
"graphInjectHint",
// Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格
// (默认关;需总闸 graphWeightEnabled 同开)。
"graphPassiveConfirm",
// Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source=
// narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。
"dreamNarrativeEnabled",
Expand Down
15 changes: 15 additions & 0 deletions dsh-mneme/lib/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2553,6 +2553,7 @@ export function createStore(path) {
).all(entityId, entityId).map(toRelation);
}

/**
/**
* 批量邻接(issue #24 · 块1 锚定层级联):给定多条实体 id,一并取全部关系行
* 并归并成「每个实体 → 相邻实体 id 去重列表」的邻接表。供 anchoring.js 的
Expand Down Expand Up @@ -2585,6 +2586,19 @@ export function createStore(path) {
if (!list.includes(neighbor)) list.push(neighbor);
}

/**
* 按记忆反查关联边(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. */
Expand Down Expand Up @@ -2971,6 +2985,7 @@ export function createStore(path) {
migrateAttrsToMemory,
getRelations,
getEntityNeighbors,
getRelationsByMemory,
setMirrorState,
getMirrorState,
markMirrorDirty,
Expand Down
6 changes: 6 additions & 0 deletions dsh-mneme/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),

// --- 图谱锚定层(issue #24 · 块1 Activation Anchoring)-------------------
// entityRecallEnabled 之上再开一层:以查询命中的实体为种子,沿实体关系表
Expand Down Expand Up @@ -743,6 +747,8 @@ const LIGHT_MODE_OFF = [
"graphWeightEnabled",
// 轻量模式不开关联提示标注(#24 块3,常驻注入文本多一段提示成本)。
"graphInjectHint",
// 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
"graphPassiveConfirm",
// 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
"dreamNarrativeEnabled",
// 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
Expand Down
21 changes: 19 additions & 2 deletions dsh-mneme/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,11 +593,28 @@ 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 关联边。
// graphWeightEnabled 是块2 立的演化总闸:它关着时任何触达都不该改边权,被动
// 确认只是它底下的一个通道(后续块还会接别的确认通道),两键同开才 bump,
// 否则单开通道键就等于绕过「演化默认关」的承诺。
// 两条路径都 best-effort,失败绝不阻断检索/注入。
const heatOn = config?.heatEnabled !== false;
const passive = config?.graphPassiveConfirm === true && config?.graphWeightEnabled === 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 */ }
}
}
Expand Down
7 changes: 5 additions & 2 deletions dsh-mneme/src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,15 @@ const FEATURE_FLAG_BOOLEANS = [
// Issue #24 块1:图谱锚定层级联——命中实体作种子、沿关系表向邻居扩散
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
"graphAnchoringEnabled",
// Issue #24 块2:边权重演化——touch 门控把被寻回路径的边权重抬升
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
// Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关;
// 依赖实体抽取产出,lightMode 强制关闭)。块4 的被动确认是它底下的一个通道。
"graphWeightEnabled",
// Issue #24 块3:关联提示防幻觉——图召回线索行以「[检索线索]」标注进注入
// (默认关=线索只参与排序不进块;依赖实体抽取产出,lightMode 强制关闭)。
"graphInjectHint",
// Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格
// (默认关;需总闸 graphWeightEnabled 同开)。
"graphPassiveConfirm",
// Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source=
// narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。
"dreamNarrativeEnabled",
Expand Down
15 changes: 15 additions & 0 deletions dsh-mneme/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2553,6 +2553,7 @@ export function createStore(path) {
).all(entityId, entityId).map(toRelation);
}

/**
/**
* 批量邻接(issue #24 · 块1 锚定层级联):给定多条实体 id,一并取全部关系行
* 并归并成「每个实体 → 相邻实体 id 去重列表」的邻接表。供 anchoring.js 的
Expand Down Expand Up @@ -2585,6 +2586,19 @@ export function createStore(path) {
if (!list.includes(neighbor)) list.push(neighbor);
}

/**
* 按记忆反查关联边(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. */
Expand Down Expand Up @@ -2971,6 +2985,7 @@ export function createStore(path) {
migrateAttrsToMemory,
getRelations,
getEntityNeighbors,
getRelationsByMemory,
setMirrorState,
getMirrorState,
markMirrorDirty,
Expand Down
5 changes: 3 additions & 2 deletions dsh-mneme/test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,9 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
// issue #254 新增 writeAdmission.enabled/writeAdmission.enforce,
// issue #34 恢复(#333)新增 injectTimePrefix、
// issue #24 块1 新增 graphAnchoringEnabled/graphSeedCap/graphCascadeDepth、
// 块2 新增 graphWeightEnabled/graphWeightDelta、块3 新增 graphInjectHint/graphInjectBudget)
assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1 + 2 + 1 + 3 + 2 + 2);
// 块2 新增 graphWeightEnabled/graphWeightDelta、块3 新增 graphInjectHint/graphInjectBudget、
// 块4 新增 graphPassiveConfirm)
assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1 + 2 + 1 + 3 + 2 + 2 + 1);
assert.equal(data.effective.dreamSkipInvalid, true);
assert.equal(data.effective.allowCrossTypeMerge, false);
assert.equal(data.effective.dreamMinIntervalMinutes, 0);
Expand Down
Loading
Loading