跳转到内容

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-storestate_5.sqlite线程派生拓扑

长期记忆:ext/memories

ext/memories 是一个 Codex Extension,以 MCP 工具形式暴露记忆读写能力:

存储后端LocalMemoriesBackend,根目录为 ~/.codex/memories/

路径安全resolve_scoped_path 拒绝:

  • ..(父目录引用)
  • 根目录访问
  • 隐藏文件(以 . 开头)
  • 符号链接

暴露的工具

工具功能
memories/list列出目录内容,支持 cursor 分页(最大 2000 条)
memories/read按行读取文件,支持 line_offsetmax_linesmax_tokens(默认 20K)
memories/search全文搜索,支持 Any/AllOnSameLine/AllWithinLines 匹配模式

集成方式

  • MemoriesExtension 实现 ContextContributorToolContributorThreadLifecycleContributor
  • 向模型提供 build_memory_tool_developer_instructions 提示(教导模型如何使用记忆工具)
  • Feature::MemoryToolconfig.memories.use_memories 开关控制

状态持久化:三库拆分

state/src/lib.rs 将状态拆分为三个 SQLite 文件:

数据库文件用途
State DBstate_5.sqlite线程元数据、Agent 图、backfill 状态
Logs DBlogs_2.sqlite结构化日志(tracing 事件)
Goals DBgoals_1.sqlite线程目标(ThreadGoal)管理

SQLite 配置

  • journal_mode = WAL:读写不冲突
  • synchronous = NORMAL:平衡安全与性能
  • busy timeout:5 秒
  • 迁移:sqlx::migrate,支持向前兼容(ignore_missing: true

日志批量写入

state/src/log_db.rsLogDbLayer

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 图、目标管理)

参考来源