Case study
The destination is the only witness
An engine that gives an agent's irreversible calls an identity the model can't regenerate, a real state for not knowing, and a benchmark that could prove it unneeded.
· Revised · 19 min read
An agent asks an API to create a shipment. The API creates it. The response never arrives. That gap is the failure I built Irrevon for, and it has one property that separates it from every other failure in the stack: the only party that knows what happened is the one that stopped talking to you.
Both of the caller's options are wrong. Retry, and if the destination committed, you have shipped twice. Don't retry, and if it didn't commit, a real order silently never happens. Nothing in the caller's own state distinguishes the two cases, because the distinguishing fact was written on the far side of a connection that went quiet.
Irrevon is public under Apache-2.0, published to PyPI as irrevon==0.1.0 with a GitHub release on 25 July 2026. It needs Python 3.13 and Postgres, and ships with a local read-only workbench, a deterministic demo and a benchmark harness. It is alpha software, and as of October 2026 that release is still the only one.
The most important sentence in this write-up comes first. There are no performance results here: no duplicate rates, no latencies, no accuracy figures, because none exist. Nothing in the benchmark is frozen, no confirmatory run has happened, and neither provider adapter has ever called a live destination. Every behavioral claim below rests on a synthetic reference destination I also wrote, which is exactly the kind of evidence you should discount. The preregistration says in capital letters at the top that nothing in it is frozen.
What I did build is a mechanism, a state model, and a measurement apparatus designed to be hostile to me.
The problem
Distributed systems have handled lost responses for decades, and the standard defenses lean on an assumption agents quietly violate: a retry resends the same bytes. A conventional client retries by resending a buffer. An agent retries by thinking again, and thinking again produces different words, a different reference ID and a different argument shape for the same business intent. The project site reports a survey of twelve agent frameworks that found none enforcing exactly-once behavior at the tool boundary.
The usual tools each solve an adjacent problem. A request hash identifies request bytes, which is not the same thing as one business intent, so it splits an order the agent reworded. A freshly generated UUID changes the moment the agent rebuilds the tool call, which is the moment you needed it stable. A provider idempotency key is genuinely good, but only when the provider supports keys for that operation, keeps them long enough to cover your recovery window, and applies the semantics you assumed. A durable workflow engine retries reliably and will still hand a duplicate to a destination that doesn't deduplicate, because its exactly-once guarantee covers its own state and not somebody else's database. An outbox proves you handed off the intent and says nothing about what the destination committed after the response was lost.
In an earlier essay I argued that agents need a nervous system and named idempotency keys and receiver-side dedup as what closes the window a durable runtime merely narrows. Irrevon is me finding where that advice stops working: a receiver that won't dedupe, and a caller that can't keep a key stable.
How it works
Everything a client can learn about an effect it may have caused is bounded by what the destination exposes, and no amount of caller-side cleverness moves that boundary. The engine names three tiers.
C1 destinations accept a caller-supplied idempotency key with a defined replay window. Stripe is the reference example. Duplicates are prevented natively, and the project's own expected result for Irrevon here is no advantage at all.
C2 destinations have stable external references and a status or list query, but no dependable native idempotency: shipment APIs, message APIs, order creation. This is the engine's primary scope. Duplicates are detected by querying, and re-dispatch is safe only after absence is confirmed.
C3 destinations expose no key, no query and no stable identifier. Lost and orphaned effects are undetectable. On C3 the engine parks the record and escalates to a human, because the information was never emitted and no client-side method can recover it.
The adapter guide is explicit that the tier describes the destination rather than grading it, and it fully determines what I can promise.
Identity from facts the model did not choose
The least reversible decision in the codebase is that effect identity comes from stable business facts and nothing else.
intent_id is a SHA-256 over the RFC 8785 canonical JSON of exactly three members: effect_type, scope and stable_ids, such as the order or invoice ID. The model's prose is deliberately absent. When the agent re-synthesizes its arguments for the same order, the identity doesn't change, the retry collapses onto the same record, and the new wording is kept as a parameter_variants row: evidence, not new work.
Rules like that decay into comments unless something enforces them, so three things do. The registrar wraps incoming parameters in ModelTainted, an opaque container where every implicit conversion a hashing path might reach for raises an error, with one sanctioned exit at the adapter boundary. An import-linter contract forbids the identity module from importing the module that handles raw model arguments, so CI checks the isolation. And JSON Schema guards the trust boundary. The idempotency key is the operation_id itself, intent_id plus a ledger-allocated step, and a Postgres CHECK constraint refuses any row where the two disagree.
A rule from voice intake shows up here in reverse. Semantic similarity may propose a duplicate and can never act on the proposal: the advisory module emits a type no mutation API accepts. A classifier that can merge two irreversible effects on a hunch is a worse bug than the one it was hired to catch.
Persist first, and never hold a lock across the network
Nothing crosses the irreversible boundary before a durable record of the intent exists. Ledger.register_intent writes the record and its first two transitions in one transaction, with named crash points on either side of the commit so a test can kill the process in each position and check the difference. A crash before persistence is provably effect-free. A crash after it leaves a record that recovery is obliged to adjudicate.
The claim transaction commits before any network I/O. Locks are held only inside short claim transactions, never across a call to the destination, because holding a row lock while a destination hangs turns one slow API into a stalled system. Correctness comes from locked SQL functions and unique constraints rather than from a strict isolation level. The engine runs a single writer, enforced three separate ways, and multi-writer support is designed but not implemented. That limit is stated on the project site in three places, which is where a limit belongs.
AMBIGUOUS is a state, not an error
Most systems have an error state where unknown outcomes go to die, and an error state is a claim: it says the effect did not happen. When the response was merely lost, that claim is a coin flip presented as a fact. So in the state table, a timeout or a lost response maps to AMBIGUOUS, a non-terminal state, and never to FAILED. Unknown destination outcomes also map to AMBIGUOUS. An adapter author who wants to call something a clean failure has to cite the destination's documentation proving that error is side-effect-free.
Leaving AMBIGUOUS requires reconciliation or replay evidence. Edges are four-tuples of from-state, to-state, cause and actor, so a transition claiming a cause no actor is licensed to produce can't be written. A retry after a proven failure gets a new step and therefore a new idempotency key, which makes it a different operation rather than the same one sent twice.
The decision I'd defend hardest is that three questions get three separate records. What did we do (the execution lifecycle). What does the destination say happened (DUPLICATE, LOST, ORPHANED, CONTRADICTED and so on). What did a human or policy decide (compensated, re-dispatched, accepted, escalated). An effect can be settled and contested at the same time, and neither fact overwrites the other. Re-dispatching a confirmed duplicate is not a legal resolution, because it manufactures more of the problem. And an orphan, a real effect with no ledger record, can only exist as a finding, since a state machine keyed on ledger records can't express it.
Absence is the hard direction
Proving an effect exists is easy: query the destination, find it, settle and cite the probe. Proving it doesn't exist is where the work is, because "I asked and didn't find it" has several explanations and only one of them means it isn't there.
Reconciliation queries in a fixed order of key strength and parks the effect if no key covers it. Confirmed absence then needs three things together: a covering key, enough elapsed time that the destination's own read path has caught up, and a second query a re-read gap later that also comes back absent. Both probes are recorded and cited in the settle transition, and a read-only auditor checks the citations after every integration test. Absence has to be an authoritative answer, such as a 404 with the right error code. A failed query is indeterminate and is never treated as absent. There are no retries anywhere in the adapter layer, because hidden retries corrupt attempt accounting and manufacture duplicates on exactly the tier this engine exists for.
When a destination publishes no bound on how long its reads lag, an absent result is still recorded, but automatic re-dispatch is forbidden and the finding goes to a human. Absence without a stated visibility bound supports human judgment and nothing else. Late responses are kept too: a reply that arrives after settlement and contradicts it is flagged for audit, because by the time it lands the record already looks finished.
The evidence surface
If the ledger is the argument, it has to be hard to quietly edit. Triggers on every fact table refuse updates and deletes at the statement level. The application role can insert evidence but can only change execution state through four locked functions owned by the migration role. A separate read-only role serves the workbench. Migrations are hash-journaled, so a changed migration file is a hard error.
irrevon serve is a local workbench that binds only to loopback, implements GET and HEAD only, and connects through the read-only role. Its banner saying the data is a synthetic fixture is permanent, because a workbench that renders development fixtures with the confidence of production data is lying.

Stable-identifier keys stay visible while their values render as digests, so you can see an effect is keyed on an order_id without the screen naming the customer. The keys are what you need to debug identity. The values are what you need to violate someone's privacy.

Results
Everything above is a design, which is a hypothesis with a lot of tests. Whether any of it reduces duplicate, orphaned and lost effects compared with what a competent engineer would already build is a measurement question, and I couldn't think of an honest way to answer it without an instrument capable of telling me no.
IrrevonBench counts, per destination tier, how often duplicate, orphaned, lost, contradicted, unresolved-ambiguous and falsely suppressed effects occur at the destination. Model quality and end-to-end task success are named non-targets. Each trial hands one business intent to an "arm" with the fixture's answer labels stripped, and seeds are derived without the arm's identity so every arm sees the same world. Seven fault kinds hang off the same timeline anchors for every arm. Semantic re-synthesis is template-driven rather than model-driven: a real model would add realism and remove the determinism that makes two arms comparable.
The baselines run from naive retry (B0) up through argument-hash dedup, agent-generated keys, a stable workflow-issued operation ID, a durable journal and status-check-before-retry. The preselected comparator is the strongest realistic combination of those, and the engine has to beat both it and the durable journal. Tests lock the baselines' strength: one requires the composite to show zero duplicates where it honestly should, and the model-assisted baseline refuses to run rather than ship a strawman. Weakening a baseline is the cheapest way to win a benchmark, so these tests fail when baselines get weaker.
Two independent oracles count the same anomalies. One reads the reference destination's ground truth, attributing an effect to an intent only when exactly one intent matches and counting ambiguous matches instead of guessing. The other compiles a causal history of client and destination events and checks eight invariants, a design credited to Jepsen's Elle. cross_check_metrics requires the two counts to match exactly, and any disagreement invalidates the run as harness corruption instead of trusting the friendlier number. A metric bug that under-reports duplicates, the classic favorable-measurement failure, breaks the build. A test doctors a metric and asserts the run dies.
Arms cannot import the oracle, the engine cannot import the benchmark, and a token scan fails any arm module that mentions the destination's control API. Cells that don't apply are marked as such and never zero-filled, so an arm with no concept of ambiguity reports "not applicable" instead of a flattering zero.
Statistics, the kill rule, and power I don't have
The statistics module uses only the standard library, so the confirmatory analysis runs from the published wheel. It ships no default margins or alpha: every one is a required argument, and the verdict command exits with an error rather than choose for you. The unit of inference is the seed, because trials nested inside a seed are not independent.
The equivalence margin is derived from a named adopter rather than picked. A team with a million irreversible C2 dispatches a year, an ambiguous-fault rate of 0.5%, 200 dollars per incident and a 10,000-dollar annual adoption cost breaks even at 10,000 / (1,000,000 × 0.005 × 200) = 0.01, so δ = 0.01. These are parameters a person has to freeze. No code path may choose them.
Then the part the preregistration says about itself that I'd rather it didn't. With pooled event rate p, cross-arm correlation ρ, N trials per seed per arm and seed-level heterogeneity τ, the per-seed variance of the paired difference is:
At the planned 60 trials per cell (N = 240), with p = 0.05, ρ = 0.5, ten seeds and τ = 0, the equivalence test's power at a true difference of zero is about 0.45. A coin flip. At 160 per cell it rises to about 0.96 at τ = 0 and collapses for τ at or above 0.01, which no trial count fixes. τ is deliberately not estimated from the development pilots, because using pilot data to pick the parameter that decides whether the test can fail is the move the whole document exists to prevent. So the honest statement is that the power is unknown.
The falsification rule is published. The engine is declared unnecessary if, on every primary metric, the strongest conventional comparator is either statistically equivalent to it or statistically better, with no confirmatory cell where the comparator is worse by a set margin. The second clause matters: a benchmark that only fails on a tie was written by someone who never considered losing. There is also a registered null for C1 destinations predicting no advantage over native idempotency, to be reported as prominently as any win.
The refusals are mechanical. In confirmatory mode, bench run exits with an integrity error unless both freeze registrations verify, and today they can't, because no registration exists. Three Make targets for the confirmatory benchmark, the live sandbox and the release gate always print REFUSED and exit. A crashed arm is scored rather than reclassified as a bad run, and holdout generation refuses to write inside the repository.
The preregistration also discloses that synthetic engineering pilots already happened against the reference destination, so the design cannot honestly be described as preceding every observation. It leaves open whether that requires a design reset or an independent review before freezing, and states that no agent may resolve that ruling. The pilot counts are harness-development evidence, not results.
Limits
A list of a system's real gaps, written by the person who built it, is worth more than its claims:
- No identity-collision guard. The exact canonical preimage is stored, which is what a guard needs, and nothing compares it. A SHA-256 collision would merge two distinct intents silently. It's roughly one
ifaway. - No version or domain separator in the identity hash. A future change to the identity tuple would remap every existing ID with no detectable discontinuity.
- A blocking 60-second sleep sits inline in the confirmed-absence path, stalling the single writer for a minute per absent effect.
- The advisory lock keys on
hashtext(), an undocumented Postgres function whose stability across versions nobody promised. - Single writer only.
- Both provider adapters, Stripe and EasyPost, have never made a live call. This is the largest discount a reader should apply.
- The cross-check covers five of eight metrics. Three secondary metrics have no independent check.
- Four harness output formats are never schema-validated, including the second oracle's own output.
false_suppression_rateis probably unreachable on the current matrix: the only faults where an arm legitimately suppresses are labeled not legitimate.- The committed development matrix is far narrower than the preregistered one, and the confirmatory stratum has no fixtures in the repository.
- The ADRs authorizing the benchmark design and the identity derivation are still "proposed." The status page says repository automation never converts implementation into ratification.
What's next
The next thing that matters is not a feature. It's a freeze a person has to perform, on parameters no code path may choose, in a document that still says nothing in it is frozen. Until then the refusal stands, and I'd rather find out I built something unnecessary than never build the instrument that could tell me.
Source appendix
The detail this write-up compresses lives in the repository:
- Mechanism and state: identity, state table, ledger, reconciler, adapter policy
- Database enforcement: tables and triggers, locked functions, grants, read-only role
- Benchmark: design, preregistration, oracle, metrics, freeze verification, integrity check
- Decisions and status: ADRs, project status
Comments