Codex 工程实践(四):会话持久化与长期记忆如何协作

一个 Agent 重启后还能接着聊,和它能在新任务里想起过去的经验,是两种能力。前者需要保存和重建会话状态,后者需要从历史中筛选值得复用的信息。

《Codex 源码分析》第 19、20 章恰好展示了这两条路径。它们共享历史材料,却有不同的读取方式、更新时机和正确性要求。把两者混在一起,很容易让系统既保存了大量日志,又无法稳定地继续工作。

本系列日期按博客时间轴编排,资料统一于 2026-11-05 核对。参考章节固定到 f31e90b79f594c606746626a56bdf1e49497b275;另核对 Codex 上游 86ac0916597244a2e67a197822a5483924660ff1 的持久化策略和记忆管线文档。文件名、数据库版本与默认参数属于实现细节,不能据此假定所有客户端具有相同行为。

一、四种容易被叫作“记忆”的东西

对象主要回答什么问题对应能力
会话轨迹这个任务已经发生了什么?恢复与审查
查询索引某个会话在哪里、有哪些元数据?列表、搜索与定位
压缩后的历史当前任务怎样在有限窗口内继续?上下文管理
长期记忆过去哪些经验对新任务仍有价值?跨会话复用

把日志全部保存,解决的是第一行的一部分。把历史压缩成摘要,解决的是第三行。两者都不能自动保证新任务会找到正确的旧经验。

二、会话保存为什么采用事件序列

参考第 19 章介绍的 rollout JSONL,一行对应一个结构化记录。记录可以携带会话元信息、模型输入输出项、回合上下文、压缩记录或运行事件。

这种形式适合追加,也方便逐条读取和处理。但不要因此理解成“所有内容都原样保存”。本次核对的上游持久化策略包含条目选择及输出裁剪逻辑;参考章节自身也讨论了事件过滤。因此,恢复得到的是实现定义的可恢复历史,不是每个内部状态的完整镜像。

可以用下面这个示意序列理解事件如何携带语义,字段和类型名称以实际版本为准:

1
2
3
4
5
6
7
会话元信息
用户提出任务
模型发起工具调用
工具返回结果
回合完成
历史压缩,记录替换历史
用户提出下一步任务

遇到历史压缩记录时,恢复器不能把全部旧内容和替换历史一起无脑拼接;它需要按记录的语义重建当前有效历史,否则已经压缩的内容会重新膨胀,甚至重复出现。

三、日志和索引分开,意味着要处理不一致

rollout 适合保存顺序记录,但会话列表通常需要按时间、目录、来源等字段快速筛选。第 19 章描述的 SQLite 层承担查询索引与部分运行状态职责,rollout 则承载会话历史。

这两份存储可能暂时不同步:日志存在但索引尚未补齐,或索引仍然指向已经被移动的文件。参考实现包含扫描、补录、租约和进度记录等机制,用于协调这类恢复工作。

这里必须保留一个边界:能从日志重建部分索引,不代表整个数据库中的所有状态都能无损重建。记忆作业状态、调度信息或使用统计可能具有独立价值。备份与迁移应按实际组件设计,不能只复制一个数据库文件就假定历史齐全。

类似地,用来回看输入的历史文件、诊断 trace 与产品会话日志,也不应互相替代。它们记录的对象和保留策略不同。

四、恢复会话不等于重新执行工具

持久化最危险的窗口之一,是外部动作已经发生、完成记录尚未写入。

1
2
3
日志记录:准备发布
外部系统:发布成功
进程崩溃:尚未来得及保存完成结果

重启后如果根据“最后一条是调用请求”自动重试,就可能重复发布。恢复首先应重建已知状态,再核对实际产物。对于无法确认的操作,应保留未知结果;有幂等键或外部状态查询能力时,再据此决定后续动作。

会话 ID 也不等于磁盘进度。参考第 19 章说明了延迟创建日志文件的路径:逻辑会话已经建立时,持久化产物未必已经物化。即使执行了普通 flush,也不能不加区分地宣称获得了掉电后的完整耐久保证。

因此,对自己的 Harness,应分别定义“会话已创建”“记录已写入”“进度已满足恢复要求”的含义,避免前端拿到一个 ID 就显示所有进度已安全保存。

五、长期记忆的写入为什么分两阶段

本次核对的上游 Memories 文档仍明确采用两阶段后台管线。运行有门槛:根会话启动、非临时会话、功能启用,并且状态数据库可用;符合条件也不意味着全部历史都会立刻被处理。

第一阶段按会话抽取。系统选择符合来源、时间和空闲条件的 rollout,过滤内容,调用模型生成结构化结果,再存入状态库。结果包括较详细的 raw_memory、较简短的 rollout_summary 和可选标识。作业通过租约领取,失败后退避重试,避免多个进程重复处理同一份输入。

第二阶段做全局整合。系统选择一组有界的候选记忆,同步摘要与原始记忆材料,比较工作区变化;有变化时,再让专门的整合代理更新高层产物,例如 MEMORY.md 和 memory_summary.md。

1
2
3
4
5
6
合格 rollout
→ 按会话抽取
→ 状态库中的候选记忆
→ 选择、同步、比较变化
→ 全局整合
→ 可供以后任务读取的记忆产物

这个拆分让局部抽取可以并行,而共享文件的整合由全局锁串行协调。它也把记忆生成成本从当前任务的主执行路径中分离出来,但后台处理仍会消耗资源,不能视为免费功能。

六、选择哪些记忆,与怎样排列材料是两回事

上游文档描述的候选选择考虑使用次数、最近使用时间和保留窗口。没有使用记录的新记忆会借助生成时间参与选择,避免它们还没被尝试就被排除。

选出候选后,合并原始记忆的排列又采用稳定的线程 ID 顺序。这样做能减少排名变化造成的文本抖动:选择策略决定谁进入集合,稳定排序决定如何展示集合。

这对基于 diff 的整合很实用。如果每次使用次数变化都打乱整份材料,整合代理看到的差异会包含大量无意义移动。

文档还明确区分了两种信号:数据库水位记录输入进度,工作区是否有实际变化则决定是否需要运行整合代理。不能仅看到水位变化,就推断必须重新生成全部记忆。

七、读路径应提供入口,而非塞入全部历史

参考第 20 章的读路径先提供一份受预算限制的记忆概要,其他详细材料保留为可检索文件,由代理按任务继续读取。

这与上一篇讨论的 Skill 渐进加载相似:概要负责定位,详细文件负责证据。新任务没有相关历史时,不应因为“已经有记忆”就全量读取。

例如概要只需要提示“某项目存在特殊发布步骤,并指向对应记录”。真正发布前,再查那份记录和当前仓库配置。过去的经验可能已经失效,因此引用旧记录之后,还应验证版本、路径和实际条件。

长期记忆应当保留来源和适用范围。把“某次任务临时允许跳过检查”提炼成“这个项目可以跳过检查”,会将临时例外变成后续偏差的来源。

八、遗忘、纠正与权限边界

记忆系统不只需要新增,还需要删除和替代。过期配置、失败猜想、被用户纠正的习惯,都不应无限延续。

参考章节讨论了过滤注入片段、敏感内容处理与受限整合环境;上游文档也说明整合代理无网络、仅本地写入,并关闭递归协作。这些机制有助于限制影响,但秘密过滤不保证识别所有敏感信息,使用次数也不等于事实正确。

如果用户要求删除一项记忆,还要考虑原始轨迹、候选记录与派生文件的关系。仅删除最终摘要中的一句话,旧材料仍可能让它在下一轮整合时出现。具体删除流程必须遵循所用产品或系统提供的接口和保留规则。

对自己的系统,我会让每条经验尽可能回答:来自什么任务,在什么条件下成立,是否被后来的决定替代,以及怎样回到证据。这样新任务使用的是可核对的经验,而不是越来越难追溯的结论集合。

会话持久化让任务能够继续,压缩让当前历史保持可用,长期记忆让未来任务有机会复用经验。三条路径需要协作,也需要各自清楚地标记未知、失败和过期信息。

参考阅读

系列回顾:使用漂移 · 上下文压缩与拼接 · 工具、权限与沙箱。