commanders run on Sonnet, not the account default

every new commander came up on whatever the owner last picked in a chat —
Fable, currently. that default is chosen for interactive work, and a
commander is the opposite: long-running, mostly repeating one settled job,
and started by a button rather than by somebody thinking about which model
they want.

COMMANDER_DEFAULT_MODEL, in both places, because the two ways of starting a
commander do not share code — the server's /commanders/new route and the
detail component's own first turn.

per-surface defaults belong in settings eventually. "commanders default to
Sonnet" is the decision; this is where it lives until there is a better
place for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 05:06:39 +00:00
co-authored by Claude Opus 5
parent af41144689
commit facf1a5f9e
4 changed files with 40 additions and 9 deletions
+14
View File
@@ -47,6 +47,20 @@ import { snapshotClaudeUsage } from './usage-store';
// Default model when no user preference is set // Default model when no user preference is set
export const DEFAULT_MODEL = 'claude-code'; export const DEFAULT_MODEL = 'claude-code';
/**
* What a commander runs on, regardless of the account's default.
*
* A commander is long-running and mostly does the same settled thing over and over, so it is the one
* surface where the account default — chosen for interactive work — is the wrong answer. Pinned here
* rather than left to the picker so a commander created today does not depend on what the owner last
* selected for a chat.
*
* Temporary in the sense that per-surface defaults belong in settings; permanent in the sense that
* "commanders default to Sonnet" is the decision, and this is where it lives until there is a better
* place for it.
*/
export const COMMANDER_DEFAULT_MODEL = 'claude-code/sonnet';
// Harness selection: the `claude-code` provider runs through the Claude sidecar; every other provider // Harness selection: the `claude-code` provider runs through the Claude sidecar; every other provider
// (opencode/anthropic/openai/… — all `providerID/modelID` ids) runs through the OpenCode server. // (opencode/anthropic/openai/… — all `providerID/modelID` ids) runs through the OpenCode server.
const isClaudeModel = (model: string): boolean => model.startsWith('claude-code'); const isClaudeModel = (model: string): boolean => model.startsWith('claude-code');
+3 -2
View File
@@ -28,7 +28,7 @@ import {
type ChatIdentity, type ChatIdentity,
type ClaudeSessionSummary, type ClaudeSessionSummary,
} from '../chat/claude-sessions'; } from '../chat/claude-sessions';
import { collectRunningTasks, startClaudeTurn, getUserDefaultModel, DEFAULT_MODEL } from '../chat/websocket'; import { collectRunningTasks, startClaudeTurn, COMMANDER_DEFAULT_MODEL } from '../chat/websocket';
import { import {
listCommandHqCategories, listCommandHqCategories,
createCommandHqCategory, createCommandHqCategory,
@@ -373,7 +373,8 @@ commandHqRouter.post('/commanders/new', async (ctx) => {
if (credentialError) console.warn(`[command-hq] ${row.dirName}: no credential minted — ${credentialError}`); if (credentialError) console.warn(`[command-hq] ${row.dirName}: no credential minted — ${credentialError}`);
const sessionId = randomUUID(); const sessionId = randomUUID();
const model = (await getUserDefaultModel(user.id)) || DEFAULT_MODEL; // Not the account default: a commander runs on Sonnet — see COMMANDER_DEFAULT_MODEL.
const model = COMMANDER_DEFAULT_MODEL;
const outcome = await new Promise<{ ok: true; transcriptId: string } | { ok: false; error: string }>((resolve) => { const outcome = await new Promise<{ ok: true; transcriptId: string } | { ok: false; error: string }>((resolve) => {
// 30s covers a slow spawn (member login checks, a cold sidecar) without holding an HTTP connection // 30s covers a slow spawn (member login checks, a cold sidecar) without holding an HTTP connection
@@ -9,6 +9,7 @@ import { useClient } from 'hooks/useClient';
import { errorText } from 'helpers/error-text'; import { errorText } from 'helpers/error-text';
import type { ClaudeSessionDetail } from 'state/useClaudeSessions'; import type { ClaudeSessionDetail } from 'state/useClaudeSessions';
import { useChat, EmbeddableChat } from '../Chat'; import { useChat, EmbeddableChat } from '../Chat';
import { COMMANDER_DEFAULT_MODEL } from './types';
import { DirPickerModal } from '../ChatHistory/DirPickerModal'; import { DirPickerModal } from '../ChatHistory/DirPickerModal';
import type { ChatMessage } from '../Chat/types'; import type { ChatMessage } from '../Chat/types';
import { categoryFromParam, commandHqListPath, commandHqSessionPath } from './command-hq-routes'; import { categoryFromParam, commandHqListPath, commandHqSessionPath } from './command-hq-routes';
@@ -102,13 +103,15 @@ const startBrief = (name: string, referencePath: string): string =>
'', '',
'Read the **Commanders** skill before you reply. It is how this works, and it is short.', 'Read the **Commanders** skill before you reply. It is how this works, and it is short.',
'', '',
'You watch for the thing I name and you respond when it happens. You never cause it — not while we', 'Go and LOOK at that path, then talk to me.',
'are setting you up, and not on a first run. If you find work of that kind already waiting here, it',
'is mine to start, not yours to volunteer for.',
'', '',
'So: go and LOOK at it first, then talk to me. How many things are in there, what kinds, how they are', 'Your first message is SHORT. A sentence or two on what is actually in there — how many, what kind —',
'named, how recently they arrive. Open with what you actually found and one question about it — that', 'and then one question. Nothing else. Not how the platform works, not what you could be told to do,',
'is worth ten generic ones, and it tells me straight away whether you are looking at the right thing.', 'not what you are declining to do, not what happens next. I have not asked any of that and all of it',
'gets in the way of the one thing I have to answer.',
'',
'Plain words. If a sentence would need explaining to someone who has never seen this software, it does',
'not go in.',
'', '',
'The name tells you the subject. It never tells you what to do about it, so do not decide that part on', 'The name tells you the subject. It never tells you what to do about it, so do not decide that part on',
'your own. One question, and wait.', 'your own. One question, and wait.',
@@ -303,7 +306,10 @@ function ActiveNewCommander({
const { t, tUi } = useTranslation(); const { t, tUi } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const chat = useChat(undefined, undefined, { // Sonnet, not the account's default. A commander is long-running and mostly repeats one settled job, so
// whatever the owner last picked for an interactive chat is the wrong answer here. Mirrors
// COMMANDER_DEFAULT_MODEL on the server, which the button-created path uses.
const chat = useChat(undefined, COMMANDER_DEFAULT_MODEL, {
replaceUrl: false, replaceUrl: false,
context: 'command-hq', context: 'command-hq',
// Known from this component's very first render — see the docstring above for why that timing is the // Known from this component's very first render — see the docstring above for why that timing is the
@@ -1,3 +1,13 @@
/**
* What a commander runs on, regardless of the account's default.
*
* A commander is long-running and mostly repeats one settled job, so the account default — chosen for
* interactive work — is the wrong answer here. Mirrors `COMMANDER_DEFAULT_MODEL` in
* `api/chat/websocket.ts`, which the server uses for the button-created path; both exist because the two
* ways of starting a commander do not share code.
*/
export const COMMANDER_DEFAULT_MODEL = 'claude-code/sonnet';
// Mirrors the JSON shapes `src/servers/api/command-hq/router.ts` actually returns — hand-kept rather // Mirrors the JSON shapes `src/servers/api/command-hq/router.ts` actually returns — hand-kept rather
// than imported from officerdb, the same way every other frontend API type in this codebase is (dates // than imported from officerdb, the same way every other frontend API type in this codebase is (dates
// arrive as ISO strings over the wire, not `Date`, which is the one thing importing the server's own // arrive as ISO strings over the wire, not `Date`, which is the one thing importing the server's own