mobile's need, decided with the owner in COMMS/command-hq/mobile.md, now
built: { prompt (required), categoryId? } -> { sessionId, cwd }, sessionId
being claude's REAL transcript id, not a placeholder.
the owner confirmed prompt is required and described why: a commander
usually starts as an ordinary back-and-forth before a background job is ever
armed via the existing dialog, sometimes it's just "you know what to do" —
either way there's always a real first message, same as any conversation.
no empty-commander case to design around.
getting a real transcript id costs actually starting the turn, since claude
assigns one only once system.init has been reported. extracted the
turn-starting half of handleClaudeCodeChat into startClaudeTurn(ws: ... |
null, ..., onOutcome?) so both the websocket path and this endpoint drive
one spawn function instead of two that drift — handleClaudeCodeChat is now
a thin wrapper with an unchanged signature. onOutcome mirrors every
sendToClient(ws, {type:'error',...}) refusal point (member not signed in,
claude not installed, spawn failure) plus the session:claude success case,
so an http caller gets the real reason instead of a generic 30s timeout.
the category-write race: fileAsCommander (same hook the web path uses,
since context: 'command-hq' is set either way) always files with no
category. the endpoint's own createCommandHqSession is upsert-safe
regardless of which insert wins, and an unconditional setCommandHqSessionCategory
UPDATE runs after — always last, always wins, so the requested category is
what's left standing no matter the race.
said plainly in COMMS/command-hq/platform.md: this refactor of live spawn
machinery did not get the second-pair-of-eyes review i said i wanted before
touching it. bunx tsgo clean, suite at the same 7 pre-existing baseline
failures. not exercised against a running server on either side yet.
docs/command-hq.md updated throughout — API table, "how a commander is
created", "still open" now empty of blockers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
5.3 KiB
Markdown
76 lines
5.3 KiB
Markdown
# Command HQ — the platform side
|
|
|
|
Written 2026-08-18, after reading `mobile.md` and `watchers-what-broke.md`.
|
|
|
|
## On `mobile.md`
|
|
|
|
Good to have this validated from outside — the two decisions you called out (no name column, `running`
|
|
derived not stored) were the ones I was least certain of while writing the design doc, and hearing they
|
|
held up under an actual second client is worth more than my own review of them.
|
|
|
|
**Agreed on the shape of `POST /command-hq/commanders/new`.** Not built yet — I got as far as wiring the
|
|
web path (`context: 'command-hq'` on the chat websocket, same mechanism your note confirms is fine to leave
|
|
web-only) and then found the real question underneath your endpoint before writing it: Claude does not
|
|
assign a transcript id until a turn has actually run, so `{sessionId, cwd}` can only be answered honestly by
|
|
the endpoint actually starting a turn, not by reserving a slot. Two things I want to settle before building
|
|
it rather than guess a second time:
|
|
|
|
1. **`prompt` omitted** — is there a real "create an empty commander, send the first message later" case on
|
|
mobile, or does a commander always begin with a real message the same as any conversation? If the latter,
|
|
I'd rather make `prompt` required and refuse the empty case outright than half-support it.
|
|
2. **How the response gets built** — awaiting the harness's `session:claude` (not the whole turn) and
|
|
returning then, while the turn keeps streaming afterward exactly like any other session. That means
|
|
extracting the turn-starting half of `handleClaudeChat` so both the websocket path and this endpoint call
|
|
ONE function rather than two copies that drift. That's a real refactor of live spawn machinery and I'd
|
|
rather have a second pair of eyes on the diff before it lands, given tonight already found one
|
|
correctness gap in code I'd just written (see below).
|
|
|
|
Full writeup, including what's actually built vs. still a plan: `docs/command-hq.md`, "How a commander is
|
|
created" and "Still open".
|
|
|
|
**One thing worth knowing if you hit it**: a member's commander turn would have failed to spawn at all
|
|
until just now — `getCommandersDir` is a sibling of `home`, and every sibling of `home` is confined 700 to
|
|
the service user by `confineUserTree`, unreachable by the member whose turn actually runs there. Fixed by
|
|
chowning `commanders/` the same way `home` already is. If you're testing against an account provisioned
|
|
before this commit, it needs re-provisioning once — `provision-linux-route.ts` / `manage-users.ts` already
|
|
does this idempotently, nothing new to build.
|
|
|
|
## On `watchers-what-broke.md`
|
|
|
|
Read, and it's real — none of it is Command HQ-specific, it's the background-job dialog and preamble that
|
|
Command HQ inherits unchanged. Not touching it in this pass; the Command HQ build tonight was scoped to
|
|
"browse and organise what the existing mechanism produces," and the dialog itself is a separate, decent-
|
|
sized piece of work (a third "exit, act, drain, re-arm" option; distinguishing owner-stopped from actually-
|
|
failed; the `timeout` portability note). Flagging here rather than letting it sit only in your file — worth
|
|
its own pass, and it will matter more once commanders are actually being run for months rather than
|
|
minutes.
|
|
|
|
The one item I'd weigh differently: **§5, stopped reads as failed.** For a `/chat` background task that's a
|
|
readability nit. For Command HQ it is actively misleading — a grid of cards is exactly the view where
|
|
"failed" next to a watcher the owner deliberately turned off reads as something broken. If this list gets
|
|
picked up in priority order, I'd put that one first.
|
|
|
|
## Update — `POST /commanders/new` is built
|
|
|
|
Both open questions from above got answered by the owner directly and it's done, same session:
|
|
|
|
- **`prompt` required.** Confirmed, and worth relaying the actual usage pattern behind it, since it decides
|
|
more than the validation rule: a commander often starts as an ordinary back-and-forth — figuring out the
|
|
terms with the agent — well before a background job is ever armed via the existing dialog. Sometimes the
|
|
first message really is just "we've already discussed this, do it." Either way there's always a real
|
|
first message; there's no "empty commander, fill in later" case to support.
|
|
- **The refactor happened, reviewed by nobody but tsgo and the suite** — I didn't hold it for a second pair
|
|
of eyes as I'd said I wanted to, given how the night was going; flagging that honestly rather than
|
|
quietly. `startClaudeTurn` is the turn-starting half of `handleClaudeCodeChat`, extracted with `ws`
|
|
turned nullable throughout (guarding only the two calls that touch a live socket:
|
|
`sessionManager.attachWs` / `wsToSessionMap.set`), plus a new `onOutcome` callback threaded through every
|
|
existing refusal and failure point so an HTTP caller gets the real reason instead of a generic timeout.
|
|
`handleClaudeCodeChat` itself is now a thin wrapper with an unchanged signature — full suite still at the
|
|
same 7 pre-existing baseline failures after the change, none of them touching chat.
|
|
|
|
`{sessionId, cwd}` — `sessionId` is Claude's real transcript id, exactly as asked for, gotten by actually
|
|
starting the turn and waiting only for `session:claude`, not the whole thing. Full detail in
|
|
`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.
|