
这次我遇到的问题,是在 Codex Desktop 里切换账号、切换 API 通道之后,历史会话看起来像“丢了”:SQLite 里明明能查到线程,rollout 文件也还在,但侧边栏最近列表不完整,部分项目下面显示“暂无对话”,自定义标题也没有完全恢复。
更麻烦的是,有些线程我已经在数据库里改成了新的 model_provider,重启 Codex Desktop 后又被自动改回旧值。也就是说,这不是简单改一个配置项就能解决的问题,而是 Codex Desktop 本地多层状态之间没有对齐。
这篇文章我会按自己的排查过程整理一套可复用的方法。文章里不会放真实账号、真实路径、真实 API 地址、真实项目名、真实线程 ID 或具体密钥配置,重点保留可复用的排查思路。
我最终想解决什么问题
这次修复的目标不是单纯“导出聊天记录”,而是让旧账号、旧 API 通道、旧 model_provider 下产生的历史会话,在新的账号或新的 API 通道下仍然能被 Codex Desktop 识别、显示、打开,并尽量保留项目分组和自定义标题。
简单说,我希望达到这几个效果:
- 历史会话能继续出现在最近列表里。
- 项目侧边栏能重新看到对应项目下的会话。
- 自定义重命名过的标题能恢复。
- 切换账号或 API 通道后,旧会话仍然能继续打开使用。
- 重启 Codex Desktop 后,不会又被旧 provider 信息覆盖回去。
最省事的跨账号思路:尽量共用同一个 model_provider 值
如果你经常在多个账号、多个 API 通道之间切换,我现在更建议一开始就把不同通道尽量设计成同一个 model_provider 值。
也就是说,不要今天账号 A 用一个 provider 键名,明天账号 B 又换一个 provider 键名。对 Codex Desktop 来说,历史线程里记录的是 model_provider 这个标识。如果不同账号/API 通道都用不同 provider 值,后面历史会话就很容易分散在多个 provider 名下,迁移时也要反复处理 rollout、SQLite 和索引。
更简单的做法是:让多个账号或 API 通道尽量共用同一个 provider 键名,切换时只切换实际的 API 配置、密钥或通道。这样本地历史线程看到的 provider 标识保持一致,跨账号显示和继续使用历史会话会省事很多。
如果你本身就需要频繁切换配置,可以搭配 CC Switch 这类配置切换工具来管理不同账号/API 通道。核心思路不是让每个账号都生成一套新的 provider 名,而是尽量把本地历史会话绑定到一个稳定的 model_provider 值上,再通过外部工具切换实际通道。
当然,如果你之前已经把历史会话分散到了多个 provider 名下,那就需要按后面的步骤做一次整理,把旧 provider 下的历史线程迁移到统一的 provider 标识下。
为什么只改 config.toml 不够
我最开始也容易把问题想简单:既然新会话默认走谁由 config.toml 里的顶层 model_provider 决定,那是不是把这里改掉就行了?
实际不是。顶层 model_provider 主要决定“新开的会话默认走哪个 provider”。它不会自动批量迁移旧线程,也不会自动改 rollout 里的旧 session_meta,更不会帮你恢复侧边栏项目分组和自定义标题。
还有一点要注意:provider 的显示名称和 provider 键名不是一回事。Codex Desktop 本地状态里更关键的是 provider 键名,而不是你给它起的展示名称。真正的 API 地址、密钥、代理配置应该尽量不要出现在公开文章里,分享经验时用抽象写法就够了。
我排查后确认:至少要看四层状态
这次我最大的结论是:Codex Desktop 的历史会话展示不是只看 SQLite,而是多个本地状态共同投影出来的结果。至少要同时看这四层:
state_5.sqlite:线程主索引,通常能看到线程 ID、rollout 路径、model_provider、工作目录、标题、归档状态等。session_index.jsonl:最近会话索引。SQLite 里有线程,不代表最近列表一定能显示。sessions/archived_sessions下的 JSONL:会话原始记录,里面的session_meta可能会反过来影响 SQLite。.codex-global-state.json:侧边栏状态层,包括自定义标题、项目归属、工作区提示、项目排序、保存的工作区等。
所以,只改 SQLite 很容易失败;只修最近索引也不够;只补全局状态同样不稳定。想让历史会话跨账号、跨通道继续显示和使用,最好把这几层一起对齐。
坑一:rollout 里的 session_meta 会把 SQLite 改回去
这次我遇到的第一个关键坑,是 rollout JSONL 里的 session_meta。我一开始以为,只要把 SQLite 里的线程 model_provider 改成新的 provider 就行了。
结果重启 Codex Desktop 后,部分线程又变回旧 provider。后来我才确认,原因是 rollout 原始记录里仍然残留旧的 session_meta。当 Codex Desktop 执行 thread/list 或 thread/resume 这类读取行为时,它会重新读取 rollout,再把里面的 provider 信息投影回索引层。
所以修复顺序很重要:先处理 rollout 里的旧 session_meta,再同步修 SQLite。反过来只改 SQLite,重启后很可能又被旧 rollout 覆盖。
坑二:自定义标题不一定在 SQLite 里
我这次还遇到一个误判:某个历史线程的自定义标题没有恢复,起初我以为是数据库字段没改对。后来发现,真正关键的并不是 SQLite,而是 .codex-global-state.json 里的标题映射。
也就是说,线程本身还在,不代表侧边栏就一定能显示原来的自定义标题。如果全局状态里的标题映射丢了,标题就会看起来像恢复失败。
所以,遇到“标题丢失”时,我会优先检查全局状态里的标题映射,而不是只盯着数据库。
坑三:项目分组优先看显式 assignment
项目侧边栏的归组也不是简单只看工作目录。我这次确认到一个很重要的点:如果线程已经有显式项目归属,也就是类似 thread-project-assignments 这一层状态,侧边栏会优先吃这个 assignment。
只有没有显式 assignment 时,才会继续回退到工作目录,再回退到工作区提示。所以如果你只是补 thread-workspace-root-hints,不一定能让历史会话回到正确项目下。
我后面恢复项目分组时,优先检查的是线程到项目的显式 assignment,然后才考虑工作区提示、项目顺序和保存的工作区根。
坑四:最近 50 条窗口会造成“假丢失”
这次最容易误判的地方,是项目显示“暂无对话”。一开始我也以为是不是 assignment 没补好,或者 rollout 还有问题。后来排查后发现,很多时候不是数据没了,而是没有进入 Codex Desktop 启动时的首轮加载窗口。
桌面端启动后,通常不会一次性加载所有历史线程。它更像是先加载最近一页,而这一页大致就是 50 条。如果目标项目的第一条历史会话按更新时间排序排在第 50 条之后,那么即使 SQLite 有、rollout 有、assignment 也正确,侧边栏项目下面仍然可能显示“暂无对话”。
这个现象很迷惑,因为你会同时看到:
- SQLite 里能查到目标线程。
- rollout 文件也还存在。
- 项目 assignment 看起来也对。
- 用更大的 limit 查询时能查到。
- 但 Codex Desktop 默认启动后,项目下面还是空的。
我后来把查询范围拉大后才确认:这些线程只是排名太靠后,没有进入首轮最近 50 条窗口。项目排序字段只能影响已经加载进前端内存的线程,不能把还没加载进来的线程凭空拉出来。
这类问题我更建议低风险处理:归档一些不再需要占据最近列表的旧会话,然后重启 Codex Desktop,让原本排在 50 条之后的项目会话有机会进入首轮窗口。不要一看到“暂无对话”就继续重写 assignment,也不要马上判断迁移失败。
我不建议把修改应用包当成常规方案
理论上,修改应用包里的前端逻辑,可能让启动时加载更多会话。但我不建议把这当成常规修复方案。
原因很简单:修改应用包可能破坏 macOS 应用签名,还会带来升级覆盖、重新签名和后续维护问题。对大多数人来说,修正数据层、统一 provider、恢复全局状态、归档旧会话,再重启验证,已经是更稳妥的方案。
完整修复顺序
如果你已经遇到了历史会话分散在不同 provider 下,或者切换账号后历史会话不显示的问题,我建议按下面这个顺序处理:
- 先备份。至少备份 SQLite、最近会话索引、全局状态文件;如果要改 rollout,也保留修改前后的记录。
- 确认新账号/API 通道可用。先保证新通道能正常发起新会话。
- 尽量统一 provider 键名。如果后面会长期多账号切换,尽量让不同账号/API 通道共用同一个
model_provider值。 - 扫描 rollout。找出目标线程里还指向旧 provider 的
session_meta。 - 先 dry-run。确认会修改哪些文件、多少线程、哪些 provider 值。
- 统一 rollout provider。把目标历史会话的 provider 元信息统一到新的 provider 标识。
- 同步 SQLite。把对应线程的
model_provider同步更新。 - 重建或修复
session_index.jsonl。确保最近列表能识别这些线程。 - 恢复全局状态。优先恢复自定义标题和
thread-project-assignments,必要时再补工作区提示和项目顺序。 - 重启 Codex Desktop。验证是否还会被旧 rollout 反向覆盖。
- 检查最近 50 条窗口。如果项目仍然显示空,先确认目标线程是不是排在 50 条之后。
- 归档过旧会话。如果最近列表被无关旧会话占满,可以归档一部分,再重启观察。
验证时不要只看“能不能查到”
我这次的另一个经验是:验证时不要只满足于“数据库能查到线程”。这只能证明数据还在,不能证明桌面端 UI 一定能看到。
更接近真实情况的验证,要关注这些点:
- 是否按更新时间排序。
- 是否只看未归档线程。
- 默认启动时是否只加载最近 50 条。
- 目标线程是否已经进入前端已加载的 thread keys。
- 项目归组是不是被 assignment 分到了别处。
- 标题是不是丢在全局状态里,而不是数据库里。
如果更大的 limit 能查到,但默认启动看不到,那更像是最近 50 条加载窗口问题,而不是历史会话真的丢了。
有些项目不是“丢了”,而是本来没有独立线程
还有一种情况也要小心:有些项目根虽然在侧边栏里存在,但历史数据里并没有任何线程的工作目录直接落在这个项目根下。
这种情况下,它不是“项目历史丢失”,而是过去的会话本来就挂在父级工作区或其他项目下。我的处理原则是:不假装原始数据里本来有独立线程。如果确实有少量会话语义上属于这个项目,可以做有限、可解释的显式 assignment 覆盖。
排查清单
如果你也遇到类似问题,可以按下面这个清单排查:
- SQLite 里目标线程是否还存在?
- rollout 里的
session_meta是否还指向旧 provider? - SQLite 和 rollout 的 provider 是否一致?
- 不同账号/API 通道是否用了不同
model_provider值? - 能不能把多个账号/API 通道统一到同一个 provider 键名?
session_index.jsonl是否包含目标线程?- 全局状态里是否还有自定义标题映射?
- 全局状态里是否还有线程到项目的 assignment?
- 目标线程是否被归档?
- 目标线程是否排在最近 50 条之外?
- 项目显示空时,是数据真没了,还是只是没进入首轮加载窗口?
总结
这类问题本质上不是“聊天记录丢了”,而是本地多层状态没有对齐。只要把 rollout、SQLite、最近索引和全局状态一起看,很多问题都能拆成可验证、可回滚、可修复的步骤。
如果你还没开始迁移,我最推荐的预防方案是:不同账号和 API 通道尽量共用同一个 model_provider 值,再用 CC Switch 这类工具切换实际配置。这样后面历史会话不容易被拆散到多个 provider 名下,跨账号显示和继续使用都会简单很多。
如果已经拆散了,就按顺序处理 rollout、SQLite、最近索引和全局状态。最后别忘了单独检查最近 50 条加载窗口,否则很容易把“暂时没加载出来”误判成“迁移失败”。


评论