Codex 源码解析 · 第 29 讲
Codex 源码解析系列 第 29 讲:State Runtime Store 基于 OpenAI Codex 源码 · 2026-10-23 💡 本讲一句话: Codex 的线程元数据、日志、目标记账、记忆任务、用户消息队列全部落在 SQLite 上——这一讲拆它为什么拆成 7 个库文件、怎么在"多进程并发 + 数据库损坏"两种最脏的场景下保住数据,读完你能看懂一套生产级嵌入式存储的完整防御体系。 一、为什么是 7 个 SQLite 文件,而不是一个大库? state crate(约 2 万行)的入口是 codex-rs/state/src/sqlite.rs 。它把全部运行时状态按"写入频率 × 数据量"拆成独立文件,每个文件一个连接池: 📄 codex-rs/state/src/sqlite.rs (第 29-34、298-311 行) const LOGS_DB_FILENAME: &str = "logs_2.sqlite"; // 日志库:高频追加、按线程分区裁剪,单独成文件避免锁住主库 const GOALS_DB_FILENAME: &str = "goals_1.sqlite"; // 目标记账库:goal 状态与续跑延迟记录 const MEMORIES_DB_FILENAME: &str = "memories_1.sqlite"; // 记忆库:stage-1/stage-2 任务表 + 提取产物 const QUEUE_DB_FILENAME: &str = "queue_1.sqlite"; // 持久化用户消息队列:崩溃不丢未发送的输入 const STATE_DB_FILENAME: &str = "state_5.sqlite"; // 主状态库:线程元数据等,文件名后缀是 schema 世代 pub async fn open_read_write_pool(&self, path: &Path) -> Result<SqlitePool, Error> { // 所有可写库的统一打开入口 let options = SqliteConnectOptions::new() // 构造连接选项,集中一处便于审计 .filename(path) // 指定目标文件路径 .create_if_missing(true) // 首次启动自动建文件,老用户则直接复用 .journal_mode(SqliteJournalMode::Wal) // WAL 模式:读者不阻塞写者,多进程并发的核心选择 .synchronous(SqliteSynchronous::Normal) // NORMAL 级别 fsync:WAL 下兼顾持久性与吞吐的平衡点 .auto_vacuum(SqliteAutoVacuum::Incremental) // 增量 vacuum:空闲页可小批量回收,避免整库 VACUUM 卡前台 .busy_timeout(Duration::from_secs(5)) // 锁竞争时最多等 5 秒再报 busy,多进程抢写不立即失败 .log_statements(LevelFilter::Off); // 关掉 SQL 日志:高频查询会刷爆日志盘 SqlitePoolOptions::new() // 连接池构建器 .max_connections(5) // 每库最多 5 条连接:SQLite 单写者,更多只会加剧锁竞争 .connect_with(options) // 用上述选项真正建池 .await // 异步等待连接就绪 为什么这样设计: SQLite 是单写者模型——一个库文件同一时刻只能有一个进程持有写锁。Codex 的 TUI、app-server、后台记忆任务可能同时活着,如果全挤在一个库里,日志的高频追加会直接卡死线程列表查询。拆成 7 个文件后,"日志写入热路径"和"元数据读路径"物理隔离, busy_timeout(5s) 只兜底同库内的竞争。WAL + synchronous=NORMAL 是 SQLite 官方推荐的"近实时持久化"组合:崩溃最多丢最后一条未 checkpoint 的 WAL 帧,换来的是写放大减半。 库文件 承载内容 迁移文件数 state_5…
阅读全文:Codex 源码解析 第 29 讲