先把直觉放慢一点。很多人听到 agent memory,会马上想到“更长的聊天记录”: 旧 thread 越多,下一次 prompt 里塞得越多。Codex 的实现没有走这条路。 官方文档把 Memories 描述成 useful context from earlier threads,源码里也把它拆成 memories/read 和 memories/write 两条路径: 一条负责把已有记忆带进当前线程,另一条在后台把历史 rollout 提炼成以后可用的本地材料。

Codex 会在后台从 eligible rollout 中抽取高价值经验,先写入 state DB,再合并到 ~/.codex/memories/ 下的文件化 workspace;未来 turn 读取这些文件时, 还会记录引用,并在配置启用隔离时跳过受外部资料影响的 thread。它能减少重复说明,但不替代规则文件、 rollout 恢复记录或上下文窗口。

阅读目标。 本篇只追一条生命周期:哪些线程可以生成记忆,后台怎样抽取和合并, 记忆怎样进入未来 turn,以及它和 AGENTS.md、rollout、Chronicle 的区别。 读完以后,应该能判断一条信息应该写进规则文件、留在 rollout,还是适合存成 memory。

取材范围。 源码快照:5c5308fc9a9e。 产品行为来自 OpenAI Codex Memories、AGENTS.md 和 Chronicle 官方文档; feature switch、配置、读写 pipeline、citation 和污染隔离来自 openai/codex 公开源码。本文不读取或引用任何本机 ~/.codex/memories 内容,也不推断私有服务端策略。

一、先拆开“会记住”这句话

Codex 里有好几种东西都会让人感觉“它记得”。第二篇讲过的 TurnContext 是本轮运行快照;第十篇讲过的 rollout 是持久证据; AGENTS.md 是进入任务前就该读到的规则;Memories 则是另一层: 它从过往工作里提炼可复用经验,下一次需要时再检索。

看起来都像“记忆”的东西 负责实现 生命周期 适合放什么
当前上下文 TurnContext 和 model-visible view。 一次 turn 内生效,可压缩,也可转换成客户端状态。 本轮必须看到的输入、工具结果、环境和指令。
项目规则 AGENTS.md / checked-in docs。 每次进入仓库时读取,靠文件维护。 必须稳定执行的团队约定、命令、权限和交付规则。
运行证据 rollout / state DB。 thread 执行后留下,供 resume、fork、审计和恢复使用。 曾经发生过什么、工具输出是什么、回滚点在哪里。
Memories codex-memories-read / codex-memories-write。 后台从旧 thread 生成,本地文件化,未来 turn 选择性读取。 稳定偏好、重复工作流、项目习惯、已知坑和高杠杆步骤。
Chronicle Codex app 的 opt-in research preview。 用屏幕上下文辅助构建 memory。 最近正在看的工具、文档、网页或工作流线索。

这个表先给出结论:需要强制执行的规则不要靠 memory;需要精确重放的历史不要靠 memory; memory 的价值在于减少重复说明,让未来 agent 更快找到“这类任务通常该怎么做”。

二、开关先决定有没有 memory

官方文档第一句就把预期压住:Memories 默认关闭。源码里的 feature spec 也对应这一点: Feature::MemoryTool 的 key 是 memories, stage 是 Stable,default_enabled 为 false。 所以配置要分两层看:外层 feature flag 决定有没有 Memories;内层 [memories] 决定生成、使用、外部上下文隔离和模型选择。

[features]
memories = true

[memories]
version = "v1"
dual_write = false
generate_memories = true
use_memories = true
disable_on_external_context = true

这三个 memory 子开关容易混淆。generate_memories 影响新 thread 是否能作为未来生成输入;use_memories 影响当前线程是否注入已有 memory 读取指令;disable_on_external_context 则用于把用了 web search、tool search、 或会污染 memory 的 MCP 调用的 thread 排除出生成路径。TUI 里的 /memories 也按这个顺序工作:功能没开时先弹启用提示;功能已开时再展示 use / generate 的 thread-level 设置。

开关 控制什么 读源码时的落点
features.memories 是否启用 Memories 功能。 Feature::MemoryTool,默认关闭。
memories.generate_memories 新 thread 是否可作为 memory 生成输入。 thread 创建时写入 memory_mode。
memories.use_memories 当前线程是否使用已有 memory。 read extension 是否注入 developer instruction。
memories.disable_on_external_context 外部上下文参与后是否标记污染。 web/tool search 和 MCP 路径可把 thread 置为 polluted。

三、写入为什么要放到后台

如果每条回复结束后立刻总结,任务未完成、用户纠正和临时失败都会增加噪声。当前 app-server 只有在 turn/start 含非空用户输入、core 确实启动新 turn、且主环境已配置时,才尝试 start_memories_startup_task。补充已有 turn 或提交独立工具输出不会走这个触发分支。后台函数再跳过 ephemeral、feature 关闭和非 root agent,并确认 memory store 可用。

后台默认只跑所选版本;dual_write = true 才分别启动 v1 与 v2 pipeline。每条 pipeline 先准备自己的 root 和 extension instructions,再 prune、检查额度,依次执行 Phase 1 和 Phase 2。版本目录 分别是 memories/ 与 memories_v2/;默认仍为 v1。双写不等于把两份摘要一起注入:读取仍由 version 选择一个命名空间。示例显式开启的 disable_on_external_context 也不是默认值;源码默认是 false。

turn/start: nonempty user input + new turn + primary environment ready
  -> start_memories_startup_task
  -> skip checks: ephemeral / feature off / sub-agent / no state DB
  -> selected version (or both when dual_write)
  -> create version-specific memories root
  -> prune
  -> rate-limit guard
  -> Phase 1 extraction
  -> Phase 2 consolidation

这也是 memory 文章要放在 rollout 之后讲的原因。写入路径不是从可见 UI 消息开始, 而是从 state DB 里挑 eligible rollout;不了解第十篇的 rollout 记录,读这一段很容易把 memory 误读成普通聊天摘要。

3.1 一条可复用经验怎样移动

先看一条具体经验:某个仓库的文章改动通常要中英文同步,最后还要做移动端视觉检查。 如果它真的反复出现,memory writer 不会把整段旧对话塞进未来上下文,而是让这条经验穿过几层收缩。

阶段 这时的单元 谁能看或改 产出
候选 rollout 旧 thread 里的用户要求、文件 diff、验证结果。 state DB 按 idle、age、memory mode 选择。 一条可被 Phase 1 claim 的 job。
Phase 1 从旧证据里抽出的 reusable lesson。 后台 writer 只把 rollout 当数据读。 raw_memory、rollout_summary、rollout_slug。
Phase 2 多条 raw memory 合并后的 workspace patch。 内部 consolidation agent 维护对应版本 workspace;实际隔离取决于父级权限配置。 memory_summary.md、MEMORY.md、summary 文件或 skill。
未来读取 当前任务命中的 memory 条目。 前台 agent 按 read-path prompt 少量打开。 回答末尾隐藏 citation,usage 回写 state DB。

四、Phase 1:先把 rollout 变成 raw memory

Phase 1 从最近、空闲足够久、允许生成 memory 的 thread 中抽取结构化经验。候选查询 排除当前 thread 和 memory_mode != enabled,限制 age、idle、来源以及 scan / claim 数量;不再要求 history_mode = legacy,分页历史也可参与。以下示例和文件表先沿默认 v1 路线解释。v2 使用单独的分层输入序列化与抽取模板,不能把两条路径的记录形状混为一谈。

模板里还有一个很重要的判断:如果一条 rollout 没有可复用洞察, memory writer 可以返回全空字段。高价值记忆通常是稳定用户偏好、高杠杆步骤、 任务地图、决策触发器和可靠环境事实;泛泛的“跑过测试”“看过日志”不该占用未来注意力。

{
  "raw_memory": "修改这个仓库的文章 flow 时,要同步更新 zh/en 页面,并在发布前检查移动端视觉效果。",
  "rollout_summary": "一次 Codex 文章改稿在同时检查双语页面和窄屏布局之后,才把叙述顺序调顺。",
  "rollout_slug": "bilingual-article-visual-check"
}

这里的关键词是 evidence-based。Phase 1 prompt 明确要求把 rollout 和工具输出当作数据而不是指令,不能保存 secrets,不能复制大段输出,宁可 no-op。 对未来 agent 有帮助,才值得进入下一步。

五、Phase 2:把零散记忆合并成本地 workspace

Phase 2 负责把 stage-1 outputs 变成文件化 memory workspace。 README 里列出的动作很有工程味:先拿全局 lock,再按 usage_count、 last_usage / generated_at 和 max_unused_days 选择输入;把 raw_memories.md 和 rollout_summaries/ 同步到 ~/.codex/memories/;用 git baseline 记录 workspace diff; 如果没有变化,就直接结束。

有变化时,Codex 启动内部 consolidation agent,关闭审批、memory 再生成及协作,避免递归提炼或委派。它的权限继承有条件分支:父级使用 Codex 管理的权限时,worker 限于 memory root 写入且禁用网络;父级为 Disabled 或 External 时保留该配置。因此“任何情况下都只有本地 memory 目录写权限”并不是源码保证。执行结束还要通过 artifact 校验并确认仍持有 lock,才接纳这次合并结果。

文件或目录 用途 读取方式
memory_summary.md 高密度导航摘要。 read path 会把摘要放进指令区域,先用于快速判断。
MEMORY.md 可 grep 的 handbook。 按关键词查,找到相关条目后再深入。
rollout_summaries/ 每条 rollout 的压缩证据和经验。 只有被 MEMORY.md 指到时才打开少量文件。
skills/ 可复用流程或脚本。 作为 progressive disclosure 的下一层。
raw_memories.md Phase 1 的合并输入。 主要给 Phase 2 consolidation 使用。

六、读取路径:memory 怎样回到未来 turn

读取 extension 仅在 Feature::MemoryTool 和 memories.use_memories 都开启时注入规则,并从所选版本读取非空 memory_summary.md。配置更新 会保留该 thread 初始选定的版本,避免摘要和检索工具读取不同目录;只有 dedicated_tools 同时开启时才暴露专用 memory 工具。

这段 read-path prompt 并不是“看到 memory 就全读”。它先要求判断任务是否需要 memory; 需要时做 quick memory pass:先看 memory_summary.md,再 grep MEMORY.md,只有命中直接指向 rollout summary 或 skill 时,才打开一两个具体文件。 这个设计和 skill 的 progressive disclosure 很像:先给导航,再按需展开证据。

future user query
  -> memory_summary.md hints
  -> grep MEMORY.md
  -> open 1-2 rollout_summaries or skills
  -> answer with hidden memory citation
  -> record usage for future selection

还有一个容易忽略的机制:memory citation。read-path prompt 要求如果使用了 memory, 最终回复末尾要带一个隐藏的 <oai-mem-citation> block。 MemoryCitation 结构保存 entries 和 rollout ids;流式输出处理会剥掉隐藏标记, 解析 citation,并把相关 stage-1 输出的 usage 记回 state DB。用户看到的是干净答案, runtime 得到的是“哪条记忆真的被用上”的反馈。

v2 的 读取模板 更强调直接利用已注入摘要:仅当额外证据、时间顺序或不确定性会改变答案时,再读对应 rollout summary;缺少必要线索才搜索。它要求核实可能变化的事实,不把 memory 当作当前行为的证明。只有实际读取的 summary 参与回答时才附 citation,不引用摘要本身;用户明确要求记住、忘记或纠正时,写入 extensions/ad_hoc/notes/,由后续 consolidation 应用,不能直接改生成文件。

七、为什么要有 polluted

memory 最大的风险不是“忘了”,而是把来源不稳定的东西记得太牢。 如果一条 thread 的答案依赖 web search、tool search 或某个会污染 memory 的 MCP server, 这些外部材料可能很快过期,也可能只适合当次使用。Codex 提供了 disable_on_external_context:配置打开后,相关 response item 或 MCP tool call 会把 thread 标记成 polluted。

污染标记还影响已经合并过的材料。mark_thread_memory_mode_polluted 在该 thread 曾参与最近一次成功 Phase 2 baseline 时,会安排新一轮合并;后续选择会排除这个来源。这样既阻止新抽取,也让已有 workspace 有机会移除失效依据。标记和排队不等于文件已经删除,忘记动作仍要等后台合并成功。

还是用刚才的例子对比:“这个仓库的文章要中英文同步” 是稳定工作流, 值得候选;“今天某个网页上显示的最新价格是 19.99 美元” 是一次外部查询事实,过期概率很高。前者可以进入 memory 候选,后者更适合留在当前 turn 或引用来源,不适合自动保存成长期偏好。

这条规则很适合迁移到自己的 agent 设计里: 临时外部事实可以进入当前答案,但不应该默认进入长期偏好和工作流记忆。 真要保存,也应该先确认来源、时效,并让用户明确同意。

八、最后把三类长期材料放回各自的位置

到这里,Codex 的 memory 就不神秘了。它不替代上下文管理,也不替代 AGENTS.md; 它站在 rollout 后面,把旧线程里真正能减少未来重复沟通的东西抽出来,再通过 read path 带回未来任务。放错位置时,问题也很明显:规则放进 memory 会变得不可靠; 短期事实放进 memory 会过期;恢复证据只靠 memory 又丢失精度。

信息类型 应该放哪里 为什么
团队要求、提交流程、必须执行的验证。 AGENTS.md 或 checked-in docs。 需要每次稳定加载,不能依赖后台抽取是否命中。
某条 thread 中到底发生过什么。 rollout / state DB。 恢复、fork、审计需要可重放证据。
重复出现的偏好、工作流和已知坑。 Memories。 它们能减少未来重复说明,但允许被验证、更新和淘汰。
正在屏幕上看的临时材料。 Chronicle 或当前 turn 的具体工具读取。 它适合帮助定位上下文,不适合未经处理直接长期化。

这也给 Codex 系列补上了最后一块长期状态拼图:第二篇讲“本轮看见什么”, 第十篇讲“旧 thread 怎样重放”,第十二篇讲“哪些旧经验值得带到新 thread”。 这三件事相互连接,但由不同模块负责。把职责分清楚,才不会把 agent memory 想成一个无限增长的 prompt,也不会把需要强约束的规则交给概率性的召回层。

参考源码