Skip to content

The claim doors have never serialized against each other, and the record logic is about to have three copies #402

Description

@bdelanghe

Raised by the check-in session while reviewing desk#92, which adds a third dispatch door so a session without .github attached can claim (desk#91). That PR states, honestly, that its concurrency group cannot serialize against this repo's copy. The measurement below says the existing two doors never serialized against each other either, so the PR widens a hole rather than opening one — and the repair belongs here, not there.

Not claimed. Recorded so the next reader does not re-derive it.

Measured, from the files

door runs in concurrency.group group for .github-private#929
claim-ticket.yml (door 1) bounded-systems/.github claim-${{ inputs.repo }}-${{ inputs.issue }} claim-.github-private-929
_claim.yml via claim.yml (door 2) bounded-systems/.github-private claim-${{ github.repository }}-${{ inputs.issue }} claim-bounded-systems/.github-private-929

Two independent reasons they cannot collide: GitHub scopes concurrency groups per repository, and the two expressions do not even produce the same string — one keys on the target repo, the other on the calling repo.

CLAUDE.md describes door 2 as the route for .github-private issues when door 1 is unreachable. Both can therefore target one issue, and nothing has ever ordered them.

Why the backstop does not catch it

claim-ticket.yml's read-back is:

after="$(gh api "$api")"
if [ "$(jq -r '[.labels[].name] | contains(["claimed"])' <<<"$after")" != "true" ]; then

That asserts a claim recorded, not that this run's claim did, and the label add is idempotent. So in a genuine race both runs read the issue as unclaimed, both add the label, both post a claim comment, both read back a present label, and both exit green. Two claimants each hold a truthful-looking record.

Why it has not bitten

Since #264 every mechanized claim needs a distinct passkey approval, and the keeper renders {action, repo, issue, claimant} to the approver. A cross-door race needs one human to approve two ceremonies for the same issue inside the read-write window. The human is the de facto serializer, which is not a mechanism and is not what the ladder claims.

Proposed repair, cheaper than a lease

desk#92 concludes "the repair is one door with a lease behind it." A lease is the right end state and is not needed for this. Two steps, both small, both in this repo:

1. Make the read-back assert sole ownership. After writing, re-read the issue's comments, select those carrying the <!-- claim-ticket --> marker, and order them by created_at then comment id. If the earliest is not this run's claimant, delete this run's comment, remove the label if this run added it, and fail naming the holder. Both racing runs compute the same winner from the same record, so the loser stands down deterministically. This works across doors and across repositories, because it is a property of the issue's own record rather than of a concurrency group — which is exactly what the table above shows the groups never were.

2. Extract the cross-repo claim step into _claim-ticket.yml (workflow_call), and make every dispatch door a thin caller. Today the record logic exists twice: inline in claim-ticket.yml, and again in _claim.yml. desk#92 copies the first byte-identically, making three. CLAUDE.md already tells .github-private's door "fix claim behaviour there, never here", and claim.yml already is a thin caller — this makes the same true of the ticket door, so step 1 lands in one file instead of three.

Do 1 first; it is the correctness fix and it is a dozen lines. 2 is what stops the next door re-copying it.

Not in scope

Whether desk#92 should merge. It should, on its own terms: it inherits this gap rather than creating it, and holding a cold-start door for a pre-existing race in a different repo would be the wrong trade. Its caveat text is accurate about the mechanism and wrong only in implying a single-door world preceded it — corrected on that PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions