ADR 0004: Claim lifecycle¶
Status: Accepted and partially implemented (Console 2.0 P0a, issue #150). PR #242 (issue #156) landed claim-token verification on the editor detail page and the
no-storegeneralization to/media/{uuid}/editor*paths. The full claim lifecycle (heartbeat, release) lands in a later phase.
Context¶
Review edits are guarded by a claim: the console holds a per-claim token that is
both a soft lock (one operator edits a run at a time) and the CSRF defense for
claim-gated mutations (the token rides in the URL as ?token=, so a forged
cross-site POST cannot supply it). The editor keeps this model, but changes the
addressing from run to media, and it wants multi-tab use to be safe rather than a
self-inflicted lock conflict.
Two current behaviors constrain the design. The claim token appears in the URL,
so any token-bearing page must never be written to a browser or proxy cache, and
that no-store handling is currently keyed to the /review path prefix. And a
claim acquired on a GET would mean merely opening a page locks a run, which breaks
multi-tab browsing and back/forward navigation.
Decision¶
-
Claims are
(media, run)-scoped. A claim names the specific run being edited, not just the media item, so switching runs (ADR 0003) is a distinct claim rather than an ambiguous reuse. -
Never acquire a claim on GET. Opening the editor, choosing a run, and scrolling acquire nothing. A claim is acquired on the first edit intent via a POST, so navigation and read-only viewing are free of locks.
-
Same-operator claims are reused, not rotated. A second tab or a repeated edit intent from the same operator reuses the existing claim rather than fighting it, so multi-tab editing does not deadlock the operator against themselves.
-
A bounded heartbeat renews an active claim, and a lost or expired claim surfaces a clear read-only state on a 409 rather than silently discarding an edit.
-
no-storehandling is generalized before any token-bearing/mediapage ships. The cache-control that currently keys on/reviewmoves to cover every token-bearing response, so the editor's token never leaks into a cache.
Single-operator mode (amended by #374)¶
When voxint_multi_user is off the console auto-acquires a claim on mount
via the existing POST endpoints (never GET), removing the manual "Claim for
editing" / "Claim for review" friction. Auto-acquire fires once per page
lifecycle; a lost claim (409) drops to a manual "Resume editing here" action
with no auto-retry loop. Multi-user mode retains fully manual first acquire.
The review queue hides the "Claimed by" column in single-operator mode.
Heartbeat refresh (amended by #374)¶
The heartbeat now calls a non-rotating refresh endpoint that extends the
TTL without issuing a new token. A stale tab whose token was rotated by a
newer claim receives 409 on its next refresh and drops to claim-lost state.
This breaks the two-tab heartbeat fight that would occur if both tabs
re-claimed via claim_run (same-reviewer reuse always succeeds there).
Implementation note on decision 3¶
Decision 3 says same-operator claims are "reused, not rotated."
claim_run() actually mints a new UUID on every call (the old token dies);
what is "reused" is the claim slot, not the token. The refresh endpoint
preserves the token on renewal; claim_run rotates it on ownership change.
Consequences¶
- Opening the editor is cheap and side-effect free, which is what makes the run-selection read (ADR 0003) and ordinary browser navigation safe.
- Multi-tab use by one operator works, because same-operator reuse means the tabs share a claim instead of racing for it.
- A stale claim fails visibly: the operator sees a read-only banner and a 409, not a silently dropped edit. Honest failure over a masked one.
- Undo for enroll and merge is modeled as compensating rulings on the append-only ledger, not row deletion, so the claim never needs to reach back into committed history. Generalized undo is deferred.