fix(gatedrive): stale predecessor receipt must not rotate or free a live worktree slot (change 0453) - #333
Merged
danielhanold merged 7 commits intoSep 25, 2026
Conversation
…n guard Docket-Plan-Path: docs/superpowers/plans/2026-09-25-two-successors-sharing-one-stale-predecessor-receipt-can-sti.md
… executing slot (change 0453) Apply reserveScopeDrive's staleness predicate in admitScopedWorktree's executing arm before rotateWorktreeExecutionForSuccessor, so a second successor sharing a now-stale receipt can never rotate (and later release) the first successor's live slot. A scope load failure fails closed. TestSuccessorAdmissionFailureLegsReleaseRotatedSlot pinned the old rotate-then-release behavior for a stale receipt; it now asserts the pre-rotation refusal and reaches the post-rotation release leg through a scope closed between precheck and admission (epoch-gate seam).
…ness guard (change 0453)
…oses to a stale receipt (change 0453) Review finding (important): two successors presenting the same predecessor receipt P on an unserialized (epoch-less) scope could still free a live slot. S2 passes the pre-rotation staleness guard while P is current and rotates the slot to T (reserved); S1, holding the same receipt, finds a same-scope reserved slot and adopts T, wins reserveScopeDrive, retires P, launches, and confirms T executing. S2's reserveScopeDrive then refuses ErrStalePredecessor, which isSameScopeRaceLoss does not cover, so S2 (rotated) released T and freed S1's executing slot. A freshly reserving successor had the same leg. admitScoped's reserveScopeDrive failure leg now also keeps the reservation when siblingMayHoldReservation holds: the error is ErrStalePredecessor on a successor AND the reloaded scope's PriorDriveID still names the receipt's drive (a sibling consumed this receipt, necessarily by adopting T for a rotated start, possibly still on its way to the scope slot), or the scope's current drive carries T as its AdmissionToken (a later drive adopted T), or either record is unreadable. Keeping is the fail-closed direction: a leaked reserved slot is adopted by the scope's next start and blocks other admissions until recovery, while releasing an adopted one frees a live slot. A stale receipt with neither sign still releases, so a bogus receipt leaks nothing. The admitScopedWorktree guard comment no longer claims the unlocked scope read is sufficient: it excludes only a successor arriving after a sibling launched. TestStaleSuccessorLeavesSiblingAdoptedReservation drives the interleaving deterministically through a package-private scopedAdmissionHook (fired between worktree admission and reserveScopeDrive): rotated and fresh sibling-adopts- and-wins, a later drive adopting after the scope moved on (token clause only), and an adopter still pending when S2 loses (PriorDriveID clause only). All four subtests are red with the guard disabled; dropping either clause reddens exactly its subtest.
…the pre-rotation guard (change 0453) Review finding (minor, "duplicated-gate-copies-the-whole-predicate"): the pre-rotation guard in admitScopedWorktree copied only the final staleness clause of reserveScopeDrive's ordered predicate (capability, closed, half-filled receipt, reserved ErrScopeBusy, pending ack ErrUnresolvedLaunchTransition, then staleness). A closed scope was reported to a late stale successor as ErrStalePredecessor instead of ErrScopeClosed, and a receipt naming the current drive that failed an earlier clause (a half-filled receipt, a closed scope, a wrong capability) passed the guard and rotated the live slot before the authority refused it. Extract the ordered checks into one pure helper, scopeReserveRefusal(rec, childCapability, receipt, op), called by reserveScopeDrive under the scope lock and by the guard on its unlocked snapshot; the guard refuses on any non-nil result without touching the slot. Every clause is kept at the guard: none can turn away a successor the authority would admit (capability hash is immutable, close is one-way, the authority refuses a reserved or pending-ack slot whatever the receipt, and the scope never moves back). The fail-closed LoadScope error leg and siblingMayHoldReservation are unchanged. TestSameScopeSuccessorGuardAppliesWholeReservePredicate covers each earlier clause (closed, capability, reserved, pending ack with a stale receipt; a half-filled and a closed receipt naming the current drive): typed refusal, slot and scope record untouched, and parity with reserveScopeDrive's verdict on the same snapshot. All six subtests are red on the single-clause guard. TestSuccessorAdmissionFailureLegsReleaseRotatedSlot's post-rotation release leg now closes the scope through scopedAdmissionHook (after the rotation), since a scope closed before admission is now refused before rotating.
danielhanold
deleted the
fix/two-successors-sharing-one-stale-predecessor-receipt-can-sti
branch
September 25, 2026 10:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a race in gate admission where a successor start holding a stale predecessor receipt could rotate, and then release, a worktree slot that another start was actively running a gate in. That broke one-execution-per-worktree.
scopeReserveRefusalso the two sites cannot drift. If the scope record can't be read, admission fails closed. A refusal never touches the slot.ErrStalePredecessorno longer releases a reservation that a sibling adopted: the case where the scope shows the same receipt consumed, or the current drive holds this token. Epoch-less scopes are where that interleaving is reachable.Review (docket-review-deep)
Residual (suspected, not reproduced): a check-then-release window remains for freshly reserved successors on epoch-less scopes, when the scope advances two drives during a stall. See the results file.
Build evidence
command: go run ./cmd/docket development test
result: green
head_sha: ade8b51
ran_at: 2026-09-25T10:05:17Z