Three Senders, One Busy Room
Three kinds of report reach Master Chief, and they all go through one function, master_chief.report(). A cron outcome arrives from the scheduler when a job needs Dad (the previous lesson). A follow-up arrives when a check Pippa armed before ending her turn sees delegated work succeed or block (Track 5). A Coop notice arrives through Coop's notify door when delegated work reports back in plain words. All three have to land in a room that is often busy: a long tool turn can hold the Chief's turn lock for minutes, and a report must never cut into a turn halfway.
The First Version Waited
The obvious design is to treat a report like a tiny turn: take the lock, append the row, release. The first version did exactly that, inside the caller, with no bound on the wait. The 2026-10-08 cold review (finding #6) confirmed by probe that the wait had no bound, and traced three separate failures that one line made possible. It graded the chain plausible, not yet observed: the Coop door was under a day old, so nobody had seen it happen.
- Coop's notify door could ring again and again. Coop's client gives up after 15 seconds and retries. Each retry would add another waiter on the lock, and once the Chief was free, each waiter would go on to send its own Telegram message.
- A cron job could lose its next fire. A job waiting for a long Chief turn would stay "running", and the scheduler, which runs one instance of a job at a time, would skip the next scheduled run.
- A restart could lose the report. The text existed only in a coroutine's memory, waiting. Nothing had written it down.
Every one of those would look fine in a test where the room was idle. That is why it took a separate reviewer, reading the code cold, to see it.
Durable Before Anything Waits
The fix (5536821e) changes the order of two things. report() first refuses a source conversation that is not Dad's live Pippa row, then resolves the Chief as its owner (Lesson 4 explains why), then builds an idempotency key, chief-report: plus the caller's source id. If a message with that key already sits in the Chief, it returns that message at once. Otherwise it writes the report into chief_report_outbox with INSERT OR IGNORE, keyed by that same source_key, and commits. Only then does anything wait. A retry that arrives a second later finds either the landed row or the waiting outbox row, and adds nothing.
One Drainer per Chief
Callers no longer append anything themselves. Each call starts the Chief's single drainer task if none is running, or marks it dirty if one is. The drainer takes the Chief's ordinary FIFO turn lock, so it queues behind Dad's turns exactly like a turn would, and when its turn comes it appends every pending outbox row in order: an assistant row whose parent is the newest message, with origin_surface: scheduled-task in its host context, the no-tools sentinel [] so no later read goes searching the log for tools a report never ran, and a turn_kind taken from the source key. It writes the matching JSONL line, marks the outbox row delivered, and before exiting checks the dirty mark, with no await between that check and its return, so a report that arrived mid-drain is never stranded. Stop withdraws Dad's waiting turns but never a report. At startup the main worker calls resume_reports(), which starts a drainer for every Chief with undelivered rows.
Callers Bound Their Wait
| Caller | Waits at most | Then |
|---|---|---|
| A cron outcome | 5 s | The job finishes on time; the line lands when the Chief is free. |
| Coop's notify door | 2 s | Sends a Telegram line unless coop_notify_sent already holds the Coop outbox row; a per-row lock keeps concurrent retries to one send. The row is recorded only after the send, so a crash between the two can ring twice. A refused Telegram answers 503 and Coop tries later. |
| A follow-up | 2 s | A blocker has already gone to Telegram first, before the possibly busy lock; the Chief copy lands later. |
The wait is wrapped in asyncio.shield, so a caller giving up never cancels the drainer it was waiting on. Giving up is safe because the row was durable before the wait began.
No Reset, No Interruption
One thing a report does not do: touch the turn in progress or the brain's session. Early on, each report cleared the Chief's stored session id so a stateful provider could not skip it with a resume shortcut, which forced a full replay on every report. Dad's ruling on coop #345 asked for that to stop, and d0eaaf16 removed it: every brain already builds each turn by replaying the stored conversation (live rows plus the summary that replaced the oldest), so the next turn includes the report by construction. If you read an older doc line saying reports invalidate the resume, the code moved past it.