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

Four Outcomes

~15 min · state-machines, transactions, evidence, reporting

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

Four Words and a Sentence

Once the engine has pulled a report out of a final, it hands each per-item entry to Engine.report. The first check happens before the store is even touched: the outcome must be one of exactly four words, done, released, abandoned or blocked, and the body must not be empty. Anything else is a ValueError with a message that says what is missing: a per-item outcome and explanation are required.

Four is a deliberate number. Done means the item met its acceptance criteria. Released means the worker hands it back untouched or unfinished so someone else can take it. Abandoned means the item should not be done at all. Blocked means the worker is still on it and cannot move without something from outside. There is no partially done, no mostly done, no done pending review. Those are all either a blocked report with a sentence or a done that should not have been sent yet.

Who Is Allowed to Say It

Inside the transaction, three facts must hold. The session must be the current generation and not exited, because a replaced process is fenced and its words are stale. The binding must belong to this session at this generation and still be active, because a binding that already resolved has nothing left to report. And the item's revision must equal the revision the worker accepted, or the engine answers Accepted work revision changed; reconcile before reporting.

That last check is subtler than it looks. Dad edits todos and replies to them while work runs, and those changes do not count against the worker: an edit or reply moves the active binding's accepted revision forward with it, and the new text reaches the worker as a reply review (lesson four). What the check catches is any other change to the item under the worker's feet. A report about a contract that has since been rewritten by someone else is not a report about this work.

Blocked Is Not an Ending

Three of the outcomes end the binding. One does not, and the code treats it as a different kind of event entirely.

OutcomeLedger operationBindingObligationDad hears
donedone_todo (with reply outcomes) or close_requestresolvedresolved, pointing at the report✅ done
releasedrelease_todo or release_requestresolvedresolved↩️ released
abandonedabandon_todo, or close_request with the body prefixed Abandoned:resolvedresolved⛔ abandoned
blockednonestays activedue now, unparked⏸ worker blocked

A blocked report writes a report row, makes the binding's pending obligation due immediately and clears any park on it, and enqueues an attention line with the body excerpt and the worker's for_dad lines. It returns status pending. The work stays bound to the same worker, which keeps holding the context, and the coordinating chief is woken to look at it now rather than at the next timer. The adapter keeps observing that chat, so the same worker can send a done later, once the blocker clears.

One Transaction for the Ending

For the three terminal outcomes, Engine.report calls the domain operation and then _record_outcome, and both run inside the same engine command, one BEGIN IMMEDIATE transaction with an idempotency key. The domain borrows the engine's connection rather than opening its own. In that one transaction:

  1. the report is inserted with its body and evidence;
  2. the binding becomes resolved;
  3. any pending reply review for that binding is cancelled, since there is no longer active work to amend;
  4. the obligation is resolved, and its disposition is the new report's id, so the debt points at the receipt that paid it;
  5. if no active binding remains on the dispatch, the dispatch is resolved and any work envelope nobody has received yet is cancelled;
  6. an outcome notification is enqueued carrying the worker's for_dad lines.

If the domain refuses (a todo with an unanswered reply, for example), everything rolls back together. There is no state in which the todo says done and the obligation still says owed, or the reverse.

Per Item, and Evidence That Says Which Kind

An envelope can carry several bindings, and each gets its own entry, its own command and its own key. One rejected entry leaves the others committed, and the dispatch resolves only when its last active binding does. The architecture says it flatly: each selected item gets its own disposition; partial success never closes the whole batch.

The same section is strict about what counts as evidence. It must distinguish commits, tests, installed or served artifacts and actual runtime verification, because a commit or a passing test has no universal meaning of task completion. A pushed commit does not mean the app on Dad's phone changed. A green suite does not mean the service that serves it was restarted. The engine stores evidence as given and does not grade it. The worker's own prompt asks for the same distinction in plain words: say what was done and where it landed, a pushed commit, a TestFlight build, a service.

An ending is four things at once or none of them. The item's status, the owner's binding, the debt that tracked it and the message that tells the person: if any one of them can commit without the others, you have built a ledger that disagrees with itself, and someone will spend an evening finding out which half is true.

Code

Engine.report, trimmed: the gate, the blocked branch, and the three endings·python
def report(self,key,binding_id,session_id,generation,outcome,body,evidence,metadata):
    if outcome not in ('done','released','abandoned','blocked') or not body.strip():
        raise ValueError('A per-item outcome and explanation are required')
    def run(conn):
        session=self._session(conn,session_id,generation)
        binding=self.row(conn,'engine_bindings',binding_id)
        if (binding['session_id'],binding['generation'],binding['state']) != (session_id,generation,'active'):
            raise Conflict('Binding belongs to a different session or is already resolved')
        if self.revision(conn,KINDS[binding['kind']],binding['item_id']) != binding['work_revision']:
            raise Conflict('Accepted work revision changed; reconcile before reporting')
        if outcome == 'blocked':
            rid=uid()
            conn.execute('INSERT INTO engine_reports VALUES (?,?,?,?,?,?)',(rid,binding_id,outcome,body,dump(evidence),time.time()))
            conn.execute("UPDATE engine_obligations SET due=?,parked=0 WHERE binding_id=? AND status='pending'",(time.time(),binding_id))
            self._enqueue(conn,None,None,'attention',{'reason':'blocked_report','binding_id':binding_id,'report_id':rid,
                'item':{'kind':binding['kind'],'id':binding['item_id']},'text':body[:600],'for_dad':for_dad(evidence)})
            return {'report_id':rid,'status':'pending'}
        # done -> done_todo / close_request; released -> release_<kind>;
        # abandoned -> abandon_todo / close_request('Abandoned: ' + body)
        ...
        return self._record_outcome(conn,binding,outcome,body,evidence)
    return self.command(key,'report',[binding_id,session_id,generation,outcome,body,evidence],{**metadata,'session':session_id},run)
An illustrative report entry with made-up values: evidence that keeps its kinds apart·json
{
  "binding_id": "exact binding id from the envelope",
  "outcome": "done",
  "body": "Settings screen now saves on blur. Committed and pushed; unit suite green; build installed on the phone and the save verified by hand.",
  "for_dad": "아빠, 설정 화면 저장 고쳤어.\n커밋 푸시했고 폰에 깔린 빌드로 직접 확인했어.\n아빠가 할 일은 없어.",
  "evidence": {
    "commits": ["abc1234 pushed to origin/main"],
    "tests": "unit suite: 214 passed",
    "installed": "build 57 on the phone",
    "runtime": "changed a setting, left the field, relaunched: value kept",
    "reply_outcomes": []
  }
}

External links

Exercise

Look at the last five tasks you marked finished in any tracker you use. For each, write down which of the four kinds of evidence you actually had: a commit, a test run, an installed or served artifact, a check of the running thing. Then find the place in your tracker where closing an item and notifying someone happen, and check whether they can succeed or fail independently.
Hint
If closing and notifying are two separate calls, crash between them on purpose (kill the process, or throw right after the first call) and see what each side believes. That gap is what one transaction removes.

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.