Skip to content

选择性遗忘后,明确保留的更正事实答回旧值(Mneme 0.9.4,正常DSH GUI) #532

Open

Description

@catnipluss

摘要

空库开始、正常桌面GUI、同一临时助手、4个新对话共6次用户输入。自然聊天后能在新对话召回;更正颜色和整理时间后,又一个新对话能正确召回新值。随后要求只忘颜色、保留整理时间;最后一个新对话不再返回颜色,却把整理时间答回已过时的旧值。

期望:不知道已忘颜色;仍回答“每周六上午九点”。
实际:不知道颜色;回答“每周三晚上八点”。

环境与配置

  • Windows,DSH桌面0.2.1-rc.5,Mneme 0.9.4。
  • 主对话:ChatGPT subscription / GPT-6.1-Sol / Low。
  • 纯原生YAML临时预设,含官方persona、time-context及Mneme;没有自写可执行插件。
  • 同一临时预设下的新对话使用同一独立空库;未导入任何真实资料。主Mneme全局bundle不启用,仅预设内组件参与。
  • 没有工程脚本直接写记忆行,也没有用显式保存代替首次自动采集;更正和遗忘允许助手使用正常管理工具。
  • 相关显式设置如下,存储路径为独立空目录,此处不公开本机路径:
language: zh
lightMode: true
autoInject: true
autoSummarize: true
autoDream: true
dreamThresholdCount: 100
dreamThresholdChars: 10000
dreamMinIntervalMinutes: 60
dreamMaxTokens: 2048
summarizeReasoningEffort: low
summarizeMaxEntriesPerRun: 3
summarizeMaxRunsPerSession: 4
summarizeParseFailureMaxRetries: 1
summarizeMinWindowChars: 0
distillMaxChars: 4000
distillRateLimitRetries: 0
maxInjectedItems: 4
scopeEnabled: true
strictScope: true
scopeState:
  enabled: true
  enforce: true
sleepModeEnabled: false
profileExtractEnabled: false
entityExtractionEnabled: false
rerankEnabled: false
autoReindexOnBoot: false
externalApiEnabled: false
actionPipelineEnabled: false
actionRunEnabled: false
actionNudgeEnabled: false

临时persona要求简短回答、没有记忆证据就说不知道、区分用户事实/助手陈述/推断/计划;禁止手工save替代自动捕获,但明确更正/忘记请求可使用管理工具。它还要求不读外部文件/其它会话、不用终端/联网/IM/子Agent。上述是提示约束,不是OS沙箱或已验的工具硬隔离。

六次合成输入及观察

A、B、C、D均为该临时助手的不同GUI对话;没有重启Host。以下输入逐字保留。未记录的完整回复不补写。

1. 对话A

我叫林澄。我把纸模工具放在雾蓝色的收纳盒里,每周三晚上八点整理一次。对了,解释事情先给结论、再列两点理由就好。

正常完成回复。随后只读聚合计数显示3条记忆、1个持久游标、1条摘要调用审计。

2. 新对话B

我的纸模工具平时放在哪里,通常什么时候整理?请按我习惯的方式回答。

回复:

你的纸模工具平时放在雾蓝色的收纳盒里,通常在每周三晚上八点整理。

3. 同一对话B

改一下:我的收纳盒已经换成浅橙色了,整理时间也改成每周六上午九点。之前说的颜色和时间已经过时。

观察:助手确认了浅橙色及周六09:00。

4. 新对话C

我现在的纸模工具放在哪儿,什么时候整理?从这些安排你能看出我什么习惯?请把我说过的事实和你自己的推测分开。

观察:助手答浅橙色/周六09:00,并把周三20:00标为过时;区分明确事实和推测。但未确认工具与盒子的放置关系,关系保留并不完整。

5. 同一对话C

请把我的收纳盒颜色忘掉,别再保存这个细节。整理时间和回答偏好可以保留。

回复:

已将含收纳盒颜色的记忆设为遗忘,之后不再保存或使用这个细节。整理时间和区分事实与推测的回答偏好保留。

这只是助手声称完成,未据此判定物理删除、永久拒收或具体工具/行操作正确。

6. 新对话D

我的收纳盒是什么颜色?我通常什么时候整理纸模工具?请只按你确实知道的信息回答。

回复:

我不知道你的收纳盒是什么颜色。你通常在每周三晚上八点整理纸模工具。

聚合证据与未验证项

  • 最终13条记忆、4个持久游标、6条摘要调用审计、0次dream。
  • 后台6次summarize_compress均success,model_id为openai-codex:gpt-6.1-sol;这不证明提炼语义正确。没有把主聊天费用与后台计数混在一起。
  • 没有查读记忆行正文或逐个工具调用参数,故不知道颜色和时间是否在同一条记忆,也不知道忘记了哪个ID。
  • 没有确认每步后台落库与下一步注入之间的完整时间线。不能排除并发、提炼、召回排序/条数、工具选择或模型回答等因素。
  • 仅lightMode单组观察,无向量模式对照、另一个助手隔离负例、IM入口、日周月、重启重放或导出恢复验收。
  • 后续只读源码发现memory_forget按ID抑制整条记录,留存原文可恢复。这是接口契约,不是本例根因证据。
  • 未用0.9.5重测,不声称新版本仍可复现;目前未确认已发变更覆盖此现象。测试结束已关闭临时预设bundle和主bundle,未删除证据库。

希望维护者确认

  1. 当前受支持的正常配置中,用户更正两个事实、随后只忘其中一个时,推荐怎样保住另一个更正事实?是否应由自动摘要拆成独立条目,还是有正常的部分更新机制?
  2. 更正所取代的旧事实,是否有持久失效关系,使删除/隐藏新条目后旧值也不会重新成为当前答案?这与历史回溯查询应如何区分?
  3. 上述配置是否遗漏了该语义必需的原生设置?若已有修复,请给已发布版本与对应回归测试;若需要更多证据,优先指出最小的合成工具轨迹/时序信息,不需要真实用户库。

不请求通过修改本次测试数据来让结果通过。期望区分配置限制、模型行为与插件缺陷,再决定是否值得有界复测。

披露:本报告由AI辅助整理,用户已审阅并批准提交。

Activity

  1. modusensus commented on Oct 30, 2026

    @modusensus
    Member

    谢谢这份报告——复现路径、四段对话的输入原文、完整配置快照都给全了,这类报告对定位最有帮助。我们把仓库代码(main,0.9.5 之后的工作树)逐段对过,也在本地空库上照着你的场景跑了一遍,你说的现象全部能复现,而且不是模型幻觉,也不是配置漏设。

    结论先说:这是存储语义的设计缺口。mneme 里没有「事实」这一层,只有「记忆行」。你要求「忘掉颜色、保留整理时间」时,系统能执行的最小粒度就是一整行——所以当色彩与时间在物理上住在同一行,这个请求在语义上不可满足。

    下面把我们查到的三条独立通路列清楚,你可以直接核对行号。

    通路 1:更正与原事实被合并进同一行(不是覆盖)

    src/service.js:1439-1444

    const direct = memory._overwrite === true || memory._humanEdited === true;
    const content = direct ? newContent : appendContent(existing.content, newContent);

    appendContent(src/service.js:159-162)是追加语义,不是替换:

    function appendContent(oldContent, newContent) {
      const ts = new Date().toISOString();
      return `${oldContent}\n\n---\n[${ts}] ${newContent}`;
    }

    (type, title, scope) 三元组命中就并入同一行。所以「盒子是雾蓝色、周三20:00 整理」和「改成浅橙色、周六09:00」只要标题相同(或语义去重判成同一主题),就会变成同一行里的前后两段正文——旧值并没有消失,它还在那里,只是被后面那段带时间戳的更正压在下面。

    注意 direct 那条分支:只有 dream 的概要重生成、bootstrap、人类在镜像文件里手改这几类调用才带 _overwrite。普通对话里的更正永远走追加路。

    通路 2:memory_forget 是整行粒度

    src/store.js:1550-1556

    function setForget(id, forgotten) {
      runAtomically(() => {
        db.prepare("UPDATE memories SET forgotten = ?, updated_at = ? WHERE id = ?")
          .run(forgotten === true || forgotten === 1 ? 1 : 0, nowIso(), id);
        incrementGeneration();
      });
      return getById(id);
    }

    它只写一个布尔位。没有「抹掉这一行的某一段」这种操作,也没有段级定位(正文里的段落没有 ID)。所以「忘掉颜色、保留整理时间」在这次调用里必然退化为整行抑制——你看到助手声称已遗忘、颜色确实没了,是因为整行被压下去了,连同本来要保留的整理时间。

    通路 3:旧行从来没有失效关系,新行一走它就回来

    这是你问的第 ② 问的正面回答:没有。

    我把你的场景在 :memory: 空库上跑了一遍(两条不同标题,模拟更正时蒸馏拆成了两条):

    [B] 写 "storage place"  :: box is mist blue; tidy every Wed 20:00.
    [B] 写 "storage update" :: box now light orange; tidy every Sat 09:00.
    [B] forget(新的那条)
    inject candidates: [ 'storage place :: box is mist blue; tidy every Wed 20:00.' ]
    

    旧行 forgotten=0、archived=0、updated_at 从未被碰过——它从头到尾都是「活跃且当前」。新行只是与它并列,不是取代它。所以新行一旦被抑制,旧行自动成为唯一候选,注入侧没有任何办法知道它已经过时了。

    唯一会写失效关系的是 supersede 动作(src/dream/decisions.js:710 写 supersededBySuffix),它属于 sleepActionSet="full" 那一档,而 sleepModeEnabled=false(你的配置里就是 false,lightMode=true 会把它强制关掉)。这条分支在你的配置下永远不会执行。 顺带说:dreamThresholdCount=100 而库里 13 条,巩固确实一次没跑过——你的 dream_runs 审计读到 0 次是对的,不是审计坏了。

    通路 4(我们额外发现的):注入截断只切头,不切尾

    这条和 forget 无关,但会独立地造成同一个症状,值得一并知道。

    src/inject.js:160-164

    function injectMemory(m, maxLength = maxContent) {
      const text = String(m?.content ?? "");
      if (text.length <= maxLength) return text;
      return `${safeSlice(text, maxLength)}…${STR.truncatedHint[...]}`;
    }

    只保留前 maxContent 个字符(默认 injectContentMaxChars ?? 300,src/inject.js:147)。而更正恰恰是追加在尾部的。实测一条 415 字符的旧正文并进一条更正后总共 476 字符:

    cap=300 -> 更正(浅橙色) 可见? false | 旧值(每周三晚上八点) 可见? true
    cap=600 -> 两者都可见
    

    也就是说:即使不 forget,只要这一行够长,注入里看到的永远是旧值,更正被切在最外面。(pinned 行上限放宽到 2000,src/inject.js:262,但你的 maxInjectedItems=4、行数又少,未必命中 pin。)

    回答你的三个问题

    ① 用户更正两个事实、只忘其中一个,怎么保住另一个?

    按现在的代码:做不到,且不该由提示词去硬凑。蒸馏提示词(src/lang.js:14)已经明确要求「原子记忆原则:每条记忆只装一个独立事实……宁可拆成多条」,但写入侧 appendContent 会把同标题的两条物理合并——提示词在要求拆,存储层在合并,两边契约是打架的。所以靠「让模型拆得更细」治不了根:只要标题撞上,合并照样发生。

    ② 被取代的旧事实有没有持久失效关系?

    没有。见通路 3。要让「删了新条目旧值不复活」,需要一条跨行的取代边(row A superseded_by row B),而不是行内的追加文本。这跟历史回溯(content_history 已经在存,src/service.js 的 pushContentHistory 每次并入都会留档)是两件事:历史是「当时写了什么」的账本,失效是「现在哪个算数」的判据,现在只有前者。

    ③ 配置有没有漏掉必需项?

    没有漏。你的 lightMode=true 会强制关掉 autoDream / sleepModeEnabled / entityExtractionEnabled 等一批开关(src/config.js 的 LIGHT_MODE_OFF),0 次 dream 与配置完全自洽。这个 bug 不是 dream 干的,也不是配置少了一个勾。

    修复方向(想先和你对齐口径)

    这不会是一个小补丁,因为要动的三处都是防御段。我们倾向分三步走:

    1. 止血(低风险):injectMemory 的截断改成保留头尾(中间省略),至少让尾部的更正可见;
    2. 记账(中风险):给 memory_forget 之外补一层「取代」语义,让新行落库时能标记它取代了哪些行(而不是只靠标题撞车合并);
    3. 结构(需要设计定案):引入「事实」这一层,或者至少让 appendContent 在有明确更正时走覆盖——但这会动到 Bug5 的历史保留契约,得先定清楚。

    在你补一条信息之后我们可以更准:那两条(或三条)记忆行的 id、content 全文、updated_at,以及当时 memory_forget 传进去的是哪个 id。 面板的记忆列表或 memory_get 能直接拿到。有了这个我们就能确认到底是「同标题合并成一行」还是「两行并列」,两者的修法不一样。

    再次感谢——这条报告把「选择性遗忘」这条链路的语义缺口照出来了,价值比单个 bug 大。🙏

  2. modusensus commented on Nov 13, 2026

    @modusensus
    Member

    #532 的存储层半边已经在 #681 合并(merge commit 1fb1c4a)里修掉了:supersededIds() 的守卫从「未归档且未遗忘」收敛成只看 archived——遗忘是用户主动抛弃,不是版本失效,所以被遗忘的旧值不该再被当成「继任者」的判据。memory_forget 的回执现在会多报 predecessors / activePredecessors / chainActive 三个数。

    仍未做的一半(写侧):什么时候该生成一条「取代」关系,判断仍靠调用方(模型)自己给 supersedes,服务端没有独立的 LLM 判断。也就是说,这条 issue 标题说的「明确保留的更正事实答回旧值」,在继任者由模型正确写入的前提下已经不复现;但没有继任者被写进来时,它还在。

    另一条已知边界(本次未修,留档):继任者被物理删除时(单条删除与批量清理两条路径),JOIN 会失配,旧值照样回潮——与遗忘同构,但走的是另一条代码路径。要不要把它并进「选择性遗忘」的语义里,得先定口径。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:storage存储层(SQLite/表/索引)needs-decision要桉桉拍口径才能往下走

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions