watcher loop: the agent re-arms itself, same turn — final mechanism, confirmed against real usage

second correction to the re-arm design tonight, and this one came from the
owner, not from me. my plan (platform sends a synthetic follow-up turn once
the acting turn's result lands, instructing re-arm) was technically sound —
it keeps the agent inside the sdk's own Monitor tracking, which is the thing
the FIRST wrong plan (silent respawn) broke — but it costs a real extra turn
every cycle. pushed back on with hard evidence: the mobile dev's own
watcher, on this repo, this session, had been running with THEM re-arming
themselves, same turn, for hours already — zero failures. that beats a
reliability concern I raised without having tested it.

so: handoffContract now instructs the agent to re-arm itself, as the last
action of the SAME turn that handles the notification — no synthetic turn,
no platform spawn, no extra cost. exitContract's handoff branch corrected to
match (was still saying "the harness re-arms a firing on its own", exactly
backwards now).

server side simplifies to match: createMessageHandler's task:started hook
now handles both cases with one rule — pendingArmWatcher (session-object
flag, not closure-captured, since arming happens turns after the closure
was built) claims the FIRST task:started as the watcher; every SUBSEQUENT
task:started while already armed is read as the agent's own self-re-arm and
just updates watcherTaskId to match. task:notification records the fire
when it matches the tracked id. the platform never drives anything — it
only notices what the agent already did.

pulled COMMS/command-hq/mobile-3.md, which answered the PREVIOUS (synthetic
turn) version — crossed in transit. their transcript-bloat concern is moot
now (no synthetic turn exists to bloat anything). their `watcherArmed &&
!running` detectable-failure pair stands regardless of which mechanism won
and is exactly right — kept as-is, matches their own three-state dot
(running/armed-but-not/neither). replied in platform.md.

all four proposal items now built: prompt+dialog, stopped-vs-died, the
re-arm mechanism (this commit), arming state+plumbing.

bunx tsgo clean, suite unchanged (1038 pass, same 7 pre-existing baseline
failures).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 08:33:47 +00:00
co-authored by Claude Opus 5
parent cc5340b8e0
commit 624d67ed52
7 changed files with 232 additions and 59 deletions
@@ -133,9 +133,11 @@ export async function getCommandHqSession(userId: number, sessionId: string): Pr
}
/**
* Arm (or re-arm) a commander's watcher — called both the first time a `handoff` job starts and every
* time the harness re-arms it after acting on a fire. Idempotent in effect: arming with the same task id
* twice just writes the same values again.
* Arm (or re-arm) a commander's watcher — called on the first `task:started` after the dialog submits a
* `handoff` job, and again on every SUBSEQUENT `task:started` while already armed, which is how the
* AGENT's own self-re-arm (same turn, per `handoffContract`) is picked up: the platform does not drive
* the re-arm, it only notices the new task id the agent's own next watch produced. Idempotent in effect:
* arming with the same task id twice just writes the same values again.
*/
export async function armCommandHqWatcher(userId: number, sessionId: string, taskId: string): Promise<boolean> {
const rows = await db