Skip to content
C.W.K.
Lesson 07 of 07 · published

What Waits for Dad

~14 min · notifications, approvals, telegram, object-oriented

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

The Opposite of Noise

Every lesson in this track so far took something away from Dad's attention: a job that is not a turn, a report that waits its turn, a mirror that copies only its own, a line about one item that rings once. Quiet has a failure of its own, though. A line that asks Dad to decide something rings once, sinks under the next twenty, and the decision waits for a reminder nobody sends. On 2026-10-26 a reply Pippa had drafted in Herald, the engine behind her voice on YouTube, had waited 38.7 hours for his approve. Its one Telegram line came when she wrote it, was buried under Coop notices, and nothing ever said it again. Dad, in Granada:

"스캔할 때 승인 안 된 답글 있으면 계속 알려줘야 해, 치프도 알아야 하고. 안 그럼 까먹고 몇날 며칠 갈 수도 있어."
"When a scan finds a reply that hasn't been approved, you have to keep telling me, and the Chief has to know too. Otherwise I'll forget and it can go on for days."
— 아빠 (Dad), 2026-10-26, translated from Korean

So the answer is the inverse of the last lesson's settle window: one line that does not go away until the decision is made.

One Root, Four Gates

services/waiting_for_dad.py asks one question of every place where work waits for Dad's decision, and the question is a class. WaitingSource is an abstract root with one method, waiting(), which returns every item that waits as a WaitingItem: its source, an id, a one-line label, a link, the time it started waiting, and whether Dad himself is holding it. Four children answer it today:

  • HeraldDrafts: Herald drafts waiting for his approve, read from Herald's own summary. A draft he held is reported as held.
  • JournalEntries: Pippa Journal entries waiting for his approve, approved but not yet published, or whose publish failed; held entries likewise.
  • SiteEscalations: cwk-site comments and Soul Stream posts that Maia's review handed to him and that neither he nor Maia has decided.
  • CoopParked: Coop obligations the chief parked for Dad, read from Coop's own parked flag and never worked out again here.

A new gate is one more child and one more row in SOURCES; nothing that reads the list changes. The module's docstring names the rule it follows, the family's Fifth Rule (non-object-oriented architecture is a sin): one parent that says what every gate must answer, children that each know how their own gate is read, and consumers that know only the parent.

One Living Message

refresh() reads every source at once and tries to keep one reminder on Telegram. While anything waits, it sends a fresh message listing everything, oldest first, each item with its age and link, and then deletes the previous reminder, so the message on Dad's phone is normally the latest and does not pile up. Two limits remain in the code. The Telegram helper cuts every message at 4,000 characters and there is no second page, so on a very long list the header still counts every item while the last items, and the unread note, fall off the end. And the delete's answer is not checked: if Telegram refuses it, the old reminder stays on the phone while the state already points at the new one, and nothing tries that delete again. What happens depends on what changed since the last one:

What changedWhat Dad's phone gets
Something new started waitingA new reminder at once, with sound
Nothing new, and the last reminder is 55 minutes old (REPEAT_S, a little under the hourly Herald scan)A new reminder, with sound
Something only left the list, and the last reminder is under 55 minutes oldA replacement without sound; the hourly clock stays where it was (past 55 minutes the same change counts as a repeat: with sound, and the clock restarts)
Nothing waits any moreThe last reminder is deleted and nothing is sent
Any of these between 23:00 and 08:00 where Dad isSent without sound

"Where Dad is" means the zone his device last reported (dad_location.dad_clock_zone), with Seoul as the fallback; that week he was in Spain, and a Seoul night is a Granada afternoon. If a send fails, the old reminder stays and the next tick tries again. Items Dad holds himself are his own decision to wait, so they stay out of the reminder. The reminder is in Korean, because Dad reads it; the second code block below shows one.

An Unreadable Gate Is Not an Empty One

Three sibling engines and a hosted database can each fail on their own. When a source cannot be read in time, read_sources keeps that source's last known items and names it in the reminder as unread. It never guesses empty. An empty answer from a broken gate would delete the reminder, and the one decision that should stay in sight would vanish exactly when something else had gone wrong. Lesson 1 of this track made the same choice for approval snapshots: an unavailable snapshot is unknown, never an empty queue.

The Chief Knows, in One Line

Dad asked for the Chief to know as well, and the sanctuary rule (Track 2, lesson 6, "The Chief Is a Sanctuary") decided how. A stored reminder row would sit in the replay, repeat every hour, and cost every later turn. So nothing is stored. chief_line() builds one line from the last refresh, and master_chief.turn_context_block adds it to the turn's own context, after the origin line and the reasoning note and before the turn's notes, only when something waits. The line counts items by source, gives the oldest age, the ids and anything Dad holds, says how long ago it was checked and which gates were unread, and stays within 400 characters (CHIEF_LINE_BUDGET), dropping the ids first when it must. It reads the state file the last refresh wrote, so nothing on the turn's path touches the network, and the cached prefix does not move, because the turn context rides with the current message only (Track 3, lesson 7, "One Prefix From Every Doorway").

When Dad Answers

A reminder that outlives the decision is noise again, so an item has to leave the list where Dad's answer lands. For Herald, the Journal and the site, his decision in their own app is the answer, and the next read no longer finds the item. For Coop the path is longer. Dad answers the Chief, the Chief relays his words to the work with a coop reply, and until 2026-10-26 a reply settled nothing about the park: the reminder kept listing #662 and #666 every hour after Dad had answered both. The fix is Coop's (cwkCoop ae26639). A reply on the work from anyone but its bound worker unparks the pending parked obligation in the same transaction, keeps its review time and records the disposition as answered; a later park is Dad's again, and at start the engine unparks any park a reply has already answered. Pippa's side (b4703081) only wrote the rule down and tested it: CoopParked keeps reading Coop's flag and never works it out for itself, so the reminder, the Chief's line and Coop's own context agree.

No Model Anywhere

Nothing here calls a model. The re-check rides the sources' own scheduled ticks: at the end of every cron run the heartbeat calls after_tick, which starts a background refresh when the run was one of four (SOURCE_TICKS: Herald's comment scan, the Journal's rethink gate and Maia's two reviews) and never raises into the cron. The heartbeat's crons run only on the Pippa machine (Track 2, lesson 7, "The Room Is a Role, Not a Mac"), so there is one reminder, not one per Mac. PIPPA_WAITING_FOR_DAD=0 closes every seam, and under the test runner they stay shut unless a test opens them.

Quiet is for what is finished. A decision still owed stays in sight. Silence success and repeats, but give every open decision one living line that only the decision itself can remove.

Code

Verbatim excerpt, services/waiting_for_dad.py, as of 2026-10-27: refresh(), one living reminder·python
async def refresh(*, now: datetime | None = None) -> dict[str, Any]:
    """Read every source, keep the one reminder current, and remember what
    waits for the Chief's line. Returns the new state."""
    from services import telegram_notify

    async with _lock:
        now = now or datetime.now(timezone.utc)
        previous = dict(_load())
        items, gaps = await read_sources(previous)
        active = [item for item in items if not item.held]
        ids = sorted(f"{item.source}:{item.id}" for item in active)
        state = {**previous, "checked_at": _iso(now), "items": [asdict(item) for item in items], "gaps": gaps}
        message_id = previous.get("message_id")
        if not active:
            if message_id:
                await telegram_notify.delete_telegram_message(message_id)
            state.update(message_id=None, sent_at=None, sent_ids=[])
            _save(state)
            return state
        sent_ids = set(previous.get("sent_ids") or [])
        sent_at = previous.get("sent_at")
        fresh = bool(set(ids) - sent_ids)
        repeat = not message_id or not sent_at or (now - _parse(sent_at)).total_seconds() >= REPEAT_S
        if not (fresh or repeat or set(ids) != sent_ids):
            _save(state)
            return state
        # Something only left the list: replace it quietly. New or repeated:
        # with sound, except at night where Dad is.
        silent = is_dad_night(now) or not (fresh or repeat)
        new_id = await telegram_notify.send_telegram_message_id(render(items, gaps, now=now), silent=silent)
        if new_id is None:
            # Not sent: keep the old reminder and try again next tick.
            _save(state)
            return state
        if message_id:
            await telegram_notify.delete_telegram_message(message_id)
        # A removal-only replacement keeps the hourly clock where it was.
        state.update(message_id=new_id, sent_ids=ids, sent_at=_iso(now) if (fresh or repeat) else sent_at, silent=silent)
        _save(state)
        return state
What Dad's phone and the Chief's turn read, for three invented items (the engine's own render() and chief_line(), run on 2026-10-27)·text
Three invented items: a Herald draft waiting 30 hours, a Coop park 2 hours old,
and a Journal entry Dad is holding. cwk-site could not be read this time.

Telegram, from render():

⏳ 아빠 결정 기다리는 것 2개 · 가장 오래된 건 30시간째

1. [Herald] Reply to a viewer under the Ember video · 30시간
https://example.invalid/herald/d-41

2. [Coop] todo #123: Choose the cover for the trip journal · 2시간
https://example.invalid/coop

(이번엔 못 읽음: cwk-site · 마지막으로 읽은 목록 그대로)

The Chief's turn context, from chief_line():

Waiting for Dad's decision (oldest 30.0h): herald 1 [d-41]; coop 1 [todo #123]. Held by Dad: journal 1 [j-2026-10-25]. Checked 19m ago; unread: site.

Exercise

List every place where work in your own system waits for one person's yes: pull request reviews, expense approvals, a draft in a content system, a deploy gate. For each, write down how you would learn today that something has waited three days. Then build one reminder: a single message that lists everything waiting, oldest first, with age and link, that replaces itself instead of stacking, goes quiet at night in the reader's own time zone, and disappears when nothing waits.
Hint
Make each queue a class with one method that answers 'what waits now', and let it raise when the queue cannot be read. Keep the last known answer for a queue that raised and say so in the message: an outage that empties the list would hide exactly the item you built this for.

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.