Skip to content

fix(worker-pool): 模型不再冻结进会话,每次启动跟随 bot 配置 - #773

Open
xu4wang wants to merge 2 commits into
deepcoldy:masterfrom
xu4wang:fix/session-model-follows-bot-config
Open

fix(worker-pool): 模型不再冻结进会话,每次启动跟随 bot 配置#773
xu4wang wants to merge 2 commits into
deepcoldy:masterfrom
xu4wang:fix/session-model-follows-bot-config

Conversation

@xu4wang

@xu4wang xu4wang commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

会话创建时会把 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 等根本不会写;
  • 所以对绝大多数会话来说,冻结值只是继承来的默认值,却压过了之后人为在 dashboard 做出的显式配置——优先级正好反了。

同一个 bot 上还存在语义分叉:reasoningEffort 没有 botCfg 回填,只认显式来源;model 是「继承也钉死」。两个「运行时档位」两套规矩。

改动

model 退出冻结集合,改为每次 spawn(含 resume)按 live bot 配置解析。 cliId / cliRuntime / cliPathOverride / wrapperCli 的冻结保持不变——那几个被中途换掉会真丢能力(ttadk codex wrapper 掉成裸 codex 会丢网关),而 model 是人主动配的、本就该生效。

解析规则集中在新增的 resolveSessionLaunchModel()src/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 不匹配的情况。

配套:

  • 不做数据迁移:存量 session.model 记录留在原地、不再被读(Session.model@deprecated,只作历史记录 + 上述第 3 条兜底)。没有需要回滚的写操作。
  • trigger API 的 options.model 修正为名副其实的 per-turn:以前写进持久字段 session.model,一次性调用会变成永久覆盖(文档写的是「仅新建会话生效」);现在落在内存态 spawnModelOverride,daemon 重启后不复活。options.reasoningEffort 行为不变(仍随会话持久化)。
  • 两处展示面改用同一解析函数,与实际启动保持一致:关闭卡里的 resume 命令、本地终端打开命令。前者原本对每个已冻结会话都拿不到 model,会让复制出去的 ttadk 命令静默退化成 ttadk 内置默认模型(glm-5.1),而不是 bot 配的那个。
  • 文档:bots-json(zh/en)说明 model 每次启动解析、改动对存量会话生效;api-task-trigger(zh/en)说明 options.model 只驻内存、不落盘。

影响面

  • 会话类型:普通话题会话、chat 会话、trigger/HTTP 会话、fork 子会话、restore 冷恢复都走同一个 sessionAgentConfig,规则统一;adopt 只观察不 spawn,不受影响。
  • 跨 CLI:所有适配器都从 init 消息拿 model,没有单独的取值路径;ttadk wrapper 的 -m 同步改成 live 配置。
  • Codex App 线程接管daemon.ts):cliId 被钉成 codex-app,与通知 Bot 的 cliId 不一致 → 走规则 3、且遗留记录已清空 → 不会继承通知 Bot 面向别的 CLI 的 model,与改动前行为一致。
  • 不会中途换模型:活着的 CLI 进程不受影响,新配置在下一次启动/恢复时生效。
  • 已知的语义变化:如果 bot 的 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 保持为空。
  • 变异验证:分别把 (a) 冻结行加回、(b) 解析改回 session.model ?? botCfg.model、(c) trigger 不写 spawnModelOverride、(d) 关闭卡改回读 session.model,四种变异各让对应用例转红,无一漏网。
  • 全量 pnpm test:见下方评论贴出的结果。

会话创建时会把 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`,四种变异
  各让对应用例转红,无一漏网。
@xu4wang
xu4wang requested a review from deepcoldy as a code owner August 6, 2026 16:24
@xu4wang

xu4wang commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

全量测试结果(本机 macOS,node 24)

$ pnpm test        # vitest run --project unit
 Test Files  2 failed | 813 passed | 3 skipped (818)
 Duration    87.91s

两个失败都与本改动无关,逐个核过:

文件 单独重跑 干净 upstream/master (b70f160) 上重跑 结论
test/maintenance.test.ts ✅ 通过 满载并发下的超时型 flaky
test/command-handler.test.ts ❌ 同样失败 同样失败expected 'Session: sess-001…' to contain ':8800/s/sess-001',与本分支一字不差) 既有失败,与本改动无关(本机环境相关)

pnpm build 通过。变异验证四项见 PR 描述。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审结论:Request Changes。当前改动覆盖了 daemon refork/restore,但仍有 3 条可达的模型状态转换没有闭合:

  1. src/core/worker-pool.ts:950:活 worker 的 /restart / dashboard restart / CLI crash auto-restart 不会重新解析 model。daemon 的 restart IPC 只携带最新 env,worker 随后用旧 lastInitConfig respawn,因此同 CLI 修改 model 后,物理重启仍继续用旧模型。这与“每次启动(含 resume)按当前配置解析”的核心目标直接冲突。
  2. src/daemon.ts:4583:Codex App 完成通知接管只清理 session.model,没有清理新迁移到内存态的 spawnModelOverride。解析规则 1 无条件优先,导致 trigger 的一次性 model 覆盖泄漏到接管后的 codex-app 启动;改动前该值位于 session.model,会被这里正确清掉。
  3. src/core/session-model.ts:51:PR 后创建的会话不再写 session.model,所以 CLI mismatch 时的规则 3 对新会话恒为 undefined。该路径不只来自手改配置:/botconfig set cli 与配置卡通过 applyConfigField 热更新 bot.config.cliId,但没有 dashboard PUT 的 closeCliMismatchedSessionsForBot sweep;旧会话冷停/崩溃后 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 / 接管不清覆盖 / 去掉启动记录回写,
  四种变异分别让对应用例转红。
@xu4wang

xu4wang commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

三条都已修,推在 73a32a8a。逐条回:

1|活 worker 的物理重启不重新解析模型 —— 已修

确认成立,而且不止 /restart:dashboard 重启按钮、dashboard cwd-move、CLI 崩溃 auto-restart 共四个生产者都走同一条「不 refork、worker 用 lastInitConfig 原地 respawn」的路。

修法直接复用仓库里 per-bot env 已有的通道与三分态,保持一致:

  • 新增 latestModelForRestart(ds):字符串 = 用它;null = 当前不该传模型(bot 未配 / 已清空,worker 清快照);undefined = 取不到(bot 已注销),保持快照 = 旧行为;
  • 四个生产者全部捎带 model
  • worker 侧在合并守卫之前覆盖 lastInitConfig.model(与 env merge 同位置,被合并的重复 restart 也要带走更新)。

2|Codex App 接管没清 spawnModelOverride —— 已修

确认成立。接管处补 ds.spawnModelOverride = undefined;,与既有的 delete ds.session.model 并列。

3|新会话缺历史模型记录,CLI mismatch 兜底恒空 —— 已修(选了你给的第二个方案)

确认成立,是真回归。没有走「对齐所有 CLI 热切入口的 mismatch sweep」(那是另一个独立问题,牵扯 /botconfig set cli 与配置卡两条路径的会话生命周期语义,不宜混在本 PR 里),而是按你给的备选维护一个只用于 mismatch 兜底的记录

session.model 语义从「创建时冻结值」改为**「本会话上次实际启动用的模型」——每次 spawn 用解析结果回写;显式的 per-trigger 覆盖不**写入(一次性值持久化正是本 PR 要消灭的错误)。它只在规则 3 被读,不参与正常优先级,因此不会把原 bug 带回来,而新老会话的兜底都成立了。

测试

新增 test/restart-live-worker-model.test.ts(14 例):三分态纯函数(含 override 优先、CLI mismatch)、live-worker restart 消息体、四个生产者的 wiring、worker 侧 merge 位置与 null 语义、接管清覆盖。

session-lifecycle-start 新增一例端到端串起规则 2/3:会话在 codex bot 下首次启动记录 glm-5.1 → bot 换成 claude-code + opus → 该会话 refork 仍用 glm-5.1(既不掉 CLI 默认,也不串到另一个 CLI 的模型)。

顺带更新了两个被消息体变化波及的既有测试:crash-loop-diagnostic(restart 消息全等断言)、restart-worker-null-reattach(source pin)。

变异验证:restart 不带 model / worker 不 merge / 接管不清覆盖 / 去掉启动记录回写,四种变异分别让对应用例转红。

pnpm build ✅   tsc --noEmit ✅
pnpm test  →  4 failed | 812 passed | 3 skipped (819)

4 个失败逐个核过,均与本改动无关:command-handler.test.ts 在干净 upstream/master(b70f160) 上报一模一样的错(既有失败);codex-app-runner.integrationworker-argv-reaction-status.integrationworkflow-c0-isolation 单独重跑全绿(满载超时型 flaky,同一份改动的两次全量跑出的失败集合不同也印证了这点)。

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