Appearance
Codex 修复 Reconnecting 后历史记录消失?统一 model_provider 找回旧对话(Mac/Windows)
更新时间:2026年5月17日
适用场景:你为了修复 Codex Desktop 新会话反复
reconnecting,把model_provider切到了openai_http,结果右侧历史列表只显示新对话,看不到以前openaiprovider 下的旧对话。出品:极拓工坊
结论先行
如果你把 Codex Desktop 从默认 openai provider 切到 HTTP-only provider,例如 openai_http,新会话确实可能不再反复 reconnecting,但也可能带来一个副作用:右侧历史列表只显示 openai_http 下的新对话,旧的 openai 对话看起来像丢了。
旧记录通常没有真的丢。它们还在本机的 Codex 数据目录里,只是 provider 名称分裂了:
text
旧对话: model_provider = openai
新对话: model_provider = openai_http修复方法不是把自定义 provider 强行命名为 openai。更稳妥的做法是:以当前 config.toml 里的 model_provider 为目标,把旧历史里的 openai 迁移到当前 provider,并同时更新 SQLite 索引和 JSONL 源文件。
为什么会出现历史记录缺失
Codex Desktop 的本地历史至少涉及两层数据:
| 数据层 | 常见位置 | 作用 |
|---|---|---|
| SQLite 索引 | ~/.codex/state_*.sqlite 或 %USERPROFILE%\.codex\state_*.sqlite | 历史列表、线程索引、快速查询 |
| JSONL 会话源文件 | sessions/**/*.jsonl、archived_sessions/**/*.jsonl | 原始会话记录,第一行通常是 session_meta |
当你新增 HTTP-only provider 时,配置可能长这样:
toml
model_provider = "openai_http"
[model_providers.openai_http]
name = "OpenAI HTTP only"
wire_api = "responses"
supports_websockets = false
requires_openai_auth = true这个配置能避开部分代理环境下的 WebSocket 抖动,但它也让新会话的 provider id 变成了 openai_http。如果 Codex Desktop 的历史 UI 按当前 provider 加载,就会出现:
| 时间 | provider | UI 表现 |
|---|---|---|
| 修复 reconnecting 之前 | openai | 旧会话仍在本机,但不属于当前 provider |
| 修复 reconnecting 之后 | openai_http | 新会话正常显示 |
| provider 分裂后 | openai + openai_http | 右侧历史列表像是“少了一半” |
所以真正要统一的不是模型,而是本地历史记录里的 model_provider 字段。
不能直接覆盖内置 openai provider
OpenAI Codex 官方配置参考里说明:model_provider 是来自 model_providers 的 provider id,默认是 openai;自定义 provider 可以写在 model_providers.<id> 下,但内置 provider id,比如 openai、ollama、lmstudio 是保留的,不能覆盖。
这意味着不要这样做:
toml
[model_providers.openai]
name = "OpenAI HTTP only"
supports_websockets = false更推荐保留自定义 provider,例如 openai_http,然后把旧历史记录中的 openai 迁移到当前 provider。
修改前安全原则
这次修复会改 ~/.codex 或 %USERPROFILE%\.codex 下的本地状态文件。动手前先记住四条:
- 完全退出 Codex Desktop。
- 先备份
config.toml、state*.sqlite*、sessions、archived_sessions。 - 只把旧的
openai迁移到当前config.toml里的 provider。 - 不要动
custom、anthropic、local、ollama、lmstudio或其他 provider 的记录。
Mac 修复步骤
1. 退出 Codex 并检查进程
先从菜单栏完全退出 Codex Desktop,然后在 Terminal 里检查:
bash
pgrep -fl 'Codex|codex' || true如果还有明确的 Codex Desktop 主进程,确认可以关闭后再处理。
2. 备份本地 Codex 状态
bash
python3 - <<'PY'
from pathlib import Path
import datetime
import shutil
codex = Path.home() / ".codex"
if not codex.exists():
raise SystemExit(f"Missing Codex directory: {codex}")
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
backup = codex / "backups" / f"provider-merge-{stamp}"
backup.mkdir(parents=True, exist_ok=False)
files = [codex / "config.toml"]
files.extend(codex.glob("state*.sqlite*"))
for src in files:
if src.exists() and src.is_file():
shutil.copy2(src, backup / src.name)
for dirname in ("sessions", "archived_sessions"):
src = codex / dirname
if src.exists() and src.is_dir():
shutil.copytree(src, backup / dirname)
print(f"Backup created: {backup}")
PY看到 Backup created: 之后,再继续下一步。
3. 统计当前 provider 分布
bash
python3 - <<'PY'
from pathlib import Path
from collections import Counter
import json
import re
import sqlite3
codex = Path.home() / ".codex"
config = codex / "config.toml"
target = None
if config.exists():
for line in config.read_text(encoding="utf-8", errors="replace").splitlines():
if line.strip().startswith("["):
break
match = re.match(r'\s*model_provider\s*=\s*"([^"]+)"', line)
if match:
target = match.group(1)
break
print(f"Current config model_provider: {target or '(not set, Codex may default to openai)'}")
for db in sorted(codex.glob("state*.sqlite")):
try:
conn = sqlite3.connect(db)
exists = conn.execute(
"select name from sqlite_master where type='table' and name='threads'"
).fetchone()
if not exists:
conn.close()
continue
rows = conn.execute(
"select model_provider, count(*) from threads group by model_provider order by count(*) desc"
).fetchall()
conn.close()
print(f"\nSQLite: {db}")
for provider, count in rows:
print(f" {provider}: {count}")
except Exception as exc:
print(f"\nSQLite read failed: {db}: {exc}")
counts = Counter()
for dirname in ("sessions", "archived_sessions"):
root = codex / dirname
if not root.exists():
continue
for path in root.rglob("*.jsonl"):
try:
first = path.open("r", encoding="utf-8", errors="replace").readline()
if not first:
continue
obj = json.loads(first)
if obj.get("type") == "session_meta":
provider = obj.get("payload", {}).get("model_provider")
if provider:
counts[provider] += 1
except Exception:
pass
print("\nJSONL session_meta provider counts:")
for provider, count in counts.most_common():
print(f" {provider}: {count}")
PY如果你看到 openai 和 openai_http 两类计数,并且当前配置是 openai_http,基本就对上了这个问题。
4. 同步 SQLite 和 JSONL
这个脚本只把旧的 openai 改成当前 config.toml 顶部的 model_provider。如果当前 provider 还是 openai,脚本会直接退出。
bash
python3 - <<'PY'
from pathlib import Path
import json
import re
import sqlite3
codex = Path.home() / ".codex"
config = codex / "config.toml"
source_provider = "openai"
if not config.exists():
raise SystemExit(f"Missing config: {config}")
target_provider = None
for line in config.read_text(encoding="utf-8", errors="replace").splitlines():
if line.strip().startswith("["):
break
match = re.match(r'\s*model_provider\s*=\s*"([^"]+)"', line)
if match:
target_provider = match.group(1)
break
if not target_provider:
raise SystemExit("No top-level model_provider found in ~/.codex/config.toml")
if target_provider == source_provider:
raise SystemExit("Current provider is already openai. No provider merge is needed.")
print(f"Migrating provider: {source_provider} -> {target_provider}")
sqlite_changed = 0
for db in sorted(codex.glob("state*.sqlite")):
conn = sqlite3.connect(db)
try:
exists = conn.execute(
"select name from sqlite_master where type='table' and name='threads'"
).fetchone()
if not exists:
continue
cur = conn.execute(
"update threads set model_provider = ? where model_provider = ?",
(target_provider, source_provider),
)
conn.commit()
changed = cur.rowcount if cur.rowcount is not None else 0
sqlite_changed += changed
print(f"SQLite updated: {db} rows={changed}")
finally:
conn.close()
jsonl_changed = 0
for dirname in ("sessions", "archived_sessions"):
root = codex / dirname
if not root.exists():
continue
for path in root.rglob("*.jsonl"):
raw = path.read_text(encoding="utf-8", errors="replace")
if not raw:
continue
line_end = raw.find("\n")
if line_end == -1:
first = raw
rest = ""
else:
first = raw[:line_end]
rest = raw[line_end + 1:]
try:
obj = json.loads(first)
except Exception:
continue
if obj.get("type") != "session_meta":
continue
payload = obj.setdefault("payload", {})
if payload.get("model_provider") != source_provider:
continue
payload["model_provider"] = target_provider
new_first = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
new_raw = new_first + "\n" + rest if rest else new_first + "\n"
path.write_text(new_raw, encoding="utf-8")
jsonl_changed += 1
print(f"Total SQLite rows changed: {sqlite_changed}")
print(f"Total JSONL files changed: {jsonl_changed}")
PY5. 重启 Codex Desktop 验证
重新打开 Codex Desktop,进入原来的项目或工作区,观察右侧历史列表。
期望结果:
text
修复 reconnecting 之前的旧对话,以及切到 HTTP-only provider 之后的新对话,都能同时显示。Windows 修复思路
Windows 也是同一个问题,只是路径不同:
text
Mac: ~/.codex
Windows: %USERPROFILE%\.codexPowerShell 打开配置文件:
powershell
notepad "$env:USERPROFILE\.codex\config.toml"如果要把上面的 Python 脚本改成 Windows 版,核心只需要把:
python
codex = Path.home() / ".codex"保留不变即可。Python 的 Path.home() 在 Windows 上会自动指向当前用户目录,例如:
text
C:\Users\你的用户名\.codex也就是说,只要你的 Windows 已经有 Python 3,上面的备份、统计、迁移脚本可以在 PowerShell 里用这种形式运行:
powershell
@'
from pathlib import Path
print(Path.home() / ".codex")
'@ | python -实际迁移前同样要先退出 Codex Desktop,并完整备份 .codex 目录。
回退方法
如果迁移后历史列表不符合预期,先退出 Codex Desktop,再从备份目录恢复。
Mac 示例:
bash
BACKUP_DIR="$(ls -td "$HOME"/.codex/backups/provider-merge-* | head -n 1)"
test -n "$BACKUP_DIR"
echo "Restoring from: $BACKUP_DIR"
cp "$BACKUP_DIR"/config.toml "$HOME/.codex/config.toml"
cp "$BACKUP_DIR"/state*.sqlite* "$HOME/.codex/"恢复 sessions:
bash
BACKUP_DIR="$(ls -td "$HOME"/.codex/backups/provider-merge-* | head -n 1)"
test -n "$BACKUP_DIR"
echo "Restoring sessions from: $BACKUP_DIR"
rm -rf "$HOME/.codex/sessions.restore-tmp" "$HOME/.codex/archived_sessions.restore-tmp"
cp -R "$BACKUP_DIR/sessions" "$HOME/.codex/sessions.restore-tmp"
cp -R "$BACKUP_DIR/archived_sessions" "$HOME/.codex/archived_sessions.restore-tmp" 2>/dev/null || true
mv "$HOME/.codex/sessions" "$HOME/.codex/sessions.after-provider-merge"
mv "$HOME/.codex/sessions.restore-tmp" "$HOME/.codex/sessions"
if [ -d "$HOME/.codex/archived_sessions.restore-tmp" ]; then
mv "$HOME/.codex/archived_sessions" "$HOME/.codex/archived_sessions.after-provider-merge" 2>/dev/null || true
mv "$HOME/.codex/archived_sessions.restore-tmp" "$HOME/.codex/archived_sessions"
fiWindows 回退也一样:从 .codex\backups\provider-merge-* 里恢复 config.toml、state*.sqlite*、sessions 和 archived_sessions。
常见问题
旧对话是真的丢了吗?
通常不是。更多时候是旧对话仍在 sessions 或 archived_sessions 里,但历史索引和当前 provider 不一致,导致 UI 没把它们显示出来。
为什么 SQLite 和 JSONL 都要改?
SQLite 更像索引,JSONL 更像源记录。只改 SQLite,Codex 重启后可能根据 JSONL 第一行的 session_meta.payload.model_provider 重新导入;只改 JSONL,当前 UI 又可能继续读旧 SQLite 索引。所以两边一起同步最稳。
迁移目标一定是 openai_http 吗?
不一定。迁移目标应该是你当前 ~/.codex/config.toml 或 %USERPROFILE%\.codex\config.toml 顶部实际写的 model_provider。如果你叫它 openai_sse,目标就是 openai_sse。
会影响 custom 或本地模型历史吗?
脚本只迁移 source_provider = "openai" 的记录,不动其他 provider。不要把所有 provider 都批量改成当前 provider,否则不同来源的历史会混在一起。
这篇和 Reconnecting 修复文章是什么关系?
openai_http 是为了解决代理环境里 WebSocket 重连的问题;这篇是修复切换 provider 后带来的历史列表分裂。建议顺序是:先确认 HTTP/SSE provider 能解决 reconnecting,再做历史 provider 迁移。
给 AI 搜索和传统搜索的结构化说明
这篇文章按 2026 年的 SEO/GEO 习惯做了几件事:
| 页面元素 | 作用 |
|---|---|
| 标题直接包含问题和结果 | 覆盖“Codex 历史记录消失”“model_provider”这类长尾查询 |
| 开头先给结论 | 方便 AI 摘要直接抽取答案 |
| 表格拆分数据层、路径和原因 | 降低歧义,便于引用 |
| Mac 和 Windows 路径并列 | 覆盖跨平台搜索意图 |
| FAQ frontmatter | 生成 FAQPage 结构化数据 |
| date / updated | 让搜索系统知道这是 2026年5月 的当前版本 |
| keywords | 给传统 TDK 和站内搜索提供关键词线索 |
Google 搜索中心说明,AI 概览和 AI 模式仍适用基础 SEO,重要内容应以文字形式提供,结构化数据要和页面可见文本一致。Bing Webmaster 在 2026年2月发布 AI Performance 报表,说明站长已经可以单独观察内容在 Copilot、Bing AI 摘要等 AI 搜索场景里的曝光、点击和引用表现。
参考:
