watcher loop: arming state and end-to-end plumbing, item 3's mechanism found wrong before writing it
item 4 first (schema + plumbing), ahead of the proposal's own suggested
order, because item 3's trigger needed somewhere to write to before it could
be tested at all.
command_hq_sessions gains watcherArmed/watcherTaskId/watcherLastFiredAt.
watcherArmed is allowed to be stored unlike `running` — it's the owner's own
intent, not a live-process fact nothing external can silently falsify the
way a sidecar restart falsifies running. armCommandHqWatcher/
recordCommandHqWatcherFired/disarmCommandHqWatcher in queries.ts, exposed on
GET /commanders, GET /commanders/:sessionId, and PATCH accepts
{watcherArmed:false} to disarm (arming itself has no route — needs a live
task id only a running turn has).
armWatcher threaded the full client-to-server path: BackgroundJobDialog ->
sendWithHiddenPrefix -> QueuedPrompt -> sendPrompt's new last param -> the
chat websocket message -> ClientMessage -> startClaudeTurn's msg. typechecks
end to end, consumed NOWHERE yet — safely inert, not a shipped mechanism.
the real finding: my plan to reuse task-restart.ts's restartBackgroundTask
for the actual re-arm trigger is wrong, and I caught it before writing it
rather than after. that function respawns a process OUTSIDE the sdk's own
Monitor tracking — chat.ts's restart route emits its own task:notification
via emitToSession specifically because the harness stopped watching that
process. it updates the ui; it cannot reach back into the agent's context,
because there's no mechanism by which it could. a handoff watcher needs the
opposite — the agent must be TOLD again when it fires again, which only
happens through the sdk's own notification delivery, which only exists for
a job the sdk itself is still tracking.
so re-arming can't be a silent platform respawn. it has to drive the AGENT
to reissue its own run_in_background/Monitor call — the platform's job is
making that automatic and correct (right command via commandForTask, right
moment, no double-arm), not doing the reissuing itself. likely shape: a
synthetic follow-up turn, not from the user, once the acting turn's result
lands. not implemented — raised with the owner before building around a
wrong assumption a second time tonight.
docs/command-hq.md corrected to match — the earlier "reuse restartBackgroundTask"
claim removed and replaced with what's actually true.
bunx tsgo clean, suite unchanged at baseline (1038 pass, same 7
pre-existing failures).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+42
-15
@@ -335,23 +335,50 @@ only Command HQ, so it is not optional groundwork the way most of this document
|
||||
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:**
|
||||
**Built, item 4 (ahead of item 3 — plumbing needed to exist before the trigger could be tested):**
|
||||
|
||||
- **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).
|
||||
- **`command_hq_sessions` gained `watcherArmed`, `watcherTaskId`, `watcherLastFiredAt`.** Unlike `running`,
|
||||
`watcherArmed` is allowed to be stored — it is the owner's INTENT ("keep re-arming this"), not a live
|
||||
process fact the platform is reporting on, so nothing external can silently falsify it the way a sidecar
|
||||
restart falsifies `running`. `watcherTaskId` is closer to that rejected shape and accepts the same known
|
||||
gap `running` already lives with — see the schema file's docstring.
|
||||
`armCommandHqWatcher` / `recordCommandHqWatcherFired` / `disarmCommandHqWatcher` in `queries.ts`; exposed
|
||||
read-only on `GET /commanders` and `GET /commanders/:sessionId`, and `PATCH /commanders/:sessionId` accepts
|
||||
`{watcherArmed: false}` to disarm (arming itself has no route — it needs a live task id, which only a
|
||||
running turn has).
|
||||
- **`armWatcher` threaded end-to-end, client to server, and CONSUMED NOWHERE YET.** `BackgroundJobDialog`
|
||||
computes it (`watching && onFire === 'handoff'`) → `sendWithHiddenPrefix` → `QueuedPrompt.armWatcher` →
|
||||
`sendPrompt`'s new last parameter → the chat websocket message → `ClientMessage`'s `'chat'` variant →
|
||||
`startClaudeTurn`'s `msg.armWatcher`. Every step type-checks and the full suite is unaffected, because
|
||||
nothing reads the field on the server yet — safely inert plumbing, not a shipped mechanism.
|
||||
|
||||
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.
|
||||
**Not yet built — item 3, and a real design question found while trying to build it:**
|
||||
|
||||
The original plan here was wrong and is worth recording as wrong rather than quietly fixed: `task-restart.ts`'s
|
||||
`restartBackgroundTask` looked like the obvious reuse — it already recovers a command from a transcript and
|
||||
respawns it detached. **It is not reusable for this.** A restarted job is deliberately outside the SDK's
|
||||
own `Monitor` tracking — `chat.ts`'s restart route emits its own `task:notification` via `emitToSession`
|
||||
specifically because the harness stopped watching. That path updates the UI; it does not reach back into
|
||||
the agent's own context, because there is no mechanism by which it could. A `handoff` watcher's whole
|
||||
point is the OPPOSITE: the agent must be told again when it fires again, which only happens through the
|
||||
SDK's own notification delivery, which only exists for a job the SDK itself is tracking.
|
||||
|
||||
So harness-owned re-arm cannot be a silent platform-side respawn. It has to drive the AGENT to reissue its
|
||||
own `run_in_background`/`Monitor` call — the platform's job is to make that reissue AUTOMATIC and
|
||||
CORRECT (the right command, the right moment, no double-arming) rather than something the model has to
|
||||
remember across turns, not to do the reissuing itself. Concretely, likely shape: once a fired watcher's
|
||||
"acting" turn ends (`result`, for a commander whose `watcherTaskId` just produced a `task:notification`),
|
||||
the platform sends a SYNTHETIC follow-up turn — not from the user — instructing the agent to re-arm using
|
||||
the exact command `commandForTask` recovers from the original transcript. `commandForTask` is still the
|
||||
right reuse; `restartBackgroundTask` is not. Not implemented — raised with the owner before writing it, for
|
||||
the second time tonight, because getting the mechanism itself wrong is more expensive to discover after
|
||||
the fact than before.
|
||||
|
||||
Suggested order from the proposal: (1) prompt + dialog — done, (2) stopped-vs-died — done, (4) arming
|
||||
state and plumbing — done ahead of schedule, (3) the actual re-arm trigger — open, blocked on confirming
|
||||
the synthetic-turn design above. "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 this is
|
||||
still open rather than rushed.
|
||||
|
||||
## Later — explicitly deferred, not forgotten
|
||||
|
||||
|
||||
Reference in New Issue
Block a user