Skip to content

v0.31.3

Choose a tag to compare

@Kewton Kewton released this 06 Sep 14:32
· 28 commits to main since this release
47df9ad

[0.31.3] - 2026-09-06

Highlight: エージェントの「いまどのモデルか」と「いま許可を求めているか」を、画面の読み違いに頼らず正しく届けるリリース。スマホにモデル表示が無く低いモデルへの降格に気づけなかった問題を直し、claude の /model 切替はフレームと PostModelSwitch hook の 2 経路で、Command Code は起動バナーで検知するようにした。あわせて antigravity の許可ダイアログが「選択リスト」や「生成中」に化けて yes/no に到達できず、wait も抜けなくなる欠陥を agy 専用リーダで塞いだ。

Added

  • feat(hooks): claude の PostModelSwitch hook を登録し、/model /fast の切替を構造化イベントで受ける (#2363): #2361 は claude の /model 切替を画面読み(確認行とバナー)で検知していたが、同じ実測で claude 2.1.263 が専用 hook PreModelSwitch / PostModelSwitch を発火することが分かっていたので、こちらを主経路にし画面読みを fallback に降ろした。着手前に実機で採り直した(2026-09-06、claude 2.1.263、CLAUDE_CONFIG_DIR 隔離+私設 socket tmux -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、picker s→claude-haiku-4-5-20251001、/model opus→claude-opus-5、/model default→claude-opus-5[1m])、/fast ON は Haiku から to_model: claude-opus-5[1m] で届くので #2361 で意図して読まなかった確認行の綴りゆれ(Opus 5 vs Opus 5 (1M context))は問題にならない。モデルが変わらない操作(同じモデルを /model で再指定、picker の Esc、/fast ON が既に Opus 1M、/fast OFF、/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) のため — 固定語だと picker s 直後の /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_changed frame / push の 3 経路が 1 回だけ動くことを固定。測定で判明した別件: 2.1.263 は PreModelSwitch / PostModelSwitch hook(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 の private sameModel とは別のローカル実装で、同一チャネル内は完全一致(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_changed room 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-response API の送信前再確認も同じリーダを先に通すので、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 ポップアップは従来どおり選択リスト)。