Skip to content

fix(land): settle a speculation the lease abandoned, not just one main outran - #381

Merged
wenzowski merged 2 commits into
mainfrom
claude/semgrep-guardian-research-t6va58
Aug 12, 2026
Merged

wenzowski merged 2 commits into
mainfrom
claude/semgrep-guardian-research-t6va58

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Closes CLOUD-495.

Why

A speculative bet was resolved only by asking whether main had moved: won
when the base became an ancestor of it, lost when it moved elsewhere,
pending otherwise. A holder that goes away while main stays put is none of
those, so it read as pending — and pending returned 0 forever.

The waiter kept the borrowed tree, later won the lease over an unmoved main,
and the re-confirmation inside the hold asked only the question that had already
been answered. What the fast-forward then landed was another branch's unmerged
commits. Measured on this loop: a holder whose CI died in a provider incident
held the lease going nowhere, a sibling linearized onto its published head, and
the two branches ended at the identical SHA with neither able to land.

What changed

  • Pending is a positive claim now. bet_is_live re-reads the lease: a bet is
    live only while the branch it names now is somebody else's and still carries
    the base. Lease freed, lease passed on, lease won by us, lease unreadable — all
    stale, all unwound. Fails closed everywhere.
  • The remote is unwound too. A bet that was already published is re-drafted
    before its branch is rewound, so the correction buys no matrix and no stop
    leaves origin holding commits that are not landing.
  • The bet is re-settled inside the hold, after the main-moved check and before
    the ready, the push and the fast-forward comment — the last computable moment.
    An unwind there moves HEAD, so the lap ends rather than pushing a commit the
    branch no longer has.

A successor readying a live borrowed tree is unchanged; that is the mechanism's
whole economy. What changed is that the bet must be fresh, not absent.

Tests

Six new cases in tests/land.bats, plus three stub levers (rc.spec_live,
rc.fetch_live, lease_abandons). Five of them fail against the previous land
and pass against this one; the sixth is the negative that stops the unwind firing
on every lap. The lap-count sensor caught the new continue and is updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QHmaY5qiHMntdm2A2d6vTW

A backgrounded task's exit already re-invokes the session with its status,
so a wait built to detect that exit duplicates a guaranteed event — it
cannot fire earlier and cannot carry more information. Measured
2026-08-12: one session had nine such waiters live at once, four shadowing
`verify`, two `land`, one a merge, one `bats`, each a hand-rolled
re-implementation of a notification the harness was already going to
deliver.

`mise run alive` (CLOUD-425) is the answer for "is it still going, and
where is it", and it beats `pgrep -f` on correctness rather than manners:
`pgrep -f` matches a command-line substring, so in a container with
sibling sessions it answers "some process matches this string", which is
not the question. A registered task whose process is gone reports
`crashed`, which no `pgrep` can express.

Lands in the section that already names the two habits which destroy the
supervised-process interface and fail green, because this is the third and
it fails the same way — plausibly, with a green exit.

Refs: CLOUD-489, CLOUD-490
@linear-code

linear-code Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
CLOUD-495 An admitted successor publishes the holder's unlanded commits, and cannot rewind when that base never lands

Why

A waiter pre-warms by linearizing onto the lease holder's branch tip, and the admitted successor then readies and pushes with no lease (land:847-857, :905, :911). speculate runs a few statements earlier in the same lap with no re-settle between, so the pushed branch carries the holder's unlanded commits by construction — pinned as intended behaviour by tests/land.bats:1811.

Speculation is justified as free because local execution costs nothing. Publishing it is not free: it makes one branch's published history depend on another branch's unlanded work, and nothing undoes that coupling.

The severe case is a bet that stays pending forever. settle_speculation (land:299-322) has three arms: won (base is an ancestor of origin/main), lost (origin/main moved elsewhere → unwind to spec_undo), and pending (origin/main unchanged → keep the tree, return 0). Nothing else resolves a bet — no lap budget, no re-read of who holds the lease, no check that the holder's PR is still open.

So when the holder abandons while main stays put — died, went red, lost the lease — the bet reads pending indefinitely. The waiter keeps the borrowed tree, later wins the lease, and the in-hold re-confirmation at land:894 asks only whether origin/main moved. It did not. The waiter comments /fast-forward and lands another branch's unmerged commits onto main.

That is a correctness hazard for trunk, not a coupling annoyance. The header comment at land:268-274 names the hazard; the implemented guard covers only the "main moved" arm of it.

The conflict case is already handled and is not this: a speculation that fails to apply resets to the origin/main-linearized HEAD (land:354-364), on the reasoning that a conflict against a base that may never land is information rather than an error. The gap is a rebase that applied cleanly and whose base then never landed — worse precisely because nothing signals it.

Measured

  1. A branch took the lease and pushed one docs commit.
  2. Its CI died in setup — every job failed fetching a pinned release artifact during a provider incident. It held the lease while going nowhere.
  3. A sibling linearized onto that published head and pushed three commits on top. Its branch now carried another branch's unlanded commit as its base.
  4. The first branch's next lap linearized onto the sibling's head — a no-op, its own commit already being an ancestor — leaving both remote refs at the identical SHA and two PRs as duplicates racing the same lease.

A setup failure on the holder propagated into a sibling's published history. Neither branch can now land independently without rewriting the other's published base, and the fleet-wide bound of holder-plus-one-successor is silently exceeded whenever this repeats.

Two supporting facts

No remote-side unwind exists. After the push at land:911, only a later ordinary push rewrites the branch, so a die or a LAND_MAX_LAPS exit leaves origin holding the foreign commits with nothing scheduled to remove them.

The base taken is not the base advertised. land:820 reads land-lock peek branch and speculate (land:328-371) fetches that ref and rebases onto whatever tip it finds. The lease does mint a head: field (land-lock:274-278, served by peek head at :852) but peek head has no production consumer — its only references are two bats cases. So the base actually taken can differ from the head the lease advertised, and is recorded nowhere.

Mechanism

The successor needs a rewind, decided before it readies:

  • Detect an abandoned base. The speculative base is stale when it is neither an ancestor of origin/main nor the head of the current lease holder. Computable from the lease ref, origin/main, and the branch's own merge-base — no coordinator, no new shared state.
  • Rewind to the branch's own commits. Drop the borrowed range and re-linearize onto the current holder's head, or onto origin/main when the lease is free.
  • Bail to origin/main on conflict, the fallback already sanctioned for the conflicting-speculation case, applied to the rewind.
  • Never buy a matrix for a borrowed tree. The check runs before ready, so a confirming run is never spent on a tree containing another branch's unlanded work.

Two smaller defects surfaced by the same instance, recorded here rather than filed separately:

  • Two PRs at the identical head both hold claims and both spend matrices, and nothing notices. No predicate anywhere asks whether HEAD contains commits reachable from another open PR's branch but not from origin/main.
  • The red-CI message read "a red run on a verified branch means verify and CI disagree — fix the mismatch locally" when CI never ran at all. That is the same wrong-advice shape already tracked for the lease-stopped variant; this is a second instance of it, not a new issue.

Refinement — Ready (a summary)

  • Source of truth (§1). mise-tasks/land remains the one authority for admission and for what a successor publishes; settle_speculation stays the single place a bet is resolved. The rewind extends the existing lease protocol — a second admission authority is the failure mode, not an implementation option.
  • Computable predicate (§2). Each criterion resolves to a command and an exit code, never a model verdict. A bet is stale when its base is neither an ancestor of origin/main nor the tip of the branch the lease currently names — decidable from the lease ref, origin/main, and the branch's own merge-base, with no coordinator and no new shared state. The remote-side unwind is decided by comparing the pushed head against that same predicate.
  • Output & exit (§5). Pointer-only: a count and a PR number, never a diff and never a run log. Exit codes follow the one contract with no per-verb exception.
  • Commit / bump (§6). fix → patch until 0.1.0.
  • Test obligation (§7). New cases in tests/land.bats, each asserting an exit code: a published bet whose holder fails while main stays put is detected and unwound rather than read as pending; the unwind re-linearizes onto the branch the lease now names, or onto origin/main when the lease is free; a conflicting rewind bails to origin/main; a bet whose base is still the current holder's tip does not unwind; and no path comments /fast-forward on a head containing commits absent from both origin/main and the holder's tip. Existing coverage stops short of all of these — :1772 loses the bet only via main moving and switches the successor path off, while :1811 and :1883 leave the bet pending and never resolve it. The harness split at land.bats:221-255 (rc.spec_rebase vs rc.spec_ancestor) is the fixture to extend.
  • Blockers (§8). None.

Done

main carries the rewind and its cases, landed by fast-forward with CI green on the merge commit. A successor whose speculative base is abandoned re-linearizes instead of publishing a branch nobody can land.

Review in Linear

…n outran

A bet was resolved by asking whether `main` had moved: won when the base
became an ancestor of it, lost when it moved elsewhere, pending otherwise.
A holder that goes away while `main` stays put is none of those, so it read
as pending — and pending returned 0 forever. The waiter kept the borrowed
tree, later won the lease over an unmoved `main`, and the re-confirmation
inside the hold asked only the question that had already been answered.
What the fast-forward then landed was another branch's unmerged commits.

Pending is now a positive claim: a bet is live only while the branch the
lease names now is somebody else's and still carries the base. Lease freed,
lease passed on, lease won by us, lease unreadable — all stale, all unwound
before anything can be readied, pushed or merged. A published bet is
re-drafted before its remote is rewound, so the correction buys no matrix
and no stop leaves origin holding commits that are not landing.

Refs: CLOUD-495
@wenzowski
wenzowski marked this pull request as ready for review August 12, 2026 23:12
@wenzowski
wenzowski force-pushed the claude/semgrep-guardian-research-t6va58 branch from 17809e8 to 795847b Compare August 12, 2026 23:13
@sonarqubecloud

Copy link
Copy Markdown

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 795847b into main Aug 12, 2026
8 checks passed
@wenzowski
wenzowski deleted the claude/semgrep-guardian-research-t6va58 branch August 12, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants