Skip to content

feat(codex): 支持空闲会话自动升级已安装版本 - #1306

Open
anarkh wants to merge 1 commit into
masterfrom
feat/codex-session-auto-upgrade
Open

feat(codex): 支持空闲会话自动升级已安装版本#1306
anarkh wants to merge 1 commit into
masterfrom
feat/codex-session-auto-upgrade

Conversation

@anarkh

@anarkh anarkh commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

本机 Codex 安装已经更新时,长期运行的会话仍可能停留在旧进程,继续使用旧版本的模型列表和行为。此改动让 Botmux 在确认会话安全空闲后,切换到本机已安装的较新 Codex,并静默恢复原线程;安装更新与会话进程换代由此衔接起来。

改动

  • 新增会话运行版本监测,比较实际 Codex 进程与已安装目标版本,只升级、不降级。解析官方 standalone 安装入口,同时保留显式运行时配置;此功能不下载或安装 Codex。
  • Dashboard 新增“自动升级会话 Codex 版本”开关,对应 dashboard.autoUpgradeCodexSessions,默认开启,修改后热生效。
  • 升级时保留 Botmux worker、原线程、工作目录、环境及待处理输入。确认旧进程退出后启动新版,以空 prompt、非 fork 方式恢复原线程,不覆盖原会话模型和推理强度;维护恢复禁用 TUI 自动 recap,不重复执行启动命令或标题生成。
  • 新进程 ready 后,继续核对实际版本和原线程占用证据,通过后才释放输入队列。RPC 恢复必须返回原线程和 resumed 结果,失败不会降级为新建线程。
  • 首次 ready 和升级成功后上报实际运行版本,更新 Dashboard 会话内存状态。异步探测结果受 backend、spawn generation 和当前 worker 所有权约束,旧进程的晚到结果不能覆盖新状态;不改写会话持久化数据或全局安装版本缓存。

安全边界与影响面

  • 仅自动管理 Botmux 自有的本地 codex / codex-app 运行进程。adopt 会话、外部或共享 App Server、配置了 CLI wrapper 的会话不参与自动换代,其他 CLI 不启用此能力。
  • 有进行中的回合、待处理输入、Hook review 输入保持、TUI 交互、终端连接、后台子进程或原生 Goal 时延后。无法确认进程身份、原线程独占关系或 Goal 状态时同样延后;仅允许明确识别的无状态直属 helper 随进程重启。
  • 触及 worker 公共的停止、ready 和输入释放路径,但维护逻辑受 Codex 条件约束。PTY、tmux 及本地 RPC 路径共用原线程恢复校验;普通启动、其他 CLI 和非维护恢复沿用原行为。适配器测试包含其他 CLI 的参数回归。
  • 覆盖 Linux 与 macOS 进程探测。Linux 以运行进程的实际 executable 验证版本;macOS 若原二进制已被覆盖、无法追溯旧进程版本,则拒绝自动换代,不将磁盘上的新版本当作旧进程版本。
  • 停止或恢复失败时保留 worker 和已接受的输入,保持输入阻塞并提示手动检查、重启,不自动新建线程,也不自动重发已提交给旧进程的消息。

验证

macOS 与 Linux 均执行以下定向测试,各 638 passed、1 skipped

bun run test \
  test/worker-codex-session-upgrade.test.ts \
  test/worker-startup-retry-wiring.test.ts \
  test/hook-review-input-hold-wiring.test.ts \
  test/stuck-detector.test.ts \
  test/codex-session-upgrade.test.ts \
  test/codex-upgrade-target.test.ts \
  test/codex-upgrade-helpers.test.ts \
  test/cli-adapters.test.ts \
  test/auto-upgrade-codex-sessions-config.test.ts
  • 两端 bun run build 全量构建通过;Linux 单文件二进制构建及 smoke 通过。
  • 原生 Codex 0.153.4 配合隔离的本地 mock provider,验证 TUI 和 App Server 静默恢复:观察窗口内没有新增 HTTP 请求或 usage。该测试不使用真实账户计费。
  • 开发机完成实际 0.153.2 → 0.153.4 会话换代:原线程与 Botmux worker PID 保持,原生 CLI 进程换代,未新增模型回合或 token 用量。
  • 最新构建重新接管已有会话后,新 worker 保留原生 CLI PID 与原线程,Dashboard 显示实际版本 0.153.4;观察窗口内 task_started 增量为 0,总 token 用量不变。这项验证覆盖接管后的版本显示,没有再次执行原生版本升级。

真实 Hook review 菜单尚未手工验证,目前由行为回归测试覆盖阻塞与解除后的升级。以上是本地和开发机验证结果,不代表 CI 已通过,也不包含真实计费压测。

开关与回滚

关闭 Dashboard 开关或设置 dashboard.autoUpgradeCodexSessions: false 可停止后续自动换代,无需重启 daemon;关闭开关不会取消已经开始的换代,也不会回退已运行的 Codex 版本。需要回滚 Botmux 时,先关闭开关并等待当前换代结束,再恢复上一版构建并按既有流程重启。此功能不主动下载安装 Codex,也不改变 Botmux 会话的持久化结构。

界面

入口:全局设置 → 通用设置 → 实验性设置,默认开启。

自动升级会话 Codex 版本开关

@anarkh
anarkh requested a review from deepcoldy as a code owner September 7, 2026 13:04
@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个 PR,设计上的安全边界做得相当细致 —— 只升不降、waitForCodexExit 确认旧进程真退出、失败时保留 worker 与已接受的输入而不降级成新建 thread、macOS 二进制被原地替换时拒绝换代,这些都是很稳的取舍。下面主要是一处需要修改后才能合入的问题,以及几个非阻断的观察。

阻断项:4 条既有单测被打红(必需 check,CI 已红)

CI 上 test (1/3) test (2/3) test (3/3) 三个 shard 与 bun-test 均为 failure。我在本地基于最新 origin/master13413f856)rebase 后做了同环境 A/B 对照:

  • 干净 master:2 红(都是 mojo-launcher-env-quarantine,root 环境的已知问题,与本 PR 无关)
  • 本 PR:6 红 = 上述 2 条 + 下面 4 条新增

值得强调的是:这 4 条守卫的语义都还在,不是真的安全回归,纯粹是"源码文本断言的锚点/计数被改动打破"。但它们属于必需 check,所以仍需处理:

1. test/worker-pipe-initial-screen-order.test.ts:433
expect(source.match(/await spawnCli\(/g)).toHaveLength(3) —— 本 PR 在 worker.ts:2328 新增了第 4 个 await spawnCli( 调用点,实际变成 4。新调用点本身是合理的,建议改测试断言为 toHaveLength(4)

2. test/restart-worker-null-reattach.test.ts:264 + 3. test/raw-input-followup-atomicity.test.ts:686
这两条都用 indexOf('const observedBackend = backend;') 锚定 setupBackendHandlers。新增的 observeCodexRuntimeVersionOnReady()worker.ts:2263 引入了第二处逐字相同的字符串,且位置更靠前,于是 indexOf 锚到了新函数上,4 个 handler 的查找全部返回 -1。

我验证过:按正确的(第 2 处)锚点重算,四个 fence 的偏移与 master 逐字相同(3245 / 4401 / 5540 / 6092),backend !== observedBackend 也都在 —— 守卫没丢。

最小改法是把新函数里的局部变量改个名(不必动测试):

// src/worker.ts, observeCodexRuntimeVersionOnReady()
const versionObservationBackend = backend;   // 原为 observedBackend

4. test/startup-commands.test.ts:95
断言是单行 toContain('hasRunStartupCommands = !shouldRunStartupCommandsOnSpawn({ willReattachPersistent })'),而本 PR 把该赋值折成了两行。把新条件放到后半段即可保持单行形式(|| 在这里可交换):

hasRunStartupCommands = !shouldRunStartupCommandsOnSpawn({ willReattachPersistent })
  || codexAutoUpgrade?.stage === 'restoring';

以上 4 处我都在本地实测过:改完后这 4 个文件全绿,tsc --noEmit 干净(rc=0),连同你 PR 描述里列的 9 个文件一起跑 15 文件 / 892 passed / 1 skipped

另外供参考:你描述里列的定向测试命令不包含这 4 个文件,所以本地 638 passed 不会暴露它们;这 4 条都是读 src/worker.ts 源码文本做结构断言的用例,改 worker.ts 时容易被无声打破。

非阻断观察

  1. hasCodexAutonomousGoal 遇到残缺 JSONL 行会 throw。 实测畸形行会抛 JSON Parse error。它在 check() 里,所以会让该次 tick 进入 failed 状态并刷一条 log(fail-closed 方向是对的,不会误升级)。但 rollout 正在被写入时尾行撕裂应该不算罕见,按行 try/catch 跳过可能更贴合"未能证明有 Goal ⟹ 保守等待"的本意。

  2. 换代失败后会话需要人工介入。 失败时 stage='failed' 会持续钉住 cliRestartInProgress / rawInputRestartGate,只有 restart IPC 能解除。消息不丢、user_notify 也提示了手动重启,方向理解;只是从用户视角这个会话在被重启前是"不响应"的,提示语里明确指出需要 /restart 之类的具体动作可能更友好。

  3. 轮询成本。 每个 ready 的 codex 会话每 60s 会跑一次 ps -axo pid=,ppid=,comm= 全表扫描加一次 codex --version spawn。本机进程数 2659 时 ps 实测约 250ms/次;如果一台宿主上 codex 会话较多,聚合开销值得留意(可考虑把进程表在一次 tick 内复用,或仅在版本确实落后时才展开进程树)。

  4. resolveCodexUpgradeCommandhomedir() 读宿主安装位置,注释也写明了"安装属于宿主用户、而非 bot 重定向的 CODEX_HOME"——这点是对的。真机 5 组探测(默认 / configured / official / legacy-path / cliPathOverride 直通)行为都符合预期。

顺带确认一下:tui.auto_recap 是真实存在的 key(0.153.4 二进制里能命中,对照组:check_for_update_on_startup 命中、我编的假 key 零命中),--strict-resume-c tui.auto_recap=false 的接线看起来没问题。


以上是自动评审的初步意见,可能有误判或遗漏;最终以维护者审阅为准。阻断项集中在那 4 条测试上,改动量很小,其余部分我这边没有发现功能性缺陷。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants