From 92841efac917ba24789a339b07418d7b48486cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 06:52:39 +0000 Subject: [PATCH] the chat panel seeded the model picker from the wrong id space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /chat and Command HQ both pass detail.officerModel into the picker, each with a long comment saying why. ChatPanelWrapper passed detail.model — what the API served (claude-fable-5) rather than what the picker offers (claude-code/fable). The server already resolves the right one, falling back to a family guess from the transcript, so the panel was discarding a correct answer to show a raw id. It is sent back, too. isClaudeModel is startsWith('claude-code'), so claude-fable-5 fails it and the turn dispatches to OpenCode. The clamp that would catch it only fires while officer still holds the session in memory, which is exactly not the case after a restart. This is one half of the reported "the dropdown just says claude-code". The other half — DEFAULT_MODEL being the bare harness, every adopted session carrying it, and record() rightly refusing to store it so nothing self-heals — is written up in todo/known-bugs/the-model-picker-shows-the-bare-harness.md with the fix it needs: model:resolved already carries the answer and is already display-only on the client, but it is durable:false, so a refresh never sees it. Co-Authored-By: Claude Opus 5 Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420 --- TODO.md | 6 +- .../src/apps/Chat/ChatPanelWrapper.tsx | 9 ++- ...the-model-picker-shows-the-bare-harness.md | 62 +++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 todo/known-bugs/the-model-picker-shows-the-bare-harness.md diff --git a/TODO.md b/TODO.md index e0c954a1..5d08f278 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,7 @@ the owner's OS user and can never be granted. Indirection there really is accide ## Themes -**91 open, 30 closed**, across 31 themes. +**92 open, 30 closed**, across 31 themes. | Theme | Open | Closed | | --- | --- | --- | @@ -37,7 +37,7 @@ the owner's OS user and can never be granted. Indirection there really is accide | [Fleet — one command across every server](todo/fleet/) | 5 | 2 | | [Headscale](todo/headscale/) | 0 | 5 | | [Infra (alpha)](todo/infra/) | 7 | 0 | -| [Known bugs](todo/known-bugs/) | 3 | 3 | +| [Known bugs](todo/known-bugs/) | 4 | 3 | | [Mobile apps](todo/mobile-apps/) | 3 | 0 | | [Multi-user](todo/multi-user/) | 16 | 5 | | [Notifications](todo/notifications/) | 1 | 0 | @@ -68,6 +68,7 @@ the owner's OS user and can never be granted. Indirection there really is accide - `2026-08-25` **An affordance: a scoped agent warns when a request lands outside its scope** — [affordances/an-affordance-a-scoped-agent-warns-on-the](todo/affordances/an-affordance-a-scoped-agent-warns-on-the.md) - `2026-08-25` **Resurrect Projects/Apps** — [backburner/resurrect-projects-apps](todo/backburner/resurrect-projects-apps.md) +- `2026-08-25` **The model picker shows `claude-code` instead of the model that is running** — [known-bugs/the-model-picker-shows-the-bare-harness](todo/known-bugs/the-model-picker-shows-the-bare-harness.md) - `2026-08-23` **A worker cannot verify that the owner delegated to a manager, so it has to ask every time** — [agent-coordination/a-peer-cannot-verify-a-delegation](todo/agent-coordination/a-peer-cannot-verify-a-delegation.md) - `2026-08-23` **An agent cannot learn its own transcript id, so it cannot sign its own work** — [agent-coordination/an-agent-cannot-learn-its-own-session-id](todo/agent-coordination/an-agent-cannot-learn-its-own-session-id.md) - `2026-08-23` **The SendOff file size limit is one constant for everybody** — [sendoff/per-user-size-limit](todo/sendoff/per-user-size-limit.md) @@ -75,4 +76,3 @@ the owner's OS user and can never be granted. Indirection there really is accide - `2026-08-22` **Revisit the bun 1.3 pin once 1.4 has been watched for a while** — [infra/revisit-the-bun-1-3-pin-once-1-4-has-been](todo/infra/revisit-the-bun-1-3-pin-once-1-4-has-been.md) - `2026-08-22` **A plugin's permission should be on for every role by default, off only on request** — [multi-user/a-plugin-s-permission-should-be-on-for-every](todo/multi-user/a-plugin-s-permission-should-be-on-for-every.md) - `2026-08-22` **A user's settings should export and import as a plain file** — [settings/a-user-s-settings-should-export-and-import-as](todo/settings/a-user-s-settings-should-export-and-import-as.md) -- `2026-08-22` **The same for system settings, owned by the Super Admin** — [settings/the-same-for-system-settings-owned-by-the](todo/settings/the-same-for-system-settings-owned-by-the.md) diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx index 3ed50643..fd88afac 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx @@ -255,7 +255,14 @@ export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProp if (cancelled) return; setResume({ id: selectedSessionId, - model: detail.model, + // `officerModel`, not `detail.model` — the same rule the /chat screen and Command HQ both state + // at length, and the only one of the three call sites that had it wrong. `detail.model` is what + // the API served (`claude-fable-5`); the picker offers Officer ids (`claude-code/fable`). Seeding + // the picker from the wrong id space showed the raw API string in the trigger, and then SENT it: + // `isClaudeModel` is `startsWith('claude-code')`, so `claude-fable-5` fails it and the turn + // dispatches to OpenCode — caught only when officer still holds the session in memory, because + // that is the one case where the server's own record overrules the socket. + model: detail.officerModel ?? null, messages: detail.messages as unknown as ChatMessage[], total: detail.total, offset: detail.offset, 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 new file mode 100644 index 00000000..c9e428e9 --- /dev/null +++ b/todo/known-bugs/the-model-picker-shows-the-bare-harness.md @@ -0,0 +1,62 @@ +--- +id: known-bugs/the-model-picker-shows-the-bare-harness +title: 'The model picker shows `claude-code` instead of the model that is running' +theme: known-bugs +status: open +created: 2026-08-25 +date_source: stated +order: 5 +--- + +**The model picker shows `claude-code` — the bare harness — instead of a model.** Reported by the owner +2026-08-25 with a screenshot: the composer's harness pill reads "Claude Code" (correct) and the model +dropdown beside it reads `claude-code` (useless). He reports it happens often, both mid-turn and idle, and +cannot reproduce it deliberately. What he wants is simple and worth quoting: **the dropdown should always +show either the running model, or the model last used when the conversation finished.** + +**Where the raw string comes from.** `ModelSelector.tsx:329` deliberately prints the raw id when the +effective model matches no catalogue entry — the alternative is a blank control, and the comment there +already names `claude-code` as one of the ids that reaches it. So the picker is reporting honestly; the +question is why it is being handed a harness with no tier. + +Three sources of a bare `claude-code`, all verified in the tree: + +1. **`DEFAULT_MODEL` is `claude-code`** (`websocket.ts:48`), used when the client names no model and the + user has no default (`websocket.ts:829`). +2. **Every adopted session** — `adoptOrphanedSession(ws, sessionId, DEFAULT_MODEL, '')` + (`websocket.ts:1631`), the path a browser refresh takes when officer no longer holds the session. That + record's model is then pinned for its life and sent to the client as `session:init.model`. Observed + directly in the boot log after a `pm2 restart officer`: two of three adopted sessions logged + `model=claude-code`, one `model=claude-code/sonnet`. +3. **It cannot self-heal.** `record()` refuses to store a model that names no tier + (`websocket.ts:471`, and the comment there explains why: storing it would render blank and then feed + itself). So a session with no `chat_session_models` row sends no model, the server re-derives the bare + harness, and nothing ever writes a real one. Sixteen rows exist for a great many transcripts. + +**The event that would fix it already exists and is thrown away.** `model:resolved` carries exactly this +answer — the harness's own resolved model, mapped through `officerModelForApiModel` — and the client +already handles it as display-only, deliberately not touching `selectedModel` +(`useChat.ts:645`, and the comment is right that it must not invent a choice). It is emitted +`durable: false` (`turn-stream.ts:163`), so **a refresh never receives it**. That is the shape of the fix: +on `attach`, tell the client the session's resolved model the same way, rather than leaving it on the bare +default. Display-only, no choice invented, no new vocabulary. + +--- + +**One half fixed 2026-08-25: the dashboard chat panel was seeding the picker from the wrong id space.** +`ChatPanelWrapper.tsx` passed `detail.model` where `/chat` and Command HQ both pass `detail.officerModel` +— and both of those carry a long comment saying exactly why. `detail.model` is what the API served +(`claude-fable-5`); the picker offers Officer ids (`claude-code/fable`). The server already resolves the +right one, falling back to a family guess from the transcript when no row is stored +(`chat.ts:248`), so the panel was discarding a correct answer and showing a raw API id. + +Worth more than the label: that value is **sent back**. `isClaudeModel` is `startsWith('claude-code')` +(`websocket.ts:66`), so `claude-fable-5` fails it and the turn dispatches to **OpenCode**. The clamp above +it only fires when officer still holds the session in memory (`websocket.ts:845`) — which is precisely not +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.