the chat panel seeded the model picker from the wrong id space

/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 <noreply@anthropic.com>
Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420
This commit is contained in:
2026-08-25 06:52:39 +00:00
co-authored by Claude Opus 5
parent f07a1c8cc7
commit 92841efac9
3 changed files with 73 additions and 4 deletions
+3 -3
View File
@@ -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)
@@ -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,
@@ -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.