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

An Outcome Is Owed

~14 min · obligations, durability, timers, sqlite, coordination

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

The Failure That Was Actually Common

Coop's architecture opens its problem statement with two sentences worth memorizing. Workers occasionally start assigned work without taking its record. They more often finish long work after losing the reminder to close it. The second failure is the expensive one. The code shipped, the push went through, the session said so in a cheerful paragraph, and the ledger still shows the todo as taken, three days later, until Dad notices and repairs it by hand.

You cannot fix that with a better reminder. A reminder is a sentence in a context window, and the long work that made the session forget is exactly what pushed it out. So the architecture asks the engine to hold two facts independently of any model's memory: which exact session accepted which exact items, and which accepted items still need an outcome. The first fact is the binding you met in Track 3. The second is the subject of this whole track: the obligation.

The Debt Is Written With the Take

When a worker accepts an invitation, Engine.accept does several things in one transaction: it takes the item in the domain, inserts a binding, and inserts a row into engine_obligations with kind outcome and a due time of now plus OUTCOME_REVIEW_SECONDS, which is two hours. There is no moment where the work is taken and nothing is owed. If the store refuses the transaction, neither exists; if it commits, both do.

The command-line door gets the same treatment with a longer backstop. A todo take typed inside a session Coop can identify becomes a binding with an outcome obligation due in LEGACY_REVIEW_SECONDS, three hours. A take with no session reference at all is still a take: instead of refusing it (the refusal only pushed sessions to the untracked fast path), the engine writes a legacy_binding obligation, also due in three hours, whose reason reads "Taken without a session reference; the chief reviews its outcome".

Due Is Not Done

The due column is the most misread field in the table, so be precise about it. It is the time the obligation becomes eligible for review. Nothing happens to the work when it passes. The architecture says it plainly: a normal turn ending without a report makes the obligation eligible for review, not closure. Idle never means finished.

Only a disposition discharges an outcome obligation. A worker's own report of done, released or abandoned goes through _record_outcome, which writes the report, resolves the binding and sets the obligation to resolved with the report's id as its disposition, all in one transaction. A compatibility close from the CLI takes the same path. The chief's close, release or abandon takes it too. A report of blocked is the instructive exception: the engine stores it, sets the obligation's due to now and clears parked, and the work stays bound. A blocker is evidence that someone needs to look, not an ending.

The Lifetime Obligation and One Review

Here is the distinction that keeps this machinery honest. The obligation lives as long as the binding does. A review is one look at it. The architecture separates them on purpose: a persisted, version-checked decision to keep waiting can resolve the current review, but it must keep the outcome obligation and atomically arm the next due time. A wake receipt, a final sentence or an idle chief acknowledges neither.

Decision on one reviewThe obligationNext due
deferstays pendingnow + the requested delay (30 s to 30 days, default 300 s)
escalatestays pending; Dad is toldnow + the requested delay
parkstays pending, parked = 1; Dad is toldnow + 30 days (PARK_SECONDS)
close, release, abandonresolvednone

A parked obligation waits for Dad or the worker alone. Observations of the worker's session no longer pull it forward; a fresh blocked report from the worker unparks it, and Dad's own disposition resolves it. If neither comes, the thirty days run out and it is owed again, because nothing in this table ever forgets a debt on its own.

One case got a tighter rule. Dad's ruling of 2026-10-08 reads "A blocked report is never parked for good", and the engine carries it as BLOCKED_SWEEP_SECONDS, six hours. When the worker's latest report says blocked, a defer or escalate is capped at six hours, a park comes due in six hours instead of thirty days, and a parked blocked item the chief skipped is still pulled back within six. The reason is practical: blockers clear. A push goes through later, Dad installs the build, a proposal gets answered, and the work should be closed without Dad having to ask.

Invariant Five

The architecture's fifth invariant is the sentence this lesson exists to earn: unresolved decisions survive worker, chief and engine context loss. The worker compacts and forgets its task: the obligation is a row, and the adapter restores the envelope from the store. The chief's turn crashes: its run is left uncertain, the obligation is still pending, and a failed run pushes the obligation's due fifteen minutes out instead of dropping it. The engine restarts: persisted due times drive the timers, and the architecture forbids the other design in one line, "an in-memory timer is never the obligation."

Write the debt in the same transaction as the promise. If accepting work and owing an outcome are two writes, there is a crash between them where work is taken and nothing is owed. If they are one write, the only way to lose track of the outcome is to resolve it.

Code

A minimal sketch (not Coop's engine): an obligation that only a disposition discharges, with a backstop, a park and a sweep·python
import sqlite3

OUTCOME_REVIEW = 2 * 3600   # backstop after a managed accept
PARK = 30 * 86400           # a parked row waits for Dad or the worker

db = sqlite3.connect(':memory:')
db.execute("""CREATE TABLE obligations (
  item TEXT PRIMARY KEY,
  status TEXT NOT NULL DEFAULT 'pending',  -- pending | resolved
  due REAL NOT NULL,                       -- next eligible review
  parked INTEGER NOT NULL DEFAULT 0,
  reviewed REAL,                           -- last time a review looked
  disposition TEXT)""")

def accept(item, now):
    # In the real engine this shares a transaction with the take.
    db.execute('INSERT INTO obligations(item, due) VALUES (?, ?)', (item, now + OUTCOME_REVIEW))

def evidence(item, now):
    # Fresh worker evidence (a note): review now. Parked rows ignore it.
    db.execute("UPDATE obligations SET due = MIN(due, ?) "
               "WHERE item = ? AND status = 'pending' AND parked = 0", (now, item))

def sweep(now, limit=50):
    # What a review wakes for: pending and due. Nothing else.
    return [r[0] for r in db.execute(
        "SELECT item FROM obligations WHERE status = 'pending' AND due <= ? "
        "ORDER BY due LIMIT ?", (now, limit))]

def defer(item, now, after):
    # Resolves this review, keeps the obligation, arms the next one.
    db.execute("UPDATE obligations SET reviewed = ?, due = ?, parked = 0 WHERE item = ?",
               (now, now + after, item))

def park(item, now):
    db.execute("UPDATE obligations SET reviewed = ?, due = ?, parked = 1 WHERE item = ?",
               (now, now + PARK, item))

def report(item, outcome):
    # Only a disposition discharges the obligation.
    db.execute("UPDATE obligations SET status = 'resolved', disposition = ? "
               "WHERE item = ? AND status = 'pending'", (outcome, item))

H, D = 3600, 86400
accept('todo 1', 0)
accept('todo 2', 0)
print(sweep(10 * 60))        # []          accepted, nothing due yet
evidence('todo 1', 10 * 60)
print(sweep(10 * 60))        # ['todo 1']  a note arrived
defer('todo 1', 10 * 60, 30 * 60)
park('todo 2', 10 * 60)
print(sweep(1 * H))          # ['todo 1']  its deferral came due
report('todo 1', 'done')
evidence('todo 2', 2 * H)    # ignored: parked rows wait
print(sweep(3 * D))          # []
print(sweep(31 * D))         # ['todo 2']  the park ran out; still owed
Excerpt from Coop's engine: the backstop constants (comments as in the source)·python
# An outcome is reviewed when evidence arrives (a worker final, a blocked
# report, a lost session); these timers are only the backstop behind that.
OUTCOME_REVIEW_SECONDS = 2 * 3600
LEGACY_REVIEW_SECONDS = 3 * 3600
PARK_SECONDS = 30 * 86400
# A blocked report is never parked for good: the chief sweeps it again within
# this window, so a blocker that cleared (a push that later went through, a
# build Dad installed) or a proposal Dad already answered is picked up without
# Dad asking (Dad, 2026-10-08, todo #348).
BLOCKED_SWEEP_SECONDS = 6 * 3600

External links

Exercise

Pick any place in your own work where something gets "taken": a ticket assigned to you, a job claimed by a worker, a review you agreed to do. Write down every way it can end. Then write a single table with an item, a status, a due time and a disposition, and one query that lists what is owed and due. Run the sketch above, then change it so that a review that defers forgets to set the next due time. What does the sweep return afterwards, and is that a safe failure or a silent one?
Hint
A defer that clears the debt instead of rescheduling it makes the item vanish from every sweep: nobody is told, and nothing is wrong on any screen. A defer that forgets to move due makes it come back on every sweep, which is noisy but visible. Noisy and visible is the failure you want.

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.