Skip to content
C.W.K.
Stream
Lesson 03 of 05 · published

Arm a Check Before Returning

~15 min · followups, scheduling, verification, idempotency

Level 0Visitor
0 XP0/33 lessons0/11 achievements
0/100 XP to next level100 XP to go0% complete

The Turn Ends Before the Work Does

The Chief delegates a todo, or starts an upload, or asks for a service restart. The result will exist in ten minutes or an hour. Her turn ends now. That leaves two bad options, and Master Chief makes both of them worse than usual.

Poll in the foreground. Keep the turn open and check a status every so often. In an ordinary chat that wastes tokens. In Master Chief it holds the room: one turn at a time, in order, so every doorway Dad might use waits behind a model watching a status field. This is the desk-leaving from the last lessons in a new costume.

Say “I'll let you know.” A promise with nobody behind it. When the turn ends, the model has no clock, no memory of the promise and no way to wake itself up. The sentence is true only if something durable was written before it was said.

The answer is the followup family tool, born on 2026-10-07 (commit c2dee4c4) next to the coop tool and offered to every vessel the same way. Before a turn that started deferred work returns, it arms a durable, read-only check.

The Rule Is Written Twice

The instruction to arm reaches the model from two places. The tool's own description says: Arm BEFORE ending the turn, state the returned wake_plan, and keep the work's stable key on retries. And the shared environment header that every Pippa system prompt carries has a block titled Deferred work — arm, then return, which ends with the line that keeps the promise honest: If arming fails, say the check is not armed and resolve that blocker; never promise a later notification without a durable receipt. Two placements, on purpose: the description travels with the tool, and the header travels with every turn, including the ones where the tool is far from the model's mind.

What Gets Armed

An arm call takes exactly seven fields, and arm() refuses anything else:

  • key, a stable name for the operation. The check's id is the SHA-256 of the conversation id and this key, so arming twice with the same key and the same contract returns the same watch. The same key with a different contract is refused: This key already names a different wake contract; inspect it before choosing a new key.
  • inspect, in words: what evidence to read.
  • probe, the machine check, coop or http.
  • success_message and blocked_message, one line each, in Dad's language. A newline is refused: Notifications must be one line.
  • interval_seconds (default 30) and timeout_seconds (default 3,600), integers of at least 5, the deadline no shorter than the interval.

The source must be a saved turn of one of Dad's own Pippa conversations: the tool reuses the coop tool's source_context word for word, so Sidekicks, Council and scheduled turns cannot arm anything. What comes back is the saved record with its wake_plan spelled out, which the model is told to state to Dad. It is four sentences, verbatim from the code:

  • pending: Recheck quietly; busy/idle or a queued receipt is not completion.
  • succeeded: Post success_message once to Chief for Dad's next turn; no external alert.
  • blocked: Alert Dad immediately with blocked_message and the worker's for_dad lines, record it in Chief, and keep checking until the deadline.
  • deadline: Stop checking and report that completion could not be verified; never repeat the work.

Two Probes, Both Read-Only

A coop probe names work by work_kind and item_id and reads it through Coop's show. Only a done or closed item is success. A blocked, abandoned or released report, or a failed, cancelled or expired assignment while nobody holds the work, is blocked. Busy, idle, a queue receipt or a stale disconnected status is pending. Adding message_id waits instead for that one reply's applied or not_applied answer.

An http probe GETs a credential-free JSON status URL: no user or password in it, no redirects followed, no proxy from the environment. A success predicate is required, dotted JSON keys mapped to exact scalar values, and even the type must match; a blocked predicate is optional. A 401 or 403 is a blocker, since waiting will not fix authorization; any other failure is a transient pending. For a restart or a deploy, the predicate pins the identity you expect, a build hash or a version, because a healthy old process is not success. The sketch below was checked against the real validator: an engine still answering ready on the old build does not match.

Arming Is Not Doing

The check never launches, repeats, approves or cancels the underlying work. cancel withdraws the check, never the work. And arming is not a claim that anything completed: the tool returns a contract, not a result. Short waits can be read again with status in the same turn. Long waits belong to the scheduler, and nothing new is scheduled for them. At startup Pippa registers one interval job, followup-checks, every 5 seconds, on the heartbeat service's existing APScheduler, which runs only in the process holding the cross-process scheduler lease. Each tick lists the checks that are due, leases each row for two minutes, starts its probe as a separate task and returns. The records live in their own SQLite file beside the conversation database and survive restarts. No cron entry, no launchd job, no model call per check. The Coop Quest's Track 7 ends with this same tool seen from Coop's side, probe verdict by probe verdict.

“I'll tell you when it's done” is true only if something durable was written before it was said. The armed record is that something. Only a probe that saw a done item or the pinned build can turn it into news.

Code

The wake contract and its guards (services/followups.py, verbatim excerpt)·python
def arm(spec: dict, source: dict) -> dict:
    if set(spec) - {"key", "inspect", "probe", "success_message", "blocked_message", "interval_seconds", "timeout_seconds"}:
        raise ValueError("Unsupported follow-up fields")
    if not source.get("conversation_id") or not source.get("message_id") or source.get("surface") != "pippa":
        raise ValueError("A canonical Dad conversation turn is required")
    for key in ("key", "inspect", "success_message", "blocked_message"):
        if not isinstance(spec.get(key), str) or not spec[key].strip():
            raise ValueError(f"{key} is required")
    if any("\n" in spec[key] for key in ("success_message", "blocked_message")):
        raise ValueError("Notifications must be one line")
    followup_probes.validate(spec.get("probe"))
    spec = {"interval_seconds": 30, "timeout_seconds": 3600, **spec}
    for key in ("interval_seconds", "timeout_seconds"):
        if type(spec[key]) is not int or spec[key] < POLL_SECONDS:
            raise ValueError(f"{key} must be an integer of at least {POLL_SECONDS}")
    if spec["timeout_seconds"] < spec["interval_seconds"]:
        raise ValueError("timeout_seconds must be at least interval_seconds")
    # Stable within the originating conversation even if a tool result was lost
    # and Dad asks again on the next turn. A new operation needs a new key.
    wid = hashlib.sha256(_json([source["conversation_id"], spec["key"]]).encode()).hexdigest()
    ...  # BEGIN IMMEDIATE; same id with a different spec is refused; else INSERT as 'pending'
    return get(wid, source["conversation_id"])
A minimal sketch: one arm call for a restart check (made-up host and hash; validated against the real probe validator)·json
{
  "action": "arm",
  "key": "restart:render-engine:7f3c2e1",
  "inspect": "GET the engine's status JSON; done when it serves build 7f3c2e1 and reports ready",
  "probe": {
    "kind": "http",
    "url": "https://engine.example.test/api/status",
    "success": {"build.sha": "7f3c2e1", "state": "ready"},
    "blocked": {"state": "crashloop"}
  },
  "success_message": "엔진 재시작 끝났어. 새 빌드 7f3c2e1로 떠 있어.",
  "blocked_message": "엔진 재시작 확인이 막혔어. 새 빌드 7f3c2e1이 아직 안 보여.",
  "interval_seconds": 15,
  "timeout_seconds": 900
}

External links

Exercise

Pick something you deploy or restart by hand: a web app, a home server, a bot. Write its wake contract before you next touch it: a stable key, an http status URL that needs no credentials, a success predicate that only the new version can satisfy, an optional blocked predicate, an interval and a deadline. Then write a twenty-line script that polls that URL until success, blocked or the deadline, prints one line for each outcome, and never re-runs the deploy itself.
Hint
If your app has no status endpoint that reports its own version or commit, that is the first finding: add one. Compare the value with what you just shipped, not with whatever was there before, and make the deadline outcome print that completion could not be verified rather than retrying.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.