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.