Skip to content
C.W.K.
Stream
Lesson 01 of 04 · published

Markdown Was Never a Multi-Writer Format

~14 min · concurrency, sqlite, coordination, multi-agent

Level 0Loose Note
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Four Files at the Root

Before Coop had a name, the family's AI sessions coordinated the way most projects start: through text files at the top of the repository. TODO.md held the work. HELP-REQUEST.md held asks from one brain to another. DELEGATION-REQUEST.md held handed-off tasks. NEW-SESSION-REMINDER.md held a note from one session to whoever came next. And a history/ folder held one markdown file per finished piece of work.

Every session read them at start and edited them as it went: Claude Code, Codex, any brain that touched the repository. It worked the way these things always work at first. Then it stopped working, and by the day the files were retired, TODO.md was 811 lines long and history/ held 330 files.

The Header That Asked Nicely

The old TODO.md opened with a block that announced itself as Principles + Template Block — IMMUTABLE, followed by "Do not modify or delete" and "This block itself must always remain at the top, exactly as is." Its first principle was: "Only keep things that are in progress. Delete immediately when done — git handles the changelog." Its last: "The goal is to save tokens + save mental energy."

Read those two facts side by side. A file whose first rule is only what is in progress had grown to 811 lines. The word IMMUTABLE was a request, typed in a comment, addressed to every writer that would ever open the file — and nothing in the file could refuse a writer who did otherwise. That is the whole problem in miniature: every guarantee lived in prose, and prose cannot say no.

Notice the second principle too. "Delete when done" means finished work leaves the board entirely. The file can tell you what someone thinks is open; it cannot tell you what was done, by whom, or when. Git remembers the deletion, but nobody coordinating live work reads diffs.

Five Failures

The design document that replaced these files names exactly five ways they broke:

  • They live in the repo. Every edit is a commit. Several brains editing in parallel means merge conflicts and worktrees that drift apart.
  • They bloat fast. Every session pays for the whole file in its context window, every time, whether or not the work concerns it.
  • They get mis-edited. Brains lose track of which item is which, rewrite the wrong entry, drop rows. In the document's words: Markdown was never a multi-writer format.
  • They hold no state. There is no "read?", no "acknowledged?", no "in progress?", no "done?" — and no thread for a sender and receiver to go back and forth.
  • They accumulate forever. The history folder crossed the point where reading it became an onboarding burden of its own.

Why a Text File Is the Wrong Primitive

The third failure is the one worth understanding mechanically, because it is not about discipline. Saving a text file replaces the whole file with what the writer computed from a copy it read earlier. That is a read-modify-write with no compare step: if two sessions read the same bytes, each changes them, and each saves, the second save erases the first. Neither save fails. The session that lost is never told.

And the item itself has no identity a machine can hold onto. "The third bullet" stops being the third bullet when another session inserts one above it. "The line that says fix the backup script" stops matching when another session rewords it. An AI editing tool that replaces text by string match makes this worse, not better: it needs the exact old text, and the old text is precisely what a parallel writer just changed.

What a coordination surface for many writers actually needs is short:

  1. a stable identity per item, so "todo #7" means the same thing to everyone forever;
  2. a state that is a field, not a sentence, so "taken" can be checked rather than interpreted;
  3. a write that can fail — "mark it taken, but only if it is still open" — and that tells the writer whether it won.

A database row gives all three. The runnable sketch below puts the same race through a text file and through a ledger row: the file silently keeps whichever save came last, while the guarded update lets exactly one session win and hands the other a zero row count it cannot misread.

The Bootstrap

On 2026-05-12, cwkPippa commit 2616ed8a — "Bootstrap Pippa Coop System" — turned all four root files into pointers at the new spec. Three became nine-line deprecation notices; TODO.md alone went from 811 lines to 9, and the old contents stayed in git history. NEW-SESSION-REMINDER.md, until then a three-line placeholder, grew into a fifty-line pointer that taught the new letter ritual. In their place: a single SQLite database outside the repository, a small helper module, and a command-line shim. Four tables at first: todos, requests, messages, history_entries. The history files followed in a later phase.

Two choices in that bootstrap carry the rest of this course. Moving the store out of the repo ended the commit-per-edit tax and the merge conflicts in one move. And the database ran in WAL mode (write-ahead logging), where readers do not block the writer and the writer does not block readers, while writes themselves still go one at a time. Coop's write volume is tiny; what it needed was not throughput but a place where a write can be refused.

A shared text file can only ask its writers to behave. A ledger can refuse them. When many sessions coordinate, the item needs an identity, its state needs to be a field, and the claim needs to be a write that can lose — out loud.

Code

A minimal sketch (runnable, stdlib only): the same race on a text file and on a ledger row·python
import os, sqlite3, tempfile, threading

work = tempfile.mkdtemp()

# --- 1. A shared text file: two sessions "take" the same item -----------
todo_md = os.path.join(work, "TODO.md")
with open(todo_md, "w") as f:
    f.write("- [ ] #7 fix the backup script\n")

def take_in_markdown(who):
    with open(todo_md) as f:
        text = f.read()                   # both sessions read the same bytes
    return text.replace("- [ ] #7", f"- [taken by {who}] #7")

edit_a = take_in_markdown("session-a")
edit_b = take_in_markdown("session-b")
for edit in (edit_a, edit_b):             # A saves, then B saves over it
    with open(todo_md, "w") as f:
        f.write(edit)
with open(todo_md) as f:
    print("markdown:", f.read().strip())
print("  each session saw its own save succeed; nothing told A it lost\n")

# --- 2. A ledger: the same race, settled by one guarded UPDATE ----------
db = os.path.join(work, "coop.db")
setup = sqlite3.connect(db)
setup.execute("PRAGMA journal_mode = WAL")
setup.execute("CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT,"
              " status TEXT NOT NULL DEFAULT 'open', taken_by TEXT)")
setup.execute("INSERT INTO todos (id, title) VALUES (7, 'fix the backup script')")
setup.commit()
setup.close()

start = threading.Barrier(2)
results = {}

def take_in_ledger(who):
    conn = sqlite3.connect(db, timeout=10)
    start.wait()                          # both writers fire together
    cur = conn.execute(
        "UPDATE todos SET status = 'taken', taken_by = ?"
        " WHERE id = 7 AND status = 'open'",   # the guard lives in WHERE
        (who,),
    )
    conn.commit()
    results[who] = cur.rowcount == 1           # rowcount is the verdict
    conn.close()

threads = [threading.Thread(target=take_in_ledger, args=(w,))
           for w in ("session-a", "session-b")]
for t in threads:
    t.start()
for t in threads:
    t.join()

for who, won in sorted(results.items()):
    print(f"ledger: {who} won? {won}")
with sqlite3.connect(db) as conn:
    print("ledger row:", conn.execute("SELECT status, taken_by FROM todos").fetchone())
What one run prints (which session wins varies from run to run; that exactly one wins does not)·text
markdown: - [taken by session-b] #7 fix the backup script
  each session saw its own save succeed; nothing told A it lost

ledger: session-a won? True
ledger: session-b won? False
ledger row: ('taken', 'session-a')

External links

Exercise

Find one text file in your own work that more than one process, person, or AI session edits: a TODO list, a shared notes file, a status page, a prompt library. For each kind of entry in it, write down three things: what identifies the entry, where its state lives, and what happens if two writers act on it within the same second. Then run the sketch above and change it so both sessions edit different lines that describe the same item. Does anything notice?
Hint
If the honest answer to "what identifies it" is "the line that says..." or "the third item", the identity is positional and any concurrent edit can move it. If the answer to "where does its state live" is a word inside a sentence, nothing can check it without reading prose.

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.