show the model that answered instead of the bare harness string

the picker rendered the literal `claude-code` for any chat that named no
tier. that is DEFAULT_MODEL — the bare harness — and it routes correctly
(no --model is passed, so the cli picks for itself) but names no catalogue
entry, so radix matched no item and the fallback from 773f0e1c printed the
raw string's last segment.

officer cannot answer this because it deliberately did not choose. the
harness does, in its system.init `model` field, which the parser was
discarding. so: emit it as a `model:resolved` chat event, translate it to an
officer id in turn-stream, and forward it to the client only when the turn
named no tier.

that last condition is the load-bearing one. the alias map is lossy in one
direction — opus[1m] and opusplan both answer as claude-opus-5 — so
forwarding unconditionally would silently relabel a user's own choice as
plain opus.

display only, and three ways: not durable (like session:claude), never
recorded to chat_session_models, and never written into the client's
selectedModel or the reattach ref. storing it would pin a session to
whatever the cli defaulted to on its first turn, which is the freedom the
bare harness exists to preserve.

untested against a live turn — the parser and translator have unit tests,
the websocket gate does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 03:58:35 +00:00
co-authored by Claude Opus 5
parent 0868e637c0
commit 77f90a91f0
8 changed files with 137 additions and 0 deletions
@@ -193,6 +193,14 @@ export type ServerMessage =
| { type: 'compact:done'; trigger: 'manual' | 'auto'; preTokens: number; durationMs?: number }
/** The conversation moved onto a different model. See the `model` message role. */
| { type: 'model:switched'; from: string | null; to: string; ok: boolean; error?: string }
/**
* What the harness is actually running, when officer did not choose.
*
* Sent only for a turn that named no tier — the composer's `model` (never `selectedModel`) so the picker
* stops showing the bare harness string `claude-code`. No transcript row: it is not a boundary, nothing
* changed, and it says only what has been true since the turn began.
*/
| { type: 'model:resolved'; model: string }
/** Plan mode went on or off. See the `planmode` message role. */
| { type: 'plan:mode'; enabled: boolean; ok: boolean; error?: string }
/** The agent is blocked, waiting for this plan to be approved. See the `plan` message role. */
@@ -504,6 +504,18 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
}
break;
case 'model:resolved':
/*
Display only, and the two lines it does NOT contain are the point.
`setSelectedModel` would make the next turn request this tier explicitly, pinning a session that
was deliberately left to the CLI's own default. `modelRef.current` is the reattach handshake and
would do the same after any reconnect. The server sends this only when the turn named no tier, so
both would be inventing a choice the user never made.
*/
setModel(msg.model);
break;
case 'plan:mode':
// Committed first, like every other boundary marker: the notice says where the rules changed, so
// whatever was streamed under the old ones has to land above it.