oh-my-pi 记忆编辑指南:深入解析memory_edit的 update / forget / invalidate 操作与 memory:// 协议
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
导读
memory_edit是 oh-my-pi(coding agent with the IDE wired in)在启用 Mnemopi 本地长期记忆后端时提供的记忆维护工具:它允许 Agent 按 ID 对长期记忆执行update(更新工作记忆)、forget(硬删除工作记忆)、invalidate(软作废旧记忆,可指定替代记忆)三种操作。本文以 memory-edit.md 为骨架,结合 memory-edit.ts、mnemopi/state.ts、Mnemopi Beam 存储层实现与契约测试,讲清每条编辑规则背后的源码依据、参数语义与安全操作流程。读完你将掌握:何时该用哪种编辑操作、为什么 fact 记忆只读、为什么 update 前必须先read memory://<id>,以及每一步在仓库源码中的具体落点。
前置条件:Mnemopi 后端与工具注册
memory_edit并非默认可用。它的工厂函数在 memory-edit.ts 中做了严格的门控:
static createIf(session: ToolSession): MemoryEditTool | null { const backend = session.settings.get("memory.backend"); if (backend !== "mnemopi") return null; return new MemoryEditTool(session); }即只有当memory.backend配置为mnemopi时,工具才会被注册进内置工具表(见 tools/index.ts 的memory_edit: MemoryEditTool.createIf)。契约测试 memory-tools.test.ts 验证了这一点:off、hindsight后端下createIf返回null,而mnemopi后端下retain/recall/reflect/edit四个工厂均返回工具实例。
启用 Mnemopi 的最小配置(详见 mnemosyne-memory-backend.md):
memory: backend: mnemopi可选的常用参数:
mnemopi: scoping: per-project-tagged # global / per-project / per-project-tagged autoRecall: true # 会话首轮自动召回 autoRetain: true # 自动留存已完成的对话轮次 retainEveryNTurns: 4 # 自动留存的最小用户轮次间隔 recallLimit: 8 # 提示词块中最大召回条数记忆属于“背景上下文”而非指令:当它与当前用户消息或工具输出冲突时,以当前用户消息和工具输出为准(见 mnemosyne-memory-backend.md)。这也是为什么编辑记忆需要谨慎——你修改的是影响未来会话召回的背景信息。
memory_edit的输入参数与操作语义
工具的参数模式定义在 memory-edit.ts:
const memoryEditSchema = type({ op: type("'update' | 'forget' | 'invalidate'").describe("memory edit operation"), id: type("string").describe("memory id from recall output"), "content?": type("string").describe("replacement content for update"), "importance?": type("number").describe("replacement importance for update (0–1)"), "replacement_id?": type("string").describe("replacement memory id for invalidate"), });| 参数 | 类型 | 必填 | 含义 |
|---|---|---|---|
op | 枚举'update' \| 'forget' \| 'invalidate' | 是 | 要执行的编辑操作 |
id | string | 是 | 来自recall输出的记忆 ID |
content | string | 否(update 时需二选一) | update的替换内容 |
importance | number (0–1) | 否(update 时需二选一) | update的替换重要性 |
replacement_id | string | 否 | invalidate时记录的替代记忆 ID |
三种操作的语义,与文档 memory-edit.md 一一对应:
update:面向工作记忆(working memory),替换其内容和/或重要性。适用于更正过时、不准确或表述不佳的既有记忆。update是整体替换(wholesale replace)——传入content会整段覆盖旧内容,因此必须先读取完整内容再合并,防止丢失未预览的尾部。forget:永久删除工作记忆(hard delete),面向需要硬删除的内容。删除是物理性的:Beam 层的forgetWorking会执行DELETE FROM working_memory并顺带清理该记忆的派生工件(见下文源码剖析)。invalidate:软作废(softly supersede),面向仍可能有用历史的过时记忆。它不删除任何行,只打上失效标记并记录替代记忆 ID,历史仍可被追溯。
工具在调用前对参数做了一层前置校验(memory-edit.ts):若op === "update"且content与importance都未提供,直接抛错"memory_edit update requires content or importance."。同时importance会被夹紧到[0, 1]区间(Math.max(0, Math.min(1, params.importance)),见 memory-edit.ts),与模式描述中的(0–1)约定一致。
工具执行时会调用会话状态的editScopedMemory(memory-edit.ts),并把结果(含所在 bank / store)格式化为人类可读的返回文本;对not_found与not_editable两种失败场景返回不同提示,其中 fact 记忆会提示Read it with memory://<id>.(memory-edit.ts)。
编辑的分层路由:从工具到 Beam 存储
一次memory_edit调用跨越三个层次,理解这条链路有助于定位任何异常行为:
- 工具层(Tool):
MemoryEditTool.execute负责参数校验、importance 夹紧、调用state.editScopedMemory(op, id, {...})、格式化结果文本(memory-edit.ts)。 - 会话状态层(Session State):
MnemopiSessionState.editScopedMemory负责在多个 bank 之间按顺序解析目标记忆、判定可编辑性、分派具体操作(mnemopi/state.ts)。 - 存储层(Beam):
Mnemopi门面把update/forget分别转发给beam.updateWorking/beam.forgetWorking(memory.ts),invalidate直接调用beam.invalidate(beam/index.ts)。
会话状态层:作用域解析与可编辑性判定
editScopedMemory是编辑语义的核心裁决者。它先把会话可触及的 bank 去重排序为「retain 目标、recall 目标、global bank」(mnemopi/state.ts),然后按顺序查找目标记忆,命中即中止(first hit wins)。对每条命中的记忆,它根据memory_store字段判定归属:
memory_store === "fact":返回not_editable——fact 表是只读的,任何编辑操作都不修改 facts 表,因此即使 ID 能解析也必须精确报告为不可编辑(mnemopi/state.ts,注释引用了 issue #4725)。op为update或forget但store !== "working":返回not_found——这两类操作只面向工作记忆(mnemopi/state.ts)。update命中工作记忆:调用target.memory.update(id, content, importance)(mnemopi/state.ts)。forget命中工作记忆:调用target.memory.forget(id)(mnemopi/state.ts)。- 其余走
invalidate:调用target.memory.beam.invalidate(id, replacementId),可同时作用于工作记忆与情景记忆(episodic memory)(mnemopi/state.ts)。
若所有 bank 都未命中,返回not_found;若中途遇到不可编辑目标但最终没有成功操作,则返回首个ineligible(即not_editable或not_found),保证错误信息尽量精确(mnemopi/state.ts)。
存储层:三种操作的真实 SQL
Beam 存储层的实现把文档语义落到具体 SQL(store.ts):
updateWorking(store.ts):动态拼接 SET 子句——提供content时更新content并置空embed_text(触发重新嵌入调度scheduleEmbedding);提供importance时更新importance。两者都未提供则返回false。更新成功后使查询缓存失效(invalidateCaches),保证后续召回能看到新内容。forgetWorking(store.ts):在事务中执行DELETE FROM working_memory WHERE id = ? AND session_id = ?,命中后调用purgeWorkingMemoryArtifacts清理关联工件,并同样使缓存失效。注意 WHERE 条件带session_id——删除被限定在调用者所属会话作用域内。invalidate(store.ts):执行两条UPDATE——先尝试working_memory,未命中再尝试episodic_memory,将valid_until置为当前时间、superseded_by置为replacement_id。这是一种软删除:行仍然存在,但召回查询统一以valid_until IS NULL OR valid_until > now且superseded_by IS NULL为过滤条件(见 recall.ts),因此被作废的记忆不会再进入召回结果,历史却完整保留。superseded_by字段让替代记忆与被替代记忆之间形成可追溯的链接。
从 schema 看,working_memory与episodic_memory两张表都定义了valid_until TIMESTAMP DEFAULT NULL与superseded_by TEXT DEFAULT NULL(schema.ts),这正是invalidate软作废机制的物理基础。
只读的 fact 记忆与 not_editable
文档明确:recall结果中标记为[facts]的是只读事实,任何编辑操作都会返回not_editable(memory-edit.md)。仓库证据有两处:
- 存储层注释直接声明:
memory_store: "fact"标记为只读——不允许 update/forget/invalidate(store.ts)。 - 状态层的
editScopedMemory对 fact 专门返回not_editable,且即使后续 bank 也找不到该 ID,仍保留这个精确错误而非退化为not_found(mnemopi/state.ts)。
对 fact 记忆,正确做法是用read memory://<id>检视内容(只读查看),而不是编辑。这保证了事实性知识(例如实体关系、已知约束)不会被 Agent 随意改写。
为什么 update 前必须read memory://<id>
这是本工具最重要的安全约束,文档用加粗的 MUST 强调(memory-edit.md):
MUST read full memory before
update. Recall previews clipped: trailing…marks truncation;full_lengthoriginal size.updatereplaces content wholesale → updating a preview deletes its unseen tail. Firstread memory://<id>; pass merged content incontent.
原因链如下:
- 召回结果是预览(preview):
recall返回的是截断的内容预览,超出预览上限的部分以尾随…标记,并附带truncated: true与full_length(原始长度)字段(见 recall.md)。 - update 是整体替换:
updateWorking直接用新content覆盖旧内容(store.ts),没有增量合并机制。因此若拿一段被…截断的预览去 update,被省略的尾部会被永久抹掉。 memory://<id>返回完整行:getScopedMemory从 retain/recall/global 各 bank 按序查找并组装完整行(含 content、source、timestamp、importance、veracity、metadata 等),其设计目的正是支撑memory://<id>读取,让 Agent 在整体替换前看到未裁剪的全文(mnemopi/state.ts 的注释明确引用 issue #4443)。
memory://属于内部 URL 协议族(agent://、artifact://、memory://、skill://等,见 internal-urls/types.ts)。其中memory://<id>这一命名空间只有在memory.backend=mnemopi时可用——在 Hindsight 后端下read memory://<id>会得到明确纠错提示(memory-protocol.ts)。而memory://root则指向项目级记忆摘要文件(system-prompt.md),与本工具的按 ID 编辑无关。
安全的 update 流程
- 用
recall定位目标记忆,记录其id。 - 用
read memory://<id>获取完整内容(而非被截断的预览)。 - 在完整内容基础上合并你的修改,构造新的
content。 - 调用
memory_edit:op: "update", id: <id>, content: <合并后的完整内容>。
如何选择:invalidate 与 forget 的取舍
文档给出的决策准则(memory-edit.md):
Prefer
invalidatefor stale memory whose history may still be useful. Useforgetonly for content requiring hard deletion.
| 场景 | 推荐操作 | 理由 |
|---|---|---|
| 记忆过时但仍可能有追溯价值(如决策沿革、曾被采纳后被推翻的方案) | invalidate | 软作废:保留历史,可选replacement_id关联替代记忆,召回自动过滤 |
| 需要物理删除的内容(隐私、错误数据、敏感信息) | forget | 硬删除:DELETE行并清理派生工件,不可恢复 |
| 记忆内容不准确或表达欠佳,主体仍有价值 | update | 保留同一 ID,整体替换内容/重要性 |
invalidate的replacement_id参数把新旧记忆串成superseded_by链:invalidateSQL 会把superseded_by写成替代记忆的 ID(store.ts),使“谁取代了谁”在数据层面可查询。这一软删除设计也避免了硬删除导致的引用悬空:作废行仍可被合并、压缩或人工审计读到,只是不再出现在面向 Agent 的召回结果中。
错误信息速查
memory_edit的可能返回状态(来自editScopedMemory的结果与工具格式化逻辑,memory-edit.ts):
| 状态 | 含义 | 提示文本示例 |
|---|---|---|
updated/deleted/invalidated | 操作成功,并附带所在 bank(及 store) | Memory <id> updated in bank <bank> (<store>). |
not_found | 在任何作用域 bank 中都找不到该 ID | Memory <id> was not found[ in bank …]. |
not_editable | 命中 fact 只读记忆 | Memory <id> is a read-only fact…; cannot be edited. Read it with memory://<id>. |
此外工具层会先抛参数错误:memory_edit update requires content or importance.(memory-edit.ts);若会话尚未初始化 Mnemopi 状态,则抛Mnemopi backend is not initialised for this session.(memory-edit.ts)。
总结
memory_edit是 oh-my-pi Mnemopi 记忆体系中的“写操作”入口,与recall(读)、retain(写新)、reflect(综合)共同构成完整的记忆生命周期管理。它的设计处处体现数据安全优先:fact 只读、update 强制先读全文、invalidate 软作废优于 forget 硬删除、SQL 更新带session_id/scope作用域约束。在使用时牢记三条铁律即可:只编辑recall返回的 ID;update前先read memory://<id>;能invalidate就别轻易forget。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考