The One Place Work Changes Hands
Everything so far left the todo open: the row, the launch, the discovery, the enrollment, the invitation. Engine.accept is the one place a managed assignment's work actually changes hands, and it does it inside one engine command. One BEGIN IMMEDIATE, then one commit or one rollback.
Before the Transaction Opens
The adapter calls accept only for a completed final that carries COOP_ACCEPT with the right nonce. Even then, accept refuses before opening anything unless the receipt names the exact native message that said yes: "A correlated native receipt is required." A yes with no message id is a rumor.
Inside: The Steps, in Order
- The session. It must be at the generation the adapter holds and not exited: "Stale or exited session generation."
- The dispatch. Its session, generation, nonce and status must equal this session, this generation, the nonce that was answered, and
invited. Cancelled, expired, already accepted, or someone else's invitation: one refusal covers them all, "Invitation is stale or belongs to another receiver." - Each item, one after another:
- still
openat the revision the dispatch recorded, or "Work changed before acceptance"; - for a todo whose invitation carried a reply review, acknowledge exactly those reply ids, as the session's actor;
- take it with the domain's own take operation, as the session's actor;
- insert the binding: item, session, generation, dispatch;
- insert the outcome obligation, due in two hours (
OUTCOME_REVIEW_SECONDS); - record the binding's
work_revision, the item's revision after the take.
- still
- The dispatch becomes
accepted, with the receipt stored on it. - The work envelope is built (dispatch id, bindings, write intents,
shared_checkout, the work itself, and the scope when there is one) and enqueued as aworkrow in the outbox, still inside the same transaction.
Notice the items are interleaved: item one can be taken before item two's revision check fails. That is fine, because nothing commits until the last step succeeds. When item two fails, the rollback takes item one's take, binding and obligation with it.
Why It Has to Be One
The architecture states the rule as step four of dispatch: validate every selected row and revision, bind the attempt, take exactly that selection, append the events and the outcome obligation, all in one transaction. "Any conflict claims none of the selection." Split it and look at each crash point. A take committed without its binding is owned work no session is bound to. A binding without its obligation is owned work nobody is owed an outcome for, which is the original failure this whole track exists to prevent. A work envelope sent before the commit lets a worker start on work the ledger may still refuse. In one transaction, a crash anywhere leaves all of it or none of it.
The envelope deserves its own note. It is not sent from inside accept; it is written there, as an outbox row the adapter delivers after the commit. If the commit fails, the envelope never existed. That is the transactional outbox pattern, and it is why the invitation promised "The engine will commit the claim before sending the task."
Old Helpers, Borrowed Transaction
The take itself is Coop's original domain operation, the same take_todo a CLI todo take calls: one guarded UPDATE verified by its row count, never read-then-write. It runs no transaction of its own. Outside the engine, connect() opens a connection in autocommit mode, so its update and its event row each commit as they run, and it closes that connection when it is done. Inside the engine, connect() hands it a borrowed connection instead: the engine's own, already inside the accept's BEGIN IMMEDIATE, so the take lands in that one transaction and its close() does nothing. The same wrapper covers the helpers that do run their own transaction, such as take_scope: their BEGIN IMMEDIATE becomes a harmless SELECT 1, their commit does nothing, and a rollback makes the engine refuse the whole command. The context module's docstring says it in one line: "Legacy helpers may close/commit; the enclosing engine owns the transaction." That is how a helper written for standalone use joins an atomic accept without being rewritten.
The Store Backs It Up
Two unique indexes make the important invariants true even if code were wrong: engine_active_binding allows at most one active binding per work item, and engine_binding_obligation allows one obligation of each kind per binding. And the command key, accept: plus the invitation's outbox id, makes the whole thing idempotent: an adapter that crashes after the commit and asks again gets the stored result back, not a second take.