Skip to content

Codex 修复 Reconnecting 后历史记录消失?统一 model_provider 找回旧对话(Mac/Windows)

更新时间:2026年5月17日

适用场景:你为了修复 Codex Desktop 新会话反复 reconnecting,把 model_provider 切到了 openai_http,结果右侧历史列表只显示新对话,看不到以前 openai provider 下的旧对话。

出品:极拓工坊


结论先行

如果你把 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/**/*.jsonlarchived_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 加载,就会出现:

时间providerUI 表现
修复 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,比如 openaiollamalmstudio 是保留的,不能覆盖。

这意味着不要这样做:

toml
[model_providers.openai]
name = "OpenAI HTTP only"
supports_websockets = false

更推荐保留自定义 provider,例如 openai_http,然后把旧历史记录中的 openai 迁移到当前 provider。

修改前安全原则

这次修复会改 ~/.codex%USERPROFILE%\.codex 下的本地状态文件。动手前先记住四条:

  • 完全退出 Codex Desktop。
  • 先备份 config.tomlstate*.sqlite*sessionsarchived_sessions
  • 只把旧的 openai 迁移到当前 config.toml 里的 provider。
  • 不要动 customanthropiclocalollamalmstudio 或其他 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

如果你看到 openaiopenai_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}")
PY

5. 重启 Codex Desktop 验证

重新打开 Codex Desktop,进入原来的项目或工作区,观察右侧历史列表。

期望结果:

text
修复 reconnecting 之前的旧对话,以及切到 HTTP-only provider 之后的新对话,都能同时显示。

Windows 修复思路

Windows 也是同一个问题,只是路径不同:

text
Mac:     ~/.codex
Windows: %USERPROFILE%\.codex

PowerShell 打开配置文件:

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"
fi

Windows 回退也一样:从 .codex\backups\provider-merge-* 里恢复 config.tomlstate*.sqlite*sessionsarchived_sessions

常见问题

旧对话是真的丢了吗?

通常不是。更多时候是旧对话仍在 sessionsarchived_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 搜索场景里的曝光、点击和引用表现。

参考: