记忆写入:八个框架横向对比

别人怎么把对话变成长期记忆,我们的选择落在哪一格,接下来要往哪走。 机制本身在 记忆子系统:机制图解,权威正文在 docs/reference/design/memory/overview.md。这页只做对照。

第一层 · 我们现在

绿色行

对着 openprogram/memory/ 里跑着的代码核实过的行为。每张表里都有一行,底色发绿。

第二层 · 人家怎么做

八个框架

references/ 下全部八家。没有这个功能的照样列出来,因为"不做"本身是一种选择。

第三层 · 我们的定案

紫色行

节点marker(第04、05、09节)和由topics/core.md生成常驻块(第07节)均已实现。 权威描述见 written-marker.mdoverview.md 的「常驻块」一节。

两条读之前要知道的:一,references/claude-code-leakedreferences/codex-cli 都是内部构建,比各自的公开版大得多,下面写的东西有一部分在公开版里没有。 二,所有结论都对着这两份快照里的实际代码,行号照录;找不到实现的地方写"只有调用点"。
01 有没有长期记忆 02 什么时候写 03 写什么 04 哪些还没写 05 分支和重开 06 检索 07 常驻块写满怎么裁 08 实现规模 09 我们的方案落在哪

01有没有长期记忆第二层

"关掉这次对话,下次还记不记得"。八家里四家记,四家不记。不记的那四家不是漏了,是把这件事推给了别人: 推给用户手写的 AGENTS.md,或者推给被它包起来的另一个进程。

四家自己存,四家推出去 左边是自己写、自己存、自己读回来的;右边是完全不存,靠别的东西顶上。 有长期记忆 没有,另有东西顶上 claude-code 纯文件:MEMORY.md 当索引,topic 文件放细节, 还能同步到团队共享的一份 纯文件 codex-cli MEMORY.md 加摘要页加技能目录,另有一个 SQLite 库调度两级蒸馏任务 默认是关的 openclaw MEMORY.md 是正本,SQLite 只是可重建的索引: 向量最近邻加全文,两路混合排序 文件加索引 hermes-agent 内置 MEMORY.md 加 USER.md 两个 markdown, 另可外接八家托管服务,同时只开一家 文件加外部服务 OpenProgram sources/ 存原话且只追加,topics/ 存提炼, core.md 是每次都进上下文的那一小块 纯文件 opencode 顶上的是用户手写的 AGENTS.md 或 CLAUDE.md。 工具目录里一个记忆工具都没有 pi-mono 会话是一棵 JSONL 树,但只服务回放和分支。 跨会话能带过去的只有 AGENTS.md pi-ai 只是调用各家模型的 SDK,连"会话"这个概念都没有。 消息数组由调用方自己拿着,每次全量传进来 weclaw 微信到 agent 的桥。只存一个不透明的会话 id, 记忆全归它拉起来的那个进程管 这四家都存了全部原文 codex、opencode、pi-mono 的转录文件都很完整,只是没人提炼、 也没人跨会话读。存了原文不等于有记忆。
框架有没有形态核实位置
claude-code纯文件:MEMORY.md 索引加 topic 文件加日期日志;团队版另同步一份到 HTTP 服务src/memdir/paths.ts:223-235
src/memdir/memdir.ts:34,348
src/services/teamMemorySync/index.ts:1-80
codex-cli有,但默认关着~/.codex/memories/MEMORY.md、摘要页、技能目录、会话摘要;SQLite 只调度蒸馏任务memories/write/src/lib.rs:116-118
state/memory_migrations/0001_memories.sql:1-35
默认关:features/src/lib.rs:996-999
openclawMEMORY.md 是正本,SQLite 是可重建的索引,向量最近邻加全文混合排序src/memory/root-memory-files.ts:4,8
extensions/memory-core/src/memory/manager-db.ts:10
.../manager-search.ts:148
opencode没有用户手写 AGENTS.mdCLAUDE.md;工具目录里没有记忆工具packages/opencode/src/session/instruction.ts:60-68
tool/ 目录整个列过
hermes-agentMEMORY.mdUSER.md;另可外接八家托管服务,同时只能开一家tools/memory_tool.py:56-60,138-173
agent/memory_manager.py:258-280
pi-ai没有provider SDK,12 个文件,没有会话概念全仓 grep 无命中;唯一落盘的是 OAuth 凭证
pi-mono没有会话 JSONL 树只为回放和分支;跨会话只有 AGENTS.mdpackages/coding-agent/src/core/session-manager.ts:428-436
weclaw没有只存不透明的会话 id,记忆交给被包的进程agent/cli_agent.go:25,226-231
OpenProgramMarkdown 工作区:sources/topics/core.mdopenprogram/memory/store.py:39
两条容易看走眼的:一,codex 这份内部构建里长期记忆是完整实现但默认关着的, 只看默认行为会得出"codex 没有记忆"的错误结论。二,weclaw 里唯一真正落盘的知识文件 (把粘进来的网址存成 markdown)是故意绕开 agent 的,存完之后也从没被任何提示词读回去。 分类要按"会不会被读回上下文"分,不能按"是不是 markdown"分。

02什么时候写第二层

四家会写的框架,触发方式两两不同:一家每轮派个后台分身去写,一家在每轮开头甩出去一个不管结果的后台任务, 一家挂在 cron 上定时写,一家让模型自己想起来就写。我们攒够量再写。

同样一串轮次,五种下笔时机 横轴是一次会话里连续的轮次。描边亮的方块表示这一轮触发了一次写入。 claude-code 每轮派个分身 这轮主 agent 自己 写过记忆了,跳过 分身继承主会话的系统提示词和消息前缀 codex-cli 每轮开头甩出去 每轮都甩,但真干活的条件很严:额度不够就退,会话闲置不到六小时就不碰。 任务从数据库里抢一把租约,谁抢到谁做,做完写回水位。 openclaw 挂 cron 定时 每天凌晨三点 会话里一轮都不写。短期条目先攒着,定时任务按六项加权分排名,够格的才升成长期段落。 hermes-agent 模型自己决定 模型自己调了记忆工具 满十轮,派个分身问一句要不要存 分身只是提醒,存不存、存什么还是模型说了算。 OpenProgram 攒够量再写 这一段加起来够 16k token 了 会话闲置,剩多少写多少 每轮都调一次,只在过线那轮真写
框架时机触发点
claude-code每轮结束派个分身;主 agent 自己写过就跳过这轮src/services/extractMemories/extractMemories.ts:348,598-603
互斥判定::121-148
codex-cli每轮开头甩出去一个不管结果的任务;只处理闲置超过六小时的会话,靠数据库租约防重app-server/src/request_processors/turn_processor.rs:578-591
memories/write/src/start.rs:23-81
memories/write/src/phase1.rs:149-187
openclawcron 定时,默认每天凌晨三点,按六项加权分决定谁能升上去src/memory-host-sdk/dreaming.ts:17,36-39
extensions/memory-core/src/dreaming.ts:163-187
opencode不写tool/ 下无记忆工具
hermes-agent模型随时自己调工具写;每十轮派个后台分身提醒一次tools/memory_tool.py:619-686
agent/conversation_loop.py:429-439
agent/background_review.py:34-43
pi-ai不写
pi-mono不写长期记忆;原文每个事件同步落盘packages/coding-agent/src/core/agent-session.ts:515
weclaw不写
OpenProgram每轮调一次,攒够 16k token 才真写;会话闲置强制写完;03:00 重整openprogram/memory/provider.py:30,117
这里有个共识:四家会写的里面,三家(claude-code、codex、openclaw)都把写入放在会话之外, 理由一样:正在对话的模型手上是任务,让它中途改记忆文件既费一次调用又跑题。 hermes 是唯一让主模型自己动手的,代价是记忆写不写全看它想不想得起来,所以才要每十轮提醒一次。 我们和这三家站一边。
codex 的一招值得记:它的写入任务从一张 SQLite 任务表里抢租约, 表上带重试次数和上次成功的水位,还先看一眼额度够不够再决定要不要开工。 我们现在靠一把跨进程写锁加一秒超时来串行化,效果类似但没有重试计数, 一批内容连续失败两次之后只留下事件总线上的一条记录。

03写什么第二层

原话,还是模型提炼过的事实。两者都留的四家(claude-code、codex、pi-mono、我们)走的是同一条路: 原话只追加不改,提炼可以随便重写,出了问题拿原话对账。

三种存法 只留原话 opencode · weclaw user: 预算表 4 月 15 号要交 assistant: 好,我先建个 Flask 项目 tool_use: write_file(app.py) 整段照抄进库里,只为把这次会话接回来。 没人从里面提炼,也没人跨会话读它。 下次新开一个对话,这些字等于不存在。 只留提炼 hermes-agent Craig 在用 Flask 做预算表, 截止 2024-04-15 原话不进记忆库。工具说明里直接写着 "别存流水账,那些去搜历史会话"。 好处是库小、读得快。 代价是提炼错了没有底稿可以对, 只能推翻重写。 两者都留 claude-code · codex-cli · pi-mono · OpenProgram 原话 只追加,不许改 sources/ 提炼 随便重写 topics/ 每句提炼带一个脚注指回原话 提炼是给模型看的,原话是用来对账的。 读到一句离谱的,能一路点回当时到底说了什么。 脚注这一条只有我们做了,其余三家都断在这儿。
codex 是两遍提炼,不是一遍。第一遍按会话做,把一次完整对话压成结构化的原始记忆和一份会话摘要; 第二遍另派一个没有网络、不问审批的子 agent,拿着全部第一遍产物加一份工作区差异, 重写 MEMORY.md、摘要页和技能目录。写的和整理的分成两个岗位, 和我们"写入"加"夜间重整"是同一个拆法,只是它拆在流水线里,我们拆在时间上。

04怎么知道哪些还没写第二层

八家给出五种进度记录:不记录、标量位置、单个消息身份、数据内标记、逐节点身份。 OpenProgram当前使用最后一种,本页参考框架中没有第二个相同实现。

游标的五种形状 同一串消息 m1…m6,各家用什么记住"写到哪了"。橙色是游标本体。 A · 不记 hermes · weclaw m1 m2 m3 m4 m5 m6 hermes 是模型自己想写就写,压根不存在"欠着"这回事。 weclaw 一轮结束把这一轮直接交出去,交完不管。 B · 记一个数 codex · OpenProgram 迁移前 m1 m2 m3 m4 m5 m6 写到第 3 条 codex 记的是时间戳水位,我们记的是序号, 都是一个能比大小的标量。 codex 的粒度是整个会话文件,不会撞上分叉;我们的粒度是分支里第几条,一分叉就废,见下面。 C · 记一个身份 claude-code · pi-mono m1 m2 m3 m4 m5 m6 lastUuid = "m3" 从数组头往后扫,扫到这个 id 就开始算新的。 claude-code 只存在进程里,重启就没了。 id 找不到了(比如被压缩清掉)就回退成"全都是新的",宁可重写一遍也不让抽取彻底停摆。 D · 标记塞进数据 opencode · openclaw m1 m2 m3 m4 m5 m6 summary: true 没有独立的游标文件,边界就长在数据自己身上。 openclaw 是同一招的两个变体:条目上写 promotedAt 时间戳, 正文里再埋一句 HTML 注释当去重标记,两边互相印证。 好处是游标不会和数据走散。代价是要改边界就得改数据本身。 E · 记一整套身份 OpenProgram 当前 m1 m2 m3 m4 m5 m6 metadata.memory_written_scriptorium 标记保存在各消息节点 当前分支从最新节点向前检查,遇到本工作区marker即停止;中间未标记节点就是待写后缀。 为什么只有我们要集合 A 到 D 全都建立在"消息排成一条线"上:不管是数值、单个 id 还是数据里的标记,答案都是"这一点之后的都算新的"。 一个会话里分出两条分支,"这一点之后"就同时指向两串不同的消息,这句话本身失效。见第 05 节。
框架形状存在哪核实位置
claude-code单条消息 UUID,向后扫进程内的闭包变量,不落盘,重启即失效src/services/extractMemories/extractMemories.ts:305-309,340,348
找不到就当全新::103-108
codex-cli整数时间戳水位,按会话文件粒度SQLite 任务表,带租约和重试次数state/memory_migrations/0001_memories.sql:3,7,17-31
memories/write/src/phase1.rs:149-187
openclaw条目上的 promotedAt 时间戳,加正文里埋的一句去重注释条目自己身上,加 MEMORY.md 正文extensions/memory-core/src/short-term-promotion.ts:84,1602-1612,1666-1669,1737
opencode消息上的 summary: true 布尔标记,加一个 tail_start_id消息自己身上packages/opencode/src/session/message-v2.ts:535-586
.../compaction.ts:386,437-442
hermes-agent记忆没有游标;落盘另有整数下标,压缩靠正文里的标记回扫agent/agent_init.py:1026
agent/context_compressor.py:37-51,1215-1226
pi-ai
pi-mono单条 entry id firstKeptEntryId,向后走写在压缩记录里,和转录同一个文件packages/coding-agent/src/core/session-manager.ts:67-77,315-422
weclaw无;只有一个按条数硬切的上限agent/http_agent.go:151-154
OpenProgram迁移前整数序号,位置runtime.jsonwritten-marker.md 第一层
OpenProgram当前节点上的workspace marker会话节点的metadatawritten-marker.md 第三层
抄到的一招:claude-code 在"游标指的那条消息找不到了"的时候,选择把全部消息重新当成没写过, 而不是返回零条。注释写得很直白:返回零条会让抽取在这个会话剩下的时间里彻底停摆。 重写一遍的代价是有限的,静默停摆的代价是没有上限的。我们的迁移方案里"从原话档案播种游标"是同一个取舍: 宁可多写一批,不可全丢。
还有一招:openclaw 的去重记两处并互相印证:条目侧记升级时间, MEMORY.md 正文里再埋一句带 key 的 HTML 注释。任何一边丢了,另一边还拦得住重复。 我们的集合只记在游标文件里,原话档案虽然也能当种子用,但两边不会互相校验。

05分支和重开第二层

这条对我们最要紧。八家里只有 pi-mono 一家像我们一样,把分支放在同一个会话内部。 其余的要么根本不分叉,要么把"分叉"实现成另开一个会话文件、父子之间连根指针。 别人的游标不必处理"同一条会话里两条分支共享前缀",因为它们没有这个东西。

用户回到第三条消息重新问一遍,三种处理 压根不分叉 weclaw · pi-ai m1 m2 m3 m4 m5 想重来只有清空重开一条。weclaw 一个微信联系人 只允许存在一条,旧的直接丢掉。 另开一条,连根指针 claude-code · codex · hermes opencode · openclaw 会话 A,原样不动 会话 B,记着父亲是 A 每条会话内部还是一条直线,游标照旧能用。 同一条会话内部分叉 pi-mono · OpenProgram 共享前缀 新分支 一个会话 id 底下同时有两串消息,前三条是共用的。 在会话内 DAG 上,"这条之后"是个歧义句 主干(已写) m1 m2 m3 m4 m5 m6 新分支(欠着) b4 b5 b6 序号说"写到第 6 条" → b4 b5 b6 编号 4 5 6,全被跳过 单 id 说"写到 m6" → 在分支这条线上根本扫不到 m6 集合说"m1…m6 都写过" → b4 b5 b6 不在里面,正好是欠的 pi-mono 同样是会话内的树,它没撞上这个问题是因为它的那个 id 只用来划压缩起点,不用来决定哪些进长期记忆。
框架能不能分叉怎么实现核实位置
claude-code能,功能最全/branch 整份复制成新会话;/rewind 能只回退对话、只回退文件或两者一起;子 agent 写到另一个文件src/commands/branch/branch.ts:61-173
src/components/MessageSelector.tsx:31
src/utils/sessionStorage.ts:247-258
codex-clicodex fork 和 TUI 里回退改旧提问,都是另开一条线程,源线程原样不动thread-store/src/types.rs:183-190
tui/src/app_backtrack.rs:7-8
rollout/src/recorder.rs:97-98
openclaw能,只在子 agent 上派子 agent 时可选继承父转录或者干净起步;没找到改旧消息重跑的实现src/agents/tools/sessions-spawn-tool.ts:190,307,343-344
改消息:grep 无命中
opencode能复制,回退是删fork 深拷贝成独立会话;revert 只是打个标记,下一次提问时把标记之后的消息真删掉packages/opencode/src/session/session.ts:733-774
.../revert.ts:100-134
.../prompt.ts:1155-1159
hermes-agent/branch 把整段历史复制进新会话,用父 id 连着;压缩也照样分出子会话,/resume 会自动顺着链找到最新那条cli.py:6682-6809
agent/conversation_compression.py:381-397
hermes_state.py:1851-1869
pi-ai没有会话
pi-mono能,会话内部就是树改一条旧消息就变成它的兄弟分支;离开一条分支时可以给它生成一份摘要留下packages/agent/src/harness/agent-harness.ts:788-812
.../branch-summarization.ts:200-262
weclaw不能一个微信联系人一条,/new 就是把旧的删掉messaging/handler.go:340-345
OpenProgram能,会话本身是 DAG重问、重试、派子 agent 都从中间分叉,一个会话 id 下多条分支共享前缀openprogram/store/session/session_store.py:988

上面回答的是"会话结构长什么样"。真正决定漏不漏的是下一个问题: 分叉之后,游标怎么办。八家给出三种办法,没有一家是"什么都不做",因为不做的后果各家都知道。

分叉之后,游标怎么办:三种办法 下标当游标的前提是"一个游标作用域里只有一条线"。分叉威胁的正是这个前提,各家的办法就是各自保住它。 办法一 · 换一个新的游标作用域,共享前缀重抽一遍 codex · claude-code · openclaw codex:fork出来是新的thread_id,而记忆游标那张表的主键就是thread_id,新线程没有行,于是从头抽。 openclaw:fork出来是新会话,冲刷标记被刻意清空,注释写着宁可在新会话里重提一次,也不要因为哈希撞上而漏掉。 claude-code:游标是进程里的一个闭包变量,同一个进程里接着走就沿用,换个进程起来就是空的,空的意味着全部当新的。 代价:共享前缀被抽第二遍。codex靠Phase 2整合分身在语义上合并,没有任何机械去重。它们接受这个代价。 办法二 · 把边界带过去,翻译成新那套坐标 opencode · hermes opencode:fork时每条消息都换一个新id,压缩边界tail_start_id跟着查表换成新id,所以前缀不必重压。 hermes:/branch把历史整段拷进新会话,然后在分叉那一行手动把下标推到拷贝的长度。 这一招成立的前提是分叉拷贝了一份,新那套坐标从头开始,所以旧下标能被翻译或者一次性推过去。 办法三 · 前缀根本不进视野 pi-mono 它和我们一样是会话内的树,但它要的是"这条分支相对最近公共祖先的增量":从叶子往回走到公共祖先就停。 共享前缀是被走法排除的,不是被游标排除的。所以它有树,却根本不需要为这件事记游标。 迁移前实现 · 三种都没做 当时分叉不换会话id、不拷贝节点,get_branch从head走到起点,而进度仍是一个位置下标;这正是节点marker迁移替换的实现。
框架分叉之后游标作用域共享前缀会不会重抽核实位置
codex-cli换新的:fork给一个新thread_id,游标表主键就是它会,整段重抽。只有Phase 2整合分身在语义上合并thread-store/src/types.rs:78-79,97
state/memory_migrations/0001_memories.sql:1-12
state/src/runtime/memories.rs:104-108
memories/write/src/phase1.rs:290
claude-code一个进程一个:游标是闭包变量,不落盘同一进程里接着走不会;换进程起来会,而且是有意的src/services/extractMemories/extractMemories.ts:305-307,86-88
src/utils/backgroundHousekeeping.ts:34-36
src/commands/branch/branch.ts:68,117-146
openclaw换新的:fork出新会话,冲刷标记刻意清空会。注释点名理由:宁可重提一次,不要因哈希相同而漏掉src/auto-reply/reply/session.ts:756-767,793-799
src/config/sessions/types.ts:370-371
src/auto-reply/reply/memory-flush.ts:117-123
opencode换新的,但边界跟着翻译:fork重发每条消息id,tail_start_id查表换新不会packages/opencode/src/session/session.ts:748-758,767-769
.../compaction.ts:244-248
hermes-agent换新会话,下标手动推到拷贝长度不会。压缩换会话时反而重置为0,因为新会话真的是空的cli.py:6701-6705,6744-6756,6776-6777
agent/agent_init.py:1026
agent/conversation_compression.py:379-380,407-408
pi-mono不需要:只取叶子到最近公共祖先的增量不会,被走法排除packages/agent/src/harness/compaction/branch-summarization.ts:87-97
packages/agent/src/harness/agent-harness.ts:737-834
pi-ai · weclaw没有会话分叉不适用weclaw agent/http_agent.go:76-78(/new就是清空)
OpenProgram迁移前不换:同一个会话id,同一个位置游标不重抽,但分叉后可能把未写节点判断为已写written-marker.md 第一层
OpenProgram当前不换作用域,从当前分支末端向前检查workspace marker不会;共享前缀节点已有marker,检查在该节点停止written-marker.md 第三层
一句话把这一格说完:凡是用下标或者时间戳当游标的,都靠"一个游标作用域里只有一条线"这个前提活着, 而它们保住这个前提的办法是分叉的时候换一个作用域:codex换thread_id,openclaw换会话, claude-code换进程,hermes干脆在分叉那一行把下标手动推过去。 换作用域之所以可行,是因为它们的分叉都拷贝了一份,新那边的坐标从头开始。 我们的分叉不拷贝,两条线共用一张图和一个会话id,作用域没得换,坐标也没得翻译, 于是唯一剩下的办法就是别再用坐标:记id,不记第几条。 pi-mono证明了这不是唯一解,它同样是会话内的树,靠"只取到最近公共祖先"把前缀挡在视野外; 但那要求每一次取都知道要跟哪条比,而记忆要的是"这个会话还欠什么",没有第二条线可比。
pi-mono 的一招值得记:用户离开一条分支的时候,它可以先给这条分支生成一份摘要接在树上, 分支被放弃了但里面说过的话不至于凭空消失。这正好补上我们 overview.md 里承认的那个缺口: 只有 head 底下那条分支会被写,用户中途放弃的分支再也没有人走回去。
opencode 有个反直觉的:revert 本身不删东西,但下一次提问一开始就会把标记之后的消息硬删。 也就是"改一条旧消息重跑"在它那里是不可逆的,想留住旧那条只能先 fork。 我们的 DAG 是两条都留着,代价就是要有办法说清哪条写过了。

06检索第二层

存下来的东西怎么回到上下文里。四条路:全量塞进去、关键词搜、向量搜、让模型自己挑。 第四条是 claude-code 独有的,也是最便宜的一条。

四条取回路线 全量塞进去 opencode · pi-mono AGENTS.md 整份进系统提示词, 不切块也不筛选。 要更细的东西,模型自己拿 读文件和 grep 工具去找。 零基础设施。 文件一大就把预算吃光。 关键词搜 hermes · codex · OpenProgram 我们用 BM25 打分取前五条, 每轮跑一次,不花模型调用。 hermes 用 SQLite 全文索引搜历史会话。 codex 更朴素,就是逐行字符串包含。 快、便宜、结果可解释。 换个说法就搜不到。 向量搜 openclaw SQLite 里做最近邻, 再和全文索引混合排序。 换个说法照样搜得到。 还有一个专门的子 agent 在你说话前 先替你搜一遍,把结论塞回主上下文。 要嵌入、要索引、要重建,两万多行里一大半是它。 让模型自己挑 claude-code 扫一遍所有记忆文件的抬头, 列成"文件名加一句描述"的清单, 交给一个小模型挑不超过五个。 提示词里还写了条反直觉的规则: 正在用的工具,它的用法文档别选, 但那个工具的坑和已知问题要选。 不建索引,多一次便宜调用。
最值得抄的一条。claude-code 的做法是:不建任何索引,只让每个记忆文件在抬头写一句自我描述, 检索时把这些描述列成清单,花一次便宜的小模型调用挑出五个文件名。 这条路我们现在没有,而它和我们的 topics/ 结构天生匹配。 它挑的判据也写得比"相关就选"细:拿不准就不选,宁可返回空。
codex 有个闭环值得记:模型用了哪条记忆,要在回答里埋一段隐藏标记说出来; 服务端把这段解析出来记一次使用计数,然后在正文里删掉再给用户看。 使用计数回过头喂给整理阶段的排序。用得多的留下来,没人用的自然沉底, 整个过程不需要任何人手工评分。我们的夜间重整目前只按内容合并,没有这种使用反馈。

07常驻块:写满了怎么裁第二层

"每轮都进上下文的那一小块记忆",八家里四家给它设了闸,一共六道。裁法只有两类, 没有一家把裁什么完全交给模型自由发挥:要么确定性截断,要么确定性拒绝然后让模型去改。

六道闸,加我们的一共七种裁法 左边一列是闸门大小,中间是超了之后发生什么,右边是这件事有没有被说出来。 闸门 超了怎么办 说不说出来 claude-code MEMORY.md 200 行 25000 字节 两道闸串着:先砍到 200 行,还超字节就退到最后一个换行处切,绝不切在半行中间。 字节闸是后加的,注释写着线上见过 200 行以内 197KB 的索引。 说。截完在正文尾巴 追加一条警告,点名 哪道闸触发、怎么改 claude-code SessionMemory 每节 2000 token 全文 12000 token 第一级:让模型自己压 提示词点名保住哪两节 第二级:按行确定性截断 模型压不下去时兜底 说。截过的地方留一句 "本节因长度被截断" codex-cli AGENTS.md 和摘要页 32 KiB 总预算 摘要页 2500 token 项目里多份 AGENTS.md 共用一份余额,按剩余字节直接切,切在哪算哪。 而全局那份 ~/.codex/AGENTS.md 完全没有闸,多大都原样进去。 基本不说。只写一条 日志,用户和模型 都看不见 openclaw MEMORY.md 10000 字符 故意低于注入闸 12000 按归属裁:只丢自己自动升上来的段落,从日期最老的开始丢。 人手写进这个文件的内容,一个字都不动。 说。返回值里带着 这次丢掉的是哪几天 openclaw 启动注入这一层 每份 12000 字符 总共 60000 字符 一般文件留头 75 尾 25。AGENTS.md 特殊:头 45、正则挑出的要点 35、尾 15 正则认标题和列表行,外加 must、never、security、commit 这类词,不是模型判断。 说。系统提示词里 另加一段截断警告, 默认每次都加 hermes-agent MEMORY.md / USER.md 2200 / 1375 字符 按字符不按 token 不裁,直接拒。这次写入原样退回,一个字都不落盘。 删哪条由模型接着决定,但它必须先被拒一次才知道满了。 说。拒绝信息直接回给 模型,带着当前用量 和上限两个数 OpenProgram core.md 2000 token tiktoken 实际数 整笔事务拒绝。不只是这一条,整批一起退,工作区保持逐字节不变。 给写入模型一句现成的修复话术:core.md 满了,别动它,这条放进 topic 文件。 说。修复指引直接给 写入模型,它还有 一次重来的机会
框架常驻块闸门满了怎么办谁决定说不说核实位置
claude-code MEMORY.md200 行 + 25000 字节先按行截,再退到最后一个换行处按字节截确定性正文追加警告,点名哪道闸src/memdir/memdir.ts:35,38,57-103
claude-code SessionMemory每节 2000 / 全文 12000 token先让模型压,压不动再按行确定性截先模型后确定性[... section truncated for length ...]src/services/SessionMemory/prompts.ts:8-9,67,185,256-324
claude-code CLAUDE.md40000 字符只是警告,不截什么都不做,只在状态面板提示"文件过大"状态面板提示src/utils/claudemd.ts:92,1132-1134
codex-cli项目 32 KiB;摘要页 2500 token;全局那份无闸按剩余字节直接切,切在哪算哪确定性只写一条日志core/src/config/mod.rs:211
core/src/agents_md.rs:94,116-124
ext/memories/src/lib.rs:16
openclaw MEMORY.md10000 字符只丢自动升上来的段落,按日期从老到新;人写的一律保留确定性,按归属返回丢掉的日期列表extensions/memory-core/src/memory-budget.ts:25,100-164
short-term-promotion.ts:1719,1762
openclaw 启动注入层每份 12000 / 总共 60000 字符一般文件头 75 尾 25;AGENTS.md 头 45 加正则挑出的要点 35 加尾 15确定性,正则挑系统提示词里加截断警告,默认每次都加src/agents/pi-embedded-helpers/bootstrap.ts:88-89,96-102,158-165
opencode没有记忆块AGENTS.md 无闸,确认不是没找到packages/opencode/src/session/instruction.ts:91-93,155-169
hermes-agent2200 / 1375 字符硬拒这次写入,让模型自己去 replace 或 remove确定性拒绝 + 模型选删谁拒绝信息带用量和上限回给模型tools/memory_tool.py:149,295-306,359-366
pi-ai没有
pi-mono有 AGENTS.md无闸,整份拼进去packages/coding-agent/src/core/system-prompt.ts:61-68
weclaw没有配置里的固定系统提示词,不含学到的东西config/config.go:28
OpenProgram topics/core.md → core.md2000 token渲染预算Runtime按确定性顺序生成预算内常驻块;Topic正本不删除确定性渲染未进入常驻块的Topic仍可检索openprogram/memory/management/config.py
.../block_views.py

上面那张表回答的是"这一刻超了怎么办"。还有一个问题它没回答,而我们的缺口正好落在那里: 写满之后,新的稳定事实还进不进得来。这取决于常驻块有没有人维护,以及它的正本是不是就是它自己。

谁在维护常驻块,写满之后还进得来吗 正本在哪,决定了裁一段是"这次没显示"还是"这条没了";有没有维护通道,决定了满了之后还进不进得来。 正本在哪 维护通道 写满之后新事实还进得来吗 claude-code MEMORY.md 主题文件目录。它是索引,不是记忆 提示词原话:绝不要把记忆内容直接写进MEMORY.md 有。后台dream分身,隔24小时且攒够5个会话触发 四段式提示词,最后一段就叫"剪枝和建索引",把它压回200行以内 进得来。写入路上根本没有闸 codex-cli memory_summary.md SQLite表,再渲染成一份原始记忆文件 常驻块是这条链的最后一环,全程派生 有。会话开始时触发的整合分身,隔6小时一次,整份重写 另有一条数据库剪枝:30天没被选用过的原始条目直接删掉 进得来。每一轮都重写一遍 openclaw MEMORY.md 它自己就是正本,人写的和自动写的混在一起 所以才必须按归属裁 有。每天03:00的cron,做梦管道跑完就往里升 升之前先按预算压一次,从最老的自动段落开始丢 进得来。丢光了也照样把新的写进去 注释原话:拒掉新写入等于吞掉最新材料 hermes-agent MEMORY.md / USER.md 它自己就是正本,世界上只有这一份 没有。八家里唯一没有维护通道的 grep过consolidate / distill / prune / compact / dream,零命中 进得来,但要模型自己先腾地方 拒绝信息回给的是正在对话的主模型 opencode · pi-mono 人手写的AGENTS.md,整份拼进去 不适用:没有自动写入,也就没有维护问题 不适用 OpenProgram当前 topics/core.md → core.md Topic文件是正本 core.md是Runtime按预算生成的派生视图 有。每次事务安装后确定性重建 夜间整理只修改Topic,派生视图随后重建 可以继续写入Topic 未进入常驻预算的内容仍可检索
框架正本维护通道与触发条件满了之后核实位置
claude-code主题文件目录的索引dream分身:满24小时且攒够5个会话写入路上没有闸,截断只在注入时做,磁盘原文不动src/memdir/memdir.ts:227,322-325,363
src/services/autoDream/autoDream.ts:63-66,140-141,166-171
src/services/autoDream/consolidationPrompt.ts:53-61
codex-cliSQLite表 → 原始记忆文件 → 常驻块整合分身:会话开始触发,成功后冷却6小时;另有30天未选用即删的数据库剪枝整份重写;注入时再按2500 token兜底截断state/src/runtime/memories.rs:21
memories/write/src/phase2.rs:139-166
memories/write/src/phase1.rs:110-133
ext/memories/src/lib.rs:16
openclaw文件自己每天03:00的cron,做梦管道跑完往里升升之前按预算丢最老的自动段落;全丢光也照样写入新的src/memory-host-sdk/dreaming.ts:17
extensions/memory-core/src/memory-budget.ts:25,100-164
.../short-term-promotion.ts:1706-1727
hermes-agent文件自己没有硬拒这一次写入,把用量和上限回给正在对话的主模型,由它replace或remove腾地方tools/memory_tool.py:149,295-306
consolidate / distill / prune / compact / dream全部无命中
opencode · pi-mono人手写的文件不适用没有闸packages/opencode/src/session/instruction.ts:60-93
packages/coding-agent/src/core/resource-loader.ts:57-100
OpenProgram迁移前根目录core.md没有;organize_topics只处理topics/**.md事务拒绝超预算写入memory-adoption.html 第02节
OpenProgram当前topics/core.md每次事务安装后与其他派生视图一起确定性重建按预算渲染,未进入常驻块的Topic内容仍可检索overview.md「常驻块」
采用记录。claude-code、codex和openclaw都把可编辑正本与注入预算分开;OpenProgram按同一原则把 topics/core.md作为正本,core.md作为确定性派生视图。与它们不同的是,OpenProgram不删除 超出常驻预算的Topic内容,检索仍可返回这些block。迁移前根目录core.md直接充当正本且超预算时拒绝事务, 该实现已经替换。
给"常驻块写满怎么裁"的结论:七道闸里没有一道让模型自由判断该丢什么。 五道是确定性截断(claude-code 的 MEMORY.md、codex、openclaw 的两层,加 claude-code SessionMemory 的兜底那级), 两道是确定性拒绝再把选择权交回模型(hermes、我们)。 唯一沾一点模型判断的是 claude-code 的 SessionMemory,做法是先让模型压、压不动再确定性兜底, 而且判据写死在提示词里点名保住哪两节,不是让它自由发挥。 四条可以直接用的经验:一,闸门要两道,行数管不住长行,claude-code 的字节闸就是被一份 200 行 197KB 的索引逼出来的; 二,裁的时候看归属比看时间重要,openclaw 只丢自己写的、人写的一律不动; 三,要裁就整份都要裁到,codex 给项目级 AGENTS.md 设了闸却漏了全局那份,claude-code 反过来给自己管的 MEMORY.md 设了硬闸、给人管的 CLAUDE.md 只警告不截; 四,裁完必须留痕,codex 是唯一只写日志的,模型和用户都不知道自己少看了东西。 我们的core.md现在在"确定性拒绝加留痕"这一格。 第一条和第二条我们不打算照抄:第二道闸仍然是闸,仍然在写入的路上,加了它还是会把稳定事实挡在外面; 归属区分之所以要紧,是因为openclaw的常驻文件是唯一一份、丢一行等于销毁一行, 而把core.md变成派生视图之后丢不再等于销毁,归属也就不用判了。 判决和理由在对标之后抄什么第 02 节。

08实现规模第二层

不含测试。彩条是长期记忆本身,灰条是压缩和转录存储。同一件事从 0 行到三万四千行都有人做, 差别几乎全在"要不要自己建索引"。

记忆相关代码行数 条长按同一比例。灰条不算长期记忆:转录落盘和上下文压缩每家都要做,和记不记得住无关。 openclaw 34000 向量、全文、做梦管道、升级排序。另外两个可换的后端还有 12000 行,没算进来 codex-cli 8900 + 3700 两级蒸馏加读路径 另有约六万行转录存储和分叉,不属于记忆 OpenProgram 6380 写入、事务、BM25、主题格式、夜间重整。全部是长期记忆,没有一行是压缩 claude-code 4900 + 4300 记忆目录加抽取分身 另有 1500 行 CLAUDE.md 加载器和 2200 行团队同步 hermes-agent 2800 + 2400 另有八个外接服务插件共 11600 行,默认全关 pi-mono 5000 全是会话树和压缩,长期记忆 0 行;而且同一套逻辑写了两遍 opencode 1550 压缩加存储,长期记忆 0 行 weclaw 200,全是会话 id 记账 pi-ai 0,一行都没有
复杂度都花在检索上。openclaw 的三万四千行里,向量、嵌入缓存、索引重建、混合排序占了绝大部分; 去掉这一层,它剩下的部分和 claude-code 的四千九百行是同一个量级。 我们六千三百行里没有向量必需项(嵌入是可选的),多出来的部分是事务和主题格式的校验。 真正的分水岭不是"有没有记忆",是"要不要自己维护一个索引"。

09我们的方案落在哪第三层

前八节给出了当前方案在各维度上的位置。多数维度复用已有框架的成熟做法; 节点marker由SessionDAG的分支语义决定,原因见第05节。

七个维度上,我们和谁站在一起 存什么形态 纯文件,和 claude-code 一样 openclaw 加了索引,hermes 可以外接服务。文件的好处是能直接读、能进 git 谁来写 会话之外的分身,和 claude-code、codex、openclaw 一样 四家里三家这么做。hermes 让主模型自己写,想不起来就不写 写什么 原话和提炼都留,和 claude-code、codex、pi-mono 一样 而且每句提炼带脚注指回原话,这一条只有我们做了 检索 关键词 BM25,和 hermes、codex 同一档 缺 claude-code 那条"让小模型挑文件"的路,见第 06 节 常驻块闸门 确定性拒绝加留痕,和 hermes 一样 topics/core.md是正本,core.md按2000 token预算确定性生成,见第07节 夜间整理 有,和 claude-code、codex、openclaw 一样 四家都想通了同一件事:只写不整理,文件永远只会变长 会话结构 会话内 DAG,只和 pi-mono 一样 其余六家要么线性,要么把分叉做成另一个会话文件 游标 已写消息 id 的集合,没有第二家这么做 因为上面最后一行:会话结构不一样, 别人的游标形状搬过来就是错的。 这不是我们比别人想得多。是别人的会话排成一条线,"这一点之后都算新的"这句话在他们那里没有歧义, 在我们这里有。同一个道理,pi-mono 有会话内的树却不需要集合,因为它的那个 id 只用来划压缩起点,不决定谁进长期记忆。 形状是被会话结构逼出来的,不是选出来的。
方案本身在别处。标记写在哪、什么时候打上、分叉时怎么走、标记丢了怎么办、从位置游标怎么迁移, 权威描述在 written-marker.md,机制图在 memory-architecture.html 第 04 节。这一页只回答一个问题: 这个形状和外面各家是什么关系。
对标决策的当前状态。 已实现:topics/core.md作为常驻块正本、节点workspace marker、会话边界枚举活分支、Topic脚注保留Source引用。 尚未实现:给Topic生成自描述后由小模型选择文件的无索引检索路径(claude-code,第06节);把检索使用情况作为夜间整理输入(codex,第06节);在head变化时直接通知writer,而不是等待轮后阈值或idle watcher。
实现记录(2026-08-11):八家中四家有长期记忆;按消息分支的会话结构只见于pi-mono、openclaw和OpenProgram。 OpenProgram已经用节点workspace marker替换位置游标,并用topics/core.md提供可维护的常驻块正本。 本页仍保留迁移前行作为问题证据,紫色行表示当前实现,不再表示未来计划。