Skip to content

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 pointState left behindHealed by retry?
after registry insertversion with no statement/world/jobjob: yes (replay re-enqueues). statement: no. world: yes, via worker settle
after statement appendno world status, no jobjob: yes → worker settles world. statement already durable
after world upsertno jobyes (replay re-enqueues)
after job enqueuenothing missingn/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:

  1. Every syscall write must be idempotent under its request idempotency key, and replays must not repeat side effects (the newly_registered / replayed pattern).
  2. Job handlers must tolerate at-least-once execution.
  3. 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 registered statement, 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 JobQueuePort and 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.