a commander is identified by its row, not by where it lives
Groundwork for choosing a commander's working directory. Identity was the CWD — every commander shared one directory, so the directory was the answer and it needed no query. The moment a commander can choose its own, that test is wrong in BOTH directions: a commander in the platform checkout reads as an ordinary chat, and an ordinary chat in the old shared directory reads as a commander. So identity is now the `command_hq_sessions` row, which is the only thing that survives the cwd being free. The isolation the shared directory provided is no longer carrying anything — the surfaces that needed it now filter explicitly, which is what made this possible. Four surfaces moved, and the shape of the change is the same in each: the transcript scanner stops inferring it and the ROUTE decorates from the rows. - `listClaudeSessions` says nothing about commanders any more. It is synchronous over a directory scan and identity now needs a database read, so `withCommanderFlag` applies it at the caller. - the sessions list, `/chat/live` and `GET /chat/sessions/:id` each read `listCommandHqSessionIds` once per request. A set membership test per row rather than a lookup per row: commanders are few, and the alternative turns one list into fifty queries. - `commanderBlock` resolves the row instead of parsing the account out of the path. It needs the email for the record paths, so that is a second read — once per session spawn, beside the affordances and the skills index, which are read there for the same reason. `commanderBlock` had to be hoisted out of `createSession`, which is synchronous, into `spawnClaudeStreaming`, which is async and already holds the account. Passing it in as a parameter rather than making the whole session constructor async. `isCommandersDir` survives but is documented as no longer being the identity test — it remains only for the places that legitimately care about that specific path: creating it for a new OS account, and as the historical home of commanders created before this. A commander mid-first-turn has no row and so reports false for one turn. That is the same turn it must not arm anything on, and it corrects itself the moment it is filed. Verified against real data: a filed commander renders the block, an unknown id and a null id both return null, and the live gate still answers 401. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -541,6 +541,11 @@ function createSession(
|
||||
params: ClaudeSpawnStreamingParams,
|
||||
onEvent: (event: ChatEvent) => void,
|
||||
onNotify: (kind: 'turn-ended' | 'input-needed') => void,
|
||||
/**
|
||||
* The commander block, or null. Passed IN because building it needs two database reads and this function
|
||||
* is synchronous — hoisted to `spawnClaudeStreaming`, which is async and already has the account.
|
||||
*/
|
||||
commander: string | null,
|
||||
): PersistentSession {
|
||||
const { sessionKey } = params;
|
||||
const input = makeInputQueue();
|
||||
@@ -616,10 +621,7 @@ function createSession(
|
||||
preamble, which is where this used to live and which an agent arming itself never sees. See
|
||||
`api/command-hq/commander-block.ts`.
|
||||
*/
|
||||
const commander = commanderBlock({
|
||||
sessionId: resumeId ?? null,
|
||||
cwd: params.cwd ?? resumeCwd ?? null,
|
||||
});
|
||||
// `commander` is a parameter, not computed here — see the note on it.
|
||||
const preamble = [affordances, skills, commander].filter(Boolean).join('\n\n') || null;
|
||||
|
||||
const q = query({
|
||||
@@ -892,7 +894,16 @@ export async function spawnClaudeStreaming(
|
||||
await applyModelChange(session, params.model);
|
||||
await applyPlanModeChange(session, params.planMode);
|
||||
} else {
|
||||
session = createSession(params, onEvent, onNotify);
|
||||
/*
|
||||
The commander block is built here, before the session exists, because it needs the database and
|
||||
`createSession` is synchronous. It resolves the row for this transcript id and returns null for
|
||||
anything that is not a commander — which is every ordinary chat, at the cost of one indexed lookup.
|
||||
*/
|
||||
const commander = await commanderBlock({
|
||||
userId: params.userId,
|
||||
sessionId: getClaudeSession(params.sessionKey, params.userId) ?? params.resumeSessionId ?? null,
|
||||
});
|
||||
session = createSession(params, onEvent, onNotify, commander);
|
||||
}
|
||||
session.pushTurn(params.prompt, params.images);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user