watcher loop: the prompt/dialog half, and stopped no longer reads as failed
owner's call, confirmed 2026-08-18: implement watcher-loop-proposal.md
exactly as written, before the first real tests — this governs agent
behavior generally, not just command hq. items 1-2 of the proposal's own
suggested order, done; items 3-4 (harness-owned re-arm, arming state on the
commander row) are the harder half, next.
FireBehavior replaces the stopOnFire boolean: 'stop' | 'handoff' |
'keep-going'. handoff carries the proposal's one-sentence contract verbatim
("the harness re-arms you — do not re-arm yourself, and do not build your
own polling loop on top of this one") and rewrites the closing IF IT EXITS
block so it stops contradicting rule 7 (the old universal "do not restart
it, i will start it again" directly lied about what handoff jobs actually
do).
isCommander gates the option to where the promise can be kept. the proposal
frames this entirely in terms of commanders — arming state's only home is
the commander row — so outside one there's nowhere to persist "keep
re-arming this," and offering the option there would make a promise nothing
fulfills. not wired to true anywhere yet: command hq's own composer, which
would set it, doesn't exist. every existing /chat caller is unaffected —
two options, stop default, unchanged from before tonight.
stopped-vs-died (§5): a job officer had just SIGTERM'd came back
status:'failed' (exit code 144) from the harness's own classification,
which this codebase does not control. fixed client-side — the tray already
knows the instant stop is clicked, so it now also marks that, and
useChat's task:notification handler overrides the displayed status
regardless of what the harness reported. known gap, stated in both the code
and the doc: in-memory, so it doesn't survive a refresh in the narrow window
between clicking stop and the notification landing.
docs/command-hq.md gains "the watcher loop" section: what's built, what
isn't, the proposal's own suggested order followed as given.
bunx tsgo clean. suite: 1038 pass (+9 new tests), same 7 pre-existing
baseline failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+57
-5
@@ -287,13 +287,12 @@ an independent record of what sessions exist. Consequences the owner confirmed e
|
||||
is a commander for life, and (given commanders live in their own cwd, never a `/chat` cwd) the two spaces
|
||||
never actually overlap.
|
||||
|
||||
## Built on, unchanged
|
||||
## Built on, mostly unchanged
|
||||
|
||||
Everything about _how a background job runs_ is out of scope — this only adds a place to browse, name and
|
||||
categorise the sessions the existing system produces. In particular, unmodified:
|
||||
Everything about _how a background job runs_ was out of scope for the original build — a place to browse,
|
||||
name and categorise the sessions the existing system produces. That held until real testing found the
|
||||
mechanism itself wanting; see "The watcher loop" below for what changed and why. Otherwise, unmodified:
|
||||
|
||||
- The Background Job dialog and `buildBackgroundJobPrompt` — Command vs Prompt, the watch/stop-on-fire
|
||||
rules, "run until stopped" vs "run for N minutes" (both prose in the prompt, no server-side timer).
|
||||
- The whole lifecycle: `run_in_background`/`Monitor`, `task:started`/`task:notification`,
|
||||
`chat_session_events` as the durable log, `collectRunningTasks` as the fold, the tray.
|
||||
- Stop/pause/restart routes under `/chat/tasks/:id`.
|
||||
@@ -301,6 +300,59 @@ categorise the sessions the existing system produces. In particular, unmodified:
|
||||
- `officer-notify` — assume it works and is wired up separately; do not build a parallel notification path
|
||||
for this feature.
|
||||
|
||||
## The watcher loop — exit, act, drain, re-arm
|
||||
|
||||
`COMMS/command-hq/watchers-what-broke.md` and `watcher-loop-proposal.md`, both 2026-08-18: the mobile side
|
||||
actually ran a watcher (poll `git ls-remote`, react to each push) through the dialog, and the mechanism
|
||||
broke in ways no amount of reading the code would have found — two watchers alive at once because a new
|
||||
one was armed before the old was reaped, a watcher firing on the agent's own push to the branch it was
|
||||
watching, "keep going" silently meaning the agent would never see a firing again because a background
|
||||
process reaches the model only when it EXITS. The owner's call, confirmed 2026-08-18: **implement the
|
||||
proposal as written, before the first real tests** — this governs the agent's behaviour generally, not
|
||||
only Command HQ, so it is not optional groundwork the way most of this document is.
|
||||
|
||||
**Built:**
|
||||
|
||||
- **A third dialog option**, `FireBehavior = 'stop' | 'handoff' | 'keep-going'` (was a `stopOnFire`
|
||||
boolean). `handoff` — "Exit and hand back to the agent, then re-arm" — carries the proposal's one-sentence
|
||||
contract (`handoffContract` in `background-job-prompt.ts`) verbatim: _"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."_ The closing "IF IT EXITS" section is rewritten for
|
||||
`handoff` too, so it does not contradict rule 7 by telling the model an exit means stay-stopped.
|
||||
- **`isCommander`, gating the promise to where it can be kept.** The proposal frames this entirely in terms
|
||||
of commanders — "a commander already has a row," arming state's only home — and outside one there is
|
||||
nowhere to persist "this should keep re-arming itself," so nothing would fulfil the promise. Rather than
|
||||
make it anyway, `BackgroundJobDialog` only offers `handoff` when `isCommander` is true, defaulting to it
|
||||
there and to the pre-existing `stop` everywhere else. **Not yet wired to `true` anywhere** — Command HQ's
|
||||
own composer, which would set it, does not exist yet (web) or does not use this dialog (mobile has its
|
||||
own). Every existing `/chat` caller is unaffected: two options, `stop` default, exactly as before tonight.
|
||||
- **Stopped no longer reads as failed.** `watchers-what-broke.md` §5: a job Officer had just SIGTERM'd came
|
||||
back `status: 'failed'` (exit code 144) from the harness's own classification, which this codebase does
|
||||
not control. Fixed client-side, at the one place it can be fixed cheaply: `stopTask` in
|
||||
`BackgroundTaskTray.tsx` now also calls `markStopRequested(taskId)`
|
||||
(`background-job-origin.ts`), and `useChat.ts`'s `task:notification` handler overrides the displayed
|
||||
status to `stopped` when a stop was just requested for that id, regardless of what the harness reported.
|
||||
Known gap: this is in-memory client state, so it does not survive a page refresh between clicking Stop
|
||||
and the notification arriving — an acceptable, narrow window, not attempted to be closed tonight.
|
||||
|
||||
**Not yet built — the harder half:**
|
||||
|
||||
- **Harness-owned re-arm.** The proposal's actual infrastructure ask: once the agent's turn responding to a
|
||||
firing ends, the PLATFORM (not the model) automatically respawns the original watch command as a fresh
|
||||
background task. The natural implementation reuses `task-restart.ts`'s existing `commandForTask` (already
|
||||
reconstructs a shell command from a transcript's tool_use/tool_result pairing) and its detached-spawn
|
||||
machinery — re-arming becomes "run the restart path again, automatically, keyed off a `result` event for
|
||||
a commander whose watcher just fired," rather than new command-recovery logic.
|
||||
- **Arming state on the commander row.** `armed`, the current watch task's id, `lastFiredAt` — the
|
||||
proposal's own suggested order puts this LAST, "once 3 exists to write it." Settles "one armed watcher
|
||||
per commander" as a natural side effect (the row can refuse a second) and "stopped vs died" for commanders
|
||||
specifically (the thing that re-arms already knows which it was).
|
||||
|
||||
Suggested order from the proposal, followed here: (1) prompt + dialog — done, (2) stopped-vs-died — done,
|
||||
(3) harness-owned re-arm, (4) arming state. "Nothing here blocks the first live test of the apps; all of it
|
||||
blocks trusting a watcher that is meant to run for months" — the proposal's own words, and the reason (3)
|
||||
and (4) are still open rather than rushed.
|
||||
|
||||
## Later — explicitly deferred, not forgotten
|
||||
|
||||
Raised during spec and set aside for a second version, in the owner's words: _"that's not... that's out of
|
||||
|
||||
Reference in New Issue
Block a user