当一个 Codex 任务从侧边栏消失时,最自然的疑问是“任务还在不在”。这个问题听起来只有是或否,实际至少包含四种状态:列表能否发现、能否按 ID 读取、能否恢复运行,以及新消息能否继续执行。

这些状态由不同数据层共同决定。rollout 保存任务经历了什么,SQLite 保存任务怎样被定位与组织,Provider 参与任务的执行语义,App Server 与客户端查询则决定哪些任务进入当前界面。

一个任务的正文仍在磁盘上,并不保证它会出现在当前列表;列表能够发现它,也不保证正文已经读取或任务已经具备继续运行的条件

本文建立一套任务存储与生命周期模型。公开配置与环境变量以当前官方文档为准;state_5.sqlite 表结构、绝对 rollout_pathsession_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/listthread/read发现、定位、恢复和访问任务历史

这些层次共同构成一个任务:

  • rollout 存在,说明正文仍有物理载体;
  • SQLite 行存在,说明应用仍保留对应目录元数据;
  • 列表返回任务,说明它进入当前发现集合;
  • 按 ID 定位成功,只说明应用找到了任务元数据;
  • 包含历史的读取或独立解析成功,才说明正文可读;
  • 恢复成功,说明运行环境仍能协调原有 Provider、认证和模型;
  • 新消息成功追加,说明任务已经真正回到可继续状态。

“存在、可见、可定位、可读、可恢复、可继续”是六个不同判断,不应由一个侧边栏状态代替

CODEX_HOMECODEX_SQLITE_HOME

官方文档把 CODEX_HOME 定义为 Codex 状态根目录,默认值是:

~/.codex

CLI、IDE extension、App Server 和安装器都会使用它。目录可以包含配置、认证、日志、sessions、skills 和独立包元数据。自定义 CODEX_HOME 时,目标目录需要在 Codex 启动前存在。

当前文档还公开了:

CODEX_SQLITE_HOME

它用于指定 SQLite 状态位置,默认跟随 CODEX_HOMEsqlite_home 配置项拥有更高优先级。于是,下面三个位置可能彼此不同:

配置和 sessions 的状态根目录
SQLite 的实际目录
SQLite 中 rollout_path 指向的正文文件

这一区别对排障和迁移非常重要。环境变量看起来正确,只能证明入口配置正确;数据库和正文是否真正进入同一目标,需要单独检查。

Rollout 保存任务经历了什么

rollout 是逐行 JSON 的追加式事件日志。常见记录包括:

  • session_meta
  • 用户与助手消息;
  • 工具调用和工具结果;
  • 上下文压缩事件;
  • 运行状态与协议元数据。

新事件通常追加到文件尾部。因此,一份 rollout 可以提供三类证据:

  1. 文件尾部说明最新消息实际写入哪里;
  2. 文件大小变化说明任务是否仍在追加;
  3. 两份副本可以通过严格前缀关系判断继承关系。

设较短副本为 A,较长副本为 B

len(B) >= len(A)

SHA256(A)
==
SHA256(B 的前 len(A) 个字节)

长度和前缀哈希同时成立时,可以高置信地判断 B 完整继承了 A,并在尾部增加了新事件。只比较修改时间或文件大小无法证明这一点。

rollout 开头的 session_meta 还可以包含任务 ID、创建时工作目录、Provider 和客户端来源。它既保存正文,也能参与任务身份和元数据重建。

SQLite 保存任务怎样被定位

本机 state_5.sqlitethreads 表包含过以下字段:

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 因而同时参与:

  1. 执行语义:创建或恢复任务时使用哪个模型后端;
  2. 历史元数据:任务最初记录在哪个 Provider 下。

后来修改默认 Provider,只会改变之后的执行配置,不会自动重写已有任务的历史 Provider。

这里还有两个相似名称:

名称所在层级作用
model_providerconfig.toml选择一个默认执行 Provider
modelProvidersApp Server thread/list限定一次列表查询允许返回哪些 Provider

前者是持久配置,后者是请求参数。不能把数组写进 model_provider,也不应把一次 RPC 的内部参数当成长期配置字段。

列表可见性只代表一次查询结果

任务发现通常从 thread/list 开始。它可以接收:

  • archived
  • searchTerm
  • cwd
  • modelProviders
  • sourceKinds
  • cursorlimit

因此,列表可见的准确含义是:

当前 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 改变会影响执行,也可能改变当前客户端的发现集合;
  • 修改状态根目录后,数据库或绝对路径仍可能依赖旧位置。

理解这套模型以后,“任务还在不在”可以被拆成一组更精确的问题。下一步应先判断故障落在哪一层,再选择风险最低的证据和操作,盲目重装与覆盖自然也就失去了优先级。

参考资料