别人怎么把对话变成长期记忆,我们的选择落在哪一格,接下来要往哪走。
机制本身在 记忆子系统:机制图解,权威正文在
docs/reference/design/memory/overview.md。这页只做对照。
对着 openprogram/memory/ 里跑着的代码核实过的行为。每张表里都有一行,底色发绿。
references/ 下全部八家。没有这个功能的照样列出来,因为"不做"本身是一种选择。
节点marker(第04、05、09节)和由topics/core.md生成常驻块(第07节)均已实现。
权威描述见 written-marker.md 和 overview.md 的「常驻块」一节。
references/claude-code-leaked 和
references/codex-cli 都是内部构建,比各自的公开版大得多,下面写的东西有一部分在公开版里没有。
二,所有结论都对着这两份快照里的实际代码,行号照录;找不到实现的地方写"只有调用点"。
"关掉这次对话,下次还记不记得"。八家里四家记,四家不记。不记的那四家不是漏了,是把这件事推给了别人:
推给用户手写的 AGENTS.md,或者推给被它包起来的另一个进程。
| 框架 | 有没有 | 形态 | 核实位置 |
|---|---|---|---|
| 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 |
| openclaw | 有 | MEMORY.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.md 或 CLAUDE.md;工具目录里没有记忆工具 | packages/opencode/src/session/instruction.ts:60-68 tool/ 目录整个列过 |
| hermes-agent | 有 | MEMORY.md 加 USER.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.md | packages/coding-agent/src/core/session-manager.ts:428-436 |
| weclaw | 没有 | 只存不透明的会话 id,记忆交给被包的进程 | agent/cli_agent.go:25,226-231 |
| OpenProgram | 有 | Markdown 工作区:sources/ 加 topics/ 加 core.md | openprogram/memory/store.py:39 |
四家会写的框架,触发方式两两不同:一家每轮派个后台分身去写,一家在每轮开头甩出去一个不管结果的后台任务, 一家挂在 cron 上定时写,一家让模型自己想起来就写。我们攒够量再写。
| 框架 | 时机 | 触发点 |
|---|---|---|
| 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 |
| openclaw | cron 定时,默认每天凌晨三点,按六项加权分决定谁能升上去 | 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、pi-mono、我们)走的是同一条路: 原话只追加不改,提炼可以随便重写,出了问题拿原话对账。
MEMORY.md、摘要页和技能目录。写的和整理的分成两个岗位,
和我们"写入"加"夜间重整"是同一个拆法,只是它拆在流水线里,我们拆在时间上。
八家给出五种进度记录:不记录、标量位置、单个消息身份、数据内标记、逐节点身份。 OpenProgram当前使用最后一种,本页参考框架中没有第二个相同实现。
| 框架 | 形状 | 存在哪 | 核实位置 |
|---|---|---|---|
| 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.json | written-marker.md 第一层 |
| OpenProgram当前 | 节点上的workspace marker | 会话节点的metadata | written-marker.md 第三层 |
MEMORY.md 正文里再埋一句带 key 的 HTML 注释。任何一边丢了,另一边还拦得住重复。
我们的集合只记在游标文件里,原话档案虽然也能当种子用,但两边不会互相校验。
这条对我们最要紧。八家里只有 pi-mono 一家像我们一样,把分支放在同一个会话内部。 其余的要么根本不分叉,要么把"分叉"实现成另开一个会话文件、父子之间连根指针。 别人的游标不必处理"同一条会话里两条分支共享前缀",因为它们没有这个东西。
| 框架 | 能不能分叉 | 怎么实现 | 核实位置 |
|---|---|---|---|
| claude-code | 能,功能最全 | /branch 整份复制成新会话;/rewind 能只回退对话、只回退文件或两者一起;子 agent 写到另一个文件 | src/commands/branch/branch.ts:61-173 src/components/MessageSelector.tsx:31 src/utils/sessionStorage.ts:247-258 |
| codex-cli | 能 | codex 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-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 第三层 |
revert 本身不删东西,但下一次提问一开始就会把标记之后的消息硬删。
也就是"改一条旧消息重跑"在它那里是不可逆的,想留住旧那条只能先 fork。
我们的 DAG 是两条都留着,代价就是要有办法说清哪条写过了。
存下来的东西怎么回到上下文里。四条路:全量塞进去、关键词搜、向量搜、让模型自己挑。 第四条是 claude-code 独有的,也是最便宜的一条。
topics/ 结构天生匹配。
它挑的判据也写得比"相关就选"细:拿不准就不选,宁可返回空。
"每轮都进上下文的那一小块记忆",八家里四家给它设了闸,一共六道。裁法只有两类, 没有一家把裁什么完全交给模型自由发挥:要么确定性截断,要么确定性拒绝然后让模型去改。
| 框架 | 常驻块 | 闸门 | 满了怎么办 | 谁决定 | 说不说 | 核实位置 |
|---|---|---|---|---|---|---|
| claude-code MEMORY.md | 有 | 200 行 + 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.md | 有 | 40000 字符只是警告,不截 | 什么都不做,只在状态面板提示"文件过大" | — | 状态面板提示 | 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.md | 有 | 10000 字符 | 只丢自动升上来的段落,按日期从老到新;人写的一律保留 | 确定性,按归属 | 返回丢掉的日期列表 | 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-agent | 有 | 2200 / 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.md | 有 | 2000 token渲染预算 | Runtime按确定性顺序生成预算内常驻块;Topic正本不删除 | 确定性渲染 | 未进入常驻块的Topic仍可检索 | openprogram/memory/management/config.py .../block_views.py |
上面那张表回答的是"这一刻超了怎么办"。还有一个问题它没回答,而我们的缺口正好落在那里: 写满之后,新的稳定事实还进不进得来。这取决于常驻块有没有人维护,以及它的正本是不是就是它自己。
| 框架 | 正本 | 维护通道与触发条件 | 满了之后 | 核实位置 |
|---|---|---|---|---|
| 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-cli | SQLite表 → 原始记忆文件 → 常驻块 | 整合分身:会话开始触发,成功后冷却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「常驻块」 |
topics/core.md作为正本,core.md作为确定性派生视图。与它们不同的是,OpenProgram不删除
超出常驻预算的Topic内容,检索仍可返回这些block。迁移前根目录core.md直接充当正本且超预算时拒绝事务,
该实现已经替换。
不含测试。彩条是长期记忆本身,灰条是压缩和转录存储。同一件事从 0 行到三万四千行都有人做, 差别几乎全在"要不要自己建索引"。
前八节给出了当前方案在各维度上的位置。多数维度复用已有框架的成熟做法; 节点marker由SessionDAG的分支语义决定,原因见第05节。
topics/core.md作为常驻块正本、节点workspace marker、会话边界枚举活分支、Topic脚注保留Source引用。
尚未实现:给Topic生成自描述后由小模型选择文件的无索引检索路径(claude-code,第06节);把检索使用情况作为夜间整理输入(codex,第06节);在head变化时直接通知writer,而不是等待轮后阈值或idle watcher。
topics/core.md提供可维护的常驻块正本。
本页仍保留迁移前行作为问题证据,紫色行表示当前实现,不再表示未来计划。