Skip to content

fix(relay): 重启后目标任务模型对齐,防静默换模型烧额度 (issue #2264) - #2272

Open
dongyu23 wants to merge 1 commit into
BigPizzaV3:mainfrom
dongyu23:codex/fix-2264-goal-model-drift
Open

dongyu23 wants to merge 1 commit into
BigPizzaV3:mainfrom
dongyu23:codex/fix-2264-goal-model-drift

Conversation

@dongyu23

@dongyu23 dongyu23 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Closes #2264

问题

用户用某个模型跑长线目标任务(目标模式),Codex++ 重启 Codex 后目标任务自动继续,但模型被静默换成 model_list 第一条(issue 中为 Astra 6 high),无人值守一夜烧掉大量额度。

根因

三条事实叠加:

  1. Codex 桌面版以 config.toml 的 model 作为模型选择的恢复源(本 PR 已实测,见验证第三节):UI 选模型后会把它回写 config;闪退/强杀时回写没来得及发生,config 停留在 Codex++ 上次写入的值;
  2. Codex++ 在 manager apply/切换时会把 config 的 model 写成「工具写入的隐式默认」(profile 模板值或 model_list 第一条,见 complete_relay_profile_config 原回落逻辑),不是用户意图;
  3. 重启链路上(launcher 拉起 Codex)原本没有任何步骤校正 model——apply_active_relay_profile hook 定义了但主流程未接线;直接重放完整 apply 又会覆盖用户 live 回写值,因此走下述轻量对齐。

目标模式任务恢复继续执行时跟随当前模型选择 → 漂移发生。

修复(3 文件,+543/-3,约一半为测试)

issue 原文两句话各对应一层修复,第三层防瓦解:

对应 issue 原文 行为
launcher 启动对齐 「程序闪退 或者强制重启。目标任务自动继续,但模型错的」 启动时若 live config 的 model 仍是工具写入值(空/模板值/model_list 第一条)且存在未归档目标任务,改写为目标任务模型;Codex 回写的 live 值一律不动
manager apply 对齐 「在codex++重启完codex后…模型被codex++换成了Astra 6 high」 apply 时把未归档目标任务的持久化模型注入 profile.model;用户显式声明的默认模型优先级最高
backfill 阻断 (超出 issue 字面范围,单独说明) 切换供应商时不再把工具写入的 model 固化进 profile(config_contents / profile.model),并救援「值等于 model_list 第一条」的存量污染。防修复被固化机制瓦解

数据源:automation_runs(未归档,archived_reason 为空,按 updated_at 取最新)join threads.model。表缺失/缺列/库不可读一律静默回落旧行为。带后缀语法时写入前剥后缀(复用 parse_model_suffix)。

验证

单测(relay_config.rs 内嵌测试模块,伪造 sqlite 按真实 schema):

  • 15 个新测试全部红→绿:主回归(旧代码下精确复现 issue:写出 model_list 第一条)、显式模型优先、无目标任务保持旧行为、schema 漂移回落、无库回落、后缀剥离、多条任务取最新、存量污染两种形态救援、切换→backfill→换模型→重新对齐的完整时间线、launcher 层 live 值保护对照等;
  • 全量 cargo test -p codex-plus-core:37 个测试组全部通过,0 回归(含 tests/relay_config.rs 全部 154 个存量测试)。

L2:真实 launcher 二进制链路(沙箱 CODEX_HOME + 真实 settings,不触碰真实 ~/.codex):

  • 轮 B(issue 场景):沙箱 config 预置为 model_list 第一条 + 伪造未归档目标任务(模型 gpt-5.6-luna)→ 启动真实 launcher 约 3 秒内 config 对齐为目标模型,诊断日志 launcher.goal_thread_model_aligned 留档;
  • 轮 A(对照):config 为 Codex 回写的 live 值 + 目标任务存在 → 保持不动,无对齐事件。

L3:config → Codex UI 模型通道对照(真实 Codex 26.915 桌面版,pureApi + API key 免登录,CDP 检查 UI,config 改动全程备份并已恢复现场):

  • config model = "gpt-5.6-sol" 时 UI 模型选择器显示 5.6 Sol
  • 改为 model = "gpt-5.6-luna" 并 reload → UI 变为 5.6 Luna;恢复 sol 并 reload → 回到 5.6 Sol双向可逆跟随:config.toml 的 model 是 Codex 模型选择的恢复源,写对它即写对恢复任务所用的模型。

验证链完整:sqlite 目标任务模型 →(L2)launcher/apply 写对 config →(L3)Codex UI 模型跟随 config → 目标模式恢复继续执行。

边界(如实)

  • 「目标模式自动恢复发出的 turn 请求使用 UI 当前模型」一环为代码级证据(app-server 注入层的请求形态 turn/start 自带 params.model,与 refreshCodexThreadModelBeforeTurn 的设计前提),未做消耗真实额度的端到端复跑。
  • launcher 守卫对「live = 旧目标任务固化值」不接管(目标任务换模型后立即闪丢、Codex 未回写新值的窗口);backfill 阻断保证该值不再进入 profile,触发条件苛刻,接受为边界。
  • automation_runs.status 值域未采样(本机库无数据),未归档判定用 archived_reason 为空(列语义明确)。
  • schema 依据:threads(id, model) 为仓库既有已发布代码依赖;automation_runs 列结构核对本机真实 Codex 库。

附:测试方法学说明

MSIX/AUMID 激活方式不继承 CODEX_HOME 环境变量:launcher 进程尊重沙箱 home,但被拉起的 Codex 桌面版读写真实 ~/.codex。端到端测试需要以「备份 + 改真实 config + 事后恢复」验证 Codex 侧行为,或改用环境继承可靠的启动方式(本 PR 的 L3 即按此操作并已恢复现场)。

闪退/强杀后 Codex 未及回写 UI 模型选择,config.toml 停留在工具写入的
隐式默认(model_list 第一条/模板值),目标任务自动继续时被静默换模型。

三层修复,各对应 issue 的一句原文:
- launcher 启动对齐(「程序闪退或者强制重启…模型错的」):live config
  的 model 仍为工具写入值且有未归档目标任务时改写为目标任务模型,
  Codex 回写的 live 值一律不动
- manager apply 对齐(「在codex++重启完codex后…被换成Astra 6 high」):
  apply 时把未归档目标任务模型注入 profile.model,显式配置不受影响
- backfill 阻断(超出 issue 字面范围,防修复被固化机制瓦解):切换
  供应商时不再把工具写入的 model 固化进 profile,并救援值恰等于
  model_list 第一条的存量污染

数据源:automation_runs(未归档,按 updated_at 取最新) join threads.model,
schema 与本机真实 Codex 库核对;表缺失/缺列/库不可读一律回落旧行为。

This branch has not been deployed

No deployments
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.

[Bug]: 关于切换任务包括目标模式被自动更换模型的问题。

1 participant