一次 Codex Desktop 更新后,我遇到了最让人不安的一类故障:侧边栏里的旧任务几乎全部消失,归档列表变成空白,项目中的历史也不再完整。搜索过去的标题没有结果;即使把某个旧任务重新置顶,重启后它仍会再次消失。
这些表象很容易让人得出一个结论:更新删除了历史。然而磁盘上的 Codex 状态目录依然很大,sessions、archived_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:查询、读取、恢复与分页
侧边栏只是这些数据经过查询后形成的一种视图。它没有显示某个任务,只能证明当前列表没有返回该任务。
“界面没有看见”与“磁盘已经删除”属于两个需要分别验证的命题。
这个分层后来成为整次排障的主线:先检查正文,再检查元数据和路径,最后才讨论列表、项目与界面。
先保护现场
故障出现后,我先保存了整个状态目录。备份发生在故障之后,无法证明故障前的配置究竟是什么;它仍然保留了最重要的现场证据。
只读检查依次确认:
state_5.sqlite存在,PRAGMA integrity_check返回ok;sessions与archived_sessions中仍有大量 rollout;- 目标任务可以按日期和 ID 定位;
- rollout 开头的
session_meta.payload.id与任务 ID 一致; - JSONL 能够逐行解析;
- 文件尾部仍包含预期的近期消息;
- 某些旧任务可以通过已知 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。
默认列表结果如下:
| 默认配置 | 活动任务 | 归档任务 |
|---|---|---|
openai | 37 | 30 |
chatgpt-http | 1 | 0 |
保留自定义 Provider 定义,但默认切回 openai | 37 | 30 |
随后固定列表请求中的 Provider 条件:
modelProviders | 活动任务 | 归档任务 |
|---|---|---|
[] | 38 | 30 |
["openai"] | 37 | 30 |
["chatgpt-http"] | 1 | 0 |
这些计数对应当时 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_HOME 与 sqlite_home,SQLite 可以拥有独立于 CODEX_HOME 的位置。迁移时需要同时确认环境入口、数据库位置和 rollout 路径,不能只检查一个变量。
最后一条测试消息
只看到 Codex 从新目录启动,还不足以证明迁移已经完成。最终验证发送了一条新的、可识别的测试消息,然后检查:
- 当前进程继承的
CODEX_HOME; - 用户级持久环境变量;
- 活动数据库的实际位置;
- 当前任务的
rollout_path; - rollout 尾部是否出现最新消息;
- 旧目录是否仍被任何任务引用。
终局结果为:
SQLite integrity_check:ok
任务:132
活动 / 归档:82 / 50
openai / chatgpt-http:121 / 11
目标目录中的 rollout:132 / 132
旧目录 rollout 引用:0
迁移后新消息写入目标目录:已确认
迁移真正闭环的证据,是新消息继续写入新目录,同时旧目录停止承担运行时读写。
这次排障留下了什么
表面上,这次工作恢复了历史任务、归档、项目和跨盘状态目录。更值得保留的是一套可重复使用的观察顺序:
正文有没有?
元数据有没有?
路径指向哪里?
查询筛选了什么?
界面加载了多少?
恢复运行还缺什么?
新写入最终落在哪里?
遇到类似问题时,我会先停止覆盖和清理,保存时间点快照,再按任务 ID 检查 rollout、SQLite、路径和列表条件。一次实验只改变一个变量,并记录它支持或排除了什么。只有证据链闭合之后,才进入配置修改、迁移激活与旧目录清理。
历史任务“消失”听起来像一场数据灾难。把任务系统拆成正文、元数据、发现、读取、恢复和分页几个层次之后,它就变成了一组可以逐项验证的问题。
至于“简中第一篇”,我不敢认;但我希望它至少是一篇把 Codex 历史任务“消失”这件事,从现象、误判与取证,一路写到恢复和迁移闭环的中文复盘。
参考资料
- OpenAI:Codex Environment variables
- OpenAI:Codex Configuration Reference
- OpenAI:Advanced Configuration
- OpenAI Codex App Server README
相关中文实践
- Dailin521/codex-provider-sync:一个把 Provider 可见性诊断、元数据同步、备份与恢复封装成 CLI 和桌面界面的开源工具。它提供只读的
status,并在sync或switch前创建备份;同步操作会修改 rollout、SQLite 和项目可见性元数据。仓库也明确提醒,包含encrypted_content的历史跨 Provider 或账户后,可能只能恢复列表可见性,继续对话或 compact 仍会失败。 - 极拓工坊:Codex 修复 Reconnecting 后历史记录消失
- 文武科技柜:Codex 历史会话不显示怎么办
这些资料和工具提供了不同的修复路径。codex-provider-sync 把诊断、备份、同步与恢复做成了较完整的工具化实践,值得在确认根因后参考;它仍然依赖 Codex 的内部状态格式。本文更关注修改前的证据确认、版本边界和可回滚性,不将任何一种内部数据改写方法视为长期稳定接口。
评论
由 GitHub Discussions 提供支持。