ADR 0200 — An effect is recorded before it runs
Context
ADR 0182 gave durable
bodies step, which records a result after its body. A body cut short by a
crash runs again on replay, and 0182 says so: the host must make external
effects idempotent. That is correct for a computation and wrong for an effect in
another system — sending a message, starting a model turn, answering a
permission request — where a repeat is worse than a loss.
A consuming project solved this by hand in seven places with one rule: record the intent before the effect, record what the recipient named it after, and settle an intent found without an outcome only by the recipient’s own record. One of the seven copies had drifted — it wrote nothing before a permission reply, so a lost acknowledgement could send the reply twice. A rule that lives in seven copies drifts again.
Decision
LocalStepDurability gains effect(name, { run, reconcile }, options), beside
step in the owner that already exists (I8):
- the intent is appended to the ledger before
run; the proofrunreturns is appended after, asaccepted; - a call that finds an intent without an outcome calls
reconcile, neverrun. A proof found is recorded asaccepted;nullis recorded asuncertain. Both are final:runis never called for that name again, anduncertainis not reconciled again — it is an answer the caller sees and decides on; runthrowing leaves the intent standing and rejects withEffectUnresolvedError: whether the effect happened is exactly what is not known, so the next call reconciles.reconcilethrowing or overrunning its deadline does the same — “could not ask” is not “not there”;- it is bounded (I10):
reconcileruns underreconcileTimeoutMs(default 30 s) with an abort signal, and a proof is at most 64 KiB of JSON — an identity the recipient assigned, not a payload; - in one process, concurrent calls for the same name share one
run, as concurrentstepcalls share one body.
The records are a new ledger event kind, durability/effect, with phases
intent, accepted (with via: run | reconcile) and uncertain. The first
outcome recorded wins.
Consequences
- Effects stop needing a hand-written ledger per call site; a host supplies
reconcile— the lookup at the recipient by its own identity for the effect — and the protocol is one implementation. uncertainis visible and permanent. A host that wants to retry an uncertain effect does so under a new name, by its own decision.- Across processes the guarantee is the ledger’s: two processes that append to
it concurrently are fenced by whatever lease already fences the run, as for
step(0182).effectdoes not add a lease. ToolDurability, the port a mounted tool body sees, is unchanged: adding a required method to a port that hosts implement would break every implementation. The engine’s own interface carries it.