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

A Notebook, Corrected but Never Rewritten

~16 min · append-only, event-sourcing, provenance, agent-memory, human-oversight

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

The Notebook That Became a Mess

Between runs, Pippa keeps a notebook for each game: what the game taught her. Lines are written by her, by the reflector helper, and by Dad. At first the notebook had only two operations, add and strike. Every correction was therefore one more line. After a few days the game's notebook held 123 live lines, many of them contradicting each other, and every play turn carried a pile of advice that argued with itself. Dad's verdict on 2026-11-11: we need a tool to correct notes; right now it is a total mess.

Before the tool existed, a Pippa chat session fixed it the obvious way: a hand rewrite of the file down to 55 clean lines. It worked, and it destroyed something. The old words of 52 lines now existed only in a backup, and a frozen benchmark that was supposed to read the notebook as it stood on a given day could no longer do so.

Corrections Are Rows, Not Rewrites

The rebuilt notebook (Coop 1111-17) keeps one property no matter what: the file is only ever added to. A line is changed by its number, and every change is its own row written after the line:

  • edit=#n puts new words where the old ones stood: the same number, the new text in a field named to.
  • retire=#n takes a line out of view (with into=#m when another line now says it), written as a strike row with how it was retired.
  • merge=[...] writes one line for several and retires them, pointing at it.
  • history=#n reads back every text a line has had, who changed it, when and why.

Two small choices protect compatibility. An edit's words ride under to, never text, because an older Oriel reads any row with text as a new line; a retirement is a strike row, which old readers already understand. And numbers are never reused or renumbered: a line's number is its place for good, so "line 17" means the same thing to Pippa, to Dad and to a benchmark frozen last week, which reads the file only up to its own date.

The 52 lines from the hand rewrite were recovered too: their old words came back from the backup as adopted history rows, which the view never shows but history does. A change made where a row stood reads as it was, then as rewritten.

Three Sources, Shown Apart

Every line carries its source, and the source decides where it appears. A line is Pippa's own experience (seen, or guessed), Dad's hint or rule (dad), or Dad's correction (dad_correction: he rewrote a line, and these are his words now). Turns show his corrections first, then his other lines, then hers, each tagged. The design calls this the memento guard, and the name fits the film whose hero lives by notes he cannot remember writing: a note that says who wrote it, and on what evidence, can be weighed. The count of seen lines a run adds is also the evidence that she is learning rather than being told.

Dad has his own editor too. Since 2026-11-12 the Notebook card on Oriel's page shows every line whole with its source and number, and lets him edit, retire and add. His words: helper brains alone cannot be trusted with it. A line of hers or a helper's that he rewrites becomes his correction and keeps its number; "Remove" is a retirement with his reason, never an erased row. The card also found a quieter bug: three lines had been written under a heading passed as the notebook's title, each into a notebook of its own that no turn ever read. A write under another title is now refused while a session is open.

Only What the Game Taught Her

The last rule is about scope. Over time the notebook had collected lines that were not about the game at all: a look that was refused, a request that failed, how to log a death, a test's setup. Dad's ruling the same day: write only what you learned from the game in the notebook; this is not the coordination log, why write anything else. That rule is now one sentence, GAME_ONLY, carried by every instruction that asks for a line: the tool's own description, the reflector's instructions (a run that taught nothing about the game gets no lesson), and cwkPippa's play note. No code judges a line's subject. Dad's page is where a line that slipped through is seen and removed. Tool trouble belongs to Oriel's own records; engineering lessons belong to the family's coordination system. A memory with a clear scope stays useful; one that collects everything becomes noise.

Code

An append-only notebook: edit, retire and history as rows, numbers kept for good (runnable sketch)·python
import json
import time

rows = []                                  # the file: only ever appended to


def add(text, source):
    number = 1 + sum(1 for r in rows if r["op"] == "add")
    rows.append({"op": "add", "n": number, "text": text, "source": source, "at": time.time()})
    return number


def edit(n, words, by, why):
    rows.append({"op": "edit", "n": n, "to": words, "by": by, "why": why, "at": time.time()})


def retire(n, by, why, into=None):
    rows.append({"op": "strike", "n": n, "how": "retired", "into": into, "by": by, "why": why, "at": time.time()})


def view(until=None):
    lines = {}
    for r in rows:
        if until is not None and r.get("at", 0) > until:
            break                          # a frozen reader sees the file as it stood
        if r["op"] == "add":
            lines[r["n"]] = {"text": r["text"], "source": r["source"]}
        elif r["op"] == "edit" and r["n"] in lines:
            lines[r["n"]] = {"text": r["to"], "source": "dad_correction" if r["by"] == "dad" else lines[r["n"]]["source"]}
        elif r["op"] == "strike":
            lines.pop(r["n"], None)
    return lines


def history(n):
    return [r.get("text") or r.get("to") or f"(retired: {r['why']})" for r in rows if r["n"] == n]


a = add("the bridge guard swings twice, roll after the second", "seen")
b = add("always attack first", "guessed")
edit(b, "block first, then attack after his swing", by="dad", why="attacking first got her killed")
print(json.dumps(view(), indent=1))
print(history(b))

External links

Exercise

Extend the sketch with merge(numbers, text), which adds one new line and retires the old ones pointing at it, and a view(until=...) test that shows a benchmark frozen before the merge still sees the old lines. Then write the GAME_ONLY sentence for a memory of your own (one sentence that says what belongs in it) and list three things that should go somewhere else.
Hint
A merge is just an add followed by retire rows with into=the new number. If your view function needs a special case for merges, the row design is doing too little; the strike row with into should be enough.

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.