八个框架横向对比挑出了几条别人做得比我们好的地方。 这一页只回答接下来的问题:放进我们的结构里,每一条该怎么落,或者为什么不落。 权威正文在overview.md,机制图在 记忆子系统:机制图解。
对着 openprogram/memory/ 里跑着的代码核实过,行号照录。带实测的地方写出数字。
被抄的那家的原始形状,以及它为什么长成那样。形状不一定适用,动机通常适用。
放进我们结构后的方案,或者不放的理由。每条一个判决,没有"值得研究"这种结论。
先给结论。三条改造后采纳或直接采纳,一条否掉,理由都不是"不好",是我们的结构里那件事已经有别的东西在做,或者代价落在了错误的位置上。
claude-code的做法:每个记忆文件抬头写一句自述,检索时把这些描述列成清单,花一次便宜的小模型调用挑出五个文件名,全程不建索引。我们的检索是每轮都跑的BM25,跑在用户消息和模型调用之间。
topics/ 插一段没有脚注的介绍性段落,严格解析直接拒绝
(TopicFormatError: memory block ID required),blockquote 同样被拒,只有HTML注释能过。
也就是说"抬头写一句自述"落到我们的格式里得先改解析契约,而换来的信息路径名已经给了。
inspect.search 走的是 persist=False,
每轮把两棵目录树全量重新哈希和解析。结论的方向:每轮检索的预算问题是本地的,
本地问题能修;模型往返修不掉,只能不加。
否掉,不是因为它不好。 claude-code没有索引,这条路是它绕开索引的办法,同一份对比里也正是它把实现规模压到最小的原因。 我们已经付过建索引的代价,再花一次模型往返去买同一件事,买的是重复的东西,付的是每一轮的等待。 真要提升检索质量,先做的两件事都在本地:一是让每轮检索不再全量重解析,二是给结果加一个真正的分数下限。
openclaw裁 MEMORY.md 的时候只丢自己自动升上去的段落,人手写的一字不动,并把丢掉的日期列表返回给调用方。要抄这条得先答一个前置问题:core.md 在新结构里到底是什么。答完之后会发现,归属这件事本身可以不用再区分。
前置问题的答案:core.md 是派生视图,不是独立内容。
它是 topics/core.md 这一个主题文件的渲染结果,和 timeline/、
recent_events.jsonl、relations.json 同一类,手工编辑它不起作用,
编辑要落在正本上。写入 agent 的提示词随之从"更新 core.md"变成"要每次都看见的稳定事实写进
topics/core.md",它于是只需要认识一种文件。老实现"core.md 是某个指定页的镜像"这句话仍然成立,
只是指定页从 wiki 页变成了主题文件。
| 格子 | 我们改之前 | openclaw | 我们现在 |
|---|---|---|---|
| 谁写 | 写入 agent 直接编辑 core.md | 自动升级流程加人手编辑,同一个文件 | 谁都不写它。写 topics/core.md,Runtime 渲染 |
| 闸门 | 2000 token,超了抛异常management/config.py:7 | 10000 字符 | 2000 token 变成渲染预算,不再是异常 |
| 满了怎么办 | 整笔事务拒绝,修复话术让写入方绕开它,core.md 从此冻结 | 丢自动段落,人写的不动 | 按正本文件顺序装到预算为止,装不下的留在正本 |
| 丢了会不会没 | 不适用,从来没丢过 | 会,所以必须先分归属 | 不会,正本是全量,检索照旧命中 |
| 什么时候跑 | 没有任何时候。夜间重整只列 topics/ | 写入时按预算裁 | 每次写入事务安装成功后,跟着别的派生视图一起重建 |
| 说不说 | reorganize 返回 {"status":"ok","topics":N},没有 core 的位置 |
返回丢掉的日期列表 | 返回渲染进去的token数和被挤出去的块ID |
顺便补上"这次到底动没动"。夜间重整是模型判断,
而模型判断的失败模式是安静地什么都不做,并且在它自己的判据下它是对的:
同一套重整提示词,一个52万字符的单主题对话最后挤成一个34.4k字符的文件,重整跑了很多次一次都没拆它,
因为判据写的是"覆盖了两个主题"而不是大小。所以 reorganize 的返回要同时带上这一趟真正改过的文件列表,
空列表就是"这次什么都没动",让它可见。判据本身另说,但看不见的时候连该不该改判据都无从谈起。
pi-mono在用户离开一条分支时给它生成一份摘要接在树上。这正对着 overview.md 承认的缺口:两条写入路径都只要 head 底下那条分支,用户中途放弃的分支再也没有人走回去。目的照收,摘要节点不要。
为什么不做摘要节点。
pi-mono的会话JSONL树只为回放和分支服务,跨会话只剩 AGENTS.md,被放弃的分支不摘要就真的没了。
我们有一整层按主题存放的记忆,一条分支上说过的事实该去 topics/people/dave.md,
不该去挂在DAG上的一个摘要节点里,那里没有脚注、没有块ID、检索也进不去。
我们已经有一种挂在DAG上的摘要(context/summary 的压缩摘要),它服务的是上下文窗口,不是记忆,
再加一种只会让"这段话到底在哪"多一个答案。
而且"离开一条分支"在我们这里不是一个能观察到的事件。
事件注册表里没有任何 head 移动或者分支切换的事件;head 的写入散在五处
(set_head、append_message 的链式前进、SessionNodeWriter.append、
update_session、_rewind.py 直接改索引),它们唯一共同经过的地方是
SessionMemoryIndex.set_head,而那里把旧值直接丢掉了。
照抄 pi-mono 就得先在这条链路上开一个新事件、并让它记住上一个 head 是谁,
为的是触发一件在会话结束时做同样划算的事。会话边界那个入口已经在了:
session_watcher.py:103 每次只问了 head 那一条分支。
一个要一起改的判据。
should_incremental_write 现在有两条:欠够 token 阈值,或者欠着东西且最后一条消息已经过去一小时。
被放弃的分支最后一条消息一定是旧的,第二条会让一轮的重试兄弟也被写进去。
所以非head分支只走第一条,把 idle_after 关掉。这是这条方案唯一需要新增的判断。
claude-code在游标指的那条消息找不到时,选择把全部消息重新当成没写过,注释写明返回零条会让抽取在这个会话剩下的时间里彻底停摆。我们的写入游标方案里"从原话档案播种"是同一个取舍。问题是它只覆盖了迁移那一次,日常运行里还有一条同形状的路没有堵。
和迁移那条的分工。 overview.md 的「从位置游标迁移」处理的是一次性的:老状态没法反推出账,于是从原话档案播种, 宁可多写一批也不全丢。那是同一个取舍,但它只在升级那一刻发生。 这里说的是日常运行时账本本身出问题,和迁移无关,两条都要有,方向一致。
账本换了形状之后,这一条落在哪。
写入游标最后取的是在节点上打「已写」标记,不是记忆自己存一份 ID 集合,
所以没有一个会读不出来的游标文件了。这一条的方向仍然成立,只是换了对象:
账本读不出来时按「都没写过」处理,而不是抛异常让这个会话永远写不进去。
节点标记这边对应的是标记丢了就重做一遍,以及 RuntimeStateStore.load
对着一份解析不了的 runtime.json 应当读成空计数器而不是抛。
两种做法的并排对比在 written-marker.md 第三层。
三条采纳的方案,落到文件上是这些位置。否掉的那条不落任何文件。
节点marker已经替换位置游标。
实现位于SessionDB节点metadata、runtime/online.py和writing.py的
_records边界,legacy迁移从严格解析的Source前缀恢复可验证节点。
会话边界现在先处理当前head,再枚举其他活分支;共享前缀由节点marker去重。
完整定义见 written-marker.md。
实现状态(2026-08-11):01节的无索引模型选择按延迟实测拒绝;
02节topics/core.md正本与确定性core.md派生视图已实现;03节会话边界枚举
活分支已实现;04节损坏状态按空状态恢复并配合严格legacy迁移与节点marker实现。
当前仍使用轮后阈值和idle watcher,没有由head变化事件直接通知writer。