/chat read the model from location.state, which a refresh does not have

The composer showed the bare harness "claude-code" instead of the model. Root
cause: NewChat called useChat(undefined, locationState?.model). location.state
is only set by an in-app navigation, so on a refresh or a deep link it is empty
and the model argument was undefined. That is what useChat seeds selectedModel
from, so the composer sent NO model — confirmed live in the log as
"Model selected for chat … clientModel=null" on the owner's own session. The
server then fell back to DEFAULT_MODEL, the bare harness, which names no tier,
so the picker rendered the raw string.

The same file already knew this. Twelve lines down,
initialModel={props.model ?? locationState?.model} carries a comment saying
locationState.model "is written by nothing, so it was the only source and always
undefined on resume". That was applied to the child prop and missed on the
useChat call above it — the call that decides what gets sent. props.model is
detail.officerModel, correct and populated on every refresh.

Why it looked random: clicking a session from the list carries location state,
so the picker was right; only a refresh lost it. And it never healed, because no
model sent means the server re-derives the bare harness and record() rightly
refuses to store a tierless one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420
This commit is contained in:
2026-08-25 07:01:21 +00:00
co-authored by Claude Opus 5
parent 95dd9740da
commit 9bf31eb53c
2 changed files with 39 additions and 5 deletions
@@ -56,7 +56,33 @@ it only fires when officer still holds the session in memory (`websocket.ts:845`
the case after a restart, the same condition that produces the bare harness. Not observed in the wild, and
filed here rather than separately because it is the same one-line cause.
**Still open:** sources 1–3 above. The reported screenshot was not reproduced to a single path — with
`officerModel` seeding `selectedModel`, `/chat` and Command HQ should both survive an adopted session,
which points at the panel or at a session whose transcript names no model at all. The `attach` fix stands
on its own regardless of which one he hit.
---
**Root cause found and fixed 2026-08-25 — it was `/chat`, and one argument.**
`ChatDetailPanel`'s `NewChat` called `useChat(undefined, locationState?.model, …)`. `location.state` is
only populated by an in-app navigation, so **on a refresh or a deep link it is empty** and the model
argument was undefined. That is the value `useChat` seeds `selectedModel` from, so the composer sent no
model at all — confirmed in the live log, `Model selected for chat … clientModel=null`, on the owner's own
session while he was looking at it. The server then took source 1 above, `DEFAULT_MODEL`, and the bare
harness reached the picker.
The same file already knew this. Twelve lines below, `initialModel={props.model ?? locationState?.model}`
carries a comment saying `locationState.model` "is written by nothing, so it was the only source and always
undefined on resume". That fix was applied to the child prop and missed on the `useChat` call above it —
and the `useChat` call is the one that decides what gets SENT. `props.model` is `detail.officerModel`,
correct and populated on every refresh; it was simply not passed.
Why it looked random: clicking a session in the list carries location state, so the picker was right. Only
a **refresh** — or a pasted link — lost it. The owner's screenshot shows the session list reading `opus`
for the same conversation whose composer read `claude-code`, which is exactly that split: the list has its
own data, the composer had an empty `location.state`.
This also explains why it never healed. No model sent → server falls back to the bare harness → `record()`
refuses to store a tierless model → still no row → next turn identical.
**Still open, and worth doing anyway:** sources 2 and 3. An adopted session is still pinned to
`DEFAULT_MODEL` for its life, so `session:init` still reports a bare harness to any client that has nothing
better — this fix means `/chat` now always has something better, rather than making the server's answer
honest. The `attach` remedy above (answer with `model:resolved`, which is already display-only on the
client) is the fix for that, and it is independent of this one.