The current fence state. NEVER memoized (see the module header) and never
a write: this is on every gated request path, and a read path that emits
CREATE TABLE IF NOT EXISTS is a write path wearing a read's name — it
would make a fenced deployment mutate its own database to answer a
question, and would turn a read-only replica or a revoked-write incident
into an outage instead of a degrade.
A missing table and a missing row both read as open — the 0.19 upgrade
rule. Anything else that fails becomes ExecutionFenceUnreadableError, so
no caller can mistake a storage fault for an open deployment.
Bind the proof-only state to the run it admitted, conditionally.
The condition is the whole point: between the read that ADMITTED a start and this write-back the fence may have moved, or a different run may have claimed the proof. Zero rows changed means the caller must refuse the start it was about to make — the fence is no longer the one it read. Re-writing the SAME runId is admitted so a retry of an interrupted start converges instead of deadlocking on its own earlier write.
Provisioning-time seeding: write the deployment's INITIAL fence state.
INSERT-if-absent, never an overwrite. Seeding runs on every provisioning pass (including the already-owned early return), so a crash between the deployment sentinel and this row heals on the next attempt — but a re-provision of a LIVE deployment must never silently reopen a fence an operator closed, which an upsert would do.
state has no default on purpose. The failure this closes is a migration
host forgetting to ask for migration-locked and silently getting open;
making the argument required turns that into a compile-time obligation,
while still letting a host that wants an open deployment say so.
Move the fence, compare-and-set on the CURRENT state. One conditional UPDATE, so two control-plane actors racing the same transition cannot both win: the loser changes zero rows and gets the state the winner left behind.
Unlike read() this MAY create the table — a transition is a control-plane
write, and a legacy database whose fence is implicitly open has no row to
compare against. The row is materialized as open first, which is the
state the implicit reading already reported, so the CAS that follows means
exactly what it would have meant on a seeded database.
The deployment's fence, over one D1 database — the SAME database the runner's snapshots and the deployment sentinel live in, so the fence cannot be separated from the state it fences by any binding mistake.