Skip to content

feat(worker,card): Claude 自动降级模型时在会话卡片上提示 - #1249

Merged
deepcoldy merged 11 commits into
deepcoldy:masterfrom
linvon:feat/model-fallback-card-notice
Sep 5, 2026
Merged

deepcoldy merged 11 commits into
deepcoldy:masterfrom
linvon:feat/model-fallback-card-notice

Conversation

@linvon

@linvon linvon commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

改了什么

Claude Code 在会话中自动切换模型(Fable 安全管控触发的降级、模型不可用兜底、额度确认切换)时,botmux 现在会在本会话那张流式卡片的最底部贴一行黄色小字提示,例如:

  • ⚠️ 安全管控降级:Fable 5.1 → Opus 4.8(cyber)
  • ⚠️ 模型不可用,本轮切换:Fable 5.1 → Opus 4.8(overloaded)
  • ⚠️ 额度限制已切换:Fable 5.1 → Opus 4.8

提示随每一轮卡片刷新持续显示,只有识别到会话已切回(降级之后的主线程回复来自其它模型)、或出现更新的切换记录、或换到了另一条 Claude 会话时才消失。不发单独消息,不自动切回。

同时修了一个旧问题:Claude 会话的卡片用量行里模型名一直取启动配置,降级后显示的是错的;现在改为取回复记录里的实际服务模型(ds.activeModel)。

为什么

Fable 的安全分类器命中后,Claude Code 会把整个会话切到 Opus 4.8 并停留在那里直到手动 /model,飞书侧此前完全看不出来。本机会话记录里 7 月以来有 16 次这样的静默降级,用户在飞书上并不知道后续的活是别的模型干的。

实现方式

  • 信号源:Claude 会话 JSONL 里的结构化记录(type=system,subtype 为 model_refusal_fallback / model_fallback / model_consent_fallback),以及 type=assistant 记录的 message.model。
  • worker 只上报事实(src/worker.ts + src/services/claude-transcript.ts):新的 session 级切换记录、当前回复的服务模型、绑定的 Claude 会话 id。所有喂 bridgeQueue 的 transcript drain 路径(主路径、fingerprint/rotation 切换、pid 跟随切换、secondary 补读)都经过同一个观测入口,按路径的会话 id 分流;冷启动/换会话时从文件尾部有界回扫(4MB)补种。
  • daemon 持有状态并做判断(src/core/worker-pool.ts mergeModelFallbackObservation):只在三种正面证据下清除——会话 id 不符、更新的记录不是 Fable 降级(含 fork 复制过来的 neutralizedByFork 记录)、切换之后的回复模型 ≠ 降级模型。空回扫、worker 换代、daemon 重启都不会清除。状态随 persistStreamCardState 持久化到 Session.modelFallback。
  • 门控:只对 cliId = claude-code 的会话生效;只有原模型是 Fable 的记录才产生提示;scope=local(子 agent 局部回退)忽略。
  • 渲染(src/im/lark/card-builder.ts、md-card.ts):卡片最后一个元素,text_size: x-small,<font color='yellow'>;模型名转友好名(claude-fable-5-1[1m] → Fable 5.1,日期后缀不会被当成次版本号);原因去控制字符、单行、限长 24。

影响面评估

  • 公共层改动:src/types.ts(ModelFallbackState、WorkerToDaemon 新增 model_fallback 消息)、src/core/types.ts(DaemonSession.modelFallback)、src/core/worker-pool.ts(新 IPC case、换代清理、用量快照)、src/core/session-manager.ts(持久化脏检查与 restore 回填)、src/im/lark/card-builder.ts / md-card.ts(流式卡片)、src/i18n/zh.ts / en.ts。
  • 跨 CLI:观测只挂在 Claude bridge(bridgeIngest 及其同族 drain)上,worker 与 daemon 两侧都加了 claude-code 门;Codex / TRAE / 其它 CLI 的 bridge 代码未动,跑了 codex / traex / hermes / existing-app-server 的 bridge wiring 与 codex-active-runtime-wiring 测试确认无回归。
  • 跨会话类型:话题会话、adopt、resume、/clear 与 pid 跟随的会话轮转都在观测入口的分流规则里覆盖,并有对应测试;非 Claude 会话的换代会清掉并落盘旧状态。
  • 卡片:无降级时卡片逐字不变(测试锁定元素数量差恰为 1);最终回复卡与私有快照卡不显示该提示。
  • 平台:纯 Node/TS 逻辑,无平台相关路径。

实际测试验证

在本分支(基于 origin/master e7c8cf8)上:

npx tsc --noEmit -p .                      # 0 errors
bun run build                              # 通过(含 audit:domains / dashboard:bundle / audit-dist)
npx vitest run --project unit \
  test/claude-model-fallback-transcript.test.ts test/model-fallback-daemon-state.test.ts \
  test/model-fallback-wiring.test.ts test/card-model-fallback-notice.test.ts \
  test/card-builder.test.ts test/card-builder-stop-compact.test.ts \
  test/streaming-card-usage-arg.test.ts test/card-integration.test.ts \
  test/claude-transcript.test.ts test/session-card-model.test.ts \
  test/codex-active-runtime-wiring.test.ts test/bridge-rotation-policy.test.ts \
  test/adopt-route.test.ts test/jsonl-cursor.test.ts
# 14 files passed, 568 passed / 0 failed
npx vitest run --project unit              # 全量:23 failed / 21066 passed,失败集合与 origin/master 同机基线逐条一致
                                           #(session-store-sqlite-* 缺 bun、cli-runtime-update、cli-adapters 缺二进制,另 1 条随机端口 flake)

新增 4 个测试文件覆盖:记录解析与 Fable 门、tracker 的去重/切回/会话绑定、尾部回扫、daemon 合并规则(保留/清除/会话切换/activeModel)、卡片渲染(三种文案 zh/en、位置、字号颜色、清除后元素数)、以及 worker 各 drain 路径接入观测入口的源码锁。

卡片示意

卡片底部(用量行与按钮行之后)新增一行:

上下文 58.2K/1M (6%) · 本轮 ↑4.2K ↓310 · 累计 ↑58.2K ↓3.1K · claude-opus-4-8 max     ← 用量行(现在显示实际模型)
[ 📖 显示输出 | 🖥️ 打开 Web 终端 | 🔑 获取操作链接 | 🗜️ 压缩 | ⏹ 停止 | ❌ 关闭会话 ]
⚠️ 安全管控降级:Fable 5.1 → Opus 4.8(cyber)                                        ← 新增,x-small 黄色

用真实降级状态渲染出的卡片已在飞书里预览确认过样式;生产链路上的端到端(真实 Fable 安全管控触发)尚未在线上 daemon 验证,建议合并后第一次真触发时看一眼。

已知限制

  • 用户 /model 切回后,要等下一条回复出来提示才消失(Claude 只在回复记录里留模型信息,/model 本身只有一行人类可读的 local_command 记录)。
  • 冷启动回扫只看文件最后 4MB;记录更早时不会从文件恢复,但持久化状态仍在,且只会被正面证据清除。
  • daemon 恢复期(suppressRecoveryCard)沿用所有运行时补丁共用的抑制,状态已落盘,下一次活动刷新卡片。

wanglingfeng.linvon and others added 10 commits September 4, 2026 02:40
Claude Code 在安全管控拦截、模型不可用、额度确认三种情况下会自动把会话切到
备用模型,并往会话 JSONL 写一条 type:"system" 记录(model_refusal_fallback /
model_fallback / model_consent_fallback)。切换后整个会话都留在备用模型上,
但 botmux 侧完全无感:卡片脚注的模型名走的还是启动配置,用户看不出这一轮是
谁在答。

本次把这个状态接到每轮更新的那张流式卡上——按用户要求贴在卡片里,不单发消息,
也不做自动切回,只在提示里给出手动 /model 指令。

- claude-transcript:新增 model_*_fallback 记录解析、assistant 记录的实际
  服务模型提取(忽略 <synthetic> 占位、sidechain、API 错误行)、模型 id 归一
  化(originalModel 带 [1m] 后缀而 message.model 不带),以及有界回扫
  readLatestClaudeModelFallback,供 worker 冷启动补状态
- ClaudeModelFallbackTracker 收敛 uuid 去重、scope=local(子 agent 局部回退,
  主会话模型未变)过滤,以及「切换之后才算数」的 serving model 规则
- worker:bridgeIngest 后观测并在判定变化时发新的 model_fallback IPC;
  startBridgeWatcher baseline 之后回扫 seed 一次(baseline 只 cursor 到 EOF,
  历史降级记录不会再被 drain)
- daemon:新 IPC case 带 worker 世代守卫,落到 DaemonSession.modelFallback 并
  随 persistStreamCardState 持久化;状态搭 CardUsageSnapshot 的车进卡片,
  usageDisplay 关掉用量时也照常带(这是告警不是用量)
- 卡片文案贴在限额提示之后,模型名用友好名(Fable 5.1 / Opus 4.8),/model
  后跟 family 别名;zh / en 各三种文案

用户 /model 切回后,新的 assistant 记录模型不再等于备用模型,提示自动消失。

验证:npx tsc --noEmit -p . 干净;新增 4 个测试文件 57 例全绿,连同
streaming-card-usage-arg / card-builder / claude-transcript 等周边共 422 例通过;
全量 unit 套件失败项与改动前基线完全一致(11 个文件 42 例,均为缺 dist/tmux/bun
的环境性失败)。
评审复核降级提示时发现两处小问题,均已补测试:

- worker:jsonl 在 attach 时还不存在、之后才出现的那条惰性 baseline 路径
  (bridgeIngest 里 !bridgeBaselineDone 分支)没有补种降级状态。这条路径和
  startBridgeWatcher 里的 baseline 一样会跳过文件里已有的记录——非 adopt 分支
  游标直接到 EOF,adopt 分支虽然从 0 读但直接进 bridgeQueue.absorb,都不经过
  observeModelFallback。结果是文件出现时里面已有的降级记录永远看不到。补一次
  回扫即可;回扫按 uuid 去重,重复补种不会重复发布。

- 卡片:三个模型相关字段里只有两个显示名做了长度截断,`/model` 后面的参数没有,
  未识别的超长 id 会把整行撑爆(实测 375 字符,而该行本来就按 200 字符设计)。
  抽出 truncateModelToken 三处共用。真实 Claude 模型 id 最长 25 字符,均不受影响。

验证:npx tsc --noEmit -p . 干净;全量 npx vitest run --project unit 与改动前
基线失败项完全一致(11 个文件 42 例,均为缺 dist/tmux/bun 的环境性失败),通过数
20624 → 20626,即本次新增的 2 条用例。两条新用例均已做变异验证:回退修复后分别
报 375 > 200 与缺少 seed 调用。
两个问题:
1. 提示对所有 CLI、所有原模型都生效。Opus 5 → Opus 4.8 这种日常安全降级
   也会弹提示,用户根本没用 Fable。
2. 提示会莫名消失。worker 冷启动回扫(4MB 上限)在长会话里找不到降级记录时
   会把 null 发给 daemon,daemon 顺手清掉持久化状态;worker 换代时又无条件
   清 ds.modelFallback 指望回扫补回来——补不回来就永久丢了。

改法是把职责分清:worker 只上报观测到的事实,daemon 持有状态并负责判断。

- IPC 从 { state: ModelFallbackState | null } 改成
  { fallback?, servingModel? }:worker 没有任何形状能表达「已清除」。
- daemon 合并:新 uuid 的降级记录覆盖旧的;只有主线程 assistant 记录的模型
  与持有的 fallbackModel 不一致(归一化后比较,兼容 [1m] 后缀)才清除。
  没记录、worker 重启、回扫窗口不够,一律保持原状。
- worker 换代只在会话不是 claude-code 时清除(那种 worker 不会发来纠正观测);
  claude-code 会话以持久化状态为准。
- Fable 门下沉到 parseClaudeModelFallbackEvent:originalModel 归一化后不以
  claude-fable 开头的记录直接不解析,observe / 回扫 / 后续所有消费者自动继承。
- worker 侧 observe / seed 显式判断 cliId === 'claude-code'。

渲染、文案、卡片位置、持久化字段均未改动。

验证:npx tsc --noEmit -p . 无错误;claude-model-fallback-transcript、
model-fallback-daemon-state、model-fallback-wiring、card-model-fallback-notice、
card-builder、card-builder-stop-compact、streaming-card-usage-arg、
card-integration 共 8 个文件 338 条用例全绿;全量 unit 跑完与改动前失败集合
逐一致(13 个文件为环境原因的既有失败,与本改动无关)。
评审发现两处问题:

1. tracker 的 uuid 去重把「重置 servingModel」一起跳过了。jsonl 切换
   (worker.ts pid resolver 分支)和 baseline 自愈会把 bridgeOffset 归零,
   下一次 ingest 会把整个文件当作一批重放。此时降级记录已是重复记录,被
   `continue` 直接跳过,于是记录之前的那条旧 assistant 回复成了本批最新的
   servingModel 被上报,daemon 读成「已切回」把提示清掉——正是「降级前的旧
   回复不能触发切回」这条规则要挡的情况。改为先重置再去重。

2. mergeModelFallbackObservation 的 changed 反映的是「哪条规则命中」而不是
   「状态是否变化」。冷启动回扫同时带回降级记录和一条已经换了模型的回复时,
   b、c 都命中,最终仍回到「无提示」,却仍然会走一次 scheduleActiveRuntimePatch,
   白白改一次飞书卡片。改为按 uuid 比较前后状态。

补了两条回归测试:重放批次里的重复记录不再上报旧模型(且同批记录之后的回复
仍然上报);两条规则都命中但状态没变时 changed 为 false。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- trigger / apiRefusalCategory 与模型名走同一套处理:去换行和控制字符、
  折叠空白、超长截断(原因 24 字、模型 32 字)再转义。这两个值是
  transcript 里的原始 provider 字符串,多行或超长会把一行脚注撑坏。
- claude-opus-4-20250514 之前显示成「Opus 4.20250514」:次版本号限制为
  两位,正则回退到日期分支,显示为「Opus 4」;claude-haiku-4-5-20251001
  仍是「Haiku 4.5」。
- 顺带改掉两处过期的测试名:提示早已去掉切回文案,颜色是黄色不是灰色。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- 新增统一观测入口 observeModelFallbackEvents:adopt baseline、日志回放
  恢复、fingerprint 切换、rotation 切换、drainPathInto、secondary 路径
  轮询这些同样喂 bridgeQueue 的 drain 之前全都绕开了观测,从这些路径进来
  的切换记录会被整个吞掉。现在按路径的 Claude 会话 id 分流:等于当前绑定
  会话就观测;不等于但它是当前主路径则视为换会话,先重绑再观测;不等于
  且是 secondary/旧路径则忽略(那是别的会话的尾巴)。
- 状态绑定 Claude 会话 id 并持久化。/repo、/adopt、resume 到另一条原生
  会话都会换会话,旧状态既可能让新会话显示别人的提示,也可能在新会话实际
  模型正好等于旧 fallback 时永远清不掉。每次绑定或切换主路径都从新路径
  尾部回扫并必发一条带会话 id 的消息(回扫为空也发);daemon 收到会话 id
  与所持状态不同的消息时先丢旧状态再套用消息内容,同一会话的空消息不改
  状态,worker 重启不会清掉提示。
- 接受新的切换记录时一并重置已上报的 serving model:同一批 drain 里
  「降级 → 换模型回复 → 用户切回 → 原模型回复」时,批末的原模型不再因为
  和降级前相同被去重吞掉,否则提示会永久挂着。
- 解析保留所有 session 级切换记录,新增 fable / neutralizedByFork 标记。
  之前非 Fable 记录直接被丢弃,尾部回扫会越过它继续找到更旧的 Fable 记录,
  把用户早已切走的过期提示复活;fork 会话里复制过来的中和记录也没识别。
  现在以最新一条 session 级记录为准,是非 Fable 或已被中和时用
  fallback: null 表达「明确没有提示」,daemon 据此清除,与「没找到」区分开。
- 卡片用量行显示实际运行模型:Claude 从不发 active_runtime,用量行一直读
  启动配置。daemon 收到 servingModel 后归一化写入 activeModel,沿用
  active_runtime 的「变了才改、才打卡片补丁」语义,不动 reasoning effort;
  worker 重启后由冷启动回扫恢复。
- 切离 claude-code 时清除提示后补上落盘,否则 daemon 重启会让提示在已经
  不跑 Claude 的会话上复活。
- 源码锁测试逐个切片断言:每个喂 bridgeQueue 的 drainTranscript 调用点都
  必须经过统一观测入口,新增 drain 会直接让测试失败。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
复审两处漏网:

- observe() 里遇到已上报过的 session 级记录时直接 continue,把同一批里
  更早的那条记录留在了输出上。从 offset 0 重放整个文件的路径(fingerprint
  切换、rotation 切换、drainTranscript 发现文件被截断后的重读)会同时带上
  「早于本 worker baseline、因而从没进过观测的旧记录」和「已上报过的最新
  记录」,结果把最新记录已经作废的旧提示又推给 daemon:非 Fable 的新记录
  压不住旧 Fable 记录,两条 Fable 记录时显示的是旧那条的模型和原因。现在
  已上报的记录同样会清掉本批更早的候选——不管新旧,最新一条永远说了算。
- pid 解析器切换(maybeFollowSessionRotationViaPid)是第五处会改主路径的
  地方,之前没有回扫重绑。它换路径后把游标归零、自己不 drain,所以在新
  transcript 还没有可观测内容之前,daemon 一直显示上一条会话的提示;同一
  窗口里 secondary 轮询还会把旧会话的尾巴当成当前会话的证据。现在与另外
  四处一样在切换后必发一条带会话 id 的回扫。

测试:tracker 层三条回归用例(已上报的非 Fable 记录压住更早的 Fable 记录、
已上报的 Fable 记录不被更早的顶掉、回扫后重放整个文件不产生过期记录);
源码锁改为五处必发,并锁住主路径赋值点的总数。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@linvon
linvon requested a review from deepcoldy as a code owner September 4, 2026 07:33
@deepcoldy

Copy link
Copy Markdown
Owner

自动评审初步意见(双审收敛)

感谢这个 PR,设计上有几处很扎实:worker 只报事实、daemon 持状态并只认正面证据清除,「没看到」不等于「已切回」;以及把非 Fable 记录和 fork 继承记录保留在解析结果里只打标记而不是解析时丢弃——避免倒扫跨过最新记录去复活更老的 Fable 记录。这些都是踩过坑才有的写法。

下面一条建议合入前处理,其余为非阻断观察。

F1(建议合入前修):漂移到「第三个模型」会让提示提前消失

规则 (c) 的清除判据是「服务模型 ≠ fallbackModel ⟹ 认为切回了」。但换到第三个模型也满足这个条件,而那时会话并没有回到 Fable。

这不是推演,是本机真实 transcript 里发生过的(be6da26c-…jsonl,7715 行):

L914   model_consent_fallback  claude-fable-5[1m] → claude-opus-5[1m]
L915-3678   858 条回复 @ claude-opus-5        ← 降级生效中
L3681  Exit code 137 → L3685 "Continue from where you left off."   ← resume 边界
L3696-7706  1534 条回复 @ claude-opus-4-8     ← 漂到第三个模型,全程无任何新切换记录

已确认整个文件 type=system 的 subtype 统计里 model_consent_fallback 恰好 1 条。把这份真实文件喂进生产链路(tracker.observe → mergeModelFallbackObservation):

batch@900:  none → 0f5b1221     (降级记录生效)
batch@3600: 0f5b1221 → none     (serving=opus-4-8 ≠ fallbackModel opus-5 ⟹ 清除)
FINAL = null

后果是:会话此刻仍在 opus-4-8(既不是 Fable 也不是 fallbackModel),提示却已消失——退回到这个 PR 想解决的「用户不知道后续的活是别的模型干的」原状。

我们倾向认为这是 bug 而非产品取向:commit message「清除只认切回证据」与合入的文档注释都写明清除需要「切回」证据,而漂到第三个模型不是切回 Fable,规则 (c) 与自述意图不一致。

建议修法:判据从「≠ fallbackModel」改成「服务模型是 Fable 模型」,复用解析层同一谓词:

if (serving && isFableModelId(serving)) {
  next = undefined;
}

(评审中也考虑过 === originalModel,但那样在「手动 /model 切到另一个 Fable 变体」时提示会永远挂着,isFableModelId 是更稳的超集。)

现有 568 项测试没有覆盖这一情形——只测了「等于 fallbackModel(保留)」和「等于 originalModel(清除)」两种,建议补一条第三模型漂移的回归。

F2(非阻断):source-lock 钉住了闸的形状,钉不住它的位置

把 daemon 里 if (effectiveCliId !== 'claude-code') break; 一字不改地搬到状态写入 + 落盘之后,tsc 通过且相关测试全绿——形状锁(toContain)对「原样搬位置」这类变异天然无感。

实际风险很低:worker 侧 observeModelFallbackEvents / seedModelFallbackFromTranscript 各有一道真闸,非 Claude worker 根本发不出这条消息,daemon 这道属纵深防御。建议在 source-lock 里补一条相对顺序断言(indexOf(闸) < indexOf(状态写入))即可。

F3(非阻断,仅供参考):x-small 与 yellow 在本仓首次使用

  • text_size: 'x-small' 查飞书官方文档确认是合法值(10px,最小档)
  • <font color='yellow'> 内联用法在本仓是首次(现有 grey / orange)

你在描述里提到已在飞书预览确认过样式,这条我们离线无法复核,仅提示留意深浅色主题下 10px 黄字的可读性。

另一处同族观察(不阻断本 PR)

规则 (b):若之后出现「非 Fable → 非 Fable」的自动降级并写入新记录,tracker 会报 fallback: null,同样会清掉 Fable 提示,而会话仍不在 Fable 上。这属于「最新记录说了算」的设计选择,且有新记录作证,比规则 (c) 站得住;真要改是另一个产品决策。

评审侧的验证

  • 已基于最新 origin/master(86e99a3cf)本地 rebase,零冲突,与期间合入的 feat(session): occupancy 写入 SQLite,所有权不再读心跳文件 #1202 文件集完全不相交(未改动你的远端分支)
  • tsc --noEmit 0 error;bun run build 通过;PR head 的 CI 4/4 绿
  • 新增 4 个测试文件 129/129;卡片/transcript 相关回归 665/665
  • 全量 unit 对照同机基线:新出现的红全部隔离复跑绿(并行负载 flake),其余在基线上同样红(root 造不出只读前置),净回归 0
  • 按上述 F1 修法在本地验证:真实文件重跑后提示正确存活,回到 Fable 仍正常清除

以上为自动评审的初步意见,最终以维护者审阅为准。F1 若你有不同考量(例如有意选择更宽松的清除策略),也请在评论里说明,我们不预设结论。

F1:清除规则(c)原为「服务模型 ≠ fallbackModel ⟹ 清除」,但会话漂移到
第三个非 Fable 模型(真实 transcript be6da26c:opus-5 跨 resume 静默变
opus-4-8,无新切换记录)也满足该条件,提示被提前清除——而会话仍不在
Fable 上。改为「服务模型是 Fable ⟹ 清除」,复用解析层同一 isFableModelId
判据;相较 == originalModel 还能正确处理手动切到另一个 Fable 变体的情况。

F2:daemon 闸的 source-lock 只锁形状不锁位置,把闸搬到状态写入之后
48 项全绿。补 indexOf 相对顺序断言(闸 < merge < 写状态),变异验证
能抓住。

测试:新增 3 条 F1 回归(merge 级 + tracker→merge 全链路);4 个 fallback
测试文件 132/132;卡片/transcript 回归 7 文件 360/360;全量 unit 净回归 0
(9 项 root 只读前置基线红 + 2 项并行 flake,隔离复跑绿)。
@deepcoldy

Copy link
Copy Markdown
Owner

补充说明:维护者已确认合入,我按评审结论把 F1/F2 的修补直接追加到了本分支(dc7fd4ec5)。

由于 PR 勾选了「允许维护者修改」,这次是快进追加一个新 commit,你原有的 10 个 commit 未被改动、也没有 force-push,git pull 即可同步。

追加内容:

  1. F1(评审中提到的提前清除):清除判据由「服务模型 ≠ fallbackModel」改为「服务模型是 Fable」。原判据在会话漂移到第三个模型时也会成立——真实 transcript 里存在 opus-5 跨 resume 边界静默变成 opus-4-8、全程无新切换记录的情况,此时会话仍不在 Fable 上,提示却被清掉了。新判据复用解析层同一个 isFableModelId,语义上也更贴合提示本身要表达的「掉出 Fable」;相较 == originalModel 还能正确处理手动切到另一个 Fable 变体的情形。

  2. F2:daemon 侧守卫的 source-lock 只锁形状不锁位置(把守卫原样搬到状态写入之后,测试仍全绿)。补了 indexOf 相对顺序断言。

  3. 相应新增 3 条 F1 回归用例。

在最新主干上复验:tsc 0 error、bun run build 通过、4 个 fallback 测试文件 132/132、主干新增改动的回归面 171/171、卡片与 transcript 回归 468/468;变异验证两枪均转红(还原 F1 判据 → 3 红;搬守卫位置 → 1 红)。

感谢贡献 🙏 如果你对第 1 点的口径有不同看法,欢迎直接说,可以再调整。

@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.

双审收敛,建议合入。

评审要点已在前两条评论中说明。F1(会话漂移到第三个模型时提示被提前清除)与 F2(source-lock 只锁形状不锁位置)的修补已追加到本分支 dc7fd4ec5。

在最新主干 278ac46a8 上复验:10 个 commit 零冲突,tsc 0 error、bun run build 通过;fallback 测试 132/132、主干新增改动的回归面 171/171、卡片与 transcript 回归 468/468。变异验证两枪均转红(还原 F1 判据 → 3 红;把守卫搬到状态写入之后 → 1 红),确认修补真承重。

感谢贡献。

@deepcoldy
deepcoldy merged commit 563308e into deepcoldy:master Sep 5, 2026
4 checks passed
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🚀 Released in v3.19.1

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.

3 participants