Repository navigation
A truncated roster read is a successful one, so absence from a prefix rendered as "the runner is gone" - #9531
Merged
Conversation
… rendered as "the runner is gone" OrganizationRunnerRosterEvidence had two states -- observed, or read-unavailable -- and a TRUNCATED read had no spelling. The GitHub org runners API is paginated at 100 rows; measured 2026-08-27T21:29Z the org holds 522 registrations, so a one-page read sees 19% of the roster and enters as RosterObserved carrying a prefix. Every registration past the cut then classified as absent, which renders SlotConnectivityOrphaned, which connectivity_replace_action maps to a restart. That is the empty-observation narrow (DESIGN section 5): an observation that could not express what it failed to see, rendered as the verdict "these are gone". The adjacent case was already defended -- roster_unavailable_yields_unknown_not_detached refuses to read a FAILED read as everything-detached -- and truncation slipped past it because a truncated read arrives as SUCCESS. RosterObservedPartial carries observed_count and authority_total as a variant rather than a flag, and organization_runner_roster_from_page derives completeness at the boundary: a caller cannot hand it a prefix and reach RosterObserved, since that variant requires the observed length to equal the authority's own total. The refusal is per-question rather than per-roster -- slots inside the prefix still answer normally, and only the ABSENT arm changes -- because refusing the whole read would replace a fail-open with a fail-shut that stops reconciliation on hosts the read covered. Aggregate widths do refuse, since no sub-question of an aggregate is answered correctly by a prefix. organization_roster_is_one_org_read_note asserted the false completeness fact and is DELETED rather than corrected; its three surviving facts are enumerated into organization_roster_read_locus_note, which makes no completeness claim. Evidence by execution, compile on 472cadf at 0 blocking / 435 files emitted. Five witnesses baseline true. Mutating the partial arm's unseen answer back to Orphaned flips truncated_roster_never_orphans_an_unseen_slot to false while complete_roster_still_orphans_an_unseen_slot holds at true, so the RED measures this repair and is not tracking a globally disabled Orphaned; carrier restored byte-identically and both re-run green. RUNG: mechanically preventable for the MODEL. NOT structurally guaranteed for the fleet -- the running runner-liveness-reconcile.sh still requests per_page=100 with no pagination, and this change alters no byte that executes on any host. Next-rung trigger is a render route from this authority to that script, analogous to gunbc.runner_unit_file in #9401. This fixes no observed cancellation. It repairs a false belief about the roster. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sg8fwMAaPgbsNtns1gpVd9
gunbai-bot Bot
pushed a commit
that referenced
this pull request
Aug 28, 2026
…shrinks The namespace-wave-admission phase fails on every PR with a current merge base, reporting 53 stale admissions. None of them names anything those PRs touch. It is blocking at least #9447, #9512 and #9531, and it will block every PR from here. THE MECHANISM, and the roster's own contract already prescribed the fix. The 53 rows admit exact binding deltas from #9400's owner-qualified call-target cut. Once #9436 merged, any branch with a current merge base carries that cut on BOTH sides, so the admitted deltas no longer occur, so every row matches nothing and reports stale. The comment above the const already said what to do: "this temporary transition roster must shrink with its subject." This is that shrink. MEASURED BEFORE PRUNING, because a partial roster would have made a blanket delete wrong: 53 rows authored, 53 reported stale, 0 unadjudicated deltas. Not a subset -- no row was still carrying a live admission. (My first count said 54 and was wrong: the looser pattern also matched the struct definition line. The numbering runs 01-54 with 19 already pruned earlier by this same rule.) cargo test -p v1-compiler --test namespace_wave_admission -> 32 passed, 0 failed WHAT THIS DOES NOT FIX, AND IT IS THE MORE IMPORTANT HALF. Staleness is computed only inside WaveAdmissionOutcome::Adjudicated. On main the baseline resolves to the head, the outcome is NoSubject, and no WaveAdmissionReport is built at all -- so a spent roster is structurally invisible on the one branch everyone reads as the health signal, and its cost lands on whoever opens the next unrelated PR instead of on the wave's own author. That is exactly how these 53 came to block other lanes. Nothing will surface the NEXT post-wave roster either. I am not repairing that here. Where the staleness check belongs is a design question -- making a wall fire on main is not a change to smuggle in beside an unblocking prune -- and it is recorded at the carrier and reported as a gap in the instrument. Found by sharp-ram-84, who declined to prune it themselves because namespace_wave_admission.rs is the instrument deciding their own PR's admissibility. That was the right call: a PR that edits its own gate to go green is the shape we spent today refusing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013crMNyLvjKC2Q5UF851PKy
…-roster-truncation
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.
The defect
gunbc.fleet_runner_connectivitystates, as a note, that the GitHub org runners API "returns every self-hosted registration for the organization in one response." That is false. The endpoint is paginated at 100 rows, and measured 2026-08-27T21:29Z through the gunbai-ci App installation token the org holds 522 registrations — 75 online, 447 offline — so a caller reading one page sees 19% of the roster.OrganizationRunnerRosterEvidencehad two states:RosterObservedandRosterReadUnavailable. A truncated read had no spelling. It succeeds, so it entered asRosterObservedcarrying a prefix, and every registration past the cut classified asRunnerRemoteAbsent. Atrunner_slot_connectivity_verdict_for_incarnationthat rendersSlotConnectivityOrphaned, andgunbc.runner_capacity_planconnectivity_replace_actionmapsOrphanedtoRunnerIncarnationReplace— a restart.That is the empty-observation narrow (DESIGN §5): an observation that could not express what it failed to see, rendered as the verdict "these are gone", whose remedy is a restart.
The adjacent case was already defended, which is the receipt that the distinction was understood.
roster_unavailable_yields_unknown_not_detachedrefuses to read a failed read as everything-detached. Truncation slipped past because a truncated read arrives as success.The repair
RosterObservedPartial— a variant, not a flag, carryingobserved_countandauthority_total, so a consumer cannot ask about a slot without the completeness fact in hand.organization_runner_roster_from_pagederives completeness at the boundary. A caller cannot hand it a prefix and reachRosterObserved; that variant requires observed length to equal the authority's own total. Construction over validation (DESIGN §5) — "I saw all of it" becomes a computed fact about the response rather than a claim about the request.registration_verdict_or), two unseen answers — the only thing that differs between arms is what absence means.FleetAchievedRunnerWidthsis an aggregate over the whole roster, so no sub-question is answered correctly by a prefix; a width over a prefix is wrong, not partial, and would read as an outage.organization_roster_read_locus_note, which makes no completeness claim. A prose row takes everything in it unless its contents are named first.Evidence, by execution
Compile on
472cadfcfe: 0 blocking, 435 files emitted.Five witnesses, values read from the
gunbc runrefusal (it declines to exit-code aBool, so the value is in the diagnostic):prefix_below_authority_total_is_partialtruepage_equal_to_authority_total_is_completetruetruncated_roster_never_orphans_an_unseen_slottruefalsetruecomplete_roster_still_orphans_an_unseen_slottruetruetruetruncated_roster_still_answers_slots_inside_the_prefixtrueThe mutation is the evidence. Flipping the partial arm's unseen answer back to
Orphanedflips the RED tofalsewhile the control holds attrue— so the RED measures this repair rather than coexisting with it, and is not tracking a globally disabledOrphaned. The carrier was then restored byte-identically (cmp) and both re-run.The complete-roster control is doing the heavier work:
Orphanedmust SURVIVE the repair, because a slot absent from a roster the authority itself calls complete is genuinely gone. The minimum fix is not the smallest thing that stops the false restart — it must preserve the required refusal.Both fixtures derive
authority_totalfrom the specimen list, so neither can silently change meaning when a registration row is added. An earlier revision assertedseen == 16, a literal transcribed from the tree under test — the oracle DESIGN §5 rejects.Rung
Mechanically preventable for the model. The invalid state remains writable — an author can still construct
RosterObserveddirectly with a prefix — and safety depends on the witness executing and staying enrolled.NOT structurally guaranteed for the fleet. The running
runner-liveness-reconcile.shstill requests?per_page=100with no pagination and still acts on 19% of the roster. This PR does not change one byte of what executes on any host. Claiming otherwise would be the rung inflation §4b calls worse than sitting low.Next-rung trigger: a render route from this authority to the host script, analogous to what
gunbc.runner_unit_filedid foractions-runner@.servicein #9401. That route does not exist, and building it is a replacement migration rather than a defect fix. The only alternatives available today — hand-editing a host, or committing a second hand-authored shell beside the model — are both forbidden (DESIGN §6, out-of-band actuation and parallel authority). Escalated to the operator; not resolved here.What this PR does NOT claim
It fixes no observed cancellation. It repairs a false belief about the roster. That is the whole claim.
The credential-deletion mechanism that motivated the investigation — an orphaned
Runner.Listener, survivingKillMode=process, deleting a live incarnation's.credentialsmid-job — is established at n=2 by pid-level evidence, and does not explain the 385completed with result: Canceledevents on srv4 since 2026-08-25.Both are OTHER-PID-DURING with the reconciler's own
WEDGED … restartedline inside the window.The commit message on this branch says the deletion is latent until "the ~50 min OAuth refresh". That is wrong, and the second specimen is what falsified it. The latency is the time to the next refresh — anywhere in (0, ~50] depending on where in the cycle the deletion lands — which is why the srv4-04 floor died at 41 minutes rather than past 50. The commit is left unamended because history is immutable; this paragraph supersedes it.
The corrected predicate contains no duration term: a job dies iff an orphan deletes its credentials during the job and a token refresh occurs after that deletion and before the job ends. Duration is a proxy only because a longer job is likelier to contain a refresh. That agrees with the independent finding from the API side — incarnation-epoch decoding across 58 floors showed successful non-srv4 floors on incarnations 57–115 minutes old, so no age clock kills jobs — two instruments, same conclusion.
A counterexample still sits inside our own sample and is stated rather than closed: srv4-08's 54.0-minute floor cancelled at 20:31:06 with no orphan removal and no
rsaparamsevent — every credentials removal same-pid-after-completion. Cause unidentified.Duration and job type are confounded in every specimen produced during this investigation. Both candidate controls (srv4-11/srv4-18, srv4-08 back-to-back) vary job type along with duration. This is a limitation of the evidence base, not a caveat on a conclusion.
Also refuted during the work, from inside our own data: srv4-01 read
registration=absenton every reconciler tick while running a 61.9-minute floor to a realFloorRefusedverdict. Invisibility makes a slot eligible for a false wedge; it does not kill it. The kill additionally requires an actual restart landing inside a running job.Two findings that outlast this defect
1. A file asserting it is version-controlled when no repository holds it.
/opt/actions-runner/runner-liveness-reconcile.shopens with "Load-bearing: canonical, version-controlled copy... edit here (not the installed copy) and re-run the installer." It is in no repository.git log --allforlivenessingunb-ai/ctrlis empty andinstall-actions-runner.shnever references it. The claim fails toward "someone else already owns this", which is why nobody found it.2. The census built to catch exactly this class was structurally blind to it — the more valuable of the two, because it generalises.
gunbc.ctrl_host_provisioning_censusexists to enumerate every fact the ctrl host-provisioning surface decides, one row each, owned or unowned-with-reason. It has no row for this script, because its denominator is the ctrl repository and the file is not in it. Anything provisioned on the hosts and absent from the repo is invisible the same way.Enumerated on srv2 — host-present, repo-absent:
The last two are the unit that has been panicking on every 15-minute tick (
workspace_root: / is not inside a git checkout,v1_compiler/cli_run.rs), converging nothing for as long as the journal reaches back.A second member, different mechanism, same blindness. Measured on srv1 2026-08-27 and reported in #9506:
/opt/gunbc/gunbccarries 5650 divergent paths — 1964 tracked-and-absent, 1731 unknown to HEAD, 1500 differing from HEAD in the index and from the index in the worktree. Both status columns wrong, from one execution, no concurrency. Three transports over one repository and nothing makes them agree.The pair is what makes this a class rather than an anecdote, because the two members are invisible for different reasons:
Both are host state that no repository authority produced.
A third finding, about how the first two survived: a stale claim repeated reads as corroborated
#7940("PR B: runner connectivity recovery — replace/repair controller (model + witnesses)") merged 2026-08-07.gunbc.runner_connectivity_recoveryports the ctrl watchdog — its own note says so — and contains noHostEffect, no realize, no emit, no render. Model and witnesses, no production caller. Meanwhile the script it "subsumes" has run unsupervised ever since.The subsumption is asserted in the present tense in two carriers:
The second occurrence does more damage than the first. A stale claim repeated in two carriers reads as corroborated: a reader greps, finds two independent-looking assertions, and stops. That is the sentence that stopped anyone looking at the reconciler, doubled.
The contrast that makes this actionable rather than a complaint:
gunbc.live_deploy.repository_convergence(#9506, merged same window) is in the identical state — model, witnesses, no production caller — and declares it in the carrier itself: "NOTHING HERE IS BOUND. No production caller reaches this module." It cannot be read as done. Same state, opposite legibility, and the legible one costs nothing later.Neither finding is repaired here. All are named so they rank.