[Bug][Codex] 统一会话历史是一次性迁移:迁移后 Codex Desktop 新会话仍写入 openai 桶,导致 ChatGPT 手机 Remote 会话列表不全(同一时刻桌面可见、手机不可见)
Self Checks / 自检
- 我已阅读 README 中的常见问题(FAQ)
- 我已搜索过已有 Issue(#4710、#6340、#5974、#7362、#6503、#4873、#3225),本报告的根因组合未被覆盖,理由见文末「与已有 Issue 的区别」
Related App / 涉及应用
Codex(Codex Desktop + ChatGPT 手机端远程控制 Remote)
Summary / 问题概述
两点,第二点是新影响面:
unifyCodexSessionHistory是一次性迁移,不追新增。 台账localMigrations.codexOfficialHistoryUnifyV1写入后就不再执行。在 unify 已经成功完成、liveconfig.toml已经是custom的前提下,Codex Desktop 之后新建的会话仍然被写成model_provider = "openai",这些会话永久落在统一桶之外,没有任何机制补标。- 分桶缺陷在手机上表现为「桌面看得到、手机看不到」,而不是「切换 provider 后消失」。
codex app-server的thread/list在省略modelProviders时会按「当前配置的model_provider」过滤;手机 Remote 正是不带该参数的客户端,桌面端传的是空数组。于是同一台机器、同一个 app-server、同一时刻,两端列表不一致。现有 issue 全部只描述了桌面/CLI 侧「切换 provider 后历史互相隐藏」,隐含假设是各客户端表现一致。
Environment / 环境信息
- OS: macOS
- CC Switch: 3.20.3
- Codex Desktop(ChatGPT.app): 26.911.61220,内置 codex-cli
0.153.4→0.155.0-alpha.2.6 - 手机端: Android ChatGPT App → 远程控制(Remote)连接上述桌面
- 开关:
unifyCodexSessionHistory = true、unifyCodexMigrateExisting = true、preserveCodexOfficialAuthOnSwitch = true、enableLocalProxy = false(未开本地路由) - 当前 Codex Provider: 第三方供应商(
model_provider = "custom"桶)
Steps to Reproduce / 复现步骤
- 开启「统一 Codex 会话历史」+「迁移现有官方会话」,切到第三方供应商(使 live
config.toml为model_provider = "custom") - 确认 unify migration 已完成:
~/.cc-switch/settings.json出现codexOfficialHistoryUnifyV1 - 之后用 Codex Desktop 新建若干会话
- 查库:
sqlite3 ~/.codex/state_5.sqlite "select id, model_provider from threads where archived = 0"→ 第 3 步的新会话是openai - 手机 Remote 连同一台电脑 → 这些会话不出现;桌面端侧栏同一时刻能看到它们
Evidence / 本机证据(不含凭据、不含会话内容)
A. 迁移确实只跑过一次,且 gate 是通的(与 #6340 不同)
[2026-09-07][14:55:45][INFO][cc_switch_lib::commands::settings] ✓ Codex official history unify migration completed: jsonl_files=32, state_rows=31"localMigrations": {
"codexOfficialHistoryUnifyV1": {
"completedAt": "2026-09-07T06:55:45Z",
"targetProviderId": "custom",
"migratedJsonlFiles": 32,
"migratedStateRows": 31
}
}09-07 之后(含 09-17 两次 cc-switch 重启)日志里再无任何 unify 记录,也没有 skipped_reason。live ~/.codex/config.toml 第一行至今是 model_provider = "custom" —— 即 codex_config_text_routes_custom 判定通过,inject 没有被显式 openai 值挡住。
B. 迁移完成之后,Codex Desktop 仍持续往 openai 桶写新会话
state_5.sqlite → threads(未归档 33 条)
model_provider = custom : 29
model_provider = openai : 4 ← 全部由 Codex Desktop 于 09-10 / 09-11 / 09-11 / 09-17 创建,均在迁移之后对应 rollout 首行:
{"type":"session_meta","payload":{"originator":"Codex Desktop","cli_version":"0.153.4","source":"vscode","model_provider":"openai", ...}}同期由 CLI / Remote 路径创建的会话则是 "originator":"codex_cli_rs","model_provider":"custom"。也就是说:即使 unify 完全正常工作,Codex Desktop 自己产生的会话仍会漏出统一桶,且一次性台账不会回头补标。
C. 服务端过滤规则实测(决定性证据)
用 Codex Desktop 自带的 codex app-server 二进制另起一个 stdio 实例做只读对照(useStateDbOnly: true 避免触发 JSONL 扫描/修复),对同一个 state_5.sqlite 调 thread/list:
| 请求参数 | 返回条数 | 返回的桶 |
|---|---|---|
省略 modelProviders(= 手机 Remote 的行为) |
4 | 全部 custom |
modelProviders: ["openai"] |
4 | 正好是 B 里漏掉的 4 条 |
modelProviders: [](= 桌面端的行为) |
8 | custom + openai |
临时 -c model_provider="openai" 后省略该参数 |
4 | 返回的反而是那 4 条 openai |
即:省略 modelProviders 时的默认值是「当前配置的 model_provider」,而不是「全部 provider」。 协议 schema 里 ThreadListParams.modelProviders 只写了 “when present but empty, includes all providers”,没有写明省略时的这个默认行为,所以很容易被上游调用方(也包括 cc-switch 的文档/设置页文案)忽略。
顺带一条同源的隐藏过滤:省略 sourceKinds 时服务端默认只返回交互式来源(实测 source = exec / subagent 的会话在任何客户端默认列表里都不出现)。这条是 Codex 的设计行为,不影响本 issue 结论,列出来是为了帮「会话看不到」类反馈分流排查。
Actual Behavior / 实际行为
- 统一历史只在「迁移那一刻」生效一次;之后任何被写成非统一桶的会话永久对该桶的客户端不可见。
- 因为不同客户端传给
thread/list的modelProviders不一致,用户看到的是跨端不一致(桌面有、手机没有),比「切换 provider 后列表消失」更難归因,且设置页/日志全程无提示。
Expected Behavior / 期望行为
- 增量补标:每次启动、每次切换 provider 时,扫一遍
threads表与sessions/**/rollout-*.jsonl首行session_meta,把落在非统一桶的会话并入统一桶(幂等、先备份、UI 可预览数量)。或者退一步,至少在设置页提供一个「重新统一历史」按钮。 - 可观测:设置页显示当前各
model_provider桶的未归档会话计数;发现存在桶外会话时给出提示,而不是静默。 - 文案:说明「统一会话历史」同时影响 ChatGPT 手机端远程控制(Remote)的会话列表,并说明上述
thread/list默认按当前 provider 过滤的行为。
与已有 Issue 的区别
- #6340:根因是
inject_codex_unified_session_bucket因显式model_provider = "openai"早退 → live config 永停在openai→ gate 永不过。本例 gate 通过、迁移已成功完成,问题发生在迁移之后,因此 #7386 Fix 1 不能覆盖本场景。 - #5974:本地路由(代理接管)模式下官方会话写
cc-switch-official桶。本例enableLocalProxy = false,未开本地路由。 - #4710 / #7362 / #6658 / #4873:描述的都是「桌面端/CLI 在切换 provider 后历史消失」,前提假设是各客户端表现一致。本例是同一份 config、同一个 app-server 下桌面与手机不一致,属于同一分桶机制的新影响面,也说明影响不必等到切换 provider 才发生。
- #7154(open PR):把统一迁移升级到 v2、同时覆盖
openai/cc-switch-official+ JSONL + SQLite,方向正确,但仍是一次性补跑,不解决「迁移之后新增会话持续漏」。 - #3225(最初的功能请求,标题即「会话持续合并」):目前实现不满足「持续」。
Workaround / 临时绕过(供其他用户参考)
切换 provider 或发现手机缺会话后,跑一次下面的脚本把桶外会话并回当前桶。注意只改数据库不够:rollout JSONL 首行 session_meta.model_provider 必须一起改,否则索引重建时会被冲回去。
import json, os, re, shutil, sqlite3, time
HOME = os.path.expanduser("~")
cfg = open(os.path.join(HOME, ".codex", "config.toml"), encoding="utf-8").read()
tgt = re.search(r'^\s*model_provider\s*=\s*"([^"]+)"', cfg, re.M)
tgt = tgt.group(1) if tgt else "openai" # 官方模式不写 provider,回退内置 openai
db_path = os.path.join(HOME, ".codex", "state_5.sqlite")
bak = os.path.join(HOME, ".cc-switch", "backups", "retag-" + time.strftime("%Y%m%d_%H%M%S"))
os.makedirs(os.path.join(bak, "jsonl"), exist_ok=True)
shutil.copy2(db_path, os.path.join(bak, "state_5.sqlite"))
con = sqlite3.connect(db_path)
rows = con.execute("select id, rollout_path from threads where ifnull(model_provider,'') <> ?", (tgt,)).fetchall()
for tid, path in rows:
if path and os.path.exists(path):
shutil.copy2(path, os.path.join(bak, "jsonl", os.path.basename(path)))
lines = open(path, encoding="utf-8").read().splitlines(keepends=True)
meta = json.loads(lines[0])
if meta.get("type") == "session_meta" and meta["payload"].get("model_provider") != tgt:
meta["payload"]["model_provider"] = tgt
lines[0] = json.dumps(meta, ensure_ascii=False) + "\n"
open(path + ".tmp", "w", encoding="utf-8").writelines(lines)
os.replace(path + ".tmp", path)
con.executemany("update threads set model_provider=? where id=?", [(tgt, t) for t, _ in rows])
con.commit(); con.close()
print("retagged:", len(rows), "| backup:", bak)补充说明
另外建议 cc-switch 在 preserveCodexOfficialAuthOnSwitch + unify 同时开启时,明确告知用户 Codex Desktop 会以哪个桶落盘(实测见 B 组),否则「统一」这个名字与实际行为差距较大,用户很难自己排查到 threads.model_provider 这一层。
以上实验均可在 macOS / Windows 上按「复现步骤」重现。感谢维护者,如果需要我补充特定字段或再跑一轮验证,请在 issue 里说明。
Source: farion1231/cc-switch