ADR-0013: Syscall atomicity via idempotent replay, not transactions
ADR-0013: Syscall atomicity via idempotent replay, not transactions
Status
Accepted
Date
2026-07-03
Context
Act(RegisterResourceVersion) performs four writes through four separate
ports: registry insert (catalog.resource_version), truth statement append
(truth.statement_log), world projection upsert
(world.resource_status_current), and job enqueue (process.job_request).
The kernel calls these sequentially with no shared transaction — ports are
independent contracts, and an adapter-spanning transaction would leak storage
topology into the kernel (ports may not even share a database in future
profiles).
Adversarial QA (2026-07-03) proved this matters: a replayed request duplicated truth statements and permanently regressed a settled world status. That specific bug is fixed — the registry reports insert-vs-replay and the syscall skips side effects on replay — but the underlying question remains: what is the consistency story when a syscall dies between writes?
The partial-failure matrix after the replay fix, assuming the client retries with the same idempotency key:
| Failure point | State left behind | Healed by retry? |
|---|---|---|
| after registry insert | version with no statement/world/job | job: yes (replay re-enqueues). statement: no. world: yes, via worker settle |
| after statement append | no world status, no job | job: yes → worker settles world. statement already durable |
| after world upsert | no job | yes (replay re-enqueues) |
| after job enqueue | nothing missing | n/a — complete |
Two mechanisms do the healing: idempotent replay (registry/job dedupe on idempotency keys; replay skips duplicating statements/world) and the at-least-once worker (profiling settles world regardless of whether the provisional upsert landed; retries with backoff; stale-claim reaper recovers crashed claims).
Decision (proposed)
Accept replay + at-least-once workers as the v1 consistency mechanism. Do not introduce cross-port transactions or an outbox yet.
Rules this imposes on all future syscalls and job handlers:
- Every syscall write must be idempotent under its request idempotency key,
and replays must not repeat side effects (the
newly_registered/replayedpattern). - Job handlers must tolerate at-least-once execution.
- Ordering: durable intent (the job) is enqueued by both the fresh and the replay path, so downstream repair never depends on the truth/world writes having succeeded.
Alternatives Considered
- Single transactional port method (one adapter call doing all four writes): atomic, but collapses the port boundaries into a storage-shaped mega-contract and forces every adapter to be a single ACID store. Rejected for now; contradicts the ports-not-databases design rule.
- Transactional outbox: write statement/world/job intents into an outbox table in the registry transaction; a relay applies them. The standard end-state for this class of problem and the likely successor to this ADR — deferred until there is more than one syscall, so the outbox shape is informed by real variety rather than a single case.
- Derive world/jobs from the truth log (event-sourcing): statements become the only synchronous write; projections and jobs follow from tailing the log. Architecturally attractive (truth-first), but requires log consumers and offset management we don’t have yet.
Consequences
- One acknowledged gap: if the statement append fails after the registry
insert, the replay path (correctly) never appends the
registeredstatement, so that observation is missing from the truth log. The resource still profiles and settles via the job path; the audit trail is what suffers. Tolerable for v1 observations; the outbox alternative closes it. - The at-least-once contract is now documented on
JobQueuePortand enforced culturally by this ADR: reviewers should reject non-idempotent handlers. - Revisit trigger: the second or third syscall implementation, or the first time the missing-registered-statement gap is observed in practice.