codex 迁移会话 换成自定义API后找不到原会话 多会话 解决方案
描述:之前使用账号直登的会话现在使用提供商的API之后,会话没了,重新认证又会出现
真实保存在本地,但是没有识别,这是因为
Codex 左侧会话列表不是直接扫 sessions 文件夹来显示的,而是先读 state_5.sqlite 里的 threads 表,再按当前客户端状态做过滤。你这里至少命中了两个过滤条件:
1、按当前 model_provider 分桶
你把配置从 openai 改成了别的 provider 之后,左侧更倾向于只显示“当前 provider 对应的线程”。所以旧的 openai 历史虽然还在,但不会自动出现在当前列表里。
2、按当前工作区 cwd 过滤
当前 provider 下本来只有少量线程,但这些线程在数据库里被写成了 \\?\D:\work\Paper 这种路径,而不是普通的 D:\work\Paper。这样一来,左侧再按工作区筛选时,就会把它们也排除掉,于是看起来像“全没了”。
让ai写了个批处理脚本,这俩文件放在一起,
执行流程:
1、保存俩文件到目录
2、修改python文件的codex目录,只替换自己的电脑名就行,codex默认安装在用户目录下
3、关闭codex进程,包括VScode等IDE codex插件的运行
4、cmd运行.bat 脚本,或者双击.bat
最好在cmd里面运行,这样可以看到报错信息,我下面有运行截图
5、重新打开codex就发现可以了
脚本会先备份当前的配置,不用担心丢失
2026年6月7日更新 转移多个文件下的会话。感谢*荣、*兄两位老师的投喂!
from __future__ import annotations import datetime as _dt import json import shutil import sqlite3 import sys from pathlib import Path # ========================== # 配置区 按照文件目录一行一个,python执行,注意要把所有codex进程退出,才能执行
# 聪明的你想必已经发现了修改TARGET_PROVIDER成config.toml里面的model_provider就能回去了!也就是可以自由切换 # ========================== DEFAULT_CODEX_HOME = Path(r"C:\Users\***\.codex") TARGET_PROVIDER = "openai" WORKSPACES = [ r"D:\work\cuda", r"D:\work\Go" ] # ========================== def now_tag() -> str: return _dt.datetime.now().strftime("%Y%m%d-%H%M%S") def backup_paths(paths: list[Path], backup_dir: Path) -> None: backup_dir.mkdir(parents=True, exist_ok=True) for path in paths: if path.exists(): dst = backup_dir / path.name if not dst.exists(): shutil.copy2(path, dst) def normalize_cwd(cwd: str) -> str: if cwd.startswith("\\\\?\\"): return cwd[4:] return cwd def find_rollout(codex_home: Path, thread_id: str) -> Path | None: sessions = codex_home / "sessions" for p in sessions.rglob(f"*{thread_id}*.jsonl"): return p return None def patch_rollout( rollout: Path, target_provider: str, target_cwds: list[str], ) -> bool: changed = False out_lines = [] target_cwd = target_cwds[0] for line in rollout.read_text( encoding="utf-8", errors="ignore", ).splitlines(): original = line try: obj = json.loads(line) except Exception: out_lines.append(original) continue if obj.get("type") == "session_meta": payload = obj.get("payload") or {} if payload.get("model_provider") != target_provider: payload["model_provider"] = target_provider changed = True current_cwd = normalize_cwd( payload.get("cwd", "") ) if current_cwd in target_cwds: payload["cwd"] = current_cwd else: payload["cwd"] = target_cwd changed = True obj["payload"] = payload line = json.dumps( obj, ensure_ascii=False, ) out_lines.append(line) if changed: rollout.write_text( "\n".join(out_lines) + "\n", encoding="utf-8", ) return changed def update_global_state( codex_home: Path, roots: list[str], ) -> bool: path = codex_home / ".codex-global-state.json" if not path.exists(): return False try: data = json.loads( path.read_text( encoding="utf-8", errors="ignore", ) ) except Exception: return False changed = False if ( isinstance( data.get("selected-remote-host-id"), str, ) and data["selected-remote-host-id"].startswith( "remote-ssh-discovered:" ) ): data["selected-remote-host-id"] = None changed = True old_roots = data.get( "active-workspace-roots", [], ) merged_roots = list( dict.fromkeys( old_roots + roots ) ) if data.get("active-workspace-roots") != merged_roots: data["active-workspace-roots"] = merged_roots changed = True if ( data.get( "electron-saved-workspace-roots" ) != merged_roots ): data[ "electron-saved-workspace-roots" ] = merged_roots changed = True if changed: path.write_text( json.dumps( data, ensure_ascii=False, separators=(",", ":"), ), encoding="utf-8", ) return changed def main(): codex_home = DEFAULT_CODEX_HOME db = codex_home / "state_5.sqlite" global_state = ( codex_home / ".codex-global-state.json" ) conn = sqlite3.connect(str(db)) cur = conn.cursor() placeholders = ",".join( "?" * len(WORKSPACES) ) query = f""" select id, model_provider, cwd, title from threads where replace(cwd,'\\\\?\\','') in ({placeholders}) order by updated_at desc """ rows = cur.execute( query, tuple(WORKSPACES), ).fetchall() if not rows: conn.close() print( "No matching threads found." ) return 1 backup_dir = ( codex_home / f"promote-workspace-backup-{now_tag()}" ) backup_paths( [db, global_state], backup_dir, ) db_changed = 0 rollout_changed = 0 touched = 0 for ( thread_id, model_provider, cwd, title, ) in rows: rollout = find_rollout( codex_home, thread_id, ) if rollout is None: continue backup_paths( [rollout], backup_dir, ) if patch_rollout( rollout, TARGET_PROVIDER, WORKSPACES, ): rollout_changed += 1 current_cwd = normalize_cwd(cwd) if ( model_provider != TARGET_PROVIDER ): cur.execute( """ update threads set model_provider=?, has_user_event=1 where id=? """, ( TARGET_PROVIDER, thread_id, ), ) db_changed += 1 touched += 1 print( f"[OK] {title[:60]}" ) conn.commit() conn.close() global_changed = update_global_state( codex_home, WORKSPACES, ) print() print("=" * 60) print( f"Backup folder : {backup_dir}" ) print( f"Provider : {TARGET_PROVIDER}" ) print( f"Workspaces : {len(WORKSPACES)}" ) print( f"Threads found : {len(rows)}" ) print( f"Threads touch : {touched}" ) print( f"Rollouts edit : {rollout_changed}" ) print( f"DB changed : {db_changed}" ) print( f"Global state : {'yes' if global_changed else 'no'}" ) print("=" * 60) print() print( "Completely exit Codex first." ) print( "Restart Codex after script finishes." ) return 0 if __name__ == "__main__": sys.exit(main())
原来方案如下,只能迁移单文件
codex_promote_workspace_threads.bat
@echo off setlocal cd /d "%~dp0" echo [0/2] Checking whether Codex is still running... tasklist /FI "IMAGENAME eq codex.exe" | find /I "codex.exe" >NUL if not errorlevel 1 ( echo Codex is still running. Please quit Codex completely and run this script again. exit /b 1 ) echo [1/2] Promoting all workspace threads into the current provider bucket... python "%~dp0codex_promote_workspace_threads.py" if errorlevel 1 exit /b 1 echo [2/2] Done. echo Start Codex again and check the left sidebar. exit /b 0
codex_promote_workspace_threads.py 把标黄的替换为你自己的目录
from __future__ import annotations
import argparse
import datetime as _dt
import json
import shutil
import sqlite3
import sys
from pathlib import Path
DEFAULT_CODEX_HOME = Path(r"C:\Users\*\.codex")
DEFAULT_TARGET_PROVIDER = "chaosai"
DEFAULT_CWD = r"D:\work\Paper"
def now_tag() -> str:
return _dt.datetime.now().strftime("%Y%m%d-%H%M%S")
def backup_paths(paths: list[Path], backup_dir: Path) -> None:
backup_dir.mkdir(parents=True, exist_ok=True)
for path in paths:
if path.exists():
dst = backup_dir / path.name
if not dst.exists():
shutil.copy2(path, dst)
def normalize_cwd(cwd: str) -> str:
if cwd.startswith("\\\\?\\"):
return cwd[4:]
return cwd
def find_rollout(codex_home: Path, thread_id: str) -> Path | None:
sessions = codex_home / "sessions"
for p in sessions.rglob(f"*{thread_id}*.jsonl"):
return p
return None
def patch_rollout(rollout: Path, target_provider: str, target_cwd: str) -> bool:
changed = False
out_lines: list[str] = []
for line in rollout.read_text(encoding="utf-8", errors="ignore").splitlines():
original = line
try:
obj = json.loads(line)
except Exception:
out_lines.append(original)
continue
if obj.get("type") == "session_meta":
payload = obj.get("payload") or {}
if payload.get("model_provider") != target_provider:
payload["model_provider"] = target_provider
changed = True
if payload.get("cwd") != target_cwd:
payload["cwd"] = target_cwd
changed = True
obj["payload"] = payload
line = json.dumps(obj, ensure_ascii=False)
out_lines.append(line)
if changed:
rollout.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
return changed
def clear_remote_selection(codex_home: Path, target_cwd: str) -> bool:
path = codex_home / ".codex-global-state.json"
if not path.exists():
return False
try:
data = json.loads(path.read_text(encoding="utf-8", errors="ignore"))
except Exception:
return False
changed = False
if isinstance(data.get("selected-remote-host-id"), str) and data["selected-remote-host-id"].startswith(
"remote-ssh-discovered:"
):
data["selected-remote-host-id"] = None
changed = True
roots = [target_cwd]
if data.get("active-workspace-roots") != roots:
data["active-workspace-roots"] = roots
changed = True
if data.get("electron-saved-workspace-roots") != roots:
data["electron-saved-workspace-roots"] = roots
changed = True
if changed:
path.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
return changed
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--codex-home", default=str(DEFAULT_CODEX_HOME))
parser.add_argument("--target-provider", default=DEFAULT_TARGET_PROVIDER)
parser.add_argument("--cwd", default=DEFAULT_CWD)
parser.add_argument("--limit", type=int, default=0)
args = parser.parse_args()
codex_home = Path(args.codex_home)
db = codex_home / "state_5.sqlite"
global_state = codex_home / ".codex-global-state.json"
conn = sqlite3.connect(str(db))
cur = conn.cursor()
rows = cur.execute(
"""
select id, model_provider, cwd, title
from threads
where replace(cwd, '\\\\?\\', '') = ?
order by updated_at desc
""",
(args.cwd,),
).fetchall()
if args.limit > 0:
rows = rows[: args.limit]
if not rows:
conn.close()
print("No matching threads found.")
return 1
backup_dir = codex_home / f"promote-workspace-backup-{now_tag()}"
backup_paths([db, global_state], backup_dir)
db_changed = 0
rollout_changed = 0
touched = 0
for thread_id, model_provider, cwd, _title in rows:
rollout = find_rollout(codex_home, thread_id)
if rollout is None:
continue
backup_paths([rollout], backup_dir)
if patch_rollout(rollout, args.target_provider, args.cwd):
rollout_changed += 1
new_cwd = args.cwd
new_provider = args.target_provider
if model_provider != new_provider or cwd != new_cwd:
cur.execute(
"""
update threads
set model_provider = ?,
cwd = ?,
has_user_event = 1
where id = ?
""",
(new_provider, new_cwd, thread_id),
)
db_changed += 1
touched += 1
conn.commit()
conn.close()
global_changed = clear_remote_selection(codex_home, args.cwd)
print(f"Backup folder: {backup_dir}")
print(f"Workspace cwd: {args.cwd}")
print(f"Target provider: {args.target_provider}")
print(f"Threads matched: {len(rows)}")
print(f"Threads touched: {touched}")
print(f"Rollouts changed: {rollout_changed}")
print(f"DB rows changed: {db_changed}")
print(f"Global state changed: {'yes' if global_changed else 'no'}")
print("Fully exit Codex before running. Restart Codex after this script finishes.")
return 0
if __name__ == "__main__":
sys.exit(main())

运行之后,再打开codex就可以了
附一个配置提供商API的配置格式
目录:C:\Users\yangxuezhen\.codex
auth.json:写提供商的key
{
"OPENAI_API_KEY": "sk-18a9f0a**7cc06"
}
config.toml:
model_provider = "chaosai" model = "gpt-5.2" model_reasoning_effort = "medium" [windows] sandbox = "elevated" [model_providers.chaosai] name = "chaosai" base_url = "https://*/openai-codex-oauth/v1" wire_api = "responses" requires_openai_auth = false http_headers = { Authorization = "Bearer sk-18a***a7cc06" } multi_agent = true
完毕!
如果对你真有帮助可以赞赏下,我也是搞了半天才弄出来,一两块就行,感谢!


浙公网安备 33010602011771号