Codex 切换账号后卡住,最后我是这样修的

这次遇到的问题很典型。

我在 Codex 里切换账号后,先看到 token 刷新失败:

Your access token could not be refreshed because you have since logged out or signed in to another account.

我当时第一反应其实很朴素,哦,登录态过期了,重新登一下就行。

结果不是。

重新登录后,又开始卡在:

Reconnecting... 5/5

再往后,恢复旧对话时出现了这个报错:

cannot resume running thread <THREAD_ID> with stale path:
requested `C:\Users\<USER>\.codex\sessions\...\rollout-...jsonl`,
active `\\?\C:\Users\<USER>\.codex\sessions\...\rollout-...jsonl`

看到这里我就有点烦了。因为这种错误最麻烦,它不像一个明确的红灯,而像一堆半亮不亮的灯。账号像有问题,网络像有问题,本地文件也像有问题。

这个问题的本质不是“对话文件坏了”,而是切账号时有几层状态混在一起了。

Codex 本地大概有三类状态。

第一类是账号状态,比如:

C:\Users\<USER>\.codex\auth.json
%APPDATA%\Codex\Network
%APPDATA%\Codex\Local Storage

第二类是运行状态,比如某个 thread 是否还被认为正在 running。

第三类才是你的真实对话记录:

C:\Users\<USER>\.codex\sessions\YYYY\MM\DD\rollout-xxx.jsonl

我一开始差点把第三类也当缓存清掉,这是不对的。真正要清的是账号状态和残留运行状态,对话文件要保留。

具体排查时,我先确认网络是否正常:

Resolve-DnsName api.openai.com
Resolve-DnsName chatgpt.com
Test-NetConnection api.openai.com -Port 443
Test-NetConnection chatgpt.com -Port 443

如果你用了代理或 TUN,看到 198.18.x.x 这类 fake-ip,并且 TcpTestSucceededTrue,说明网络基本没问题。

这里我松了一口气。至少不是 Codex 整个坏了,也不是账号完全不可用。网络这一层通了,剩下就是本地状态的问题。

然后查 Codex 是否还有残留进程:

Get-Process | Where-Object {
  $_.ProcessName -match 'Codex|codex'
} | Select-Object ProcessName,Id,Path

再确认出问题的旧会话文件是不是已经完成:

Get-Content "C:\Users\<USER>\.codex\sessions\YYYY\MM\DD\rollout-xxx.jsonl" -Tail 1

如果最后能看到类似:

{"type":"event_msg","payload":{"type":"task_complete"}}

说明这个对话本身是完整的,不该删除。

这个发现挺关键的。因为我一开始也差点把它当成坏会话移走,后来才意识到,用户真正想要的不是“别报错”,而是“这个旧对话还能继续用”。这两个目标差很多。

最后我把修复流程收敛成了一个脚本。核心思路是:

关闭残留 Codex 进程。

备份并移走旧账号登录态。

清 Electron Cookie、Local Storage、Network 缓存。

保留 .codex\sessions 里的所有对话。

下面是脱敏后的脚本核心版本:

$ErrorActionPreference = "Stop"
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"

function Backup-Path($path, $stamp) {
  if (Test-Path -LiteralPath $path) {
    Move-Item -LiteralPath $path -Destination "$path.bak-$stamp"
    Write-Host "Backed up: $path"
  }
}

# 1. 停掉 Codex 残留进程
Get-Process |
  Where-Object { $_.ProcessName -match "Codex|codex" } |
  ForEach-Object {
    Write-Host "Stopping $($_.ProcessName) pid=$($_.Id)"
    Stop-Process -Id $_.Id -Force
  }

# 2. 清账号状态,不动 sessions
$codexHome = Join-Path $env:USERPROFILE ".codex"
$roamingCodex = Join-Path $env:APPDATA "Codex"

Backup-Path (Join-Path $codexHome "auth.json") $stamp
Backup-Path (Join-Path $codexHome "models_cache.json") $stamp
Backup-Path (Join-Path $codexHome "cap_sid") $stamp

foreach ($name in @(
  "Network",
  "Local Storage",
  "Session Storage",
  "IndexedDB",
  "Cache",
  "Code Cache",
  "GPUCache",
  "blob_storage",
  "lockfile"
)) {
  Backup-Path (Join-Path $roamingCodex $name) $stamp
}

Write-Host "Preserved conversations:"
Write-Host (Join-Path $codexHome "sessions")

如果某个旧 thread 已经被误移走,也可以恢复回去。注意这里是复制回 sessions,不是删除备份:

$threadId = "<THREAD_ID>"
$codexHome = Join-Path $env:USERPROFILE ".codex"
$sessionRoot = Join-Path $codexHome "sessions"

$source = Get-ChildItem $codexHome -Directory -Filter "stale-sessions-*" |
  Sort-Object LastWriteTime -Descending |
  ForEach-Object {
    Get-ChildItem $_.FullName -File -Filter "*$threadId*.jsonl"
  } |
  Select-Object -First 1

if ($source.Name -match "^rollout-(\d{4})-(\d{2})-(\d{2})T") {
  $targetDir = Join-Path $sessionRoot "$($Matches[1])\$($Matches[2])\$($Matches[3])"
  New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
  Copy-Item $source.FullName -Destination (Join-Path $targetDir $source.Name)
}

为了以后切账号时不再手动跑命令,我又做了一个“干净启动 Codex”的脚本:

$cleanupScript = "C:\Path\To\fix-codex-reconnecting.ps1"
$codexExe = "C:\Path\To\Codex.exe"

& powershell -NoProfile -ExecutionPolicy Bypass -File $cleanupScript -KillCodex -PrepareAccountSwitch
Start-Sleep -Seconds 1
Start-Process -FilePath $codexExe

再给它建一个桌面快捷方式:

$desktop = [Environment]::GetFolderPath("Desktop")
$shortcutPath = Join-Path $desktop "Codex 干净启动.lnk"
$script = "C:\Path\To\start-codex-clean.ps1"

$wsh = New-Object -ComObject WScript.Shell
$shortcut = $wsh.CreateShortcut($shortcutPath)
$shortcut.TargetPath = "powershell.exe"
$shortcut.Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File `"$script`""
$shortcut.WorkingDirectory = Split-Path -Parent $script
$shortcut.Description = "Clean Codex account state, preserve conversations, then launch Codex"
$shortcut.Save()

之后切账号就很简单了。

不要直接打开原来的 Codex。

点桌面的 Codex 干净启动,它会先清掉旧账号状态,再启动 Codex。历史对话还在,新账号也不会继承上一个账号残留的 running thread。

这个方案不算优雅,但它让我安心。因为它没有碰真正有价值的东西,只是在每次切账号前,把桌面端最容易残留的那几层状态清干净。

总结一下,这个问题不要用“删除整个 .codex 目录”来解决。

正确做法是:

清 token、Cookie、Local Storage、缓存、残留进程。

保留 .codex\sessions

如果看到 stale path,优先判断是不是旧进程和旧运行态没释放,而不是直接删会话。

这才是这次踩坑里最有价值的部分。

说白了,AI agent 桌面应用已经不像以前那种“一个配置文件加一点缓存”的软件了。它有账号,有 websocket,有本地会话,有运行中的 thread。切账号这件小事,背后其实是在切一整套运行上下文。

所以别一上来就重装。

先把状态分层。

人会冷静很多,工具也会听话很多。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐