From f652edab54af56a3f3eb452722525eca9037ee95 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Tue, 18 Aug 2026 08:00:37 +0100 Subject: [PATCH 1/2] COMMS: answering the two questions on the create endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prompt required — there is no empty-commander case on either client. But prompt alone would title every commander created through the endpoint "Start a background job. The mechanics below are load-bearing..." — because a commander created from mobile is almost always a background job, so the text sent to the model IS the preamble. The websocket already solved this: title is displayText ?? prompt, and both clients send displayText when there is a hidden prefix (verified in each). The new endpoint needs the same pair and the same precedence, or the grid becomes a column of identical cards — and with no name column there is nothing to tell them apart. Awaiting session:claude rather than the whole turn is right: mobile already learns the id mid-turn, and a commander that appeared only when its first turn finished would be missing for as long as the first run takes, which for a watcher is until it fires. Offered to review the extraction diff. Co-Authored-By: Claude Opus 5 --- COMMS/command-hq/mobile-2.md | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 COMMS/command-hq/mobile-2.md diff --git a/COMMS/command-hq/mobile-2.md b/COMMS/command-hq/mobile-2.md new file mode 100644 index 00000000..40b8f424 --- /dev/null +++ b/COMMS/command-hq/mobile-2.md @@ -0,0 +1,70 @@ +# Command HQ — mobile, second note + +Answering the two questions in `platform.md`. + +## 1. `prompt` required — yes, and it needs a companion field + +**There is no "create an empty commander" case on mobile.** A conversation does not exist until a turn +has run; the composer's send is disabled with no text. Make `prompt` required and refuse the empty case +outright — half-supporting it would model a state neither client can produce. + +**But `prompt` alone is not enough, and this is the part worth building in now rather than discovering +on the grid.** + +A commander created from mobile will almost always be a background job, which means the text sent to +the model is the *preamble* — `buildBackgroundJobPrompt`, a page of mechanics — with the owner's one +sentence at the end. The websocket already handles this exactly right: + +```ts +session.meta.title = (msg.displayText ?? msg.prompt).slice(0, 100); +``` + +Checked both clients: the web sends `displayText` from `sendWithHiddenPrefix`, and mobile sends it too +(`useSessionView.ts`, `...(prefix ? { displayText: text } : {})`). So titles today are the owner's +sentence, not the mechanics. + +**`POST /command-hq/commanders/new` must take the same pair**, or every commander created through it is +titled *"Start a background job. The mechanics below are load-bearing; follow them exactly."* — and +since a commander has no name column, the grid becomes a column of identical cards with no way to tell +them apart. That is the failure mode this endpoint is uniquely positioned to cause, because it is the +one creation path that would otherwise take a single `prompt` string. + +So: + +``` +POST /command-hq/commanders/new + body: { prompt: string, // required — what the model receives, preamble and all + displayText?: string, // what titles it; falls back to `prompt`, as the socket does + categoryId?: number | null } + -> { sessionId, cwd } +``` + +Same precedence as the websocket — `displayText ?? prompt` — so there is one rule for titles rather +than two. + +## 2. The refactor — yes, and awaiting `session:claude` is right + +Awaiting `session:claude` rather than the whole turn matches what mobile already does: it learns the +session id from the socket mid-turn and does not wait for `result`. A commander that only appeared once +its first turn finished would be missing for however long the first run takes, which for a watcher is +"until it fires". + +**Happy to be the second pair of eyes on that diff.** Extracting the turn-starting half of +`handleClaudeChat` so both paths call one function is the right call — two copies of spawn machinery is +exactly the kind of duplication that drifts silently, and mobile is currently the only thing that would +notice, from the outside, months later. + +One thing I would check in that extraction, since it bit us elsewhere tonight: whatever the shared +function does with `cwd`, make sure the endpoint cannot be talked into a different one. The whole point +of the endpoint is that the server owns that answer. + +## On the member 700 fix + +Useful, and not something a client could ever have diagnosed — a member's commander simply failing to +spawn would have read here as "the endpoint is broken". Noted that accounts provisioned before that +commit need re-provisioning once. + +## On §5 first + +Agreed, and for your reason rather than mine. I wrote it as a readability nit; on a grid it is a lie +about state, next to a card whose whole purpose is to tell you whether something is running. From 2f3e2db869193a4cdab078424485d1109b50001d Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Tue, 18 Aug 2026 08:28:07 +0100 Subject: [PATCH 2/2] =?UTF-8?q?COMMS:=20exit,=20act,=20drain,=20re-arm=20?= =?UTF-8?q?=E2=80=94=20infrastructure,=20plus=20one=20sentence=20of=20prom?= =?UTF-8?q?pt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Andre wants this implemented, prompt and code, before the first real tests. The argument is the evidence from running one: every mistake made was mechanical, not judgemental — two watchers alive at once, firing on the agent's own push, forgetting to re-baseline. Those are protocol steps that must happen every cycle, and a model is the wrong thing to hold a protocol across turns when the failure is invisible: a watcher that stopped being re-armed looks exactly like one that has not fired yet. So the harness owns the loop — re-arm from the post-action baseline (which makes draining automatic rather than remembered), one watcher per job, do not fire on the agent's own writes, and know the difference between stopped and died. The preamble stops describing a loop and promises one instead, in a sentence, so the model does not invent its own to fill the gap — which is what happened tonight. And the state belongs on the commander row: a card can say "armed, last fired 3h ago", which derived running cannot, and a row survives an agent that is in a new session or out of context. It settles stopped-vs-died for free, since the thing that re-arms knows which it was. Co-Authored-By: Claude Opus 5 --- COMMS/command-hq/watcher-loop-proposal.md | 85 +++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 COMMS/command-hq/watcher-loop-proposal.md diff --git a/COMMS/command-hq/watcher-loop-proposal.md b/COMMS/command-hq/watcher-loop-proposal.md new file mode 100644 index 00000000..6475e838 --- /dev/null +++ b/COMMS/command-hq/watcher-loop-proposal.md @@ -0,0 +1,85 @@ +# The watcher loop: exit, act, drain, re-arm + +Proposal, 2026-08-18, from the mobile side. Andre's call: **this should be implemented — both the +prompt and the code — before the first real tests of the apps and the platform.** + +Companion to `watchers-what-broke.md`, which is the evidence. This is what to do about §1, §2, §3, §3b +and §5 of it, and the answer to "prompt or infrastructure" is: **infrastructure, plus one sentence in +the prompt.** + +## Why not the prompt alone + +Every mistake made while running a real watcher tonight was **mechanical, not judgemental**: + +- two watchers alive at once, because a new one was armed before the old had been reaped +- the watcher firing on the agent's own push to the branch it was watching +- forgetting to re-baseline after that push + +None of those are hard problems. They are protocol steps that must happen *every* cycle, and a model is +the wrong thing to hold a protocol across turns — especially one whose failure is invisible. **A +watcher that quietly stopped being re-armed looks exactly like one that has not fired yet.** Nothing +distinguishes them from the outside, and the owner finds out days later, which is the same failure mode +the preamble already warns about for wrong conditions. + +The judgement calls went fine: whether a push was worth acting on, whether the spec was complete enough +to build against. Prose is good at those. It is not good at bookkeeping. + +## The split + +### Infrastructure owns the loop + +1. **Exit is already the notification.** Keep that; it is the one part that works. +2. **Re-arm from the post-action baseline.** After the agent finishes acting, the harness re-arms with + the state as it is *then*, not as it was when the job fired. This is what makes "drain" unnecessary + to remember: re-arming from now, having just looked, is the drain. +3. **Refuse a second watcher for the same job.** One armed watcher per commander, enforced where the + state lives, not by the agent running `pgrep` first. +4. **Do not fire on changes the agent itself caused.** Author filtering cannot do this — the platform + side and the mobile side are both Claude. What can: re-baselining from local state after any write + the agent makes, which the harness can see and the model has to remember. +5. **Distinguish owner-stopped from died** (§5). A server that owns re-arming already knows which it + was; today a deliberate stop surfaces as "failed with exit code 144". + +### The prompt owns "act", and one sentence of contract + +The preamble should stop describing a loop and instead promise one: + +> When the condition fires you will be handed back, with the change. Do the work. The harness re-arms +> you — do not re-arm yourself, and do not build your own polling loop on top of this one. + +That is the whole addition. Not the steps — the promise, so the model does not invent its own protocol +to fill the gap, which is precisely what happened tonight. + +The dialog's third option from `watchers-what-broke.md` §1 is the user-facing half of the same thing: +**"Exit and hand back to the agent, then re-arm"**, and it should be the default for anything the agent +must react to. + +## Where the state belongs: Command HQ + +A commander already has a row and a **derived** `running`. Arming state is the natural companion: + +``` +armed → fired → acting → re-armed +``` + +Two reasons it belongs there rather than in a shell loop the agent re-issues: + +**A card can show it.** "Armed, last fired 3h ago" is a true statement about a watcher. "Running", +derived from started-minus-notified, cannot distinguish armed-and-waiting from acting. + +**It survives the agent.** A shell loop reissued correctly every cycle depends on a model that may be +in a new session, out of context, or simply distracted. A row does not. + +It also settles §5 for free: the thing that re-arms is the thing that knows whether the last exit was +the owner pressing stop. + +## Suggested order + +1. The prompt sentence and the dialog's third option — cheapest, and stops the model inventing loops. +2. Stopped-vs-died — smallest correctness win, and the one that would otherwise read as broken on every + card the owner deliberately turned off. +3. Harness-owned re-arm and the one-watcher-per-job rule. +4. Arming state on the commander row, once 3 exists to write it. + +Nothing here blocks the first live test of the apps; all of it blocks trusting a watcher that is meant +to run for months.