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

第十二篇先建立一个心智模型:Codex memory 是本地召回层。 它从 eligible rollout 中抽取高信号经验,先落到 state DB,再合并到 ~/.codex/memories/ 下的文件化 workspace;未来 turn 读取这些文件时, 还要经过 read path、引用和污染隔离。它能减少重复说明,但不承担规则文件、 恢复账本或无限上下文窗口的职责。

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

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

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

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

看起来都像“记忆”的东西 真正 owner 生命周期 适合放什么
当前上下文 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 是 Experimental,default_enabledfalse。 所以配置要分两层看:外层 feature flag 决定有没有 Memories;内层 [memories] 决定生成、使用、外部上下文隔离和模型选择。

[features]
memories = true

[memories]
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

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

如果每条回复结束后立刻总结,memory 会变得很吵:任务可能还没结束,用户可能马上纠正, 工具输出也可能只是临时失败。Codex 的写入路径把这件事放到后台。 app-server 在 turn/start 成功提交用户输入后,会尝试调用 start_memories_startup_task。这个函数先过几道门: ephemeral session、未开启 feature、非 root agent session、没有 state DB,都会直接跳过。

真正启动以后,后台 task 会创建 memories root,seed extension instructions, 先做 prune,再检查 rate limit。只有额度允许时才继续跑 Phase 1 和 Phase 2。 这解释了官方文档里那句“Memories may not update right away”:它不是同步尾巴, 而是一条带空闲时间、额度阈值和 state DB lease 的维护路径。

turn/start accepted
  -> start_memories_startup_task
  -> skip gates: ephemeral / feature off / sub-agent / no state DB
  -> create memories root
  -> prune
  -> rate-limit guard
  -> Phase 1 extraction
  -> Phase 2 consolidation

这也是 memory 文章要放在 rollout 之后讲的原因。写入路径不是从可见 UI 消息开始, 而是从 state DB 里挑 eligible 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_memoryrollout_summaryrollout_slug
Phase 2 多条 raw memory 合并后的 workspace patch。 内部 consolidation agent 只能改 memory workspace。 memory_summary.mdMEMORY.md、summary 文件或 skill。
未来读取 当前任务命中的 memory 条目。 前台 agent 按 read-path prompt 少量打开。 回答末尾隐藏 citation,usage 回写 state DB。

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

Phase 1 的任务很具体:从最近的、空闲足够久的、允许参与 memory 的 thread 中, 为每条 rollout 生成结构化输出。state 层的 claim 查询会排除 memory_mode != 'enabled' 的线程,排除当前 thread, 限制 age window 和 idle window,并用 scan / claim limit 控制启动时的工作量。 这不是“把所有历史都拿来总结”,而是先筛出值得处理的一小批候选。

模板里还有一个很重要的信号门:如果一条 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_countlast_usage / generated_atmax_unused_days 选择输入;把 raw_memories.mdrollout_summaries/ 同步到 ~/.codex/memories/;用 git baseline 记录 workspace diff; 如果没有变化,就直接结束。

有变化时,Codex 会启动一个内部 consolidation agent。它没有网络、没有 approval、 只写本地 memory workspace,并且关闭协作,避免递归委派。这个细节很能说明设计取向: memory 合并不是前台 turn 的一部分,也不应该随便碰当前项目;它是一个本地状态维护任务。

文件或目录 用途 读取方式
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 提供。MemoriesExtensionConfig 只有在 Feature::MemoryTool 开启且 memories.use_memories 为真时才启用; thread context contributor 随后调用 build_memory_tool_developer_instructions, 把 memory 读取规则作为 developer policy 片段注入。

这段 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 得到的是“哪条记忆真的被用上”的反馈。

七、为什么要有 polluted

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

state 层筛选 Phase 1 job 时只选择 memory_mode = 'enabled'history_mode = 'legacy' 的线程;一旦 thread 被标记为 polluted,后续就不会作为普通 memory 输入继续进入抽取路径。 这不是说外部上下文不能用,而是把“当下查询”与“长期沉淀”分开。

还是用刚才的例子对比:“这个仓库的文章要中英文同步” 是稳定工作流, 值得候选;“今天某个网页上显示的最新价格是 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”。 这三件事相互连接,但 owner 不同。把 owner 分清楚,才不会把 agent memory 想成一个无限增长的 prompt,也不会把需要强约束的规则交给概率性的召回层。

参考源码