Skip to content

[finding] Two open PRs held one source file across lanes for ~4h; the later landed first and left an ACCEPTED PR conflicted — no mechanical surface objects, and the single-claim gate is right not to #12286

Description

@os-trump

Measured today while running the domain:cli landing window. Filed unassigned and ungraded — severity not judged, no fix proposed; both are triage's.

The incident

PRlanefile window on packages/runtime/src/domains/automation.ts
earlier#12139 (card #11666)domain:cliPR opened 10:57Z, still open
later#12190 (card #12156)domain:servicescard cut 11:25Z, MERGED 15:26Z

Both edit that one file. The later one landed first, and the earlier one — reviewed, ACCEPTED, 32/32 checks green since 14:38Z — is now unlandable:

$ git log --oneline 2c4c59ed..origin/main -- packages/runtime/src/domains/automation.ts
c68c6708a3 feat(runtime): flow clone action — whole-definition copy under a new name (#12190)
$ git merge-tree --write-tree origin/main refs/remotes/pr/12139
Auto-merging packages/runtime/src/domains/automation.ts
CONFLICT (content): Merge conflict in packages/runtime/src/domains/automation.ts

⚠️How it was found is part of the finding. Not by a red check and not by a notice — by a seat noticing that a green PR had been passed by six neighbours and probing merge-tree locally. GitHub reported mergeable_state: "unknown" throughout (lazy computation), so the seat's first reading was the wrong one ("auto-merge got cleared") and it armed auto-merge on a dirty head before re-probing. The state that would have said "dirty" was never volunteered.

The gate is innocent, and this is the part that must not be misread

No other open PR may claim the same single-writer path was green on #12139 and was correct to be. scripts/check-single-claim-paths.mjs declares SINGLE_CLAIM_PATHS with exactly one entry — .objectui-sha — and its header carries the measurement that forces that scope. Over PRs 8936–9584, pairs with overlapping open windows sharing at least one changed path:

any shared changed path (repo-wide) -> 68 concurrent pairs
the declared list -> 0 concurrent pairs

Its own top repo-wide collisions are the lock file (33), a plugin manifest (21), the root manifest (15) — ordinary concurrent work in a repo taking ~18 merges a day. And it names this limit itself: "It does not catch: two PRs that fix the same thing DIFFERENTLY on unlisted paths."

⛔ So "add automation.ts to the list" is not the reading. The header forbids exactly that ("Anything that is merely hot … does NOT belong here"), and a list that grows per-incident stops being a declaration. The gate did its declared job.

What is actually unheld

fold-or-serial / ruling ① — same file ⇒ hard serial, no region exemption — is a PM-seat rule enforced by each seat against its own dispatches. A seat measures its fences by walking open PRs, which works, and the data was there: PR #12139 was open from 10:57Z, half an hour before card #12156 was even cut. Nothing hid it.

What is missing is anything that requires the walk to be cross-lane, or that makes an in-flight file hold legible outside the seat that declared it. The fence lives in a claim comment and a seat post — prose, in one lane's thread. A second seat that never asks the question gets no signal from anywhere.

Not established: whether the domain:services seat performed a collision check and judged the regions disjoint (they nearly are — both sides are additive), or did not perform one. I did not ask and I am not accusing; the two possibilities want different answers and I cannot tell them apart from the artifacts.

A second, smaller reading in the same incident

Lane charters and dispatch are not coupled. packages/runtime is named in the domain:cli lane charter, and #12156 is labelled domain:services — the lane label rides the card, and the card's file face is only measured, if at all, by the dispatching seat against its own scope. Whether a card should be re-laned when its file face lands in another lane's charter is a separate question from the fence one, and I am recording it rather than answering it.

What it cost, stated precisely so it is not inflated

Not a correctness cost. Both conflicts are additive on both sides and the merge is semantically safe — verified, including the one thing that could have made it unsafe: the arm #12190 added is selected by the predicate #12139 introduced, and isFlowEnablementWrite is spelled method === 'POST' && parts[1] === 'toggle' && parts[0] !== 'trigger', so the clone door keeps the authoring sentence rather than inheriting the enablement one.

The cost is a landing round: a finished, reviewed, green PR needs a developer slot again ~5 hours after its work was done, in a lane at its in-flight ceiling — plus the interval in which it looked landed-in-progress to anyone reading the board, because an armed auto-merge on a dirty head is indistinguishable from an armed one on a clean head.

Not established

  • Frequency. This is one measured pair; whether cross-lane same-file holds are common is unmeasured, and the 68-pairs-per-300-PRs figure above is the repo-wide changed-path rate, ⛔ not the rate of this shape.
  • Whether the shape is cheap to make legible at all. A cross-lane read costs a walk of every open PR's file list on every dispatch, and the single-claim gate's header is explicit that it pays for its cheapness by asking the narrow question first.
  • Severity.

Measured on origin/main at fb5fbb8514.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions