Overview
在理解 Codex 的 memory 之前,先要纠正一个非常常见的直觉:Codex 的 memory 不是“新 turn 到来时,从所有历史 turn 里做一次 top-k 检索,然后把检索到的 turn 原样塞回上下文”。
在当前开源实现里,memory 更像一条后台的数据合成流水线。它先把已经结束、或者已经 idle 的旧 session 做离线整理,再把这些整理结果落成一组 memory artifacts。之后在线阶段是否读取这些 artifacts,是另一件事。
如果只看“memory 是怎么被生产出来的”,可以把它抽象成下面这条链路:
|
|
这里面几个数据对象先定义清楚:
-
thread/session在这条管线里,基本可以把它理解成同一个概念:一条完整的会话线程,不是单个 turn,而是多个 turn 串起来形成的一整段工作历史。数据库里对应threads表的一行,也对应一个 rollout 文件路径。 -
rollout trajectory这是这条 thread 的完整轨迹,底层是 rollout.jsonl里的ResponseItem序列。里面既有用户消息,也有 assistant 回复、tool call、tool output,以及其他内部 item。 -
Stage1Output这是 Stage 1 针对一条 thread 生成的第一层记忆抽取结果。最重要的三个字段是:raw_memory:强调长期可复用价值的原始记忆文本rollout_summary:保留 thread 级细节的参考摘要rollout_slug:给这条 thread 的 summary 生成更可读文件名的短 slug
-
raw_memories.md多条raw_memory的聚合中间文件,主要给 Stage 2 的 consolidation agent 当原料。 -
rollout_summaries/*.md每条 thread 一份的参考摘要文件,正文来自对应的rollout_summary。 -
MEMORY.md高层 consolidated memory,沉淀跨 thread 的稳定知识、用户偏好、可复用模式和失败经验。 -
memory_summary.mdMEMORY.md的更短摘要版本,用作后续读取阶段的入口概览。
这条 memory pipeline 不是每个用户 turn 都触发。对应逻辑在 codex-rs/core/src/memories/start.rs。更准确地说,它是在一个 root session 启动时后台异步触发,然后扫描“别的、已经空闲的旧 threads”去做处理。所以它本质上是后台批处理,不是在线逐 turn 更新。
Memory Construction
Codex memory 的构造分成两层:
Stage 1:把一条完整 session trajectory 压成一条 Stage1OutputStage 2:把多条 Stage1Output 合成为最终的 memory 文件系统
这条链路涉及 4 个和 memory 相关的 prompt template,它们都在 codex-rs/core/templates/memories/ 下面:
-
stage_one_system.mdStage 1 的 system / base instructions,定义raw_memory、rollout_summary、rollout_slug这三个字段各自的含义和写法。 -
stage_one_input.mdStage 1 的 input wrapper,把一条 thread 过滤后的 trajectory 包进一个固定模板,交给 memory writer agent。 -
consolidation.mdStage 2 consolidation agent 的主 prompt,告诉模型怎样把多条 stage1 memories 合并成MEMORY.md和memory_summary.md。 -
read_path.md这是 memory 的读取侧 prompt,不属于构造过程本身,但它解释了为什么 Stage 2 最终要产出memory_summary.md、MEMORY.md和rollout_summaries/*.md这几类不同层次的文件。
Stage 1
先看最核心的问题:一条 session trajectory 是怎么转成 raw_memory、rollout_summary 和 rollout_slug 的?
答案不是“程序员写了一个 hand-crafted summarizer”,而是:
|
|
也就是说,Stage 1 是一个受 prompt 和 schema 约束的 LLM 变换过程。
具体实现主线在:
codex-rs/core/src/memories/phase1.rscodex-rs/core/src/memories/prompts.rscodex-rs/rollout/src/policy.rscodex-rs/core/src/contextual_user_message.rs
可以把这一步拆成几个数据处理动作。
第一步,先读取一条 thread 对应的完整 rollout trajectory。phase1.rs 会根据 thread 的 rollout_path 读出完整的 ResponseItem 序列。这里的输入单位不是单个 turn,而是整条 thread。
第二步,对完整 trajectory 做过滤。过滤规则在 codex-rs/rollout/src/policy.rs。保留的主要是事后总结这条 thread 时仍然有价值的 item,例如:
user/assistant消息- shell / function / custom tool / web search 的调用和输出
去掉的则主要是对长期 memory 没有直接价值、或者容易污染总结的 item,例如:
developermessagereasoning- image generation
- ghost snapshot
- compaction item
同时,codex-rs/core/src/contextual_user_message.rs 还会对 user message 里的注入脚手架再做清洗,例如去掉 AGENTS.md 和 SKILL 片段,避免这些内容进入 memory。
如果用算法视角表达,这一步近似于:
|
|
第三步,把过滤后的 trajectory 序列化成一个 JSON 字符串。这一点很关键。Stage 1 不是把结构化 ResponseItem[] 直接交给模型,而是先做:
|
|
所以 memory writer 真正看到的“原始 session 数据”,本质上是一段被预渲染过的 session JSON 文本。
第四步,codex-rs/core/src/memories/prompts.rs 会给这段 rollout_contents 分配 token 预算。超过预算时,先对这段字符串做截断,再把它填进 stage_one_input.md 模板。于是送给模型的输入大致是:
|
|
这一步的几个关键点是:
- 输入单位是整条 thread 的过滤后轨迹
- 截断发生在序列化之后
- 被截断的是字符串,不是结构化片段选择
第五步,把这条 input 和 stage_one_system.md 组合起来做一次受 schema 约束的模型调用。phase1.rs 里不仅提供了 prompt,还提供了 JSON schema,限制输出只能包含:
raw_memoryrollout_summaryrollout_slug
因此 Stage 1 的抽象形式可以写成:
|
|
这三个字段虽然都来自同一条 thread,但作用不同。
raw_memory 不是简单摘要,而是面向长期积累的原始记忆文本。stage_one_system.md 明确要求它偏向 durable、reusable 的内容,例如用户稳定偏好、可复用工作模式、失败教训以及未来还可能用到的知识。它后面会进入 raw_memories.md,成为 consolidation 的主要原料。
rollout_summary 则更像 thread 级的参考摘要。它保留更多上下文细节,目标是让后续 agent 或 consolidator 需要回看这条 thread 时,有一份相对完整的 reference document。它后面会原样落入 rollout_summaries/*.md。
rollout_slug 是 naming artifact。它本身不是主要知识载体,但能让 rollout summary 文件名更可读、更稳定。
所以,从数据角度看,Stage 1 是在做这样一件事:
|
|
Stage 2
当数据库里已经积累了多条 Stage1Output,Phase 2 会继续把它们合成为真正的 memory 文件系统。
这一层的关键点是:Stage 2 不是一步到位,而是分成“程序化落盘”和“LLM consolidation”两层。
主线实现位于:
codex-rs/core/src/memories/phase2.rscodex-rs/core/src/memories/storage.rscodex-rs/core/templates/memories/consolidation.md
第一层是程序化落盘。
codex-rs/core/src/memories/storage.rs 会先把一批 Stage1Output 转成两类中间 artifacts。
第一类是 rollout_summaries/*.md。对每条 Stage1Output,系统会生成一个单独的 markdown 文件,主体就是对应的 rollout_summary,前面再加一些 thread 元数据,例如:
|
|
它的作用很明确:保留每条 thread 的 reference-level evidence。后面如果需要回看某条具体历史工作轨迹,这些文件就是 thread 粒度的资料库。
第二类是 raw_memories.md。它会把多条 Stage1Output 的 raw_memory 机械拼成一个聚合 markdown 文件,结构大致是:
|
|
这个文件不是在线阶段直接消费的最终 memory,而是 Stage 2 consolidation 的原料包。它把“多个 thread 的长期记忆抽取结果”集中到一个地方,便于后面的 consolidator 统一处理。
第二层是 LLM consolidation。
当 raw_memories.md 和 rollout_summaries/*.md 准备好之后,phase2.rs 会启动一个内部 consolidation agent。这个 agent 使用的主 prompt 就是 consolidation.md。它会读取 memory 根目录下的输入,包括:
raw_memories.md- 现有的
MEMORY.md - 现有的
memory_summary.md rollout_summaries/*.mdskills/*
然后生成或更新下面这些输出:
MEMORY.mdmemory_summary.md- 必要时的
skills/*
这里最重要的是前两个。
MEMORY.md 是高层 consolidated memory。它不再按 thread 逐条罗列历史,而是把多条 Stage1Output 里反复出现、值得长期保留的东西合并成更稳定的知识结构,例如:
- 哪些任务模式反复出现
- 用户有哪些稳定偏好
- 哪些做法可以复用
- 哪些错误以后应该避免
所以 MEMORY.md 的角色是 canonical memory,也就是跨 thread 合并后的稳定知识层。
memory_summary.md 则是 MEMORY.md 的更短摘要版本。它的重点不是保留所有细节,而是作为后续读取 memory 的短入口。它存在的原因,是在线阶段上下文预算更紧,需要一个更短的入口摘要,而不是一上来就把整个 MEMORY.md 全塞进模型上下文。
从数据变换角度,Stage 2 可以抽象成:
|
|
所以 Stage 2 本质上是在做:
|
|
Selection Logic
Phase 2 不会无条件处理所有 Stage1Output,而是会先从数据库里选一批“当前值得进入 consolidation 的 memory”。这部分逻辑在 codex-rs/state/src/runtime/memories.rs 的 get_phase2_input_selection(...)。
这一步的算法目标可以概括成一句话:
只保留那些仍然新鲜、或者确实被后续 agent 用到过的 stage1 memories。
它大致分成过滤和排序两步。
先过滤:
- 只看
memory_mode = enabled的 thread - 只看非空 memory,也就是
raw_memory或rollout_summary至少有一个不为空 - 做 retention window 过滤
- 如果这条 memory 以前被真正用过,则看
last_usage是否还在窗口内 - 如果这条 memory 从未被用过,则看
source_updated_at是否还在窗口内
- 如果这条 memory 以前被真正用过,则看
再排序。排序规则在 SQL 里基本就是:
|
|
这条规则的含义很直接:
- 优先保留使用频率更高的 memory
- 其次保留最近被使用过或最近生成过的 memory
- 再用更新时间和 thread_id 做稳定排序
这里还有一个实现细节很重要。Phase 2 不只看当前 selected 集合,还会把上一次成功参与过 consolidation 的 previous_selected 一起带上,形成:
|
|
这样做的原因是,如果一条 memory 只是这次暂时掉出 top-N,它对应的 artifact 不应该立刻从文件系统中消失。否则 consolidator 会失去前一轮已经纳入知识体系的证据,造成 memory artifacts 不稳定。换句话说,previous_selected 的存在,是为了让 Stage 2 的 artifact 层具有一定的时间连续性,而不是每次都完全重建一份剧烈抖动的记忆集合。
所以从算法角度看,Selection Logic 做的事情不是简单的“取 top-N”,而是:
|
|
如果把整条 memory construction pipeline 压缩成一句话,那么最准确的表述是:
Codex 先把单条 session trajectory 提炼成 thread-level memory,再把多条 thread-level memory 合成为稳定的 long-term memory artifacts。
这也是为什么 Codex 的 memory 更像一套后台知识蒸馏系统,而不是“把历史 turn 原样检索回来”的普通对话缓存。