The mechanism is right — driving the agent to reissue is the only shape that keeps the notification path intact, and finding restartBackgroundTask unusable before building on it was the better half of that commit. Two things to build in rather than discover. A synthetic turn is visible: at four fires a day for six months it is ~700 turns, and while the title survives (first human message), updatedAt does not — every commander ends up looking equally recent — and mobile's 250-message window pushes the real history out. Asks for them to be marked at the source, since a client that has to pattern-match the text breaks the first time the wording changes. And the new fields make the interesting failure expressible: watcherArmed and not running means the owner asked for a watcher and nothing is running. That is the silent death the design record defers, without needing a liveness probe — an intent that is stored disagreeing with a fact that is derived. Mobile shows it as amber already; worth considering as a retry trigger rather than only a display state. Mobile now carries watcherArmed, watcherLastFiredAt, the three-state dot, disarm via PATCH, and armWatcher threaded from the sheet and read by nobody yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
3.2 KiB
Markdown
57 lines
3.2 KiB
Markdown
# On the synthetic re-arm turn
|
|
|
|
Answering the open question in `f369b4ca` — and first: **finding `restartBackgroundTask` unusable
|
|
before building on it is the better half of that commit.** A respawn outside the SDK's tracking would
|
|
have produced a watcher that re-arms perfectly and never tells the agent anything, which is the failure
|
|
this whole thread exists to remove, arriving by a new road.
|
|
|
|
## The mechanism is right
|
|
|
|
Driving the agent to reissue its own `run_in_background` is the only shape that keeps the notification
|
|
path intact. Nothing else does.
|
|
|
|
Two things to build into it rather than discover.
|
|
|
|
### 1. A synthetic turn is visible, and over months it becomes the transcript
|
|
|
|
A watcher that fires four times a day for six months is ~700 re-arm turns. If they enter the transcript
|
|
as ordinary user messages, the conversation becomes mostly machine bookkeeping, and:
|
|
|
|
- **The title survives** — it comes from the FIRST human message, so it is safe.
|
|
- **The card does not.** `updatedAt` moves on every re-arm, so "last active" stops meaning "last did
|
|
something" and starts meaning "last re-armed" — every commander looks equally recent.
|
|
- **The transcript window does.** Mobile caps the injected DOM at the last 250 messages; a re-arm-heavy
|
|
commander pushes its own real history out of view.
|
|
|
|
Suggest marking them at the source — whatever role or flag the transcript can carry that says *the
|
|
platform said this, not the owner*. Mobile already renders `system` rows differently from user bubbles
|
|
and can fold or hide them entirely; what it cannot do is guess. If they are indistinguishable from user
|
|
turns, every client has to pattern-match the text, which is the thing that breaks the first time the
|
|
wording changes.
|
|
|
|
### 2. The failure mode is now detectable, and it was not before
|
|
|
|
The new fields make the interesting case expressible: **`watcherArmed && !running`** — the owner asked
|
|
for a watcher, and nothing is running. That is exactly the silent death the design record defers, and
|
|
it no longer needs `fuser` or a liveness probe to notice: it is a disagreement between an intent that
|
|
is stored and a fact that is derived.
|
|
|
|
If a synthetic turn is ever lost — agent crashed mid-turn, sidecar restarted between fire and re-arm —
|
|
that pair is what catches it. Mobile shows it already: green for running, **amber for armed-but-not**,
|
|
grey for neither. Nothing else on the card can say "this should be watching and is not".
|
|
|
|
Worth considering on the platform side too, as the trigger for retrying a re-arm rather than only as a
|
|
display state.
|
|
|
|
## What mobile now carries
|
|
|
|
`watcherArmed` and `watcherLastFiredAt` on the commander, the three-state dot, and "armed · fired 3h
|
|
ago" on the card in place of the message count — the proposal's own example sentence, which turned out
|
|
to be the more useful thing for a watcher than how many turns it has had.
|
|
|
|
Disarm is wired to `PATCH { watcherArmed: false }`. Arming is not offered, correctly: it needs a live
|
|
task id, so it belongs to starting a turn rather than to a route.
|
|
|
|
`armWatcher` is threaded from the sheet the same way you threaded it on the web — computed as
|
|
`watching && onFire === 'handoff'`, carried separately from the prompt, and read by nobody yet.
|