当一个 Codex 任务从侧边栏消失时,最自然的疑问是“任务还在不在”。这个问题听起来只有是或否,实际至少包含四种状态:列表能否发现、能否按 ID 读取、能否恢复运行,以及新消息能否继续执行。
这些状态由不同数据层共同决定。rollout 保存任务经历了什么,SQLite 保存任务怎样被定位与组织,Provider 参与任务的执行语义,App Server 与客户端查询则决定哪些任务进入当前界面。
一个任务的正文仍在磁盘上,并不保证它会出现在当前列表;列表能够发现它,也不保证正文已经读取或任务已经具备继续运行的条件。
本文建立一套任务存储与生命周期模型。公开配置与环境变量以当前官方文档为准;state_5.sqlite 表结构、绝对 rollout_path、session_index.jsonl、全局状态文件和 Provider 列表行为都来自 0.145.0-alpha.18 的本机观察,随版本变化的可能性较高。
这套模型来自一次真实的历史任务不可见故障复盘。案例讲发生了什么,本文只讨论这些现象背后的数据关系。
一个任务横跨哪些数据层
当前 Codex 的本地状态可以按职责分为。表中的具体文件名与目录来自 0.145.0-alpha.18 的本机观察,只用于解释数据关系:
| 数据层 | 典型位置 | 主要职责 |
|---|---|---|
| 活动任务正文 | sessions/**/*.jsonl | 保存消息、工具调用和事件流 |
| 归档任务正文 | archived_sessions/**/*.jsonl | 保存已归档任务的事件流 |
| 任务元数据 | state_5.sqlite | 保存 ID、标题、Provider、状态和正文路径 |
| 列表与项目状态 | session_index.jsonl、全局状态文件 | 保存部分索引、项目归属和界面状态 |
| 用户配置 | config.toml | 选择 Provider、模型、MCP 与其他持久设置 |
| 持久能力 | skills/、rules/、memories/ | 提供可复用的方法、规则与上下文 |
| 应用查询 | thread/list、thread/read 等 | 发现、定位、恢复和访问任务历史 |
这些层次共同构成一个任务:
- rollout 存在,说明正文仍有物理载体;
- SQLite 行存在,说明应用仍保留对应目录元数据;
- 列表返回任务,说明它进入当前发现集合;
- 按 ID 定位成功,只说明应用找到了任务元数据;
- 包含历史的读取或独立解析成功,才说明正文可读;
- 恢复成功,说明运行环境仍能协调原有 Provider、认证和模型;
- 新消息成功追加,说明任务已经真正回到可继续状态。
“存在、可见、可定位、可读、可恢复、可继续”是六个不同判断,不应由一个侧边栏状态代替。
CODEX_HOME 与 CODEX_SQLITE_HOME
官方文档把 CODEX_HOME 定义为 Codex 状态根目录,默认值是:
~/.codex
CLI、IDE extension、App Server 和安装器都会使用它。目录可以包含配置、认证、日志、sessions、skills 和独立包元数据。自定义 CODEX_HOME 时,目标目录需要在 Codex 启动前存在。
当前文档还公开了:
CODEX_SQLITE_HOME
它用于指定 SQLite 状态位置,默认跟随 CODEX_HOME;sqlite_home 配置项拥有更高优先级。于是,下面三个位置可能彼此不同:
配置和 sessions 的状态根目录
SQLite 的实际目录
SQLite 中 rollout_path 指向的正文文件
这一区别对排障和迁移非常重要。环境变量看起来正确,只能证明入口配置正确;数据库和正文是否真正进入同一目标,需要单独检查。
Rollout 保存任务经历了什么
rollout 是逐行 JSON 的追加式事件日志。常见记录包括:
session_meta;- 用户与助手消息;
- 工具调用和工具结果;
- 上下文压缩事件;
- 运行状态与协议元数据。
新事件通常追加到文件尾部。因此,一份 rollout 可以提供三类证据:
- 文件尾部说明最新消息实际写入哪里;
- 文件大小变化说明任务是否仍在追加;
- 两份副本可以通过严格前缀关系判断继承关系。
设较短副本为 A,较长副本为 B:
len(B) >= len(A)
SHA256(A)
==
SHA256(B 的前 len(A) 个字节)
长度和前缀哈希同时成立时,可以高置信地判断 B 完整继承了 A,并在尾部增加了新事件。只比较修改时间或文件大小无法证明这一点。
rollout 开头的 session_meta 还可以包含任务 ID、创建时工作目录、Provider 和客户端来源。它既保存正文,也能参与任务身份和元数据重建。
SQLite 保存任务怎样被定位
本机 state_5.sqlite 的 threads 表包含过以下字段:
id
title
model_provider
archived
updated_at
tokens_used
rollout_path
source
thread_source
SQLite 更接近任务目录与关系索引,完整消息和工具事件仍保存在 rollout。
这意味着:
- 数据库完整,不能证明对应 rollout 一定存在;
- rollout 完整,不能证明当前列表一定返回任务;
- 数据库更新时间较新,不能证明它拥有较新的正文;
- 数据库和 rollout 可能来自不同快照时点。
本机版本中的 threads.rollout_path 保存绝对路径。复制 CODEX_HOME 后,如果数据库仍指向旧目录,任务可能从旧位置读取并继续追加。此时应用可以正常启动,迁移却仍依赖旧目录。
数据库位于新目录、正文位于新目录、数据库路径指向新正文,需要分别建立证据。
Provider 同时影响执行与历史
官方配置将 Provider 定义为 Codex 连接模型后端的方式,可以包含 base URL、wire API、认证和请求头。顶层 model_provider 从已定义的 Provider 中选择执行后端:
model_provider = "proxy"
[model_providers.proxy]
name = "OpenAI using proxy"
base_url = "https://proxy.example.com"
wire_api = "responses"
本机版本中,创建任务时使用的 Provider 会进入 SQLite 的 threads.model_provider,也会写进 rollout 的会话元数据。Provider 因而同时参与:
- 执行语义:创建或恢复任务时使用哪个模型后端;
- 历史元数据:任务最初记录在哪个 Provider 下。
后来修改默认 Provider,只会改变之后的执行配置,不会自动重写已有任务的历史 Provider。
这里还有两个相似名称:
| 名称 | 所在层级 | 作用 |
|---|---|---|
model_provider | config.toml | 选择一个默认执行 Provider |
modelProviders | App Server thread/list | 限定一次列表查询允许返回哪些 Provider |
前者是持久配置,后者是请求参数。不能把数组写进 model_provider,也不应把一次 RPC 的内部参数当成长期配置字段。
列表可见性只代表一次查询结果
任务发现通常从 thread/list 开始。它可以接收:
archived;searchTerm;cwd;modelProviders;sourceKinds;cursor和limit。
因此,列表可见的准确含义是:
当前 thread/list 查询返回了这个任务
它不保证 rollout 存在、正文可解析、原 Provider 仍可用,也不保证更早历史已经全部加载。
在 0.145.0-alpha.18 的隔离实验中,同一数据库只改变默认 Provider,默认列表就随之切换;显式查询全部 Provider 时,又能得到两组任务的并集。完整计数与往返验证保留在故障复盘中。
当时的结果支持“默认列表受到当前 Provider 影响”。需要注意,App Server README 当前把未设置、null 和空数组描述为包含全部 Provider,而公开仓库中也出现过与实际行为相关的讨论。协议说明、具体实现和 Desktop 调用方式可能在不同版本间变化。
modelProviders 的结论应同时标注 Codex 版本、请求参数和客户端入口,避免把一次本机观察写成永久规则。
读取、恢复与继续运行
已知任务 ID 后,thread/read 可以绕过标题搜索和项目分组,直接尝试定位任务。默认响应可能只包含元数据,因此成功返回不能单独证明正文已经解析。需要进一步确认 includeTurns: true 的历史响应、界面中的正文,或直接对 rollout 做只读解析。
独立检查 rollout 时,还要确认 rollout_path 指向实际文件、JSONL 可以解析,并且 session_meta.payload.id 与目标任务一致。历史能够读取后,任务仍可能无法继续发送消息。thread/resume 还需要协调任务原 Provider、当前 Provider 定义、认证状态、模型可用性、wire API 和传输能力。
所以任务状态可以分成:
| 状态 | 能够说明什么 |
|---|---|
| 可见 | 当前列表返回了任务 |
| 可定位 | 可以按 ID 返回任务元数据 |
| 可读 | 包含历史的响应、界面或 rollout 证明正文可用 |
| 可恢复 | 可以加载为运行实例 |
| 可继续 | 新消息能够执行并追加 |
故障排查时,从“可见”直接跳到“数据丢失”,会跨过中间所有可验证层级。
长历史与实验分页能力
长任务打开后只显示最近几轮,可能只是客户端尚未展示更早内容。本机界面实验中,持续向前滚动后,早期历史逐步出现。
当前 App Server 还提供实验性的分页接口:
thread/turns/list
thread/items/list
这些接口可以在特定分页模式下使用 cursor、limit 和排序方向获取 turns 与 items;默认 thread/resume 的返回方式并不等同于这套实验接口。本文无法从界面观察反推出 Desktop 当时使用了哪一条 RPC 路径。
本机无锚点对照中,多次冷启动后持续向前滚动,都能够到达最早消息。最终恢复保留了原始 rollout,没有注入伪消息。
打开任务后只看到近期内容时,先验证历史分页,再判断正文是否截断。
从创建到再次打开
一个任务的生命周期可以压缩成:
flowchart TD
A["读取 config.toml<br/>选择 Provider"] --> B["thread/start<br/>创建任务"]
B --> C["SQLite<br/>写入任务元数据"]
B --> D["JSONL rollout<br/>追加正文事件"]
C --> E["thread/list<br/>发现任务"]
C --> F["thread/read<br/>按 ID 定位元数据"]
E --> F
D --> G["包含历史的读取<br/>或独立解析"]
F --> G
G --> H["恢复条件校验<br/>Provider、认证、模型与传输"]
G --> I["界面逐步展示<br/>或实验分页接口"]
H --> J["thread/resume<br/>恢复运行"]
J --> K["新消息继续追加"]
其中任何一层发生变化,都可能改变界面表现:
- 列表没有任务,正文可能仍在;
- 已知 ID 可以读取,任务仍可能无法继续;
- 打开后只见近期内容,更早历史可能尚未分页;
- Provider 改变会影响执行,也可能改变当前客户端的发现集合;
- 修改状态根目录后,数据库或绝对路径仍可能依赖旧位置。
理解这套模型以后,“任务还在不在”可以被拆成一组更精确的问题。下一步应先判断故障落在哪一层,再选择风险最低的证据和操作,盲目重装与覆盖自然也就失去了优先级。
评论
由 GitHub Discussions 提供支持。