Repository navigation
fix(land): linearize onto the main that is coming, and admit one successor (CLOUD-369) - #369
Conversation
CLOUD-369 Admission control is half-landed: the lease serialises laps but empties the queue, starves losers, and leaves voided runs burning
Why Rescoped 2026-08-12. The admission control this issue was filed for landed while it sat in Todo: The lease decides who goes first, and nothing more. That leaves three costs the original analysis named and the mechanism does not yet address. 1. The lease serialises whole laps, so the queue empties after every merge. A waiter blocks; when it wins it starts cold — rebase, then 2. A branch can still starve. 3. A voided run burns until the next push happens to cancel it. When The three interact, which is why they are one issue rather than three. Cancelling a voided run (3) without a warm queue (1) trades CI minutes for exactly the idle gap this issue exists to forbid; and a warm queue makes the aging rule (2) cheap, because a waiter that is already rebased-and-verified loses little by waiting longer. Still not a request for a dispatcher. Coordination between sessions is impossible by construction: no lock beyond the lease ref, no coordinator, no shared state but the board and the remote. Acceptance
Explicitly rejected
What the lease already answered The three questions this issue carried while it sat in Todo, and their answers, so nobody re-derives them:
Refinement — Ready (the residue of an admission control whose lease half already landed) Refinement gate: Definition of Ready & Done. This body carries only specializations.
CLOUD-420 The landing lease is enforced only by the code path that honours it, so an agent that skips `land` still spends a full matrix
Why CLOUD-393 serialises landing behind a lease and cuts the discarded-CI-run rate. It is enforced entirely inside That is the failure this repository names on its front page: "A new rule without a runnable gate is half a change. Prose is feedforward only." The lease is a convention honoured by the cooperating path, and the threat model is the honest agent that does the wrong thing — CLOUD-200 records a session that satisfied The dominant case is residue, not defiance. Measured 05:17–05:19Z on 2026-08-12: four concurrent The enabling gap. The lease identifies a clone ( Refinement — Ready
Nothing in the table asks why a push happened, which is what makes it cover the residue case: the precondition is per job rather than per landing, so a push to a PR left ready by an interrupted The last row is the design and not a fallback: failing open costs one matrix, while failing closed on an unreadable ref stops every PR in the fleet, and a body minted before this change ages out within one TTL (120s).
Cost (§1), in the unit the invoice uses. This repository is private and every
Local execution is the unmetered tier and nothing here moves work onto the metered one. The CI-side check exists because the local one is the half an interrupted session never reaches. Test obligation
Commit / bump (§6): Blockers (§8): blockedBy CLOUD-363 — the stop conclusion is safe only once a cancelled required check reads as no verdict rather than as red, which is in flight on #302. CLOUD-393 landed in #340, so the lease exists, and adding Acceptance
CLOUD-240 The landing loop still spends CI minutes it does not need, and still wakes a human for work bash can do
Why CLOUD-238 made
There is also a feedforward gap behind #1: nothing asserts that everything CI runs is something Acceptance
Refinement — Ready (make the loop cheap, and make the cheapness a gate) Refinement gate: Definition of Ready & Done. This body carries only specializations.
|
The lease bounds confirming runs at one. That is right for cost and wrong for latency: after every merge the queue is empty, and the next branch starts cold — rebase, verify, then a full matrix — before `main` can move again. Measured on PR #325: 8 laps, 8 greens, zero commits landed. Two advisory fields, on the terms `branch:` already set (CLOUD-420) — read by CI and by waiters, never by a predicate that decides ownership: - `head:` names the commit about to become `main`, so a waiter can linearize onto the trunk that is COMING rather than onto the one the holder is about to replace. Rebasing onto current `origin/main` warms nothing; it pays the same staleness earlier. - `next:` names one admitted successor, so the matrix that overlaps the holder's merge is bought instead of started cold afterwards. The waiter writes `next:` itself through `reserve`, which is forced rather than chosen: waiters are registered nowhere, so the holder cannot name one. It re-mints the holder's lease with a single field added — same holder id, same expiry, same branch, same head — so the holder keeps holding and `mine` still answers for it. One CAS-guarded slot cannot hold two branches, so the bound is two whatever the fleet size, and `authorises` enforces it at the runner rather than by convention. `renew` and `hold` carry the reservation across each beat, or a holder would erase it within 30 seconds of a waiter writing it. `acquire` deliberately does not: a fresh turn whose predecessor's successor was carried forward would authorise a third branch, then a fourth. Refs: CLOUD-369, CLOUD-420 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172rfy7QRfyCEgw8KFYfBVJ
…essor A waiter that rebases onto `origin/main` warms nothing: the branch holding the lease is about to replace that commit, so the waiter is stale again the instant it wins — the cold window paid earlier and no cheaper. Pre-warming is a linearization, and the base worth linearizing against is the one the lease publishes as `head:`. Four changes to the lap, all inside the existing lease: - Lost the lease: rebase onto the holder's head and re-verify there, so this branch's turn costs a ready and a push rather than a rebase, a verify and a matrix. Every failure falls back to `origin/main` — a conflict against a base that may never land is information, not the one real decision the lap stops for. - The bet is recorded and settled, never assumed. A speculative rebase puts another branch's unlanded commits into this history, and fast-forwarding from there would land somebody else's work as a side effect of ours. `origin/main --is-ancestor HEAD` does not catch it: the speculated base is itself a descendant of main. So `spec_base` is settled at the top of every lap, before anything can push — won keeps the tree, pending keeps it, lost resets it. - Reserve the successor slot and, if admitted, ready and push without the lease so that matrix overlaps the holder's merge. Everything past the push needs the lease, so the pass ends there rather than duplicating the ready/push pair CLOUD-254 owns. - Re-confirm inside the hold. `acquire` waits up to a TTL, so the winner is at its most stale in the instant it wins; readying there buys a matrix the fast-forward will refuse. Asked as "did main move since this lap rebased", not as an ancestry query, for the reason above. `LAND_LOCK_AGE` carries the wait count into `acquire`, so a branch that has lost repeatedly probes a freed lease sooner — the capture effect is what turned 8 laps and 8 greens into zero commits landed on #325. `fetch_main` and `charge_wait` are extracted rather than copied: both now have two call sites, and a duplicated `die` is a second authority on what the failure means. Refs: CLOUD-369 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172rfy7QRfyCEgw8KFYfBVJ
Two defects, one in the task and one in its harness, and the harness one is why the task one looked like nine failures. The task: an admitted successor waits many laps by design, and it re-entered the ready/push pair on every one of them. Pushing an unchanged head emits no `synchronize`, so it buys nothing and drops into the `--undo` re-fire path that exists for a different case entirely. Gated on the head actually pushed, so a speculation that moves HEAD still buys the run the new commit needs. The harness: the git stub answered every `merge-base` from one file. The lap asks "am I a descendant of main"; the speculation asks "is the holder's head already in my history" and "did the base I bet on land". One rc made the second answer yes by accident, so `speculate` returned early every time and the whole chain below it failed for one reason upstream of all of them. The coverage assertion caught both new families as it is meant to: 16 stopping conditions and 12 lap-ending continues, up from 15 and 8. Refs: CLOUD-369 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172rfy7QRfyCEgw8KFYfBVJ
… the suite does Four assertion defects of my own, none in the task under test: - `call_order` joins with a trailing space, so an equality check against "ready push" could never pass. The suite's own convention is a prefix match; the once-per-head property is asserted on the push COUNT instead, which is what that case is actually about. - `lock_calls` anchored the whole line, so it counted zero for any verb taking an argument — `reserve <branch>` among them. - The continue counter is 11, not 12: the assertion matches a bare `continue` at line start, and I counted with a looser pattern. - The main-moves lever fired before the bet was placed, so the bet read as already-decided and no speculation was ever settled. It now takes the acquire number to fire from: bet first, then the world moves under it, which is the sequence the unwind exists for. Refs: CLOUD-369 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172rfy7QRfyCEgw8KFYfBVJ
…erminate The lever moved trunk once. The next lap's re-confirmation then passed, land proceeded to the answer poll, and with no terminal PR state that poll never returned — the case hung and leaked a main-watch process on every run. "Main is moving faster than a lap takes" is the condition under test, so the lever now writes a distinct sha per acquire and the case gets a wait budget it can actually exhaust. Refs: CLOUD-369 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172rfy7QRfyCEgw8KFYfBVJ
A waiter laps repeatedly while the same holder lands, and `speculate` re-bet on every one of those laps. Each re-bet overwrote `spec_undo` with a HEAD that was itself speculative — so unwinding would have restored a tree still carrying another branch's commits, which is exactly the hazard the undo exists to remove. It also re-minted a sha per lap and discarded a verify receipt for no gain, since the base had not changed. The bet is now placed once per base, and `spec_undo` is only ever this branch's last non-speculative HEAD. Settling clears both, so a bet placed after a settled one records the right undo point. Found by the two unwind cases, which could not reach the lost branch at all while every lap re-bet: the bet was perpetually pending. Refs: CLOUD-369 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172rfy7QRfyCEgw8KFYfBVJ
8faf69c to
3f9058d
Compare
|
|
/fast-forward |



Stacked on #364 — branched from its head, not from
main, so the residue builds on the cancel work rather than racing it in the same two files. #364 stays landable on its own. Rebase this ontomainonce it lands.Why
The lease serialises landing correctly, and that is right for cost and wrong for latency: after every merge the queue is empty, and the next branch starts cold — rebase,
verify, then a full matrix — beforemaincan move again. Measured on #325: 8 laps, 8 greens, zero commits landed.Pre-warming is a linearization, not a refresh. A waiter that rebases onto
origin/mainwarms nothing — the branch holding the lease is about to replace that commit, so the waiter is stale again the instant it wins. Themainworth linearizing against is the one about to exist.The economic rule throughout: speculate maximally where it is free, exactly once where it is metered. Local execution — rebase,
verify, conflict resolution — costs nothing, so every waiter does it continuously. A CI matrix is metered, so exactly one waiter buys one.What changed
land-lockgains two advisory fields on the termsbranch:already set (CLOUD-420) — read by CI and by waiters, never by a predicate that decides ownership:head:— the commit about to becomemain, so a waiter can linearize onto the trunk that is coming.next:— one admitted successor, written by the waiter itself via a newreserveverb. Forced rather than chosen: waiters are registered nowhere, so the holder cannot name one. It re-mints the holder's lease with a single field added — same holder id, expiry, branch, head — so the holder keeps holding andminestill answers for it.authorisesadmits the holder and its one successor, so the bound is two whatever the fleet size, enforced at the runner rather than agreed between cooperating sessions. One CAS-guarded slot cannot hold two branches.renew/holdcarry the reservation across each beat;acquiredeliberately clears it, or the bound would drift upward one handover at a time.landgains four things: speculative linearization onto the holder's head while waiting; a settled bet (won / pending / lost) at the top of every lap; the successor's ready+push without the lease; and a base re-confirmation inside the hold.LAND_LOCK_AGEcarries the wait count intoacquire, so a branch that has lost repeatedly probes a freed lease sooner — the capture effect is what turned 8 laps and 8 greens into zero landed.The hazard, and the invariant that answers it
A speculative rebase puts another branch's unlanded commits into this branch's history. Fast-forwarding from there would land somebody else's unmerged work as a side effect of ours — far worse than a cold window.
origin/main --is-ancestor HEADdoes not catch it: the speculated base is itself a descendant ofmain, so that check passes for exactly the case that must fail.So the bet is recorded and settled, never assumed, and settling runs before anything can push. There is no path from a losing bet to a push.
Two defects found by the tests rather than by review, both worth naming:
mainmove since this lap rebased" — a sha comparison, the same questionmain-watchanswers.speculatere-bet on every waiting lap, overwriting the undo point with a HEAD that was itself speculative, so an unwind would have restored a tree still carrying another branch's commits. One outstanding bet per base now.Tests
76/76 in
tests/land.bats(11 new), 63/63 intests/land-lock.bats(20 new). The coverage assertion caught both new families as designed — 16 stopping conditions and 11 lap-ending continues, up from 15 and 8.The load-bearing cases are the negatives: a waiter that is not admitted stays in draft (without it, "every waiter readies" would pass and spend a matrix per session); a third branch is still refused while two are admitted; a lease whose
branch/prmatch but whoseholderdoes not is not mine.Refs: CLOUD-369, CLOUD-420, CLOUD-240
Generated by Claude Code