先把直觉放慢一点。很多人听到 agent memory,会马上想到“更长的聊天记录”:
旧 thread 越多,下一次 prompt 里塞得越多。Codex 的实现没有走这条路。
官方文档把 Memories 描述成 useful context from earlier threads,源码里也把它拆成
memories/read 和 memories/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_enabled 为 false。
所以配置要分两层看:外层 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_memory、rollout_summary、rollout_slug。 |
| Phase 2 | 多条 raw memory 合并后的 workspace patch。 | 内部 consolidation agent 只能改 memory 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 中,
为每条 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_count、
last_usage / generated_at 和 max_unused_days
选择输入;把 raw_memories.md 和 rollout_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,也不会把需要强约束的规则交给概率性的召回层。
参考源码
- OpenAI Codex Memories 官方文档:默认关闭、配置、存储和 per-thread 控制
- OpenAI Codex AGENTS.md 官方文档:规则文件的发现和层级
- OpenAI Codex Chronicle 官方文档:用屏幕上下文辅助 memory 的 opt-in preview
Feature::MemoryToolfeature spec 与默认关闭MemoriesToml/MemoriesConfig配置项和默认值- TUI
/memories入口和启用提示 memories/read与memories/writecrate 分工- 后台 memory startup task 的 skip gates、rate-limit guard 与 Phase 1 / Phase 2 顺序
- app-server 在 turn 提交后触发 memory startup task
- Phase 1 startup claim 的候选选择说明
- Phase 1 查询只选择
memory_mode = 'enabled'的 legacy thread - Phase 1 prompt:从 rollout 转成 raw memory / rollout summary,且允许 no-op
- Phase 1 prompt:高信号 memory 的判断边界
- Phase 2 consolidation 的输入选择、文件同步、git baseline 和内部 agent
- Phase 2 memory folder 结构:summary、MEMORY、raw、skills、rollout_summaries
- read extension 根据 feature 和
use_memories注入 developer policy - read path prompt:什么时候使用 memory、memory layout 和 quick pass
- read path prompt:memory citation 和 ad-hoc update 边界
MemoryCitation/MemoryCitationEntry协议结构- memory citation parser 解析 citation entries 和 rollout ids
- 流式输出剥离隐藏 citation 并解析 memory citation
- response item 完成后记录 memory citation usage,并标记外部上下文污染
- MCP tool call 根据 server metadata 标记 thread memory pollution
mark_thread_memory_mode_polluted将 thread 置为polluted