不靠「最后写入者胜」的记忆同步

· 5 分钟阅读

两台机器,一套笔记,其中一台在另一台休眠时被编辑过。每个同步系统都要回答这个问题,而大多数的回答是比较时间戳,然后悄悄丢掉一边。对于一份可以重新敲一遍的草稿,这没问题。但对于 agent 每轮之前都要读的那个文件,这就不行了,因为被丢掉的,正是你为了防止某件事再次发生而写下的那一行。

所以 OHarness 的记忆同步完全没有「最后写入者胜」的路径。有的是三方合并;三方合并无法判定时,就产生一个冲突,等你来处理。

本地优先,你愿意的话也可以只在本地

笔记存放在 <project>/.oharness/memory/*.md。不登录也能用,离线也能用,而且是纯 Markdown,任何编辑器都能改。云端的任何部分都不是必需的:同步只是叠加在文件之上的一项功能,而这些文件没有同步时本身就是完整的。

登录后会开启自动同步,harness 运行期间大约 15 秒内就会检测到变化。「记忆」页面提供关闭同步和立即同步的选项。关闭状态在进程重启和重新登录后依然保留,并且会同时阻止自动和手动同步请求,直到你重新开启——一个下次启动就悄悄恢复原状的开关,算不上开关。

有一个限制在文档里就写明了,而不是等你事后发现:关闭同步会取消正在进行的请求,但云端已经接收的数据无法撤回。关闭阻止的是下一次上传,并不会删除已经在云端的内容。

三方,而不是双方

合并需要三个状态,而不是两个:这边的、那边的,以及双方上一次达成一致的版本。最后这个就是基线,保存在 ~/.oharness/memory-sync/,正是它把「这两份不一样」和「这一份改过了」区分开来。

在此基础上,各种情况都平淡无奇,而这正是目的所在:

  • 两边都没变——跳过,不读也不写
  • 只有一边变了——复制到另一边
  • 两边都变了——产生冲突,展示出来,留给你处理

删除用墓碑记录,而不是用「不存在」来表示,因为一个不存在的文件和一个还没传过来的文件无法区分,而把后者当成前者,正是同步系统在慢速网络下删掉你成果的方式。

在云端,R2 保存不可变的 Markdown 修订版本,外加一个以原子方式替换的清单文件:

users/<server-verified-user-id>/projects/<project-id>/memory/
  manifest.json
  objects/<note-id>/<revision-id>.md

清单写入使用 ETag 比较并交换(compare-and-swap),所以并发的过期更新会被拒绝,而不是被应用。修订版本不可变,所以冲突中落败的一方依然是一个存在的对象。当清单没有变化时,只会拉取清单本身——最常见的情况,也就是一台运行了一下午却无事可做的机器,只需要一次很小的读取。

项目的身份是 Git 根提交,正因如此,同一个项目的两个克隆即便位于两个不同的路径,也能被视为同一个项目。非 Git 项目退回到基于绝对路径的 id,因此不会自动在设备之间对应起来。这是一个真实的限制,而不是可以糊过去的粗糙之处:另一种做法是用启发式规则,而它偶尔会把两个毫不相干的目录合并起来,这种失败没有好的版本。

每个本地记忆目录都绑定到第一次同步它的账号和云端源。更改其中任何一项都会暂停同步,并要求你改用另一个目录。这里有意没有提供一键重新绑定,因为这个操作的一键版本,就是把一个人的笔记上传到另一个人账号里的那个版本。

它不是什么

有两点值得直说。

它不是端到端加密的。 R2 是私有的,所有传输都走 TLS,但云端能读取笔记内容。如果对你写下的内容来说这个取舍不合适,就关掉同步、让文件只留在本地——这是一种受支持的配置,而不是降级配置。

只有记忆笔记会同步。 会话日志不同步,文件不同步,凭据不同步,权限审计记录也不同步——最后这一项留在做出这些决定的那台机器上,并随会话一起删除。

此外还有一些限制,属于第一版的那种:每个项目最多 500 个清单条目(包括墓碑),每条笔记正文最大 128 KiB,每个序列化快照最大 2 MiB。旧修订版本会保留;修订版本和墓碑的垃圾回收尚未实现。被删除的笔记和冲突中落败的版本会备份到 ~/.oharness/memory-backups/,可以通过编辑器恢复,不过目前还没有备份浏览器。

为什么做成插件

记忆是一个内置的架构插件,而不是 MCP 服务器。它通过模型插件、工具插件和审计插件所走的同一个 PluginHost,注册它的工具、每轮的系统提示词索引、它的页面以及后台同步器——而在 plugins.json 里写上 { "id": "memory", "enabled": false },这四样就全部移除。

这正是一个插件系统要么通过、要么不通过的检验。如果内置功能是从一扇私门进入运行时的,那么公开的那扇门就只是装饰,所有基于它开发的人都会慢慢发现这一点。页面扩展保持精简也是同样的原因:注册的是描述符和由宿主负责的渲染器,而不是任意的前端 JavaScript,也不是 iframe SDK。一个什么都能做的接口,事后谁都改不动。


OHarness 是一套开源的 agent harness,采用 MIT 许可证。记忆与同步参考完整记录了冲突规则、存储结构和配置边界,自托管同步介绍了如何在你自己的基础设施上运行整套服务。