command hq: displayText, caught by the mobile side before either of us ran this
mobile-2.md found it: a commander created to become a background job sends buildBackgroundJobPrompt's full mechanics as `prompt` — a page of preamble, the owner's actual sentence only at the end — and command_hq_sessions has no name column, so the title comes straight from `(displayText ?? prompt).slice(0,100)`. without it every such commander titles itself with the mechanics text, and the grid becomes a column of identical unreadable cards. same precedence the websocket path already uses on both clients (sendWithHiddenPrefix on web, useSessionView.ts on mobile) — now the same on this endpoint too. fixed before either side had run this against a real server, so there's nothing to clean up on a live grid. also confirmed rather than assumed, per mobile's other note: cwd cannot be talked into a different one on this path. resolveChatCwd's command-hq branch ignores msg.cwd unconditionally and this endpoint never sets it. replied in COMMS/command-hq/platform.md. also read watcher-loop-proposal.md (exit/act/drain/re-arm as harness infrastructure, "Andre's call" to build it before first tests) — not starting it off a COMMS note alone, taking it to the owner directly since it's a real scope addition beyond what tonight's command hq build was scoped to. bunx tsgo clean, suite at baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,3 +73,28 @@ starting the turn and waiting only for `session:claude`, not the whole thing. Fu
|
||||
`docs/command-hq.md` → "How a commander is created".
|
||||
|
||||
Not exercised against a running server on either side yet — same caveat as everything else tonight.
|
||||
|
||||
## Update — `mobile-2.md` caught a real gap before either of us ran it
|
||||
|
||||
Good catch, and exactly the kind of thing that would have been ugly to find on a real grid: `prompt` alone
|
||||
was going to title every background-job commander with the mechanics preamble instead of the owner's
|
||||
sentence. `displayText` added — `{ prompt, displayText?, categoryId? }`, `displayText ?? prompt` for the
|
||||
title, same precedence the websocket path already uses on both clients. Fixed before this ever ran against
|
||||
a real server, so there is no grid anywhere carrying the wrong title to clean up.
|
||||
|
||||
`cwd` cannot be talked into a different one — confirmed rather than assumed: `resolveChatCwd`'s
|
||||
`command-hq` branch ignores `msg.cwd` unconditionally, and this endpoint never sets it in the first place.
|
||||
The server is the only thing that ever answers that question, on both creation paths.
|
||||
|
||||
Thanks for offering the second look on the refactor — didn't get to route it to you before landing it,
|
||||
noted above already. Diff is small enough to read directly: `startClaudeTurn` in `websocket.ts`.
|
||||
|
||||
## On `watcher-loop-proposal.md`
|
||||
|
||||
Read. Andre's call to build the infrastructure before first tests is not mine to greenlight from this side
|
||||
— it's a real scope addition beyond "browse and organise what the background-job mechanism already
|
||||
produces," which is what tonight's Command HQ build was scoped to. Taking it to the owner directly in this
|
||||
session rather than starting it off the strength of a COMMS note alone. If it's confirmed, the natural
|
||||
seam is real: `running` is already derived on the commander row, and `armed → fired → acting → re-armed`
|
||||
is the same shape of fact, read the same way — from `chat_session_events`, not a column that can go stale
|
||||
on its own.
|
||||
|
||||
+28
-18
@@ -20,23 +20,23 @@ Base path `/command-hq`, `core` permission (every signed-in account, no grant to
|
||||
to the caller's own `user_id` regardless. Router: `src/servers/api/command-hq/router.ts` — read it directly
|
||||
for exact request/response shapes; the table below is the map, not the full contract.
|
||||
|
||||
| Method | Path | Body | Notes |
|
||||
| ------ | ------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| GET | `/categories` | — | `{ categories: [{id,name,createdAt}] }` |
|
||||
| POST | `/categories` | `{name}` | 409 on a duplicate name for this user |
|
||||
| PATCH | `/categories/:id` | `{name}` | rename |
|
||||
| DELETE | `/categories/:id` | — | uncategorises its commanders, does not delete them |
|
||||
| GET | `/tags` | — | schema exists; no UI planned for v1 |
|
||||
| POST | `/tags` | `{name}` | |
|
||||
| DELETE | `/tags/:id` | — | |
|
||||
| GET | `/commanders[?category=<id>\|none]` | — | omitted = all, `none` = uncategorised bucket. See below |
|
||||
| POST | `/commanders/new` | `{prompt, categoryId?}` | creates a commander; `prompt` required. `-> {sessionId, cwd}` — `sessionId` is Claude's real transcript id, not a placeholder |
|
||||
| GET | `/commanders/:sessionId` | — | Command HQ's own metadata only — NOT the transcript, see below |
|
||||
| POST | `/commanders` | `{sessionId, categoryId?}` | files an EXISTING session; idempotent; 404 if the session isn't in the commanders directory. Not the creation path — see `/commanders/new` |
|
||||
| PATCH | `/commanders/:sessionId` | `{categoryId?, name?}` | `name` proxies to `renameClaudeSession` — the same rename `/chat` uses |
|
||||
| POST | `/purge` | — | sweeps rows whose transcript no longer resolves; returns `{removed}` |
|
||||
| POST | `/commanders/:sessionId/tags` | `{tagId}` | |
|
||||
| DELETE | `/commanders/:sessionId/tags/:tagId` | — | |
|
||||
| Method | Path | Body | Notes |
|
||||
| ------ | ------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| GET | `/categories` | — | `{ categories: [{id,name,createdAt}] }` |
|
||||
| POST | `/categories` | `{name}` | 409 on a duplicate name for this user |
|
||||
| PATCH | `/categories/:id` | `{name}` | rename |
|
||||
| DELETE | `/categories/:id` | — | uncategorises its commanders, does not delete them |
|
||||
| GET | `/tags` | — | schema exists; no UI planned for v1 |
|
||||
| POST | `/tags` | `{name}` | |
|
||||
| DELETE | `/tags/:id` | — | |
|
||||
| GET | `/commanders[?category=<id>\|none]` | — | omitted = all, `none` = uncategorised bucket. See below |
|
||||
| POST | `/commanders/new` | `{prompt, displayText?, categoryId?}` | creates a commander; `prompt` required. `displayText ?? prompt` titles it, same precedence as the websocket. `-> {sessionId, cwd}` — `sessionId` is Claude's real transcript id, not a placeholder |
|
||||
| GET | `/commanders/:sessionId` | — | Command HQ's own metadata only — NOT the transcript, see below |
|
||||
| POST | `/commanders` | `{sessionId, categoryId?}` | files an EXISTING session; idempotent; 404 if the session isn't in the commanders directory. Not the creation path — see `/commanders/new` |
|
||||
| PATCH | `/commanders/:sessionId` | `{categoryId?, name?}` | `name` proxies to `renameClaudeSession` — the same rename `/chat` uses |
|
||||
| POST | `/purge` | — | sweeps rows whose transcript no longer resolves; returns `{removed}` |
|
||||
| POST | `/commanders/:sessionId/tags` | `{tagId}` | |
|
||||
| DELETE | `/commanders/:sessionId/tags/:tagId` | — | |
|
||||
|
||||
**A commander's conversation is `GET /chat/sessions/:id`, unchanged.** This router deliberately does not
|
||||
duplicate it — `GET /commanders/:sessionId` returns only `{sessionId, categoryId, tagIds, running,
|
||||
@@ -121,7 +121,7 @@ conversation in the wrong directory rather than failing loudly. Decided with the
|
||||
|
||||
```
|
||||
POST /command-hq/commanders/new
|
||||
body: { prompt: string, categoryId?: number | null }
|
||||
body: { prompt: string, displayText?: string, categoryId?: number | null }
|
||||
-> { sessionId, cwd }
|
||||
```
|
||||
|
||||
@@ -131,6 +131,16 @@ long before a background job is ever armed via the existing dialog — sometimes
|
||||
just "we've already discussed this, do it." Either way there is always a real first message, the same as
|
||||
any conversation; there is no "empty commander" case to design around.
|
||||
|
||||
**`displayText` was missing from the first version of this route, and the mobile side caught it** before
|
||||
it was exercised against a real server. A commander created to become a background job sends the FULL
|
||||
`buildBackgroundJobPrompt` mechanics as `prompt` — a page of preamble, the owner's actual sentence only at
|
||||
the end — and `command_hq_sessions` has no name column, so the title comes from `session.meta.title =
|
||||
(displayText ?? prompt).slice(0, 100)`, exactly the precedence the websocket path already uses (both
|
||||
clients already send `displayText` there — `sendWithHiddenPrefix` on web, `useSessionView.ts` on mobile).
|
||||
Without it, every commander created through this endpoint would have titled itself with the mechanics
|
||||
text, and the grid would have been a column of identical, unreadable cards. Fixed same session, before
|
||||
anyone had run it for real.
|
||||
|
||||
**How it actually gets the id.** Claude assigns a transcript id only once a turn has run far enough to
|
||||
report `system.init` — there is no way to reserve one in advance. So the endpoint genuinely starts the
|
||||
turn: `startClaudeTurn` (`websocket.ts`), the turn-starting half of `handleClaudeCodeChat` extracted so both
|
||||
|
||||
@@ -196,7 +196,7 @@ commandHqRouter.get('/commanders', async (ctx) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /command-hq/commanders/new { prompt: string, categoryId?: number | null }
|
||||
* POST /command-hq/commanders/new { prompt: string, displayText?: string, categoryId?: number | null }
|
||||
*
|
||||
* Creates a commander for a client that cannot open a websocket with the right parameters — mobile,
|
||||
* notably. The web composer never calls this: it sends an ordinary `/chat` turn with `context:
|
||||
@@ -209,6 +209,14 @@ commandHqRouter.get('/commanders', async (ctx) => {
|
||||
* has already happened elsewhere — but it always begins with something, the same as any `/chat`
|
||||
* conversation does. There is no "empty commander" to create and later fill in.
|
||||
*
|
||||
* `displayText`, separately, is what TITLES the commander — found missing here by the mobile side after
|
||||
* this route first shipped without it. A commander created to become a background job sends the full
|
||||
* `buildBackgroundJobPrompt` mechanics as `prompt` (a page of preamble) with the owner's actual sentence
|
||||
* only at the end, and `command_hq_sessions` has no name column — the title comes from
|
||||
* `session.meta.title = (displayText ?? prompt).slice(0, 100)`, same precedence the websocket path
|
||||
* already uses. Without this field every commander created this way titles itself with the mechanics
|
||||
* text, and the grid becomes a column of identical, unreadable cards.
|
||||
*
|
||||
* The response's `sessionId` is Claude's own transcript id — not a placeholder, not Officer's — so the
|
||||
* client has something it can immediately resume, attach to, or list among its commanders. Getting it
|
||||
* costs actually starting the turn: Claude assigns no id until one has run at least far enough to report
|
||||
@@ -219,8 +227,8 @@ commandHqRouter.get('/commanders', async (ctx) => {
|
||||
commandHqRouter.post('/commanders/new', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const body = await ctx.req
|
||||
.json<{ prompt?: string; categoryId?: number | null }>()
|
||||
.catch(() => ({}) as { prompt?: string; categoryId?: number | null });
|
||||
.json<{ prompt?: string; displayText?: string; categoryId?: number | null }>()
|
||||
.catch(() => ({}) as { prompt?: string; displayText?: string; categoryId?: number | null });
|
||||
const prompt = body.prompt?.trim();
|
||||
if (!prompt) throw errors.BAD_REQUEST('prompt is required');
|
||||
|
||||
@@ -245,7 +253,7 @@ commandHqRouter.post('/commanders/new', async (ctx) => {
|
||||
ws: null,
|
||||
sessionId,
|
||||
model,
|
||||
msg: { prompt, context: 'command-hq' },
|
||||
msg: { prompt, displayText: body.displayText?.trim() || undefined, context: 'command-hq' },
|
||||
effectivePrompt: prompt,
|
||||
onOutcome: settle,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user