Skip to content

[Bug]: 运行时记忆 remove/replace 无法删除单字符条目(oldText 子串唯一匹配死锁) #217

Description

@baizhancaiji

提交前查重 / Duplicate check

  • 我已搜索 open/closed Issue,确认没有重复。 / I searched open and closed issues and found no duplicate.

涉及区域 / Affected area

运行时记忆 / Runtime Memory

Issue 类型 / Issue type

Bug 报告 / Bug report

摘要 / Summary

运行时记忆(Runtime Memory)的 remove 与 replace 通过 entry.content.includes(oldText) 做子串匹配,并要求命中数恰为 1。当某个条目内容只有一个字符(例如 "X"),且该字符也出现在同一 target 的其他条目中时,任何候选 oldText 都会命中多个条目,导致该条目既无法删除也无法替换。

预期结果 / Expected outcome

期望:删除/替换能基于条目的唯一标识(如稳定 id、index,或"内容完全相等"的精确匹配)定位,而不依赖"子串必须唯一"。具体地,用户应能删除内容为 "X" 的运行时记忆条目;删除后该条目不再出现在每轮上下文(MEMORY.md 投影)中,且不报 Multiple ... contain 错误。

详情或复现步骤 / Details or reproduction steps

最小复现(target=memory,工具或 UI 任一即可):

  1. 确保存在一条内容恰好为 "X" 的运行时记忆条目。
  2. 在同一 target 中再加入另一条包含大写 X 的条目(例如含 EGO_LINUX_CHROME 的条目——LINUX 里的 X)。
  3. 调用 mnemon_runtime_memory(action=remove, target=memory, old_text="X"),或在 UI 上对 "X" 条目点击"移除"。
  4. 观察到:Error: Multiple memory entries contain "X"; use a unique substring.,条目未被删除。

注意:无需手动构造冲突,未设置 branches 的热记忆在 WSL2 环境下常含 EGO_LINUX_CHROME 这类含 X 的子串,很容易自然踩中。

环境信息 / Environment

  • DSH:dsh (web) 运行于 WSL2(宿主 Windows 10,不支持 WSL2 mirrored)
  • dsh-mnemon:0.5.5
  • dsh-mnemon-source-runtime:0.5.4
  • Mnemon:0.2.5
  • Node.js / pnpm:node v24.19.0
  • OS / 浏览器:WSL2 Ubuntu 22.04,browser 为 GitHub 网页(Edge/Chrome)
  • Provider / 相关配置:provider = mnemon-native(默认);persistenceStrategy = manual(仅允许 mnemon-native)

Bug 证据 / Bug evidence

Host / CLI 路径的实际报错:
Error: Multiple memory entries contain "X"; use a unique substring.

冒烟测试 / Smoke test

  • 测试环境:同上(node v24.19.0,dsh-mnemon 0.5.5,provider=mnemon-native)
  • 执行命令或操作:直接调用 mnemon_runtime_memory,action=remove, target=memory, old_text="X"
  • 实际结果:报 Multiple memory entries contain "X"; use a unique substring.,"X" 条目未被删除,运行时热记忆条目数量不变。
  • 对照实验(控制组):
    1. 先 replace 把另一条含 X 的条目中 EGO_LINUX_CHROME 临时改写为 ego_linux_chrome(消除大写 X)→ 成功;
    2. 再 remove old_text="X" → 成功(Entry removed.);
    3. 再 replace old_text="ego_linux_chrome" 还原为 EGO_LINUX_CHROME → 成功,且该条目与操作前 sha256 逐字节一致。
    • 结论:问题确由"X 同时是另一条目的子串"导致,而非该条目本身损坏或数据缺失。

引用代码 / Code references

仓库内源码位置(omdsh-dev/dsh-mnemon,main @ 820b0b0):

  • plugins/dsh-mnemon-source-runtime/src/controller.ts:248-253 —— 匹配与报错核心(prepareMutation):
    248: const oldText = normalizeContent(request.oldText, 'oldText')
    249: const matches = entries
    250: .map((entry, index) => entry.target === request.target && entry.content.includes(oldText) ? index : -1)
    251: .filter(index => index >= 0)
    252: if (matches.length === 0) throw new Error(No ${request.target} entry contains ${JSON.stringify(oldText)}.)
    253: if (matches.length > 1) throw new Error(Multiple ${request.target} entries contain ${JSON.stringify(oldText)}; use a unique substring.)

  • plugins/dsh-mnemon-source-runtime/src/client/pages.tsx:78-81 —— UI "移除"按钮:
    78: const remove = async (entry: RuntimeMemoryEntry) => {
    81: await mutate({ action: 'remove', target: entry.target, old_text: entry.content })

说明:安装后经打包的 node_modules 内对应 bundle 为 dsh-mnemon-source-runtime/lib/index.js:209-212 与 lib/client.js:294-301,与上述源码一一对应;issue 请以上述仓库内源码路径为准。

补丁草案 / Patch proposal

建议:为 mutation 增加"内容完全相等"的精确优先匹配;更彻底是给每条运行时记忆分配稳定 id,用请求中的 id 定位。最小改法(概念 diff,放于 plugins/dsh-mnemon-source-runtime/src/controller.ts 的 prepareMutation 中,约 249-253 行):

--- a/plugins/dsh-mnemon-source-runtime/src/controller.ts
+++ b/plugins/dsh-mnemon-source-runtime/src/controller.ts
@@ (prepareMutation matches 计算,约 249-253 行)
-  const matches = entries
-    .map((entry, index) => entry.target === request.target && entry.content.includes(oldText) ? index : -1)
-    .filter(index => index >= 0)
+  // 优先精确匹配:内容完全等于 oldText 且唯一时优先命中,
+  // 解决"单字符条目且该字符也出现在其他条目"导致的死锁
+  const exact = entries
+    .map((entry, index) => entry.target === request.target && entry.content === oldText ? index : -1)
+    .filter(index => index >= 0)
+  const matches = exact.length === 1
+    ? exact
+    : entries
+        .map((entry, index) => entry.target === request.target && entry.content.includes(oldText) ? index : -1)
+        .filter(index => index >= 0)

说明:当且仅当恰好一条条目内容精确等于 oldText 时优先命中它,否则退回子串匹配(保持现有兼容行为)。更彻底的方向:为条目分配稳定 id,remove/replace 按 id 定位,并对内容碰撞提供更明确的判别。

### 补充信息 / Additional context

_No response_

Activity

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions