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

Peripheral Vision: The Wake Judge

~18 min · proactive-agents, rate-limiting, evaluation, latency, human-in-the-loop

Level 0Closed Lens
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Memory Is Not Enough to Coach

In a test broadcast, Pippa coached Dad's play from the watch-along notes, and the notes ran about 50 seconds behind the screen. Advice about a fight that ended almost a minute ago is not coaching. Dad's conclusion: coaching works only if Pippa can wake on context and speak first, cutting in the way a friend on the couch does. The watch-along stays her memory. What she needed in addition was peripheral vision (곁눈 in Korean): something that glances at the screen every few seconds and says "now is worth a word" (backend/wake_judge.py).

On Air Starts the Recording

The judge can only look at a session that is recording, and the first rehearsal ran 28 minutes live with no session open, so the judge never looked. Dad's ruling: start it automatically, since there is no reason to broadcast without it, and as soon as the show moves to Prep so Pippa can check the picture while preparing. Every 3 seconds Oriel reads cwkPippa's On Air state; when it is on with no session, it starts one on the broadcast layout's decks, with their gate opened by that standing word, once per broadcast. A recording Dad stops stays stopped until On Air goes off and on again. Only an explicit off stops it, and an unreadable state changes nothing.

One bug in this code is a pattern worth knowing. The "already started" mark used to be set before the start, so one refusal at Prep (the recorder not up yet, a card missing, the disk below its floor) left the broadcast unrecorded while the state read "stopped". Now a start that never held is reported with its reason and tried again every 15 seconds. Mark something done after it is done.

What It Reads, and Who Judges

Every 3 seconds while coaching is on, if the game deck is recording, the eyes gate covers it, and its latest frame is fresh, the judge reads the frame now and the one from its previous look, cwkPippa's room state (On Air's stage, whether a turn is running, Dad's recent words as the phone heard them, when Pippa last spoke, whether a phone is listening on air), and its own last few notes. Nothing is read off air, before Live, or with no phone listening, because nobody would hear her.

The judge is one stateless call through cwkPippa on the ledger slot oriel/wake_judge: no tools, no vault, no conversation, and no brain named in Oriel (a test holds it). A bench before shipping ran the judge's own prompt over three made-up frames (a death screen, a boss health bar, plain walking) on seven brains. All seven woke on the first two and waited on the third; the fastest answered in under a second.

The prompt is written for a human viewer, not a game. It names moments (a boss appears, a death, stuck or wandering across looks, a swear, a quiet stop at a rest point, a question into the air) and non-moments (fine fighting, cutscenes, Dad talking to someone, Pippa having just spoken). There is no per-game rule, no HUD reader and no OCR.

When a Yes Becomes a Wake

A judge's yes is a vote, not a wake. It goes up to Pippa only if:

  • no other wake is still with her;
  • she is not answering Dad (his words come first);
  • the cooldown has passed since her last wake or her last line, with a boss or a death allowed at half the cooldown, and chattiness scaling it (quiet doubles it, chatty halves it).

cwkPippa's wake door refuses anything but a live broadcast and a free room. Pippa then decides what to say, or answers [quiet]. The phone on Dad's side reads the answer into his earbuds and reports its first word, so the delay from frame to ear is measured.

Measured, Then Not Tuned Toward Zero

One broadcast of about two and a half hours, measured end to end:

StageCount
looks (one per 3.1 s, judge call 0.87 s mean)2,929
wake votes132 (60 held by cooldown, 1 by Pippa answering Dad)
wakes sent71
heard in Dad's ear48 (7 answered quiet, 14 skipped while Dad spoke or she read, 2 ended before a sound)

From a frame on screen to her first word: 11.0 seconds at the median, 15.2 at the 95th percentile (judge 1.5, her turn 7.0, phone 1.7). A long stop at a checkpoint was read as "stuck" again and again, and the cooldown and her [quiet] absorbed most of it.

The tempting next step is to count every false and missed wake and tune them toward zero. Dad ruled against it: people miss things and get things wrong too. Counting exactly would mean re-watching the whole show, and any fix would just move the errors somewhere else. Five or six misheard or "did you call me?" turns in a two-hour show are accepted as natural. The judge changes when Dad names a pattern, not to chase a count. The record still keeps every look, verdict and outcome, and Dad can mark a wake wrong or a moment missed in his own words, so the patterns have evidence when he names one.

Code

A yes from the judge becomes a wake only past every hold (trimmed excerpt, backend/wake_judge.py)·python
BIG = frozenset({"boss", "death"})
COOLDOWN_FACTOR = {"quiet": 2.0, "normal": 1.0, "chatty": 0.5}


class WakeJudge:
    def decide(self, verdict: dict[str, Any], room: dict[str, Any], at: float, chattiness: str) -> tuple[str, str]:
        """``sent`` when a wake goes up, else why not (``wait`` is the judge's own)."""
        if not verdict["wake"]:
            return "wait", verdict["reason"]
        if self._inflight is not None:
            return "held", "a wake is still with Pippa"
        if room.get("busy"):
            return "held", "Pippa is answering Dad (his words come first)"
        cooldown = float(self.engine.prefs.get("wake_cooldown_seconds")) * COOLDOWN_FACTOR.get(chattiness, 1.0)
        if verdict["moment"] in BIG:
            cooldown /= 2
        since = [at - self._last_wake[0]] if self._last_wake else []
        pippa_ago = room.get("pippa_ago")
        if pippa_ago is not None:
            since.append(float(pippa_ago))
        if since and min(since) < cooldown:
            return "cooldown", f"Pippa spoke or was woken {int(min(since))} s ago (cooldown {int(cooldown)} s)"
        return "sent", ""
Mark a start done only after it held, and retry a refused one (runnable sketch)·python
class AutoRecord:
    RETRY_S = 15

    def __init__(self, start):
        self.start = start
        self.started_for = None          # the broadcast this recording belongs to
        self.next_try = 0.0

    def tick(self, broadcast_id, now):
        if broadcast_id is None or self.started_for == broadcast_id or now < self.next_try:
            return None
        ok, why = self.start()
        if ok:
            self.started_for = broadcast_id   # marked only after it held
            return "started"
        self.next_try = now + self.RETRY_S
        return f"not started: {why} (again in {self.RETRY_S} s)"


answers = iter([(False, "the recorder is not up yet"), (True, "")])
auto = AutoRecord(lambda: next(answers))
for now in (0, 3, 15, 18):
    print(now, auto.tick("show-1", now))

External links

Exercise

Write a decide(verdict, room, now) for a proactive assistant of your own (a study buddy that comments while you read, or a dashboard bot that speaks up when a metric moves). List its moments and non-moments, its holds, and a cooldown with a factor for big moments. Then simulate an hour of votes, one every 3 seconds, with a big moment every ten minutes and a long "stuck" stretch in the middle. How many wakes does it send, and how many votes do the cooldown and holds absorb?
Hint
Track the last wake and the last time the assistant spoke as separate clocks, and take the smaller elapsed time. A long stuck stretch should produce one wake per cooldown, not one per vote.

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.