Skip to content

[Feature] 情感关系类型 affective_context 落地:抽取器白名单 + 不静默降级 + 新 sleep 相位(误归因探针 HC-7 前置) #619

Closed

Description

@modusensus

背景

误归因探针(D:/memory-hub/reports/attribution-probe/PREREGISTRATION.md)的 structured 臂依赖一个带 attribution 的情感关系类型 affective_context,用来区分「情境状态(situational)」与「稳定特质(trait)」。

代码里现在不存在这个东西。不先实现它,structured 臂只是把同样的文本换个表存,与 flat 臂在存储层没有可测差别,FGR 差值测到的是噪声。

现状实测(主 checkout 054c74edc51a0b3add746dbf45861b1c77ac3750,2026-11-09 21:5x)

  • grep -rn "affective" src/ → 零命中。整个 src/ 没有任何情感/状态类关系类型。
  • src/entities/extractor.js:2
    const VALID_RELATIONS = new Set(["uses", "depends_on", "part_of", "related_to"]);
  • src/entities/extractor.js:22 prompt 里的 JSON 结构把枚举写死成 "type": "uses|depends_on|part_of|related_to"。
  • src/entities/extractor.js:75-78
    function sanitizeRelationType(type) {
      if (VALID_RELATIONS.has(type)) return type;
      return "related_to";
    }
    → LLM 抽出的未知类型静默降级成 related_to:写入成功、不报错、图上没有情感轴。

存储层不用改,已验证:

  • src/store.js:462 relation_type TEXT NOT NULL,无 CHECK 约束。
  • src/dream/decisions.js:846 已经直写 relation_type: "supersedes" 并成功落库(live 库 108 行)。

要做的事

1. 抽取器三处(必改)

文件:行 现状 改成
src/entities/extractor.js:2 白名单只有四种 加 affective_context(及后续可能的状态类)
src/entities/extractor.js:22 prompt 枚举写死四种 同步更新,并说明何时该用 affective_context
src/entities/extractor.js:75-78 未知类型一律降级 related_to 不静默降级:报错或至少 logger.warn 带原始类型名

2. 落点:新开 sleep 相位,或挂 phasePatterns 旁

不能接 phaseRelations(src/dream/sleep.js:984 附近的既有相位):

  • 它只扫孤立实体(getRelations(e.id).length === 0)做同记忆共现补边,语义是「把没连上的连上」,与情感归因无关。
  • inferRelationType(a, b) 只有三种类型硬编码,末行 return "related_to" 兜底——它本来就没有情感输入端,接进去只是又一处假装能推断。

正确做法:新开一个相位,或挂在 phasePatterns(src/dream/sleep.js:984)旁边,前提是共用同一套 applyDecisions 校验(maxCreatePerRun / maxArchivePerRun 限额照旧生效)。

相关常量,实现时别踩:

  • src/config.js:622 sleepPatternMinMemories 默认 100(min 10 / max 1000)
  • src/config.js:626 sleepMaxPatternPerRun 默认 3(min 0 / max 10)
  • src/dream/decisions.js:358 maxCreatePerRun ?? 5、:366 maxArchivePerRun ?? 8

3. 语义护栏(产品行为,不只是探针需要)

  • 情境情感只进 affective_context 关系,带 attribution(归因到谁/什么场景)。
  • 稳定特质才进 pattern 行(memory_type = pattern 的既有铸造通道)。
  • 泛化阈值写死:同一情境类型在 N 个独立场合(不同会话)重复才允许升特质,N=1 一律不许。

验收

  1. 临时库(不碰 D:\DSH\.dsh\memory\memory.db)灌一条构造段 → 断言 entity_relations.relation_type = 'affective_context' 真实落库。
  2. 同一批断言 related_to 计数没有跟着涨(证明没被静默降级)。
  3. 未知关系类型的降级路径有测试覆盖:喂一个不存在的类型,断言产生 warn/错误而非静默 related_to。
  4. node --test test/*.test.js 基线绿;node scripts/check-sync.js 文件数一致(改了 src/ 记得跑 node scripts/sync-lib.js)。
  5. 用变异自证:把 sanitizeRelationType 改回静默降级,测试必须变红。

明确不做

  • 不绕白名单直写 saveRelation:能落库,但等于废掉整个抽取器,structured 臂测到的是一个只存在于实验里的存储层,与用户装到的东西不是一回事。
  • 不接 phaseRelations:见上,语义不对、也没有情感输入端。
  • 不把探针的判分逻辑塞进插件:插件只负责存储与巩固,评测在 D:/memory-hub/.eval/ 那边。

关联

  • 预注册 HC-7:D:/memory-hub/reports/attribution-probe/PREREGISTRATION.md
  • 上游方案:D:/memory-hub/reports/voicemem-mneme-proposal.md
  • 同批静默降级问题(另一条线,暂不并入本 issue):inferRelationType 无输入端、vision 关闭时整条消息静默丢弃——三者修法不同,合并会写成含糊清单。

Activity

  1. modusensus commented on Nov 9, 2026

    @modusensus
    MemberAuthor

    合并与版本策略(2026-11-10 定)

    • 这条线不急着合并。实现完成后开 PR,PR 挂着不并。
    • 等这批活全部做完,再一次性合并到同一个版本号里,避免零散合并打断其它版本的维护。
    • 也就是说:本 issue 对应的工作会与同批(affective_context 落地 + 抽取器不静默降级 + 探针侧脚本)一起进一个版本,不单独发版。

    工作时的口径:从 issue 出发对照实现,PR 标题带 issue 号,合并前 rebase 到最新 main。

  2. modusensus commented on Nov 9, 2026

    @modusensus
    MemberAuthor

    实现进展(本地分支 feat/619-affective-context,未 push、未开 PR)

    三笔提交,从 origin/main = 514d238 起:

    commit 内容
    62a3b3f 相位、配置、prompt、测试主体
    962d3e9 CHANGELOG
    f58ac22 审计后修的四处 + 抽取器写入权收紧

    完全按本 issue 的「合并节奏」执行:分支挂着,等同批活做完一次性并入同一个版本号。

    验收五条

    # 验收 状态 证据
    1 临时库灌段断言 relation_type='affective_context' 真落库 ✅ test/sleep.test.js 的 one-session / three-session 两条,断言 getRelations 里类型正确、source === "llm"
    2 同批断言 related_to 计数没涨 ✅ sleep: the affective phase never adds a related_to edge
    3 未知类型有测试(喂不存在类型,断言非静默 related_to) ✅ entities 两条:rejects unknown relation types、drops a relation whose type is not even a string
    4 基线绿 + check-sync 一致 + sync-lib ✅ 全量 3166 / 3165 通过 / 0 失败 / 1 跳过;check-sync 118 文件一致
    5 变异自证(改回静默降级测试必须变红) ✅ 见下表

    变异自证

    变异 结果
    sanitizeRelationType 改回 return "related_to" entities 2 条变红
    const downgraded = false; sleep 1 条变红
    落库的 relation_type 改成 "related_to" sleep 2 条变红

    三条都验完还原,还原后全绿。

    与 issue 正文不一致的一处决定

    issue 原来写的是「src/entities/extractor.js:2 白名单加 affective_context」。实现时改成不加,抽取器遇到它当未知类型拒收并 warn。

    理由:实体抽取发生在写入当下,手里没有会话边界,判不了「这一次」和「反复出现」;它写出来的边只有 {model},没有 emotion / attribution / sessionCount,同一张表会出现两种 schema。这个关系类型只有一个写入者——sleep 的 affective 相位。docs/ENTITIES.md 已写明这一点。

    审计发现的四处,均已修

    1. receipt 读成 :0:相位写边不走 applied,一轮明明写了边,审计行却是「跑了但什么都没做」。现在相位单独报 edgeWrites,totalApplied 一并累加,但两个数分开保留含义。
    2. 无幂等:entity_relations 没有 UNIQUE,同批段重放会叠出完全相同的边,把 sessionCount 抬高、主指标失真。写前先查 (from, to, memory_id),重复的进 dropped: duplicate edge。只在相位侧挡,没有动 store 层的通用约束——加 UNIQUE 会波及 phaseRelations 与抽取器两条既有写入路径,需要单独评估。
    3. 中断报 noop:原来只有「写过东西」才报 aborted。改成只要 signal.aborted 就报 aborted——没写东西时循环同样断在任意下标、没扫完,noop 等于说「整个池子看过了、没什么」。
    4. if (ok) 无 else:校验拒绝 trait 决策时照旧返回 noop,把「被拒」伪装成「没得铸」。补了 failed 分支。这条分支在合法决策下不可达(决策是按合法形状拼的),不写假用例假装测过。

    仍未做

    • PR 未开(按合并节奏,等这批活做完)
    • 版本号未 bump
    • store.saveRelation 的通用幂等:待评估是否并入另一个 issue
  3. modusensus commented on Nov 10, 2026

    @modusensus
    MemberAuthor

    Merged as 7e49452 (merge commit) after PR #628 cleared review.

    What landed, in order of the ten commits: the affective attribution phase and the affective_context relation type; the changelog entry; four audit holes (receipt accounting, edge idempotency keyed on the session set, interruption reported as aborted rather than ok, and a failed trait validation reported as failed rather than silently passing); same-occasion emotions merged by session instead of dropped; merge-only runs counted in the receipt; the target put into the trait title; patterns refreshed in place instead of appended; a refreshed pattern made countable, auditable and unborn; and the four review findings below.

    Review findings closed in bf607b6, all reproduced before fixing:

    1. The threshold fill corrupted the prompt. replaceAll("N", …) also hit the word JSON, so both the zh and the en prompt told the model to answer with a JSO3 array — the only output this phase parses. Prompts now use {{MIN_SESSIONS}}, the first named placeholder in lang.js.
    2. sameOccasion never matched for memories without provenance. memory_save writes no provenance row, so sessionsFor() is empty and the session-overlap key cannot fire; the same evidence wrote a fresh edge on every run and inflated sessionCount. Measured on the live database: 8517 active memories, 6846 with a provenance link, so 1671 (19.6%) were in that state. The session key stays primary; when either side has none, an evidence intersection decides.
    3. Trait candidates past sleepMaxPatternPerRun vanished. They were neither minted, demoted nor recorded while traits still counted them. They are now dropped with reason trait cap, the same way the edge cap accounts for edges.
    4. The changelog claimed five kinds of disappearance where the code has four. Duplicate edges became merged edges, so they are accounted as merged.

    Verification: node --test test/*.test.js → 3262 tests, 3261 pass, 0 fail, 1 skipped; node scripts/sync-lib.js then node scripts/check-sync.js → 119 files consistent. Each of the three new tests was checked by reverting the corresponding fix and watching it go red.

    Two things this does not cover, deliberately:

    • sleepAffectiveEnabled defaults to off and does not exist in the live settings row. The phase has never run against the production database: zero affective_context edges, and zero patterns carrying an appended body or a --- separator. No cleanup or migration is needed.
    • The extractor still refuses affective_context. VALID_RELATIONS keeps its four types, and an unknown type is normalised rather than written. Entity extraction has no session boundary at the moment of writing, so the sleep phase is the only writer. This differs from the original issue text, which asked for the allowlist change as well.
  4. self-assigned this
    on Nov 10, 2026
  5. added a commit that references this issue on Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions