fix(worker-pool): 模型不再冻结进会话,每次启动跟随 bot 配置 - #773
Conversation
会话创建时会把 bot 的 model 冻结进 session 记录,此后每次 resume 都显式 `--model <冻结值>` 启动,导致 dashboard 里配置的模型对**存量长会话永久失效**: 冻结值只是「建会话那一刻继承来的默认」,却压过了之后人为做出的显式配置, 优先级正好反了。 改为:model 在**每次 spawn(含 resume)时按 live bot 配置解析**,不再进冻结集合。 `cliId` / `cliRuntime` / `cliPathOverride` / `wrapperCli` 的冻结**保持不变**——那几个 被中途换掉会真丢能力(`ttadk codex` wrapper 掉成裸 codex 会丢网关),而 model 是 人主动配的、本就该生效。也不做数据迁移:存量记录留在原地不再被读。 解析优先级集中在新增的 `resolveSessionLaunchModel()`(core/session-model.ts): 1. `DaemonSession.spawnModelOverride` — 显式的 per-trigger 覆盖(trigger API `options.model`,仅 codex 家族),**只驻内存**; 2. live bot 配置,**仅当会话冻结的 cliId 与 bot 当前 cliId 一致**——被钉在别的 CLI 上的会话(bot 后来换了 CLI、或 Codex App 线程接管把 cliId 钉成 codex-app)不能 被塞进属于另一个 CLI 的 model 串; 3. 会话自己的历史 `model` 记录,仅兜底上面那种 CLI 不匹配的情况。 trigger API 的 `options.model` 顺带修正为名副其实的 per-turn:以前写进持久字段 `session.model`,一次性调用会变成永久覆盖(文档写的是「仅新建会话生效」),现在落在 内存态 `spawnModelOverride`。`options.reasoningEffort` 行为不变(仍随会话持久化)。 影响面 - 会话类型:普通话题会话、chat 会话、trigger/HTTP 会话、fork 子会话、restore 冷恢复都走同一个 `sessionAgentConfig`;adopt 只观察不 spawn,不受影响。 - 跨 CLI:claude/codex/gemini/coco 等全部适配器统一从 init 消息拿 model,规则同一条; ttadk wrapper 的 `-m` 取值同步改成 live 配置(关闭卡里给出的 resume 命令原本会 退化成 ttadk 内置默认模型,而不是 bot 配的那个)。 - 展示面:关闭卡 resume 命令、本地终端打开命令都改用同一解析函数,与实际启动一致。 - Codex App 线程接管:cliId 被钉成 codex-app,与通知 Bot 的 cliId 不一致 → 不会继承 它面向别的 CLI 的 model,行为与改动前一致。 验证 - `pnpm build` 通过。 - 新增 `test/session-launch-model.test.ts`(8 例)锁优先级;`session-lifecycle-start` 新增两例:同 CLI 的冻结会话 resume 时用**当前** bot model、显式 per-trigger 覆盖 仍然优先;`closed-session-card` 新增一例锁 ttadk resume 命令里的 `-m` 用 live 配置; `fork-session` 新增两例(不复制遗留冻结值 / 显式覆盖随运行时会话到子会话)。 - 变异验证:分别把(a)冻结行加回、(b)解析改回 `session.model ?? botCfg.model`、 (c)trigger 不写 `spawnModelOverride`、(d)关闭卡改回读 `session.model`,四种变异 各让对应用例转红,无一漏网。
全量测试结果(本机 macOS,node 24)两个失败都与本改动无关,逐个核过:
|
deepcoldy
left a comment
There was a problem hiding this comment.
复审结论:Request Changes。当前改动覆盖了 daemon refork/restore,但仍有 3 条可达的模型状态转换没有闭合:
src/core/worker-pool.ts:950:活 worker 的/restart/ dashboard restart / CLI crash auto-restart 不会重新解析 model。daemon 的 restart IPC 只携带最新 env,worker 随后用旧lastInitConfigrespawn,因此同 CLI 修改 model 后,物理重启仍继续用旧模型。这与“每次启动(含 resume)按当前配置解析”的核心目标直接冲突。src/daemon.ts:4583:Codex App 完成通知接管只清理session.model,没有清理新迁移到内存态的spawnModelOverride。解析规则 1 无条件优先,导致 trigger 的一次性 model 覆盖泄漏到接管后的 codex-app 启动;改动前该值位于session.model,会被这里正确清掉。src/core/session-model.ts:51:PR 后创建的会话不再写session.model,所以 CLI mismatch 时的规则 3 对新会话恒为 undefined。该路径不只来自手改配置:/botconfig set cli与配置卡通过applyConfigField热更新bot.config.cliId,但没有 dashboard PUT 的closeCliMismatchedSessionsForBotsweep;旧会话冷停/崩溃后 refork 会丢掉原显式 model,多个依赖启动参数的适配器会退回 CLI 默认。
建议补齐:
- restart IPC 增加 model 的三态刷新通道(或等效地在每次 worker 内 respawn 前重新解析),覆盖所有 restart 生产者和合并分支;
- notifier takeover 明确清除
spawnModelOverride; - 对齐所有 CLI 热切入口的 mismatch sweep,或继续维护一个只用于 mismatch 兜底的历史 model 记录;并为上述状态转换补行为测试。
本地验证:
pnpm build✅pnpm exec vitest run test/session-launch-model.test.ts:8/8 ✅pnpm exec vitest run test/session-lifecycle-start.test.ts:77/77 ✅pnpm exec vitest run test/codex-notifier-adopt-race.test.ts:20/20 ✅pnpm exec vitest run test/codex-notifier-adoption-wiring.test.ts:1/1 ✅
现有测试全绿,但没有覆盖以上三条状态转换。
复审指出前一提交只覆盖了 refork/restore,还有三条可达路径没闭合: 1. **活 worker 的物理重启不重新解析模型**。`/restart`、dashboard 重启按钮、 dashboard cwd-move、CLI 崩溃 auto-restart 这四条路都不 refork,worker 用 fork 时刻的 `lastInitConfig` 原地 respawn ——改完模型再重启,起来的还是旧模型,与 「每次启动(含 resume)按当前配置解析」直接冲突。 修法照搬 per-bot env 已有的那条通道与三分态:新增 `latestModelForRestart()` (字符串=用它 / null=当前不该传模型 / undefined=取不到就保持快照=旧行为), 四个 restart 生产者全部捎带,worker 在**合并守卫之前**覆盖 `lastInitConfig.model` (被合并的重复 restart 也要带走更新)。 2. **Codex App 线程接管没清内存态的一次性覆盖**。接管把 cliId 钉成 codex-app 并清掉 `session.model`,但迁到内存的 `spawnModelOverride` 优先级最高且无条件,会泄漏进 接管后的 codex-app 启动。显式清掉。 3. **新会话没有历史模型记录,CLI 不匹配时的兜底恒空**。上一版让新会话彻底不写 `session.model`,于是「会话被钉在 bot 已不再使用的 CLI 上」这条兜底对 PR 之后 创建的会话永远拿不到值:`/botconfig set cli` 与配置卡走 `applyConfigField` 热更 `cliId`,没有 dashboard PUT 那条 `closeCliMismatchedSessionsForBot` 清扫,这类 会话确实能活到下一次 refork,届时会丢掉原本的模型、退回 CLI 默认。 改为把 `session.model` 维护成**「本会话上次实际启动用的模型」记录**:每次 spawn 用解析结果回写(一次性的 per-trigger 覆盖不写入,避免重蹈「一次性值变永久」)。 它只在 CLI 不匹配时被读,不参与正常优先级,因此不会把原 bug 带回来。 验证 - `pnpm build` / `tsc --noEmit` 通过。 - 新增 `test/restart-live-worker-model.test.ts`(14 例):三分态纯函数、 live-worker restart 消息体、四个生产者的 wiring、worker 侧 merge 位置与 null 语义、 接管清覆盖。 - `session-lifecycle-start` 新增一例端到端:会话在 codex bot 下首次启动记录 glm-5.1 → bot 换成 claude-code(opus) → 该会话 refork 仍用 glm-5.1(既不掉默认也不串 CLI)。 - 变异验证:restart 不带 model / worker 不 merge / 接管不清覆盖 / 去掉启动记录回写, 四种变异分别让对应用例转红。
|
三条都已修,推在 1|活 worker 的物理重启不重新解析模型 —— 已修确认成立,而且不止 修法直接复用仓库里 per-bot
2|Codex App 接管没清
|
问题
会话创建时会把 bot 的
model冻结进 session 记录(sessionAgentConfig),此后每次 resume 都显式传--model <冻结值>。结果是 dashboard 里配置的模型对存量长会话永久失效:ds.session.model = ds.session.model ?? botCfg.model,也就是「建会话那一刻继承来的 bot 默认」;session.model的入口是 trigger API 的options.model,且被isCodexFamily门限制,对 claude / gemini / coco 等根本不会写;同一个 bot 上还存在语义分叉:
reasoningEffort没有 botCfg 回填,只认显式来源;model是「继承也钉死」。两个「运行时档位」两套规矩。改动
model退出冻结集合,改为每次 spawn(含 resume)按 live bot 配置解析。cliId/cliRuntime/cliPathOverride/wrapperCli的冻结保持不变——那几个被中途换掉会真丢能力(ttadk codexwrapper 掉成裸codex会丢网关),而 model 是人主动配的、本就该生效。解析规则集中在新增的
resolveSessionLaunchModel()(src/core/session-model.ts):DaemonSession.spawnModelOverride—— 显式 per-trigger 覆盖(trigger APIoptions.model,仅 codex 家族),只驻内存;cliId与 bot 当前cliId一致——被钉在别的 CLI 上的会话(bot 后来换了 CLI;或 Codex App 线程接管把cliId钉成codex-app)不能被塞进属于另一个 CLI 的 model 串;model记录,仅兜底上面那种 CLI 不匹配的情况。配套:
session.model记录留在原地、不再被读(Session.model标@deprecated,只作历史记录 + 上述第 3 条兜底)。没有需要回滚的写操作。options.model修正为名副其实的 per-turn:以前写进持久字段session.model,一次性调用会变成永久覆盖(文档写的是「仅新建会话生效」);现在落在内存态spawnModelOverride,daemon 重启后不复活。options.reasoningEffort行为不变(仍随会话持久化)。glm-5.1),而不是 bot 配的那个。bots-json(zh/en)说明model每次启动解析、改动对存量会话生效;api-task-trigger(zh/en)说明options.model只驻内存、不落盘。影响面
sessionAgentConfig,规则统一;adopt 只观察不 spawn,不受影响。-m同步改成 live 配置。daemon.ts):cliId被钉成codex-app,与通知 Bot 的cliId不一致 → 走规则 3、且遗留记录已清空 → 不会继承通知 Bot 面向别的 CLI 的 model,与改动前行为一致。model被清空,存量会话不再传--model,由 CLI 自行解析(claude --resume会恢复 transcript 里记录的模型)。这与「没有显式配置就沿用会话自己的历史」一致。验证
pnpm build通过。test/session-launch-model.test.ts(8 例)锁优先级三档 + 缺 botCfg 兜底。test/session-lifecycle-start.test.ts新增两例:同 CLI 的冻结会话 resume 时用当前 bot model(并确认遗留记录未被改写)、显式 per-trigger 覆盖仍然优先;原有「冻结会话不随 bot 配置漂移」的用例改为覆盖 bot 换了 CLI 的情形,仍然绿。test/closed-session-card.test.ts新增一例:冻结会话的 ttadk resume 命令里-m用 live 配置(用非 ttadk 默认值的模型名,避免与内置默认撞车而失去判别力)。test/fork-session.test.ts新增两例:不把遗留冻结值复制到子会话行;显式 per-trigger 覆盖随运行时会话到子会话。test/trigger-session-root-message.test.ts改为断言覆盖落在spawnModelOverride、且session.model保持为空。session.model ?? botCfg.model、(c) trigger 不写spawnModelOverride、(d) 关闭卡改回读session.model,四种变异各让对应用例转红,无一漏网。pnpm test:见下方评论贴出的结果。