Overview

在理解 Codex 的 memory 之前,先要纠正一个非常常见的直觉:Codex 的 memory 不是“新 turn 到来时,从所有历史 turn 里做一次 top-k 检索,然后把检索到的 turn 原样塞回上下文”

在当前开源实现里,memory 更像一条后台的数据合成流水线。它先把已经结束、或者已经 idle 的旧 session 做离线整理,再把这些整理结果落成一组 memory artifacts。之后在线阶段是否读取这些 artifacts,是另一件事。

如果只看“memory 是怎么被生产出来的”,可以把它抽象成下面这条链路:

1
2
3
4
5
6
7
session / thread
-> rollout trajectory
-> Stage 1
-> Stage1Output(raw_memory, rollout_summary, rollout_slug)
-> Stage 2
-> raw_memories.md + rollout_summaries/*.md
-> MEMORY.md + memory_summary.md

这里面几个数据对象先定义清楚:

  • 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.md MEMORY.md 的更短摘要版本,用作后续读取阶段的入口概览。

这条 memory pipeline 不是每个用户 turn 都触发。对应逻辑在 codex-rs/core/src/memories/start.rs。更准确地说,它是在一个 root session 启动时后台异步触发,然后扫描“别的、已经空闲的旧 threads”去做处理。所以它本质上是后台批处理,不是在线逐 turn 更新。

Memory Construction

Codex memory 的构造分成两层:

  • Stage 1:把一条完整 session trajectory 压成一条 Stage1Output
  • Stage 2:把多条 Stage1Output 合成为最终的 memory 文件系统

这条链路涉及 4 个和 memory 相关的 prompt template,它们都在 codex-rs/core/templates/memories/ 下面:

  • stage_one_system.md Stage 1 的 system / base instructions,定义 raw_memoryrollout_summaryrollout_slug 这三个字段各自的含义和写法。

  • stage_one_input.md Stage 1 的 input wrapper,把一条 thread 过滤后的 trajectory 包进一个固定模板,交给 memory writer agent。

  • consolidation.md Stage 2 consolidation agent 的主 prompt,告诉模型怎样把多条 stage1 memories 合并成 MEMORY.mdmemory_summary.md

  • read_path.md 这是 memory 的读取侧 prompt,不属于构造过程本身,但它解释了为什么 Stage 2 最终要产出 memory_summary.mdMEMORY.mdrollout_summaries/*.md 这几类不同层次的文件。

Stage 1

先看最核心的问题:一条 session trajectory 是怎么转成 raw_memoryrollout_summaryrollout_slug 的?

答案不是“程序员写了一个 hand-crafted summarizer”,而是:

1
2
3
4
5
6
filtered session data
+ stage_one_system.md
+ stage_one_input.md
+ output schema
-> LLM
-> Stage1Output

也就是说,Stage 1 是一个受 prompt 和 schema 约束的 LLM 变换过程。

具体实现主线在:

  • codex-rs/core/src/memories/phase1.rs
  • codex-rs/core/src/memories/prompts.rs
  • codex-rs/rollout/src/policy.rs
  • codex-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,例如:

  • developer message
  • reasoning
  • image generation
  • ghost snapshot
  • compaction item

同时,codex-rs/core/src/contextual_user_message.rs 还会对 user message 里的注入脚手架再做清洗,例如去掉 AGENTS.mdSKILL 片段,避免这些内容进入 memory。

如果用算法视角表达,这一步近似于:

1
2
3
4
filtered_items = [
    item for item in rollout_items
    if item_is_memory_relevant(item)
]

第三步,把过滤后的 trajectory 序列化成一个 JSON 字符串。这一点很关键。Stage 1 不是把结构化 ResponseItem[] 直接交给模型,而是先做:

1
rollout_contents = json.dumps(filtered_items)

所以 memory writer 真正看到的“原始 session 数据”,本质上是一段被预渲染过的 session JSON 文本。

第四步,codex-rs/core/src/memories/prompts.rs 会给这段 rollout_contents 分配 token 预算。超过预算时,先对这段字符串做截断,再把它填进 stage_one_input.md 模板。于是送给模型的输入大致是:

1
2
3
4
5
6
7
8
Analyze this rollout and produce JSON with `raw_memory`, `rollout_summary`, and `rollout_slug`

rollout_context:
- rollout_path: ...
- rollout_cwd: ...

rendered conversation (pre-rendered from rollout `.jsonl`; filtered response items):
<truncated serialized JSON string>

这一步的几个关键点是:

  • 输入单位是整条 thread 的过滤后轨迹
  • 截断发生在序列化之后
  • 被截断的是字符串,不是结构化片段选择

第五步,把这条 input 和 stage_one_system.md 组合起来做一次受 schema 约束的模型调用。phase1.rs 里不仅提供了 prompt,还提供了 JSON schema,限制输出只能包含:

  • raw_memory
  • rollout_summary
  • rollout_slug

因此 Stage 1 的抽象形式可以写成:

1
2
3
4
5
6
7
8
9
stage1_output = LLM(
    system=stage_one_system_prompt,
    user=render(stage_one_input, rollout_contents),
    output_schema={
        "raw_memory": "string",
        "rollout_summary": "string",
        "rollout_slug": "string",
    },
)

这三个字段虽然都来自同一条 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 是在做这样一件事:

1
2
3
4
5
complete thread trajectory
-> filtered trajectory
-> serialized and truncated rollout text
-> LLM extraction
-> one Stage1Output per thread

Stage 2

当数据库里已经积累了多条 Stage1Output,Phase 2 会继续把它们合成为真正的 memory 文件系统。

这一层的关键点是:Stage 2 不是一步到位,而是分成“程序化落盘”和“LLM consolidation”两层。

主线实现位于:

  • codex-rs/core/src/memories/phase2.rs
  • codex-rs/core/src/memories/storage.rs
  • codex-rs/core/templates/memories/consolidation.md

第一层是程序化落盘。

codex-rs/core/src/memories/storage.rs 会先把一批 Stage1Output 转成两类中间 artifacts。

第一类是 rollout_summaries/*.md。对每条 Stage1Output,系统会生成一个单独的 markdown 文件,主体就是对应的 rollout_summary,前面再加一些 thread 元数据,例如:

1
2
3
4
5
6
7
thread_id: ...
updated_at: ...
rollout_path: ...
cwd: ...
git_branch: ...

<rollout_summary>

它的作用很明确:保留每条 thread 的 reference-level evidence。后面如果需要回看某条具体历史工作轨迹,这些文件就是 thread 粒度的资料库。

第二类是 raw_memories.md。它会把多条 Stage1Output 的 raw_memory 机械拼成一个聚合 markdown 文件,结构大致是:

1
2
3
4
5
6
7
8
9
# Raw Memories

## Thread `...`
updated_at: ...
cwd: ...
rollout_path: ...
rollout_summary_file: ...

<raw_memory>

这个文件不是在线阶段直接消费的最终 memory,而是 Stage 2 consolidation 的原料包。它把“多个 thread 的长期记忆抽取结果”集中到一个地方,便于后面的 consolidator 统一处理。

第二层是 LLM consolidation。

raw_memories.mdrollout_summaries/*.md 准备好之后,phase2.rs 会启动一个内部 consolidation agent。这个 agent 使用的主 prompt 就是 consolidation.md。它会读取 memory 根目录下的输入,包括:

  • raw_memories.md
  • 现有的 MEMORY.md
  • 现有的 memory_summary.md
  • rollout_summaries/*.md
  • skills/*

然后生成或更新下面这些输出:

  • MEMORY.md
  • memory_summary.md
  • 必要时的 skills/*

这里最重要的是前两个。

MEMORY.md 是高层 consolidated memory。它不再按 thread 逐条罗列历史,而是把多条 Stage1Output 里反复出现、值得长期保留的东西合并成更稳定的知识结构,例如:

  • 哪些任务模式反复出现
  • 用户有哪些稳定偏好
  • 哪些做法可以复用
  • 哪些错误以后应该避免

所以 MEMORY.md 的角色是 canonical memory,也就是跨 thread 合并后的稳定知识层。

memory_summary.md 则是 MEMORY.md 的更短摘要版本。它的重点不是保留所有细节,而是作为后续读取 memory 的短入口。它存在的原因,是在线阶段上下文预算更紧,需要一个更短的入口摘要,而不是一上来就把整个 MEMORY.md 全塞进模型上下文。

从数据变换角度,Stage 2 可以抽象成:

1
2
3
4
5
6
7
8
9
rollout_summary_files = format_rollout_summaries(stage1_outputs)
raw_memories_md = concat_raw_memory(stage1_outputs)

MEMORY_md, memory_summary_md = LLM_consolidate(
    raw_memories_md,
    rollout_summary_files,
    existing_MEMORY_md,
    existing_memory_summary_md,
)

所以 Stage 2 本质上是在做:

1
2
3
many thread-level memories
-> per-thread reference files + one aggregated raw memory file
-> consolidated long-term memory files

Selection Logic

Phase 2 不会无条件处理所有 Stage1Output,而是会先从数据库里选一批“当前值得进入 consolidation 的 memory”。这部分逻辑在 codex-rs/state/src/runtime/memories.rsget_phase2_input_selection(...)

这一步的算法目标可以概括成一句话:

只保留那些仍然新鲜、或者确实被后续 agent 用到过的 stage1 memories。

它大致分成过滤和排序两步。

先过滤:

  1. 只看 memory_mode = enabled 的 thread
  2. 只看非空 memory,也就是 raw_memoryrollout_summary 至少有一个不为空
  3. 做 retention window 过滤
    • 如果这条 memory 以前被真正用过,则看 last_usage 是否还在窗口内
    • 如果这条 memory 从未被用过,则看 source_updated_at 是否还在窗口内

再排序。排序规则在 SQL 里基本就是:

1
2
3
4
5
6
ORDER BY
  usage_count DESC,
  COALESCE(last_usage, source_updated_at) DESC,
  source_updated_at DESC,
  thread_id DESC
LIMIT N

这条规则的含义很直接:

  • 优先保留使用频率更高的 memory
  • 其次保留最近被使用过或最近生成过的 memory
  • 再用更新时间和 thread_id 做稳定排序

这里还有一个实现细节很重要。Phase 2 不只看当前 selected 集合,还会把上一次成功参与过 consolidation 的 previous_selected 一起带上,形成:

1
artifact_memories = current_selected ∪ previous_selected

这样做的原因是,如果一条 memory 只是这次暂时掉出 top-N,它对应的 artifact 不应该立刻从文件系统中消失。否则 consolidator 会失去前一轮已经纳入知识体系的证据,造成 memory artifacts 不稳定。换句话说,previous_selected 的存在,是为了让 Stage 2 的 artifact 层具有一定的时间连续性,而不是每次都完全重建一份剧烈抖动的记忆集合。

所以从算法角度看,Selection Logic 做的事情不是简单的“取 top-N”,而是:

1
2
3
4
current utility filtering
+ recency / usage ranking
+ previous artifact continuity
-> Stage 2 input set

如果把整条 memory construction pipeline 压缩成一句话,那么最准确的表述是:

Codex 先把单条 session trajectory 提炼成 thread-level memory,再把多条 thread-level memory 合成为稳定的 long-term memory artifacts。

这也是为什么 Codex 的 memory 更像一套后台知识蒸馏系统,而不是“把历史 turn 原样检索回来”的普通对话缓存。