v0.31.3
[0.31.3] - 2026-09-06
Highlight: エージェントの「いまどのモデルか」と「いま許可を求めているか」を、画面の読み違いに頼らず正しく届けるリリース。スマホにモデル表示が無く低いモデルへの降格に気づけなかった問題を直し、claude の
/model切替はフレームとPostModelSwitchhook の 2 経路で、Command Code は起動バナーで検知するようにした。あわせて antigravity の許可ダイアログが「選択リスト」や「生成中」に化けて yes/no に到達できず、waitも抜けなくなる欠陥を agy 専用リーダで塞いだ。
Added
-
feat(hooks): claude の
PostModelSwitchhook を登録し、/model/fastの切替を構造化イベントで受ける (#2363): #2361 は claude の/model切替を画面読み(確認行とバナー)で検知していたが、同じ実測で claude 2.1.263 が専用 hookPreModelSwitch/PostModelSwitchを発火することが分かっていたので、こちらを主経路にし画面読みを fallback に降ろした。着手前に実機で採り直した(2026-09-06、claude 2.1.263、CLAUDE_CONFIG_DIR隔離+私設 sockettmux -L cm2363。CLAUDE_CONFIG_DIR/settings.jsonの command hook と、CommandMate が書くのと同じ形のtype:"http"hook を--settingsで注入した 2 経路を同時に採取): payload はsession_id/prompt_id/hook_event_name/from_model/to_model/requested_model/source/context_tokens/prompt_cache_warm/cache_ttl/estimated_cache_write_usd/pricing(modelキーは無い)。to_modelはSessionStart.modelと同じ id 形(/model sonnet→claude-sonnet-5、pickers→claude-haiku-4-5-20251001、/model opus→claude-opus-5、/model default→claude-opus-5[1m])、/fastON は Haiku からto_model: claude-opus-5[1m]で届くので #2361 で意図して読まなかった確認行の綴りゆれ(Opus 5vsOpus 5 (1M context))は問題にならない。モデルが変わらない操作(同じモデルを/modelで再指定、picker の Esc、/fastON が既に Opus 1M、/fastOFF、/effort、/clear)は発火しない。requested_modelは/model defaultと/fastでnull。PreModelSwitchは 2 回目の起動で毎回 2 発届き 2 発目のfrom_model/to_modelが実際のモデルと食い違った(Haiku のセッションでfrom_model: claude-sonnet-5)ので 登録しない(応答で切替を差し止められる decision hook でもあり、event receiver の固定 202 では何も決められないのに/modelのたびに最大 5 秒のブロッキング往復が増えるだけ)。実装:hook-settings-generatorの claude 用 hooks にPostModelSwitch(matcher 無し・event receiver 宛・他の http handler と同じ auth/timeout)を追加。新モジュールlib/hooks/sources/claude/model-switch.tsがPostModelSwitchをnotification(model_switch:<to_model>)に写像し(7 語に無い事象なので、状態機械が「未知の subtype は何の証拠にもならない」と扱うnotificationに載せる。turn / dialog / awaiting_instruction / status verdict はどれも動かない)、extractModelでto_modelだけを読む(from_modelは切替前の値なので絶対に読まない。requested_modelも読まない。to_modelが無ければ model null で latch 据え置き)。subtype にto_modelを含めるのは receiver の 3 秒 dedup キーが(event, detail, session_id)のため — 固定語だと pickers直後の/fastのような別モデルへの連続切替が 2 発目落ちる。agent-event-stateの #2361 ルール(FRAME_OVERTAKES_HOOK_MODEL_TOOLS)は fallback として残す: hook が届けば latch とlastAgentModelAtが進み、直後のフレーム(書き換わったバナー / 確認行)は同じモデルをラベル綴りで名乗るだけなのでisSameAgentModelName(cross-source)が hook の id に畳み、フレームは追い越さず #2357 の 3 経路は hook 由来で 1 回だけ動く(フレームが先に読めた場合も合計 1 回)。hook が届かない環境(旧版 claude、CM_AGENT_HOOKS_INJECT=0、配送ロス)ではフレームが従来どおり追い越す。他ツール(codex / copilot / opencode / gemini / command-code / antigravity)の hook 登録とto_modelの扱いは不変(テストで固定)。実 payload 7 枚をtests/fixtures/hooks/claude-model-switch-2363/に追加(tests/fixtures/hooks/claude/は「置いた fixture は全部登録済み」の走査ガードがあるため、登録しないPreModelSwitchを含む本 Issue 分は別ディレクトリ)。tests/integration/hooks-claude-model-switch-2363.test.tsが実 payload →POST /api/hooks/agent-event→GET /api/worktrees/:idのsessionStatusByInstance.claude.modelがto_modelになることと、PreModelSwitch/ 他ツール宛は 400 になることを固定 -
feat(detection): claude の
/model切替確認行を読み、切替をフレームから 3 経路へ届ける (#2361): #2357 の受入条件「Claude セッションで/modelから別モデルに切り替えると同じ 3 経路が動く」が未達だった原因は 2 つあり、両方を塞いだ。(1)extractModelInfo('claude', …)は起動バナーしか読まなかったので、バナーがスクロールアウトした実セッションでは切替が見えない →/model直後に claude が transcript へ印字する⎿ Set model to <model> …/⎿ Kept model as <model>行を読むCLAUDE_MODEL_SWITCH_PATTERNを追加し、バナーが画面にある間はバナー、無ければ最下の確認行を返すようにした。(2)mergeModelInfoは「model は hooks が勝つ」で、claude の hook はSessionStartでしかモデルを載せない(2.1.263 実測:/model/fast/effortは登録済みのどのイベントも発火せず、/clearのSessionStartにもmodelキーが無い)ため、フレームが新モデルを読んでも merged 値が古い hook id のまま動かずobserveAgentModelが変化を検知できなかった →agent-event-stateに hook のモデル報告時刻と frame のモデル変化時刻を持たせ、claude に限り「hook が最後に語った後にフレームのモデルが別モデルへ変わった」ときだけフレームを hook より優先する(同一モデルの表記ゆれは従来どおり hook の綴り、古い確認行が残る画面で再起動した新プロセスのSessionStartは hook が勝つ、codex 等 hook が毎イベントでモデルを再報告するツールは従来どおり hooks 優先)。Issue 本文の前提は 2 つとも実機で崩れた(claude 2.1.263、CLAUDE_CONFIG_DIR隔離+私設 socket、200x1000): 出発点Set model to Sonnet 5 and saved as your default for new sessionsは 3 形のうちの 1 つで、for this session only(picker のs)/and saved as your default for new sessions(Enter・/model <arg>)/Kept model as <model>(Esc)があり、effort は scope 接尾辞の後にwith high effortとして picker の effort 行を動かしたときだけ付く(無ければ null で latch 維持)。またバナーは切替のたびその場で書き換わる(fullscreen / inline とも、/effort/fastでも)ので、Issue の「末尾優先」は採らなかった —/fast後のフレームでは最下の確認行が古いKept model as Haiku 4.5でバナーがOpus 5 (1M context)になっており、末尾優先だと古い値を返す(fullscreen-fast-on.txtで固定)。Haiku のバナーはHaiku 4.5 · …と effort 節が無くバナー reader が読めないので確認行が担う。実機フレーム 15 枚をtests/fixtures/claude-model-switch-2361/に追加(バナー窓外+確認行のみの受入フレーム、確認行も窓外で 2.1.263 リリースノート prose だけの陰性対照を含む)。tests/integration/model-change-claude-switch-2361.test.tsが実フレーム →extractModelInfo→recordCapturedModelInfo(status poll と同一経路)で chat 行 /model_changedframe / push の 3 経路が 1 回だけ動くことを固定。測定で判明した別件: 2.1.263 はPreModelSwitch/PostModelSwitchhook(from_model/to_model/source)を発火する(CommandMate は未登録・modelキーしか読まない)。/fastの⎿ ↯ Fast mode ON · model set to Opus 5行は意図して読まない(バナーのOpus 5 (1M context)と綴りが違い、同一チャネル完全一致の #2357 判定で偽の変化通知になる)。どちらも配線は別 Issue -
feat(mobile,session): スマホにモデル表示を追加し、モデル変更を検知して 3 経路で知らせる (#2357): スマホの Terminal / Chat タブ最上部に固定 28px のセッション行を置き、
agent · model · effort(OpenCode はコスト/コンテキストも)を PC の分割ヘッダと同じformatAgentModelLabel/formatAgentSessionUsageで描く。モデルが未知(gemini / vibe-local / hooks 未接続)なら行ごと出さない。情報源は/api/worktrees一覧のsessionStatusByInstance(一覧も詳細もdetectWorktreeSessionStatusが同じ field を作るので PC と同一)。タップで picker を開く(OpenCode はctrl+x m、それ以外は/model送信)。サーバ側はagent-event-stateのobserveAgentModel()が hook / frame のどちらの書き込みでもgetResolvedAgentModelInfo().modelを compositeKey ごとの前回値と比較し、変わった瞬間にAgentModelChange {from, to, source, at}を 1 回だけ配る。変更と見なさないもの:null → 値(初回・再起動直後は開始時モデルとして記録のみ)、値 → null(latch が null を書かないので到達しない)、session_start(新プロセスの開始時モデルとして baseline を捨てる)、表記ゆれ。表記ゆれ判定isSameAgentModelNameは extractor の privatesameModelとは別のローカル実装で、同一チャネル内は完全一致(gpt-5とgpt-5-miniは別 — これがまさに通知したい降格)、チャネルをまたぐ時だけ包含一致(Gemini 3.7 Flash⊂gemini-3.7-flash-high)。受け手はrealtime/model-change-broadcastが 3 つへ fan-out: (1)chat_messagesに assistant 行(request_idがmodel-changed:接頭辞。chat と History の両方に出る。chat_messagesに system role が無いため assistant 行として書き request_id で識別)、(2)model_changedroom frame でセッション行を 5 分間アンバー+「変更あり」チップ、(3) Web Push を 1 遷移 1 通(既定 ON。事実のみで上位/下位の判定はしない)。各受け手は個別に try/catch で、DB 不可でも frame と push は出る。既知の制約: claude の/model切替は hook がSessionStartでしかモデルを載せず extractor も起動バナーしか読まないため、Set model to …行の reader が入るまで観測できない(Copilot の rate limit 降格はCOPILOT_MODEL_CHANGE_PATTERNがあるので観測できる) -
feat(detection): Command Code の起動バナーからモデルと reasoning effort を読む (#2358): Command Code は hooks payload に
modelを載せない唯一のツールで、extractModelInfoにもcommand-codeのルールが無かったためsessionStatusByInstance['command-code'].modelは常に空だった。# models: <model> · <taste>の行をstripAnsi後にscanFromEndで読むreadCommandCodeBannerを追加し、(latest)などの括弧付き接尾辞は/model一覧の綴りと一致させるため id の一部として保持、taste-1は Command Code 固有の「taste」設定なので·以降を捨てる。Issue 本文の「effort は無い」「切り替え後は再起動までバナー値」は 1.49.0 実機で両方とも成立しなかった:/modelは 2 画面のダイアログで、モデル選択の後にSelect reasoning effort for <model>(Default/low/high/max、集合はモデルごと)が出て、非 Default を選ぶとバナーが# models: <model> with <effort> effort · taste-1になる(起動時もconfig.jsonの値で同形)。effort は共有語彙REASONING_EFFORT_LEVELS経由で読み、low/highは載せmax(語彙外)は model だけ載せて effort は null。またモデル変更時は Command Code が出力全体を履歴を増やさず再描画するためバナー行はその場で書き換わり(1100 行の返答でスクロールバックに押し出された後でも同じ)、STATUS_DETECTION_CAPTURE_LINES=1000 行の捕捉窓に入っている間は次回 poll で切り替えが見える。窓の外に出たフレームは{null,null}を返しrecordCapturedModelInfoのラッチが直前値を保つ。同一モデルの effort だけの変更は 3 試行ともバナーに反映されなかった(Command Code 側の表示欠陥。reader は画面を写す)。実機フレーム 4 枚(起動時 max / Pro high / Kimi K3 low / バナーが窓外)をtests/fixtures/chat-dialog-card-2254/command-code-model-1-49-0-*.txtに追加(command-code-live-2250/は 13 名でピン留め、tests/unit/lib/detection/fixtures/は #2049 の compaction sweep が全走査するため置けない)。既存 18 フレーム全件で{ model: 'deepseek-v4-flash-(latest)', effort: null }を固定。表示側(formatAgentModelLabel)は変更なし
Fixed
- fix(detection): antigravity の許可ダイアログを agy 専用リーダで読み、ファイル作成(
Allow creation of this file?)と折り返しラベルの Bash 承認が yes/no に到達するようにする (#2364): #2270 の切り分けは質問文Do you want to proceed?限定だったため 1.1.27 のファイル作成ダイアログはantigravity_selection_list(矢印パッド)に落ち、コマンド本文でラベルが折り返す Bash 承認は汎用 multiple_choice パーサが 1 行 1 選択肢しか読めずrunning / thinking_indicator(「生成中」、waitが抜けない)になっていた。isAntigravityNumberedDialogを「↑/↓ Navigateフッタ + 直近の境界行以降にN. label行が 2 本以上 +Switch Model見出し無し」の構造判定にし、tools/antigravity/dialog.tsの専用リーダ(番号行に続く非番号行をラベルへ畳む・>付き行を既定・選択肢直前の?行だけをquestion・diff/コマンドはinstructionTextへ)を status(beforePrompt)と response poller(detectPromptWithOptions)の両方から呼ぶので、/current-output・保存される prompt 行・push 通知の抜粋が同じ画面に同じ答えを出す。判定を通ったのに読めないフレームはrunning / unknown_frame(isUnclassifiedActive)に落とし thinking には落とさない。prompt-responseAPI の送信前再確認も同じリーダを先に通すので、PromptPanel / MobilePromptSheet の番号回答が折り返しダイアログでも agy に届く(従来はprompt_no_longer_activeで無反応)。否認後のアンケート[1] Good [2] Fine [3] Bad [0] Skipはwaiting / antigravity_selection_listとして検出(runningにしない。タイプ入力の画面なので番号ボタンは出さない)。ANTIGRAVITY_VERIFIED_AGAINSTを 1.1.27 / 2026-09-06 / 200x1000 に更新し、実機フレーム 11 枚をtests/fixtures/antigravity-live-2364/に追加(Switch Model ピッカー・trust 画面・slash ポップアップは従来どおり選択リスト)。