Codex Desktop 历史会话跨账号迁移与侧边栏恢复实战

内容管家 AI领域 编程开发评论708字数 3435阅读11分27秒阅读模式
摘要我记录了一次 Codex Desktop 历史会话跨账号、跨 API 通道迁移的排查过程,重点说明如何统一 model_provider、恢复侧边栏项目分组,并排查最近 50 条加...
Codex Desktop 历史会话迁移与侧边栏恢复界面

这次我遇到的问题,是在 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/listthread/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 下,或者切换账号后历史会话不显示的问题,我建议按下面这个顺序处理:

  1. 先备份。至少备份 SQLite、最近会话索引、全局状态文件;如果要改 rollout,也保留修改前后的记录。
  2. 确认新账号/API 通道可用。先保证新通道能正常发起新会话。
  3. 尽量统一 provider 键名。如果后面会长期多账号切换,尽量让不同账号/API 通道共用同一个 model_provider 值。
  4. 扫描 rollout。找出目标线程里还指向旧 provider 的 session_meta
  5. 先 dry-run。确认会修改哪些文件、多少线程、哪些 provider 值。
  6. 统一 rollout provider。把目标历史会话的 provider 元信息统一到新的 provider 标识。
  7. 同步 SQLite。把对应线程的 model_provider 同步更新。
  8. 重建或修复 session_index.jsonl确保最近列表能识别这些线程。
  9. 恢复全局状态。优先恢复自定义标题和 thread-project-assignments,必要时再补工作区提示和项目顺序。
  10. 重启 Codex Desktop。验证是否还会被旧 rollout 反向覆盖。
  11. 检查最近 50 条窗口。如果项目仍然显示空,先确认目标线程是不是排在 50 条之后。
  12. 归档过旧会话。如果最近列表被无关旧会话占满,可以归档一部分,再重启观察。

验证时不要只看“能不能查到”

我这次的另一个经验是:验证时不要只满足于“数据库能查到线程”。这只能证明数据还在,不能证明桌面端 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 条加载窗口,否则很容易把“暂时没加载出来”误判成“迁移失败”。

 
内容管家

发表评论