Codex 的记忆系统设计
Codex 的记忆系统设计
学习目标
- 理解 Codex 的记忆分层和具体实现
- 掌握 Codex 的 SQLite 三库拆分架构
- 分析 Codex 的 Agent Graph Store 和日志批量写入机制
前置知识
本章涉及记忆系统的通用原理,建议先阅读:
下文假设你已理解上述概念,直接聚焦 Codex 的具体实现。
项目实践
记忆分层实现
Codex 的记忆系统分布在多个 crate:
| 层次 | Crate | 存储位置 | 用途 |
|---|---|---|---|
| 短期记忆 | message-history | ~/.codex/history.jsonl | 用户输入历史 |
| 长期记忆 | ext/memories | ~/.codex/memories/ | 文件系统记忆 |
| 状态持久化 | state | ~/.codex/state_*.sqlite | 线程元数据、日志、目标 |
| Agent 图 | agent-graph-store | state_5.sqlite | 线程派生拓扑 |
长期记忆:ext/memories
ext/memories 是一个 Codex Extension,以 MCP 工具形式暴露记忆读写能力:
存储后端:LocalMemoriesBackend,根目录为 ~/.codex/memories/
路径安全:resolve_scoped_path 拒绝:
..(父目录引用)- 根目录访问
- 隐藏文件(以
.开头) - 符号链接
暴露的工具:
| 工具 | 功能 |
|---|---|
memories/list | 列出目录内容,支持 cursor 分页(最大 2000 条) |
memories/read | 按行读取文件,支持 line_offset、max_lines、max_tokens(默认 20K) |
memories/search | 全文搜索,支持 Any/AllOnSameLine/AllWithinLines 匹配模式 |
集成方式:
MemoriesExtension实现ContextContributor、ToolContributor、ThreadLifecycleContributor- 向模型提供
build_memory_tool_developer_instructions提示(教导模型如何使用记忆工具) - 受
Feature::MemoryTool与config.memories.use_memories开关控制
状态持久化:三库拆分
state/src/lib.rs 将状态拆分为三个 SQLite 文件:
| 数据库 | 文件 | 用途 |
|---|---|---|
| State DB | state_5.sqlite | 线程元数据、Agent 图、backfill 状态 |
| Logs DB | logs_2.sqlite | 结构化日志(tracing 事件) |
| Goals DB | goals_1.sqlite | 线程目标(ThreadGoal)管理 |
SQLite 配置:
journal_mode = WAL:读写不冲突synchronous = NORMAL:平衡安全与性能- busy timeout:5 秒
- 迁移:
sqlx::migrate,支持向前兼容(ignore_missing: true)
日志批量写入
state/src/log_db.rs 的 LogDbLayer:
impl<S> Layer<S> for LogDbLayer { fn on_event(&self, event: &Event, _ctx: Context<S>) { // 通过 channel 发送事件到后台任务 self.sender.send(event.into()); }}后台任务:
- 每
LOG_BATCH_SIZE(128 条)或每LOG_FLUSH_INTERVAL(2 秒)批量插入 - 分区限制:每线程最多 10 MiB / 1000 条
Agent Graph Store
agent-graph-store/src/lib.rs 持久化线程派生拓扑:
trait AgentGraphStore { fn upsert_thread_spawn_edge(&self, parent: ThreadId, child: ThreadId ) -> Result<>; fn set_thread_spawn_edge_status( &self, edge: ThreadSpawnEdge, status: ThreadSpawnEdgeStatus ) -> Result<>; fn list_thread_spawn_children( &self, parent: ThreadId, status: Option<ThreadSpawnEdgeStatus> ) -> Result<Vec<ThreadId>>; fn list_thread_spawn_descendants( &self, parent: ThreadId, status: Option<ThreadSpawnEdgeStatus> ) -> Result<Vec<ThreadId>>;}查询语义:list_thread_spawn_descendants 使用 BFS + 深度排序 + thread_id 排序,状态过滤作用于遍历的每条边。
问题与规避
SQLite 并发锁竞争
Codex 的对策:
- WAL 模式:读写互不阻塞
- 连接池:复用数据库连接
- 短事务:尽快提交
- 5 秒 busy timeout:自动重试
日志无限增长
Codex 的分区限制:每线程最多 10 MiB / 1000 条,超过后自动删除最旧记录。
设计取舍
为什么记忆以 Extension 形式实现?
Codex 将记忆系统实现为 Extension(ext/memories)而非内置在 core 中。优势是:
- 模块化:可以独立开发、测试和发布
- 可选性:用户可以通过 Feature 开关禁用
- 标准化:遵循 Codex Extension API,便于第三方扩展参考
代价是增加了 Extension 接口的复杂度,启动时需要加载和初始化 Extension。
为什么使用 JSONL 而非 SQLite 存储消息历史?
Codex 使用 JSONL 存储用户输入历史,而使用 SQLite 存储线程元数据。原因是:
- JSONL 追加写入简单高效,无需事务
- 消息历史是纯日志性质,不需要复杂查询
- SQLite 用于需要关系查询的数据(Agent 图、目标管理)