一次 Codex Desktop 更新后,我遇到了最让人不安的一类故障:侧边栏里的旧任务几乎全部消失,归档列表变成空白,项目中的历史也不再完整。搜索过去的标题没有结果;即使把某个旧任务重新置顶,重启后它仍会再次消失。

这些表象很容易让人得出一个结论:更新删除了历史。然而磁盘上的 Codex 状态目录依然很大,sessionsarchived_sessions 和 SQLite 数据库也都存在。历史究竟丢在了哪里,还是它们从未离开,只是当前界面没有把它们列出来?

为了回答这个问题,我经历了项目映射试修、干净配置、代表性样本迁移、132 个任务的全量迁移、配置回滚、App Server 隔离 A/B 和跨盘迁移。整个协作过程消耗了超过一个完整 Plus Plan 周额度。最后,根因落在一行原本用于解决网络问题的配置上:

model_provider = "chatgpt-http"

历史正文一直保存在磁盘上;默认 Provider 改变了任务列表的可见集合,让另一组任务暂时离开了侧边栏

检索说明(2026 年 7 月)

简中网络已经有人记录过切换 model_provider 后 Codex 历史会话不可见的问题,也给出了同步或改写本地元数据的修复方案。这篇文章希望补上一段更完整的调查过程:从 rollout 与 SQLite 的只读取证开始,通过配置回滚和单变量 A/B 建立因果链,再把恢复范围延伸到长历史加载、项目映射与 CODEX_HOME 迁移闭环。

最后修改哪一行并不难。真正困难的是证明这行配置与症状之间存在因果关系。

侧边栏先把我带偏了

起初,我把“历史任务”当成一个完整对象:它要么存在,要么丢失。实际调查很快表明,一个任务至少横跨几个彼此独立的数据层:

JSONL rollout:任务正文与事件流
state_5.sqlite:任务元数据与正文路径
session index / global state:列表与项目状态
config.toml:Provider、MCP 与持久配置
Desktop / App Server:查询、读取、恢复与分页

侧边栏只是这些数据经过查询后形成的一种视图。它没有显示某个任务,只能证明当前列表没有返回该任务。

“界面没有看见”与“磁盘已经删除”属于两个需要分别验证的命题

这个分层后来成为整次排障的主线:先检查正文,再检查元数据和路径,最后才讨论列表、项目与界面。

先保护现场

故障出现后,我先保存了整个状态目录。备份发生在故障之后,无法证明故障前的配置究竟是什么;它仍然保留了最重要的现场证据。

只读检查依次确认:

  1. state_5.sqlite 存在,PRAGMA integrity_check 返回 ok
  2. sessionsarchived_sessions 中仍有大量 rollout;
  3. 目标任务可以按日期和 ID 定位;
  4. rollout 开头的 session_meta.payload.id 与任务 ID 一致;
  5. JSONL 能够逐行解析;
  6. 文件尾部仍包含预期的近期消息;
  7. 某些旧任务可以通过已知 ID 重新打开。

完成这些检查后,“历史整体被删除”的概率已经明显下降。

恢复工作的第一步应当是证明数据是否存在;覆盖、清理和数据库改写都应排在物理证据之后

这份顺序看似保守,却避免了最危险的错误:用一个较旧但界面正常的快照,覆盖仍在持续追加的活动历史。

一条很合理的错误路线

一个重要任务原本属于特定项目,因此我最先怀疑项目映射损坏。我尝试调整项目归属、工作区提示、置顶状态和无项目集合。结果确实改变了部分界面:

  • 任务可以被置顶;
  • 任务可能进入无项目集合;
  • 某些项目分组发生变化。

但它仍然无法稳定回到预期项目,重启后历史列表也没有整体恢复。

这个实验没有修好问题,却排除了一个很强的假设:项目映射负责任务放在哪一组,列表查询决定当前能不能发现它,rollout 则保存正文。三者相互关联,也需要分别诊断。

干净配置证明旧数据可读

下一步是建立一套干净配置。它经过两次冷启动后,可以正常创建项目和新任务。随后,我迁入少量旧任务、数据库行、项目映射和 rollout,旧任务仍然能够打开。

为了降低样本偏差,我又选择了五类任务:

  • 普通活动任务;
  • 归档任务;
  • 超长任务;
  • 有项目映射的任务;
  • 有工具调用或父子关系的任务。

这些样本都能被当前版本理解。这一结果支持两个判断:旧数据格式没有整体失效,调查范围应继续向旧配置、查询条件和界面加载路径收缩。

回头看,如果一开始就知道 Provider 筛选的影响,恢复旧 Provider 对应的可见集合只需切换一行配置。但在根因未知时,干净配置实验仍然证明了旧数据可读、项目和归档可以迁移、长任务正文仍在。

长任务只剩最近几轮

超长任务又制造了一个误导性很强的现象:第一次打开时,界面只显示最近三四轮。我一度准备向 rollout 注入一个“历史锚点”,希望让界面发现更早内容。

继续向前滚动后,Codex 开始逐步加载更早的原始历史。随后,我又使用完全没有锚点的副本做对照,并多次冷启动。每一次只要持续向前滚动,最终都能到达最早消息。

这说明长任务的表象发生在历史加载层。当前 App Server 还提供实验性的 turns 与 items 分页能力,但这次界面实验没有证明 Desktop 一定通过哪一组 RPC 获取更早内容。最终方案保留了原始 rollout,没有注入伪消息,也没有改写历史。

迁移成功后,故障又回来了

代表性样本验证通过后,我完成了 132 个任务的全量迁移。迁移后的状态包含:

任务:132
动态工具记录:150
父子任务边:43

SQLite 完整性、任务 ID、项目、归档、长历史和工具关系都通过了检查。收尾时曾有一个 DLL 因占用无法删除;它发生在清理旧 holding 的阶段,复制、导入和校验已经完成,因此只能说明清理阶段遇到了文件锁。

随后,我把原环境中的 Skills、MCP、插件声明、Memories、.env 和持久配置合并回干净环境。配置合并完成后,历史任务再次消失。

配置回滚后,它们立即出现;数据库、rollout、任务 ID 和项目映射几乎没有变化。这次复现把调查范围从数据库、正文、索引和项目,迅速缩小到了 config.toml

当数据保持不变,而配置回滚可以稳定切换症状时,配置差异就成为下一轮实验最有价值的变量。

那一行配置原本在解决另一个问题

故障配置中定义了一个自定义 HTTP-only Provider。下面保留的是当时环境中的诊断配置,其中端点不属于公开稳定接口,也不构成可直接复用的配置模板:

model_provider = "chatgpt-http"

[model_providers.chatgpt-http]
name = "ChatGPT HTTP"
base_url = "https://chatgpt.com/backend-api/codex"
wire_api = "responses"
requires_openai_auth = true
supports_websockets = false

它最初用于缓解代理环境中的另一类故障:

websocket closed by server before response.completed

当代理入口与 TLS 正常,而 Responses WebSocket 仍持续提前关闭时,关闭 WebSocket 能力可以让这个 Provider 使用另一条传输路径。问题在于,Provider 也会进入任务历史元数据。旧任务大多记录为 openai,使用自定义 Provider 后创建的新任务则记录为 chatgpt-http

同一个数据库由此出现了两个 Provider 集合。

单变量 A/B

为了停止猜测,我准备了三个隔离 CODEX_HOME。它们使用同一份数据库快照,只改变 config.toml 中的默认 Provider。

默认列表结果如下:

默认配置活动任务归档任务
openai3730
chatgpt-http10
保留自定义 Provider 定义,但默认切回 openai3730

随后固定列表请求中的 Provider 条件:

modelProviders活动任务归档任务
[]3830
["openai"]3730
["chatgpt-http"]10

这些计数对应当时 Desktop 默认交互来源集合。SQLite 的 132 行还包含 subagent、guardian 等内部任务,不能直接与表中数量相加比较。

实验中最关键的观察是:

  • 显式查询相同 Provider 时,三套配置返回相同任务 ID;
  • 默认 Provider 改变时,默认列表结果随之改变;
  • 数据库和 rollout 没有变化。

最后,我又在实际配置上往返切换默认 Provider,并在每次修改后完整退出、重新启动 Codex。旧任务与当前任务稳定地交替出现,因果链终于闭合:

网络传输 workaround

默认 Provider 改变

新旧任务记录在不同 Provider 下

Desktop 默认列表跟随当前 Provider

另一组任务暂时不可见

本文的实测环境为 Windows 11 专业工作站版 25H2(OS Build 26220.8764)与 Codex Desktop 0.145.0-alpha.18。App Server 的公开协议和实现仍在变化,不能把这张表当成其他系统或所有版本的长期保证。

迁移又暴露了另一种“不完整”

根因已经找到,排障过程中还出现了另一个独立问题:临时 CODEX_HOME 位于新分区,任务正文却仍在旧目录增长。

本机数据库中的 threads.rollout_path 保存绝对路径。简单复制状态目录并切换环境变量后,可能出现:

配置从新目录加载
数据库位于新目录
rollout_path 仍指向旧目录
新消息继续追加到旧目录

这样的环境可以启动,历史也可能正常显示,但旧目录仍然承担运行依赖。后来我通过任务 ID、路径边界、哈希和严格前缀关系协调了多个副本,再把全部 rollout 路径收束到新的状态根目录。

当前官方文档还提供了 CODEX_SQLITE_HOMEsqlite_home,SQLite 可以拥有独立于 CODEX_HOME 的位置。迁移时需要同时确认环境入口、数据库位置和 rollout 路径,不能只检查一个变量。

最后一条测试消息

只看到 Codex 从新目录启动,还不足以证明迁移已经完成。最终验证发送了一条新的、可识别的测试消息,然后检查:

  1. 当前进程继承的 CODEX_HOME
  2. 用户级持久环境变量;
  3. 活动数据库的实际位置;
  4. 当前任务的 rollout_path
  5. rollout 尾部是否出现最新消息;
  6. 旧目录是否仍被任何任务引用。

终局结果为:

SQLite integrity_check:ok
任务:132
活动 / 归档:82 / 50
openai / chatgpt-http:121 / 11
目标目录中的 rollout:132 / 132
旧目录 rollout 引用:0
迁移后新消息写入目标目录:已确认

迁移真正闭环的证据,是新消息继续写入新目录,同时旧目录停止承担运行时读写

这次排障留下了什么

表面上,这次工作恢复了历史任务、归档、项目和跨盘状态目录。更值得保留的是一套可重复使用的观察顺序:

正文有没有?
元数据有没有?
路径指向哪里?
查询筛选了什么?
界面加载了多少?
恢复运行还缺什么?
新写入最终落在哪里?

遇到类似问题时,我会先停止覆盖和清理,保存时间点快照,再按任务 ID 检查 rollout、SQLite、路径和列表条件。一次实验只改变一个变量,并记录它支持或排除了什么。只有证据链闭合之后,才进入配置修改、迁移激活与旧目录清理。

历史任务“消失”听起来像一场数据灾难。把任务系统拆成正文、元数据、发现、读取、恢复和分页几个层次之后,它就变成了一组可以逐项验证的问题。

至于“简中第一篇”,我不敢认;但我希望它至少是一篇把 Codex 历史任务“消失”这件事,从现象、误判与取证,一路写到恢复和迁移闭环的中文复盘。

参考资料

相关中文实践

这些资料和工具提供了不同的修复路径。codex-provider-sync 把诊断、备份、同步与恢复做成了较完整的工具化实践,值得在确认根因后参考;它仍然依赖 Codex 的内部状态格式。本文更关注修改前的证据确认、版本边界和可回滚性,不将任何一种内部数据改写方法视为长期稳定接口。