The Turn Ends Before the Work Does
Pippa delegates a todo through the coop tool, or starts a deploy, or asks for a restart. The work will finish in ten minutes or an hour. The turn ends now. That leaves two bad options. Poll in the foreground and the conversation is held hostage, burning tokens to watch a status field. Or say "I'll check on it later", a promise with nobody behind it: once the turn ends, the model has no clock, no memory of the promise and no way to wake itself.
cwkPippa's answer is the followup family tool. Before a delegating turn returns, it arms a durable, read-only check: what to look at, what counts as done, what counts as stuck, how often, until when, and what one line to tell Dad in each case. The tool's own description is blunt about timing: arm before ending the turn, and state the returned wake plan.
The Wake Contract
An arm call takes exactly these fields:
key: a stable name for this piece of work.inspect: in words, what evidence to read.probe: the machine-readable check,cooporhttp.success_messageandblocked_message: one line each, in Dad's language. A newline is refused ("Notifications must be one line").interval_seconds(default 30) andtimeout_seconds(default 3,600): integers of at least 5, and the deadline no shorter than the interval.
The source must be a saved turn in one of Dad's own Pippa conversations, the same rule the coop tool enforces. What comes back is the saved record with its wake_plan spelled out: pending means recheck quietly, because busy, idle or a queued receipt is not completion; success means post the success line once to Master Chief with no external alert; blocked means alert Dad at once and keep checking until the deadline; the deadline means stop and report that completion could not be verified, and never repeat the work.
Two Probes, Both Read-Only
The coop probe names work by work_kind and item_id and reads it through the same control facade from the last lesson, using only its read action, show. The verdicts are narrow on purpose:
- Only a
doneorcloseditem is success. - A newest report of
blocked,abandonedorreleasedis blocked, and so is a failed, cancelled or expired assignment while the work is not taken. An old expired invitation cannot override an accepted worker's report. - Everything else is pending: busy, idle, a queue receipt, even a stored disconnected status, which can predate the current turn.
Add a message_id and the probe instead waits for that one reply's applied or not_applied answer from Track 5; a failed or uncertain delivery of the reply, or a worker that is not connected, is a blocker.
The http probe GETs a credential-free JSON status URL: no user or password in the URL, no auth headers, no redirects, no proxy settings from the environment. A success predicate is required: dotted keys mapped to exact scalar values, where even the type must match. A blocked predicate is optional. A 401 or 403 is a blocker, because waiting will not fix authorization; any other failure is a transient pending. The stored evidence keeps only the fields the predicates named, never the whole response. The tool tells the caller to pin the expected build or version, because a healthy old process answering 200 is not a finished deploy.
Same Key, Same Watch
A follow-up's id is the SHA-256 of the conversation id and the key. Arm again with the same key and the same contract, and you get the existing watch back. Arm with the same key and a different contract, and the tool refuses: this key already names a different wake contract; inspect it before choosing a new key. The case this covers is mundane and common: a tool result gets lost, Dad asks again on the next turn, and Pippa arms again. One piece of work, one watch.
What Each Outcome Does
- Pending is silent. Every poll is still written to an observation log, but nobody is told.
- Success marks the alert as not required and posts
success_messageonce into Master Chief through the same durablemaster_chief.reportas lesson 2, keyed by the follow-up. No Telegram. Dad reads it the next time he talks to Pippa. - Blocked while the deadline is still ahead stores the first blocker once, with its own receipt, alerts Dad on Telegram, then posts to Master Chief. The watch stays pending: a later success still arrives as its own report.
- The deadline without success becomes blocked with the reason
deadline_without_completion_evidence. If the last probe saw no blocker of its own (a momentary outage, say), the most recent real blocker is kept with its time rather than erased. It is never success, and the original work is never retried. Dad reads the same armedblocked_messagehere as for a real blocker:_blocked_texthas no separate deadline line, and only borrows the last real blocker'sfor_dadlines when the final probe had none. So writeblocked_messageto be true in both cases ("failed, or not confirmed live in time"), never as a verdict that the work failed.
What Dad reads on a blocker is the armed blocked_message plus the worker's own for_dad lines, and nothing else. That was a fix too. Until 2026-10-10 the alert appended the worker's whole report body, the English history record, and a reason code, so one blocker (todo #373) reached Telegram and Master Chief as a wall of English. Commit 7a404b4e (todo #375) cut it to the lead line plus for_dad; the English reason stays in the observation for the status action. The same commit dropped an English "recovered" suffix from success: success_message is what Dad reads. (cwkPippa's follow-up doc still describes the older appended reason; the code is the truth.)
Reserve, Then Send
Lesson 2's notify door marks Telegram as sent after the send, accepting a rare duplicate. The follow-up deliverer does the opposite. Before it calls Telegram it saves alert_state='sending'; after, sent (or back to pending if the channel refused, to retry). If the process dies between those two saves, the next run finds sending and records uncertain instead of sending again, and the status action shows that uncertainty separately from the work's outcome. The reason it can afford this is the next step: the Master Chief report is still delivered, so even an uncertain alert leaves the line in Dad's conversation. The urgent Telegram goes first precisely so that a busy Chief, whose report call is bounded to two seconds, cannot delay it.
No New Cron
None of this adds a scheduler. At startup, cwkPippa registers one interval job, followup-checks, every five seconds, on HeartbeatService's existing APScheduler, and that scheduler only runs in the process holding the cross-process scheduler lease. Each tick lists due checks, takes a two-minute lease on each row, and starts its probe as a separate task, so the tick itself stays cheap. The records live in their own SQLite file beside the conversation database and survive restarts. If a cron snapshot restore clears the scheduler, a listener puts the job back. No launchd job, no model call per check. And cancel withdraws only the check, never the work it was watching.