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

The Worker and Its Report

~13 min · delegation, reports, outbox, idempotency

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

What the Chief Holds When Her Turn Ends

The last lesson ended with a run call returning an assignment in state prepared. Then the Chief's turn ends, and she is back at the desk for whatever Dad says next. She does not sit in the worker's chat. She does not poll it. Everything that happens between that assignment row and the line Dad finally reads happens somewhere else, and this lesson walks the road from her side only. Coop's side has its own quest, and it is deep; you will be pointed there at each turn.

The Worker Accepts Before It Knows

Coop launches the session (a new Claude Code background session on office, by default), discovers it, enrolls it, and sends it an invitation. The invitation carries a dispatch id and a nonce, and no work binding. Usually it carries no task text either. The exception is a todo that already has replies: its invitation shows the body and every reply, so the session accepts knowing exactly which replies it acknowledges, and it still says to use no tools and do no work yet. The session answers COOP_ACCEPT with that nonce, and only then, inside one transaction, does Coop take the item, bind it to that exact session, write the debt that says an outcome is owed, and queue the work envelope. A launch, a running process or a queue receipt never claims work on its own. Why the work is withheld until the yes, and why the yes and the take must commit together, is the Coop Quest's Track 3, A Launch Never Claims.

The Worker's Last Words

The worker does its job in its own session and its own project folder. Its final answer then has a fixed shape that every Coop envelope repeats, because a rule stated once is a rule a worker has lost three turns later:

  1. Two to four plain lines in Korean for Dad: what was done, where it landed (a pushed commit, a TestFlight build, a service), and what he must do now, if anything. No headings, no bullets, no English paragraph.
  2. A blank line.
  3. The report for Coop on one line, written as a Markdown link reference definition, [coop-report]: <{"coop_reports":[...]}>, which a Markdown renderer parses and then draws as nothing.

Inside that hidden line each report carries a binding_id, one of four outcome words (done, released, abandoned, blocked), a body that is the English record, evidence, and for_dad: the same Korean lines again. So one final serves two readers. Dad sees his lines; the engine finds its JSON. The Coop Quest's Track 5, Reporting Back, explains why a link definition, and the one angle bracket that once broke it open.

From Outcome to One Line

For done, released or abandoned, Coop records the report, resolves the binding and the debt in one transaction, and queues an outcome notification that carries the worker's for_dad lines (or, if the worker left them out, the first 600 characters of its English body). (A blocked report keeps the binding open and queues an attention line instead; the work stays with the same worker.) Its notifier composes one message, a head such as [Coop] ✅ done: todo #N (scope) title followed by those lines, and hands it to Pippa through a narrow engine door, POST /api/coop/engine/notify, with the id of its own outbox row.

On Pippa's side the order is fixed:

  1. Master Chief first, durably. The door calls master_chief.report with the source coop-notify: plus that outbox id. The report is inserted into chief_report_outbox, keyed chief-report: plus the source, before anything waits. The door then waits at most two seconds.
  2. The drainer lands it. One drainer per Chief takes the Chief's turn lock, so a report never interleaves with a live turn, and appends each pending row in order as an assistant message: no tools, origin scheduled-task, the source key as its request id, and the same line written to the Chief's JSONL. Then it marks the outbox row delivered. At startup, resume_reports drains whatever a restart left.
  3. Telegram next, landed or not. Under a per-row lock, the door checks coop_notify_sent. Already sent: it rings nothing. Otherwise it sends and records the send. It does not wait for the Chief line to land: if the Chief is still busy after those two seconds, the line is only queued, Telegram rings first, and the line lands when the Chief is free. And the record comes after the send, so a crash between the two can ring a second time (Lesson 4 compares that choice with the follow-ups' one).

It took a review to get this order. Until the 2026-10-08 cold review (finding #6), a report waited on the Chief's lock inside its caller, with no bound, and a probe confirmed that the wait never ended while a turn ran. The review then traced where that could lead: a Chief turn longer than Coop's 15-second timeout would hold the request, every retry would add a waiter, and when the Chief came free each waiter could ring Dad's phone again. It graded that chain plausible, not yet observed, because the Coop door was under a day old. Commit 5536821e put the durable write first and the wait second. On 2026-10-12, measured, the outbox held 341 reports of every kind, and none undelivered.

A Report Is Not a Turn

Landing a report starts no model call. The Chief does not wake up and answer it. Every brain builds its next turn by replaying the stored conversation (live rows plus the summary that replaced the oldest), so the line is simply there the next time Dad speaks, from any doorway, and the Chief reads it as part of the conversation. Since 2026-10-12 the row is also born a system line (turn_kind = coop): the WebUI and the phone draw it apart from Pippa's replies, voice mode never reads it aloud, and Telegram's sync never mirrors it, because the notify door sends Telegram its own copy.

What Dad Reads

Dad reads the worker's Korean lines under a short head. The English body reaches him only as a fallback: when a worker leaves for_dad out, the notifier sends the first 600 characters of the body instead, which is one more reason every envelope repeats the final's shape. He never reads the report JSON; the notifier strips it, whatever a worker pasted. The full record stays where it belongs: in Coop's ledger as the record, for any Pippa who needs to look. The Coop Quest's Track 7, Reaching Dad, takes the notify door apart guard by guard; what matters from the Chief's chair is that delegated work reports back into the room where Dad actually is.

Do the keyed, durable write first and the unkeyed send after it. The Chief line has a natural key and lands at most once however often it is asked for. Telegram has no key at all. Putting the line that has a memory ahead of the send that has none means no retry can ever double the part Dad keeps.

Code

The round trip from Master Chief's side (summary of the path; names are the real ones)·text
Master Chief turn      coop run  ->  assignment 'prepared'          nothing claimed yet
  (turn ends; the Chief is back at the desk)
Coop                   launch, discover, enroll the worker session
                       invitation: dispatch id + nonce, no work released
                       (a todo with replies also shows its body + replies to review)
worker                 COOP_ACCEPT <nonce>
Coop, one transaction  take + binding + outcome obligation + work envelope
worker                 does the work in its own session and project folder
worker final           2-4 Korean lines for Dad
                       (blank line)
                       [coop-report]: <{"coop_reports":[{... "for_dad": ...}]}>   draws as nothing
Coop                   report row, binding resolved, outcome notification queued
Coop notifier          "[Coop] ✅ done: todo #N (scope) title" + the for_dad lines
                       (no for_dad: the first 600 characters of the English body)
Pippa notify door      report(source 'coop-notify:<outbox id>', wait <= 2 s)
                         -> chief_report_outbox row       durable before any wait
                         -> drainer, under the Chief's turn lock:
                            assistant row, turn_kind 'coop', no tools, JSONL line
                       Telegram send after the wait, landed or not;
                       skipped once coop_notify_sent holds the outbox row
Dad                    reads the Korean lines; the next Chief turn replays them
The drainer appends a report without starting a turn (services/master_chief.py, trimmed excerpt)·python
async with turn(chief_id, store=store):          # never interleaves with a live turn
    ...  # pending rows: WHERE chief_id=? AND message_id IS NULL ORDER BY created_at, rowid
    for source_key, text, source_conversation_id in pending:
        mid = await _landed(store, chief_id, source_key)
        if mid is None:
            context = {"host_kind": "chief", "origin_surface": "scheduled-task",
                       "origin_conversation_id": source_conversation_id}
            parent_id = await store.get_last_message_id(chief_id)
            # A system line, never Pippa's own turn: every surface
            # draws it apart and voice never reads it (todo #406).
            kind = report_kind(source_key)
            mid = await store.add_message(chief_id, "assistant", text, parent_id=parent_id,
                                          external_request_id=source_key, host_context=json.dumps(context),
                                          tool_uses="[]", turn_kind=kind)
            ...  # the same line is appended to the Chief's JSONL
        ...  # UPDATE chief_report_outbox SET message_id=?, delivered_at=datetime('now')
# Every brain builds the next turn from stored history. These appends
# neither reset session provenance nor touch a live SDK session.

External links

Exercise

Take one background job of your own that tells you when it is done: a CI run, a backup, a long script that posts to a chat. Draw its path from the moment it finishes to the moment you read the result, one hop per line. For each hop, write down whether the message is durable there (would survive a restart) and what key would stop a retry from delivering it twice. Mark every hop where the answer to either is nothing.
Hint
The usual gap is a send that happens inside the job itself, with no record of whether it already happened. Moving that send behind a durable row with a natural key (the job's run id, for example) usually fixes both columns at once.

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.