Skip to content

saveWithDedupe 的候选窗口固定 100 条,库一变大就不再合并(静默降级) #505

Closed

Description

@SternChiri

@modusensus

现象

同一 (type, title, scope) 第二次写入时,saveWithDedupe 返回 created 而不是 merged,同标题记忆开始堆积。库越小越不容易触发,库一大就稳定复现。

根因

service.js 取候选用的是:

const candidates = store.list({ type: memory.type, limit: 100 });

store.list 的 order 默认是 importance,排序子句是 ORDER BY importance DESC, updated_at DESC, id。也就是说候选集是「该类型最重要的 100 条」,与「找出同标题的那一行」没有关系。当某类型里 importance 高于待写行的行数超过 100 时,待写行与它的同标题行都不在候选集里,后面的 candidates.find(...) 恒不命中,于是走创建分支。

实测:一份约 2200 行的库,某类型占 1047 行,其中 importance=5 有 180 行;以 importance=3 写两次同标题数据,第二次返回 created。把 limit 改大或换成 all() 都立刻恢复合并,可以确认是这个窗口。

影响

  • 静默降级:不报错、不告警,合并语义随库增长而失效。
  • 覆盖面不止一处:memory_save 工具、standalone POST /memories、面板新建、以及第三方适配器的写入都走这条路径。

建议

把候选查询换成定向查询,而不是按 importance 取前 N 条:

SELECT id FROM memories
WHERE type = ? AND title = ?
  AND COALESCE(archived, 0) = 0 AND COALESCE(forgotten, 0) = 0
  AND TRIM(COALESCE(agent_scope, '')) = ?
  AND TRIM(COALESCE(workspace_scope, '')) = ?
  AND TRIM(COALESCE(sensitivity, '')) = ?

(或给 (type, title) 加索引后再按同样条件查。)合并逻辑本身不用动。

关于 _mergeInto

memory._mergeInto 这个显式并入目标的口子很有用(#127 提出、PR #143 实现):调用方在外部做定向查重后把 id 交回来,合并仍走 appendContent / content_history / 质量处置的原逻辑。我们目前就靠它在桥接侧兜底。建议保留这一行为,并在文档里写明。

现状

本项目(Mneme-Bridge)已在桥接侧用 _mergeInto 补位,覆盖自己的写入路径;remote 模式与 standalone daemon 侧没有插入点,只能等上游修复。

如有与上游设计不符的地方,请直接指出。

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions