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

Dad's Correction Outranks the Model

~14 min · corrections, human-in-the-loop, re-read, authority

Level 0Mise en Place
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Three moments for one correction

Sizzle's invariant 3 says Dad's correction outranks the model, permanently: every re-read keeps every correction, and no model output silently reverts him. In code, that promise acts at three different moments. Right now, before any re-read, his correction shows over the reading. During the next reading, it rides in as a fact. And if it arrives while a reading is already running, that reading is not allowed to be the last word.

Writing it down

A correction enters through correct in the domain service. It checks that the plate exists, defaults the target to note, refuses an empty value, and records correction.added. Then it calls request_reading, which records reading.requested and wakes the worker. The correction lands in an append-only corrections table. There is no event that edits or removes one.

Both clients' correction boxes send target: "note", a free-text fact about the plate. The web's placeholder tells Dad what it does: Correct Pippa — it outranks her reading and rides into every re-read. Structured targets (summary, or a dish's name_ko, name_original or cuisine by its index) arrive through Pippa's correct action in a plate's thread, and only when Dad presses the button that applies it. Track 6 covers that path.

Now: the overlay

The stored reading body is never changed. What Dad sees is computed on every request by reading_view. It deep-copies the current reading, then walks the corrections. Any correction older than the reading is skipped, since usually that reading was told about it. The one exception, a correction that arrived while the reading was running, is why the mid-read guard below exists. For a newer correction, a summary target replaces the summary, and a target matching dish:<n>:<field> for a dish that exists replaces that field and adds the field name to the dish's corrected list. The plate page then shows "corrected by you" next to the dish.

A note correction has no field to overlay. It appears where every correction appears, in the plate's "Your word" section. Both clients list every correction there, whether or not a re-read has happened.

Next: the re-read is told

Every reading's input is built by input_text, and it includes dads_corrections: every correction on that plate, each with its target, value and note. It sends all of them on every reading, not just the newest. The rubric then says it plainly to Pippa: Dad's corrections in the input are authoritative facts, and her reading must agree with them.

The engine test checks the chain end to end. It reads a plate, posts a correction, waits for the re-read to finish, and asserts two things: the correction is listed on the plate, and its text is inside the input of the most recent model call.

Meanwhile: a correction during a read

A reading takes time, and Dad does not wait for it. So read counts the plate's corrections before it starts and compares afterwards:

  • If the call to cwkPippa, the parser or a photo fails and a correction arrived in the meantime, it does not record reading.failed. The plate stays pending, and the worker reads it again with his word instead of showing a failure.
  • If the read succeeds and a correction arrived, the reading is recorded and integrated, then a new reading.requested follows at once, because this reading predates his word.

Either way, the plate never settles on a reading made without his latest correction. It can settle on a failure, though. The branch for a missing brain assignment records reading.failed without comparing the count, so when the reading slot cannot be resolved after his correction arrived, the plate shows that failure and nothing reads it again on its own. His correction is still stored and shown, and the next read Dad asks for carries it.

Principle: A human correction should be stored apart from the model's output, shown at once, and fed into every later derivation. Then no derivation can quietly erase it.

Where the guarantee ends

I want to be exact about this part. Once the re-read lands, it is newer than the correction, so reading_view skips the overlay. The plate now shows what Pippa wrote after being told. Nothing in the code compares her new reading against his correction. If she disagreed anyway, the code would not catch it.

What the code does guarantee is narrower and still strong. His word is never lost, because the table is append-only. It stays on the plate page beside whatever she says. It goes into every future reading of that plate. And it always triggers a re-read. Agreement itself is asked of the model by the rubric. That is why invariant 3 says "silently": a disagreement could happen, but it could never hide, because his sentence is still printed under hers.

The same shape appears across the family. The prompt-quest teaches why input handed to a model should be framed as data, and Sizzle frames the whole plate that way, corrections included.

Code

cwkSizzle, backend/domain.py: correct records Dad's word, then asks for a re-read (verbatim, lines 128-143)·python
    def correct(self, encounter_id: str, *, target: str, value: str, note: str = "", source_turn: str | None = None) -> dict[str, Any]:
        """Dad's word on a reading claim: authoritative, kept across every
        re-read, and fed back into the next reading as a fact."""
        self._require_encounter(encounter_id)
        target = _text(target, 80) or "note"
        value = _text(value)
        if not value:
            raise DomainError("a correction needs a value")
        correction_id = new_ulid()
        self.record(
            "correction.added",
            {"id": correction_id, "encounter_id": encounter_id, "target": target, "value": value,
             "note": _text(note), "source_turn": source_turn},
        )
        self.request_reading(encounter_id)
        return {"correction_id": correction_id}
cwkSizzle, backend/app.py: reading_view overlays corrections newer than the reading (verbatim, lines 94-111)·python
def reading_view(reading: dict[str, Any] | None, corrections: list[dict[str, Any]]) -> dict[str, Any] | None:
    """The reading as Dad sees it: a correction newer than the reading shows
    over it until the re-read (which is told the correction) arrives."""
    if reading is None:
        return None
    view = copy.deepcopy(reading["body"])
    for correction in corrections:
        if correction["created_at"] <= reading["created_at"]:
            continue
        target = correction["target"]
        match = CORRECTABLE.fullmatch(target)
        if target == "summary":
            view["summary"] = correction["value"]
        elif match and int(match[1]) < len(view.get("dishes") or []):
            dish = view["dishes"][int(match[1])]
            dish[match[2]] = correction["value"]
            dish.setdefault("corrected", []).append(match[2])
    return view
cwkSizzle, backend/reading.py: a correction that arrived mid-read keeps the plate pending or reads it again (verbatim, lines 375-393)·python
        except (pippa.PippaError, ReadingError, OSError) as exc:
            logger.warning("reading %s failed: %s", encounter_id, exc)
            # A correction that arrived mid-read keeps the plate pending: the
            # worker reads it again with Dad's word instead of showing a failure.
            if len(self.sizzle.store.corrections(encounter_id)) == seen:
                self.sizzle.record("reading.failed", {"encounter_id": encounter_id, "error": str(exc)[:500],
                                                      "rubric": RUBRIC_VERSION})
            return
        reading_id = new_ulid()
        self.sizzle.record("reading.recorded", {
            "encounter_id": encounter_id, "reading_id": reading_id, "rubric": RUBRIC_VERSION,
            "brain": picks.get("brain"), "effort": picks.get("effort"), "model": picks.get("resolved_model"),
            "body": reading.model_dump(),
        })
        integrate(self.sizzle, encounter_id, reading_id, reading)
        if len(self.sizzle.store.corrections(encounter_id)) > seen:
            # Dad corrected the plate while Pippa was reading; this reading
            # predates his word, so it is read again.
            self.sizzle.record("reading.requested", {"encounter_id": encounter_id})

External links

Exercise

In software that shows users a model's output, add a user correction that does four things. It is stored in its own append-only table. It overlays the displayed output at once. It is sent with every later model call as an authoritative fact. And it queues a fresh derivation. Then write a test with a fake model that ignores the correction completely, and assert that after the re-derivation the correction is still shown to the user next to the new output.
Hint
Compute the overlay at read time from the stored output plus corrections newer than it, the way reading_view compares created_at, so the stored output never changes. Count corrections before a derivation starts and compare after it ends, so one that arrives mid-run triggers another pass.

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.