ADR-112: Lease-before-op fence — no retry for an interrupted write 

Status

Accepted

Date

2026-07-23

Authors

Netresearch DTT GmbH

Context 

ADR-111 classified a tool's side effect and made a write's audit fail-closed. It named two follow-ups; this is the one that closes the double-execution window itself.

The queue is at-least-once (ADR-102) and the lease renews at a step boundary — after an operation completes (ADR-104). A tool call that outlives LEASE_SECONDS is reaped and the run re-executed. For a NON_IDEMPOTENT_WRITE that is a double effect: the reaper cannot tell that the run was mid-write, because nothing is recorded until after the tool returns.

Decision 

Record the in-flight write BEFORE it runs, and refuse to retry a run reaped while that record stands.

The fence 

tx_nrllm_agentrun gains a pending_effect column. A new RunTrace::beforeToolExecution() hook fires immediately before the loop invokes a tool; the runtime resolves the tool's ToolEffect and, for a WRITE, stamps pending_effect and renews the lease in one ownership-guarded write (AgentRunRepository::markPendingEffect()). When the tool's step is recorded, the same guarded write clears the fence. Read-only tools are never fenced — repeating them is always safe, so they cost no extra write. The stamp is guarded exactly like the heartbeat: a worker that has lost the run stamps nothing and stops before the side effect.

Refuse the retry 

Both retry deciders consult the fence and treat a fenced value through AgentRuntime::mayRetryAfterFence()NON_IDEMPOTENT_WRITE is not retryable, everything else (including an unset or unrecognised value) is:

  • the stale-run reaper (ReapStaleAgentRunsCommand) dead-letters a run reaped mid non-idempotent-write instead of reclaiming it onto the queue, regardless of the remaining retry budget;
  • the in-process recovery (AgentRuntime::recoverQueuedFailure()) dead-letters such a run even when the failure class is otherwise retryable — a transient provider blip does not make a repeated write safe.

An IDEMPOTENT_WRITE converges on repeat, so it is reclaimed normally.

Consequences 

  • The guarantee is "a non-idempotent write runs at most once": if it is reaped or fails mid-flight, the run fails rather than repeating it. It is NOT exactly-once — a write that completed but whose fence-clear did not persist is still failed, never silently retried (fail-closed toward not-repeating).
  • pending_effect defaults to '' so an un-migrated or fresh row reads as "no write in flight" — the fail-safe default.
  • The lease-before-op renewal also gives each write a full lease window at its start, but the durable fence — not the extra renewal — is what makes the refusal correct.
  • Idempotency KEYS for safe dedup of an IDEMPOTENT_WRITE remain future work; this ADR only guarantees a non-idempotent write is not repeated.