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.