From 9bf31eb53c32f339d7ee53fa7b5c499c4cd76bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 07:01:21 +0000 Subject: [PATCH] /chat read the model from location.state, which a refresh does not have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420 --- .../src/apps/ChatHistory/ChatDetailPanel.tsx | 10 +++++- ...the-model-picker-shows-the-bare-harness.md | 34 ++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx index aabb9bff..1e8bd8c1 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx @@ -192,7 +192,15 @@ function NewChat(props: NewChatProps) { // context 'chat' tells the backend to run this session from the caller's own home, so // its transcript lands in Claude's own store as an isolated project group (source of truth). - const chat = useChat(undefined, locationState?.model, { + // + // `props.model` first, for the reason spelled out on `initialModel` below: `locationState.model` is + // written by nothing and is always undefined on a resume. That was fixed there and missed HERE, which + // is the call that matters — this is the model `useChat` seeds `selectedModel` from, so leaving it + // undefined meant the composer sent no model at all. The server then fell back to `DEFAULT_MODEL`, the + // bare harness, which names no tier: the picker rendered the raw string `claude-code`, and `record()` + // rightly refused to store it, so nothing ever wrote a real value and the next turn did the same again. + // Clicking a session from the list hid it — that navigation DOES carry location state. + const chat = useChat(undefined, props.model ?? locationState?.model, { resumeSummary, resumeSessionId, initialMessages, diff --git a/todo/known-bugs/the-model-picker-shows-the-bare-harness.md b/todo/known-bugs/the-model-picker-shows-the-bare-harness.md index c9e428e9..e4632768 100644 --- a/todo/known-bugs/the-model-picker-shows-the-bare-harness.md +++ b/todo/known-bugs/the-model-picker-shows-the-bare-harness.md @@ -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.