the server says which live sessions are commanders, and can return just one kind

/chat/live already classified every row from `command_hq_sessions`; what it did
not do was filter, so each client repeated that itself. Fine for the web list,
which shows both tabs at once. Wrong for the iPhone app, which swipes between
live chats, has no room for tabs, and must simply never be handed a commander.

`?kind=agent|commander` does it server-side. Counts always describe the whole
set regardless of the filter — the web tabs show both numbers side by side and
both must be right whichever tab is open, so a filter that also hid the other
count would force a second request to render one screen. An unrecognised kind is
a 400 rather than "everything": a mistyped `?kind=agents` silently returning
commanders is the precise swipe-order bug this exists to prevent, so the plural
forms are accepted too, since the client's own type is already plural.

And /chat/sessions no longer tags rows it is about to delete. It called
listCommandHqSessionIds twice — once to mark commanders, once to remove exactly
those rows four lines later — so isCommander was false on every row that
survived. A field that could only ever lie, plus a second identical query per
request to produce it. A list that excludes commanders has nothing to say about
which of its rows is one; withCommanderFlag had no other caller and is gone.

Not restarted: the shared checkout currently fails typecheck on another agent's
untracked Minesweeper work, and a restart is when half-applied work ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 22:01:28 +00:00
co-authored by Claude Opus 5
parent af918251d3
commit 749c80c877
2 changed files with 41 additions and 23 deletions
+1 -12
View File
@@ -997,7 +997,7 @@ function scanGroup(who: ChatIdentity, cwd: string): TranscriptSummary[] {
*
* Identity is now the `command_hq_sessions` row, which is the only thing that survives the cwd being free.
* It is applied by the CALLER, because that requires a database read and this function is synchronous over a
* directory scan — see `withCommanderFlag`.
* directory scan.
*/
export function listClaudeSessions(who: ChatIdentity, cwd: string): ClaudeSessionSummary[] {
return mergeChains(scanGroup(who, cwd))
@@ -1005,17 +1005,6 @@ export function listClaudeSessions(who: ChatIdentity, cwd: string): ClaudeSessio
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
/**
* Mark which of these are commanders, from the rows rather than from where they live.
*
* One query per request rather than one per session: `listCommandHqSessionIds` returns the whole set for an
* account, and a set membership test is what the per-row decoration needs. Commanders are few, so the set is
* small; the alternative — a lookup per transcript — would turn one list into fifty queries.
*/
export function withCommanderFlag<T extends { id: string }>(rows: T[], commanderIds: ReadonlySet<string>): T[] {
return rows.map((row) => (commanderIds.has(row.id) ? { ...row, isCommander: true } : row));
}
/**
* Is this the default commanders directory?
*