Codex App 的跨设备控制出了一个比较绕的问题。
两台 Mac 都能登录,同一个账号,同一个 workspace。设备列表里也能看到对方,而且状态是 online。但只要点连接,就是 connection failed。
这个现象很容易把人带偏。因为看起来像网络问题,尤其我这边还开着 v2rayN,中间又用了 gost,把 Codex 需要的代理从 10003 转到 10808。第一反应自然是去查 Wi-Fi、代理、VPN、防火墙。
最后发现,问题不在这些地方。
现象
最开始的状态是:
- MacBook 上的 Codex App 可以被另一台 MacBook Air 控制。
- 反过来想用 MacBook 控制 MacBook Air 时,显示 connection failed。
- 后来重启了 Air 上的 Codex App,结果两边都不行了。
- 重新开关远程控制、重启 App、删除设备再添加,都没有解决。
- 设备查找时能看到 online,但连接马上失败。
这个“online”很关键。
它说明设备发现这层是通的。账号、服务器、在线状态,至少有一部分是正常工作的。失败发生在真正建立远程控制会话的时候。
先排除了网络和代理
我这边的代理结构大概是这样:
Codex / system network
-> 127.0.0.1:10003
-> gost
-> 127.0.0.1:10808
-> v2rayN / sing-box
不过实际排查时发现,Codex 桌面 App 的 Electron 网络层主要在走 macOS 系统代理,也就是 127.0.0.1:10808。而 ~/.codex/.env 里的 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 更像是给 Codex CLI 或 app-server 子进程用的,不一定影响桌面 App 自己的远程控制 websocket。
这条代理链本身是通的。防火墙也不是问题。
所以如果设备已经能显示 online,但连接失败,继续只看 Wi-Fi 或代理,意义就不大了。
真正的线索在日志里
Codex 的日志里反复出现了这一句:
Remote-control client has been revoked
这个问题不是孤例。GitHub 上有一个对应的 issue:Codex Desktop remote control remains connected but unusable after remote device revoked permission, and cannot be deleted or re-authorized #23865。里面提到的关键日志也是 Remote-control client has been revoked,并且同样指向 stale enrollment 这一类本地状态残留。
这句话比 UI 里的 connection failed 准确得多。
它的意思不是“网络连不上”,而是本地 Codex 拿着一个已经被服务端作废的 remote-control client 去建立连接。服务端看到这个 client 已经 revoked,就直接拒绝。
也就是说,设备能 online,只说明远端环境还在。但连接阶段用到的本地 enrollment 身份已经不被接受了。
为什么重启和删除设备没用
一开始我以为在设置里删除远程设备、关闭再打开 Allow other devices to connect 就够了。结果不行。
后来发现 Codex 的远程控制状态不只存在一个地方。
至少涉及这两个位置:
~/.codex/.codex-global-state.json
~/.codex/state_5.sqlite
前者里会有一些远程控制的 host id、environment id、auto connect 状态。
后者更关键,里面有一张表:
remote_control_enrollments
我当时清了 JSON,重启以后还是失败。原因就是 SQLite 里还有旧的 enrollment。Codex 又把旧记录读回来,继续拿已经 revoked 的 client 去连。
这就解释了为什么 UI 操作看起来做了,但问题还会回来。
最后有效的处理方法
重点是:两台 Mac 都要做,而且要在 Codex App 完全退出以后,从系统 Terminal 里执行。不要在 Codex 自己的终端里跑。
先退出 Codex,然后备份并清理:
if pgrep -f "/Applications/Codex.app" >/dev/null; then
echo "Codex 还在运行,请先完全退出 Codex App 再运行。"
exit 1
fi
backup="$HOME/.codex/remote-control-reset-backup-$(date +%Y%m%d%H%M%S)"
mkdir -p "$backup"
cp "$HOME/.codex/.codex-global-state.json" "$backup/" 2>/dev/null || true
cp "$HOME/.codex/state_5.sqlite"* "$backup/" 2>/dev/null || true
sqlite3 "$HOME/.codex/state_5.sqlite" \
"delete from remote_control_enrollments; vacuum;"
然后清掉 JSON 里的 remote-control 残留:
node <<'NODE'
const fs = require("fs");
const p = process.env.HOME + "/.codex/.codex-global-state.json";
const s = JSON.parse(fs.readFileSync(p, "utf8"));
function clean(obj) {
if (!obj || typeof obj !== "object") return;
for (const k of [
"electron-local-remote-control-installation-id",
"electron-local-remote-control-environment-id",
"electron-remote-control-client-enrollments",
"added-remote-control-env-ids",
"codex-mobile-has-connected-device",
]) delete obj[k];
for (const k of [
"remote-connection-auto-connect-by-host-id",
"remote-connection-analytics-id-by-host-id",
"agent-mode-by-host-id",
"unread-thread-ids-by-host-v1",
]) {
if (obj[k] && typeof obj[k] === "object") {
for (const hostId of Object.keys(obj[k])) {
if (hostId.startsWith("remote-control:")) delete obj[k][hostId];
}
}
}
if (String(obj["selected-remote-host-id"] || "").startsWith("remote-control:")) {
delete obj["selected-remote-host-id"];
}
}
clean(s);
clean(s["electron-persisted-atom-state"]);
fs.writeFileSync(p, JSON.stringify(s));
NODE
做完以后重新打开两台 Codex App,再重新开启远程控制、重新配对。这个时候连接恢复了。
这次的判断
这类问题容易被“网络”两个字盖住。
但这次真正有用的判断是:
- 如果设备查找不到,才优先怀疑网络、账号、workspace、代理。
- 如果设备能显示 online,但连接失败,就要看握手阶段。
- 如果日志里出现
Remote-control client has been revoked,重点就不是网络,而是本地保存的 remote-control enrollment 已经过期或被服务端作废。 - 只清 UI 设置不一定够,SQLite 里的
remote_control_enrollments也可能要清。
这次问题最后能解决,是因为没有停在 UI 给出的 connection failed,而是继续去看日志里更具体的错误。
还有一个顺手的提醒:排查这类问题时,不要把 API token、密钥、账号 token 直接贴到公开地方。如果已经贴了,就应该当作泄露处理,及时撤销并重新生成。
Comments
No comments yet.