精读: OpenAI Codex 记忆系统
编排/Harness 视角见 Agent 框架 · Codex 总架构精读
调研元信息
| 项目 | 值 |
|---|---|
| 调研日期 | 2026-06-22 |
| Codex 仓库 | github.com/openai/codex (Apache-2.0) |
| Git commit | 98845e4 (2026-06-22) |
| 主代码路径 | codex-rs/memories/ (Rust) |
| 证据等级 | 全部 [源码] + [文档] |
一句话定位
Codex 的记忆系统是一个两阶段异步 AI pipeline:Phase1 并行从每个会话(rollout)中提取结构化记忆,Phase2 串行将所有提取结果合并为统一的记忆文件系统——整个过程在后台自动完成,用户无需干预 [源码][文档]。
日常类比
想象一个勤劳的实习生每天做完工作后会写工作笔记(Phase1),周末有个主管把所有人的笔记收集起来,整理成一份部门手册(Phase2)。实习生写笔记是并行的(最多 8 人同时写),但主管整理手册必须独占——因为同时有两个人改同一份手册会乱。手册还有个特别规则:引用次数越多的笔记越会被保留,超过 30 天没人引用的笔记会被清理掉。
类比边界:与真实主管不同,Phase2 的 consolidation agent 是一个受限沙箱中的 AI——没有网络、不能请求人工审批、写入范围仅限记忆目录。
记忆类型标注
本文主要讨论 User memory(系统/Agent 自动写入的持久化记忆),兼及 Agent profile(AGENTS.md)与 User memory 的边界划分。
架构总览
Codex 记忆系统分为两个 crate:codex-memories-write(写路径)和 codex-memories-read(读路径),外加 codex-state 中的 SQLite 持久层做协调 [源码]。
flowchart TB
subgraph trigger["触发层"]
S[Root Session 启动]
end
subgraph gates["Gate 检查"]
G1[非 ephemeral?]
G2[MemoryTool 启用?]
G3[非子 Agent?]
G4[StateDB 可用?]
G5[API 余量 ≥ 25%?]
end
subgraph write["写路径 — codex-memories-write"]
PRUNE[清理过期 stage-1]
P1[Phase1: 并行提取]
P2[Phase2: 全局合并]
end
subgraph read["读路径 — codex-memories-read"]
INJ[Developer Instruction 注入]
CIT[Citation 解析]
USE[Usage 统计]
end
subgraph storage["存储层"]
DB[(SQLite<br/>stage1_outputs<br/>jobs)]
FS[文件系统<br/>~/.codex/memories/]
end
S --> G1 --> G2 --> G3 --> G4 --> G5
G5 --> PRUNE --> P1 --> P2
P1 -->|per-rollout 结果| DB
P2 -->|读取 DB| DB
P2 -->|写入产物| FS
FS --> INJ
INJ --> CIT --> USE
USE -->|usage_count++| DB
触发条件
记忆 pipeline 在 root session 启动时自动触发,需要全部 gate 通过 [源码]:
| Gate | 检查内容 | 失败后果 |
|---|---|---|
| 非 ephemeral | 配置项 ephemeral |
跳过全部 |
| MemoryTool 启用 | Feature::MemoryTool feature flag |
跳过全部 |
| 非子 Agent | !source.is_non_root_agent() |
跳过全部 |
| StateDB 可用 | SQLite 可初始化 | 跳过全部 |
| API 余量 | min_rate_limit_remaining_percent (默认 25%) |
跳过本次 |
API 余量检查是一个精巧的设计:当用户 API 配额紧张时,记忆生成主动让路,把额度留给用户真正的编码任务 [源码]。
写路径: Phase1 — 并行提取
数据流
flowchart LR
subgraph claim["Job 领取"]
SCAN[扫描 ≤5000 threads]
CLAIM[Claim ≤2 个 rollout]
end
subgraph extract["并行提取 (concurrency=8)"]
LOAD[加载 rollout items]
FILTER[过滤非记忆内容]
PROMPT[构建提取 prompt]
MODEL[发送到模型<br/>reasoning=Low]
PARSE[解析 JSON 输出]
REDACT[Secret 脱敏]
end
subgraph persist["持久化"]
DB_WRITE[写入 stage1_outputs]
end
SCAN --> CLAIM --> LOAD --> FILTER --> PROMPT --> MODEL --> PARSE --> REDACT --> DB_WRITE
关键参数
| 参数 | 默认值 | 范围 | 作用 [源码] |
|---|---|---|---|
max_rollouts_per_startup |
2 | [1, 128] | 每次启动最多处理的 rollout 数 |
max_rollout_age_days |
10 | [0, 90] | 超龄 rollout 不处理 |
min_rollout_idle_hours |
6 | — | rollout 结束后的冷却期 |
CONCURRENCY_LIMIT |
8 | — | 并行提取的并发上限 |
REASONING_EFFORT |
Low | — | Phase1 模型推理强度 |
DEFAULT_ROLLOUT_TOKEN_LIMIT |
150,000 | — | rollout 内容截断阈值 |
CONTEXT_WINDOW_PERCENT |
70% | — | 上下文窗口分配比例 |
内容过滤
Phase1 在构建 prompt 前会做严格的内容过滤 [源码]:
| 过滤规则 | 原因 |
|---|---|
移除所有 developer 角色消息 |
系统指令不应污染记忆 |
移除 AGENTS.md instructions 片段 |
Profile ≠ User memory |
移除 <skill>...</skill> 片段 |
skill 定义不需要记忆 |
保留 <environment_context> |
环境信息可能有记忆价值 |
保留 InterAgentCommunication |
多 Agent 协作上下文 |
丢弃 SessionMeta / Compacted |
元数据和压缩残留无记忆价值 |
过滤后的 rollout 内容还会做一轮 secret 脱敏——API key、token 等敏感信息在进入模型前就被清除 [源码]。
提取 prompt 设计
Phase1 的 system prompt(stage_one_system.md,约 570 行)定义了一套完整的提取决策框架 [源码]:
最小信号门:核心判断是”未来 Agent 是否会因为有了这条记忆而表现更好?”如果答案不确定,返回空 JSON——宁可漏提也不误提。
高信号类别包括:用户偏好、程序性知识(怎么做某件事)、任务地图(项目结构概览)、环境事实(本地配置特殊性)。
输出结构是一个固定 JSON schema:
{
"rollout_summary": "任务结构化摘要",
"rollout_slug": "简短标识符或null",
"raw_memory": "YAML frontmatter + Task 块"
}
raw_memory 的格式值得注意:它用 YAML frontmatter(description / task / task_group / task_outcome / cwd / keywords)开头,后跟 ### Task N 块,每块含 Preference signals / Reusable knowledge / Failures / References 四个子节。这种结构化格式让 Phase2 的合并有据可循 [源码]。
与 mem0 的对照
| 维度 | Codex Phase1 | mem0 [已有] |
|---|---|---|
| 提取粒度 | 整个 rollout → 结构化 JSON | 单条对话 → 扁平事实 |
| 过滤机制 | 多层内容过滤 + secret 脱敏 | 无过滤 |
| 信噪比控制 | “最小信号门” + task outcome 分级 | 依赖 LLM 通用判断 |
| 并行度 | 8 并发 | 串行 |
| 存储 | SQLite stage1_outputs 表 | 向量库 |
写路径: Phase2 — 全局合并
Phase2 是记忆系统最复杂的部分。它的核心思路是:把所有 Phase1 的提取结果当作”原材料”,启动一个受限 AI Agent 将它们合并成结构化的记忆文件系统 [源码]。
数据流
flowchart TB
LOCK[获取全局锁<br/>cooldown=6h]
SELECT[从 DB 选择 top-N memories<br/>按 usage_count + recency 排序]
SYNC[同步到文件系统]
DIFF[Git diff vs baseline]
LOCK --> SELECT --> SYNC --> DIFF
DIFF -->|无变更| DONE1[标记成功, 跳过]
DIFF -->|有变更| WRITE_DIFF[写 phase2_workspace_diff.md]
WRITE_DIFF --> SPAWN[启动 Consolidation Agent]
subgraph sandbox["沙箱环境"]
AGENT[Consolidation Agent<br/>reasoning=Medium]
AGENT -->|读| RAW[raw_memories.md]
AGENT -->|读| SUMMARIES[rollout_summaries/]
AGENT -->|写| MEMORY[MEMORY.md]
AGENT -->|写| SUMMARY[memory_summary.md]
AGENT -->|写| SKILLS[skills/]
end
SPAWN --> AGENT
AGENT --> HEARTBEAT[每 90s 心跳续约]
HEARTBEAT --> VERIFY[验证 ownership]
VERIFY --> RESET[重置 git baseline]
RESET --> DONE2[标记成功]
全局锁与冷却
Phase2 是全局串行的——同一时间只能有一个 consolidation 在运行 [源码]。
| 参数 | 值 | 作用 |
|---|---|---|
JOB_LEASE_SECONDS |
3600 (1h) | 锁的最大持有时间 |
JOB_HEARTBEAT_SECONDS |
90 | 心跳续约间隔 |
PHASE2_SUCCESS_COOLDOWN_SECONDS |
21600 (6h) | 成功后的冷却期 |
DEFAULT_RETRY_REMAINING |
3 | 失败最大重试次数 |
冷却期的设计意图是避免频繁 consolidation:记忆的整合不需要实时性,6 小时一次已经足够 [源码]。
Input Selection — 使用频率驱动的遗忘
Phase2 不会把所有 Phase1 输出都喂给 consolidation agent,而是做一轮 usage-based selection [源码]:
排序规则:
1. usage_count DESC — 被引用越多越重要
2. COALESCE(last_usage, source_updated_at) DESC — 最近使用优先
3. source_updated_at DESC — 同等条件取更新的
4. thread_id DESC — 确定性打破平局
| 参数 | 默认值 | 作用 |
|---|---|---|
max_raw_memories_for_consolidation |
256 | top-N 选择上限 |
max_unused_days |
30 | 超期未引用的记忆被清理 |
这是一种隐式遗忘机制:不是主动删除记忆,而是让不被引用的记忆在 30 天后自然淘汰。对比 Graphiti 的时序失效(通过 valid_at / invalid_at 显式标记)和 mem0 的无遗忘设计,Codex 选择了中间路线 [已有]。
Consolidation Agent 沙箱
Phase2 的 consolidation agent 在极其严格的沙箱中运行 [源码]:
| 约束 | 设置 |
|---|---|
| ephemeral | true(不产生递归记忆) |
| generate_memories | false |
| use_memories | false |
| MCP 服务器 | 无 |
| 人工审批 | Never |
| 网络访问 | 禁止 |
| /tmp 访问 | 禁止 |
| 写入范围 | 仅 memory root |
| Reasoning effort | Medium |
Consolidation Prompt
Phase2 的 prompt(consolidation.md,约 881 行)定义了两种模式 [源码]:
INIT 模式(首次构建):从零开始,把所有 raw_memories 整理成 MEMORY.md + memory_summary.md + skills/。
INCREMENTAL UPDATE 模式(增量更新):利用 git diff 作为路由——新增/修改的 rollout summary 进入 ingestion queue,被删除的进入 forgetting queue,然后做外科手术式更新。
文件系统产物
| 文件/目录 | 阶段 | 作用 | 格式 [源码] |
|---|---|---|---|
SQLite stage1_outputs |
Phase1 产出 | per-rollout 提取结果 | 表:thread_id, raw_memory, rollout_summary, usage_count… |
raw_memories.md |
Phase2 输入 | 合并所有选中的 Phase1 输出 | 按 thread_id 升序排列的 Markdown |
rollout_summaries/ |
Phase2 输入 | 每 rollout 一个摘要文件 | <timestamp>-<hash>-<slug>.md |
phase2_workspace_diff.md |
Phase2 输入 | git diff 快照 | 状态列表 + unified diff (≤4MB) |
MEMORY.md |
Phase2 产出 | 合并后的结构化记忆手册 | Task Group → Task 层级,含 rollout_summary_files 和 keywords |
memory_summary.md |
Phase2 产出 | 摘要层,总是被加载 | v1 开头,含 User Profile / preferences / tips / index |
skills/ |
Phase2 产出 | 技能文件 | SKILL.md + 支撑脚本/模板 |
extensions/ad_hoc/ |
用户手动 | 临时备注 | 标记 [ad-hoc note],7 天自动清理 |
memory_summary.md 必须以 v1 开头——这不是版本号装饰,而是 consolidation agent 用于判断是否已初始化的标记 [源码]。
与 DB-Agent-Memory 的对照
| 维度 | Codex Phase2 | DB-Agent-Memory [已有] |
|---|---|---|
| 合并策略 | 全局 AI agent 做 consolidation | L0→L1→L2→L3 层级 LLM 提炼 |
| 触发方式 | root session 启动,6h 冷却 | 每次 add() 触发层级晋升 |
| 冲突处理 | consolidation agent 自行决策 | 同层去重 + MMD 稳定区 |
| 全局锁 | 有(单点瓶颈) | 无全局锁 |
| 遗忘 | usage_count + 30 天过期 | 无显式遗忘 |
| 产物形态 | 文件系统(Markdown + skills) | SQLite 行(四层级) |
读路径: Developer Instruction 注入
Codex 的记忆读路径与 Claude Code 截然不同:它不做按需向量检索,而是将记忆作为 developer instruction 直接注入 [源码]。
注入机制
memory_summary.md(摘要层)总是被加载——这意味着每个 session 都会看到完整的记忆摘要。需要更深层记忆时,Agent 可以通过文件读取工具访问 MEMORY.md 和 rollout_summaries/ [源码][文档]。
Citation 解析
当 Agent 在回答中引用了记忆,读路径会解析 citation 标记 [源码]:
| Citation 格式 | 含义 |
|---|---|
<citation_entries>path:line_start-line_end\|note=[text]</citation_entries> |
文件引用 |
<rollout_ids>thread_id1,thread_id2</rollout_ids> |
追溯到原始 rollout |
解析结果用于两个目的:(1) 给用户展示记忆来源;(2) 递增 usage_count,直接影响 Phase2 的 input selection 排序。这形成了一个正反馈循环:被引用越多的记忆越不容易被遗忘 [源码]。
与 Claude Code 读路径的对照
| 维度 | Codex | Claude Code |
|---|---|---|
| 注入方式 | developer instruction 全量注入 | MEMORY.md index 全量 + Sonnet 按需选 ≤5 |
| 检索模型 | 无(文件系统读取) | Sonnet sideQuery |
| 每 turn 成本 | 低(无额外 LLM 调用) | 高(每 turn 一次 Sonnet 选型) |
| 精确度 | 依赖 Agent 自行 grep | LLM 语义匹配 |
| 摘要层 | memory_summary.md 总是加载 |
MEMORY.md index 总是加载 |
与 AGENTS.md(Profile)的边界
Codex 明确区分了两种持久化配置 [源码]:
| 维度 | AGENTS.md (Profile) | ~/.codex/memories/ (User memory) |
|---|---|---|
| 写入者 | 人工维护 | AI pipeline 自动写入 |
| 内容 | 行为规则、编码规范 | 偏好、项目知识、历史提炼 |
| 发现方式 | 从项目根到 cwd 逐级查找 | 固定路径 ~/.codex/memories/ |
| 进入 Phase1 | 被 过滤掉 | N/A(Phase1 的产出) |
| 格式 | 自由 Markdown | 结构化(frontmatter + 层级) |
Phase1 的内容过滤中,AGENTS.md 指令被显式移除——这正是”Profile 不应被记忆系统记住”原则的代码体现 [源码]。
状态管理: SQLite 协调
记忆 pipeline 的协调完全依赖 SQLite [源码]:
stage1_outputs 表
CREATE TABLE stage1_outputs (
thread_id TEXT PRIMARY KEY,
source_updated_at INTEGER NOT NULL,
raw_memory TEXT NOT NULL,
rollout_summary TEXT NOT NULL,
rollout_slug TEXT,
generated_at INTEGER NOT NULL,
usage_count INTEGER, -- citation 递增
last_usage INTEGER, -- 最近引用时间
selected_for_phase2 INTEGER DEFAULT 0,
selected_for_phase2_source_updated_at INTEGER
);
jobs 表
CREATE TABLE jobs (
kind TEXT, -- 'phase1' / 'phase2'
job_key TEXT, -- thread_id 或 'global'
status TEXT,
worker_id TEXT,
ownership_token TEXT, -- 防止锁被窃取
lease_until INTEGER, -- 租约到期时间
retry_remaining INTEGER DEFAULT 3,
input_watermark INTEGER,
last_success_watermark INTEGER,
PRIMARY KEY (kind, job_key)
);
Watermark 机制值得注意:Phase2 完成后记录 last_success_watermark,下次 Phase2 只处理 watermark 之后的新数据。但计算时取 max(claimed_watermark, max(source_updated_at)),确保 watermark 永远不会后退 [源码]。
局限性
- Phase2 单点瓶颈:全局锁 + 6h 冷却意味着高频使用场景下 consolidation 可能跟不上 Phase1 的产出速度。
- 异步延迟:Phase1 要求 rollout 至少 idle 6 小时才处理,Phase2 还有 6h 冷却——新 session 可能看不到前一个 session 的记忆。
- Consolidation Agent 失败:如果 Agent 在 1h 租约内未完成,锁过期后重试,但只有 3 次重试机会。
- 无增量读取优化:
memory_summary.md全量注入,随着记忆增长可能占用大量上下文。 - 遗忘粒度粗:以整个 rollout 为单位遗忘,不能选择性保留某条 rollout 中的部分记忆。
证据等级汇总
| 论点 | 证据等级 |
|---|---|
| 两阶段异步 pipeline 架构 | [源码][文档] |
| Phase1 并行提取 (concurrency=8) | [源码] |
| Phase2 全局锁 + 6h 冷却 | [源码] |
| usage_count 驱动的遗忘机制 | [源码] |
| Consolidation Agent 沙箱约束 | [源码] |
| Git-as-diff-engine 增量更新 | [源码] |
| Developer instruction 注入(非向量检索) | [源码][文档] |
| AGENTS.md 内容在 Phase1 被过滤 | [源码] |
| memory_summary.md 总是加载 | [源码] |
| Secret 脱敏多层应用 | [源码] |
| API 余量检查(25% 阈值) | [源码] |
与开源记忆项目对照总表
| 维度 | Codex | mem0 [已有] |
DB-Agent-Memory [已有] |
Graphiti [已有] |
Letta [已有] |
|---|---|---|---|---|---|
| 组织范式 | 两阶段 pipeline → 文件系统产物 | 扁平事实列表 | L0–L3 语义金字塔 | 时序知识图谱 | core/archival 虚拟内存 |
| 写入触发 | root session 启动异步 | add() API 调用 | add() 触发层级晋升 | add_episode() | Agent 工具调用 |
| 检索方式 | developer instruction 全量注入 | 向量相似度 | 混合检索 + MMD | 向量 + 图遍历 + Cross-Encoder | recall(向量) |
| 存储后端 | SQLite + 文件系统 | 向量数据库(25+ 后端) | SQLite + sqlite-vec | Neo4j + 内建向量 | PostgreSQL |
| 冲突处理 | consolidation agent 自行决策 | LLM 二选一 | 同层去重 | 时序失效 (valid_at/invalid_at) | core_memory_replace |
| 遗忘机制 | usage_count + 30天过期 | 无 | 无 | 时序失效 | 无显式遗忘 |
| 外部依赖 | 无(内建) | 需向量 DB | 零外部 API | 需 Neo4j + LLM | 需 LLM + PG |
| 与 harness 耦合 | 高(深度集成到 Codex CLI) | 低(独立库) | 低(Host-Adapter 解耦) | 低(独立库) | 高(完整框架) |