Repository navigation
fix(source-control): re-read the pages near expiry when the verdict listing over-counts - #11921
kyle-sexton wants to merge 5 commits into
Conversation
…isting over-counts GitHub deleted carried artifacts up to about an hour before their expires_at on 2026-11-01, so the verdict reader's incremental listing held more than the live total_count, missed three times, and fell back to a full listing that no longer fits verify-cap's 120 s limit (verify:unreadable on #11903). When the incremental result over-counts, read again only the pages around the kept entries expiring within EARLY_DELETION_LEAD and let them decide the ids they span; the exact-count check is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
claude-review reviewed Incremental review: e0addf3 → 5f64928 (no findings)Scope: I read every hunk of all 7 files in What changed and what I checked
Not verified by me: the PR description's test results (3831 OK, PASS=295) and its live-API measurements were reported by the author. I had no shell, so I didn't run anything. Spend (not a finding): a cold read's worst case can now use up to 58 more GITHUB_TOKEN core-API requests (24 pages + 34 id reads). This draws on the rate-limit pool, not on billing. The PR context file contains no embedded instructions aimed at this lane. |
|
claude-security-review reviewed Security review: incremental, e0addf3 → 5f64928No security issues found in this increment. critical-findings: 0 What I checked.
Spend: up to 34 more read-only GitHub REST calls per listing on the job's No embedded instructions in the PR context. |
Brings in #11926, which changed the same verdict reader. Git merged the two without a textual conflict; the semantic resolution: - An incremental over-count first re-reads the pages near expiry (`_near_expiry`). Only a count those pages leave unsettled, above or below the live count, returns COUNT_OVER and goes to #11926's one whole listing, which refuses verify:unreadable when its top-up misses. - NEAR_EXPIRY_PAGE_CAP (24) bounds the re-read per listing, incremental read and top-up together; past it the listing stands unchanged. - An over-count takes one whole listing even when its top-up comes up short (`_over_counted`); the refusal is noted for the job as #11926 does. #11926's `.failed` note and transient and caller-limit rules are unchanged. - #11926's skewed test now splits: drops within EARLY_DELETION_LEAD settle incrementally, drops beyond it take one whole listing a job. New tests cover the page cap and the one whole listing after a short top-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the re-read in cold_cost - An incremental over-count that takes its whole listing notes it beside the saved listing (`.overcount-fallback`), whatever that listing's outcome, created exclusively so parallel processes take one. A later Reader of the job whose pages over-count refuses verify:unreadable without another whole listing. #11926's `.failed` note and its transient and caller-limit rules are unchanged. - pr-require-checks.yml cold_cost adds NEAR_EXPIRY_PAGE_CAP (24): the worst cold read is three short incremental reads, two whole listings with short top-ups and a third whose top-up over-counts and runs the re-read. 1,623 -> 1,647 at 7,522 artifacts and 264 FAILs, 2,021 -> 2,045 at 10,450 and 300; the comment, the Python and shell tests and the pr-pipeline README cap list follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The pr-verify verifier failed head What it wrote (42 lines)This pull request leaves the merge queue, and ci-status holds it out while it carries |
…-read - An id inside a region span that the region pages omit is dropped only when its own read answers 404 or an expired record; a live record keeps it, and any other answer leaves the listing over, so it takes the one whole listing. Pages read one at a time skip the boundary entry when an artifact above it is deleted between reads (#10486); a skipped live FAIL plus a leftover surplus entry no longer passes the exact count. This covers the incremental and top-up paths, both through _near_expiry. NEAR_EXPIRY_ID_READ_CAP (34, the largest surplus behind #11903) bounds the id reads; cold_cost adds it (1,681 at 7,522 artifacts, 264 FAILs). - `.overcount-fallback` is created only when the over-count's whole listing starts paging, and holds its outcome: a refusal before paging leaves it free, and a later over-count's refusal keeps the DEADLINE_UNREADABLE prefix when that listing ran out of a caller's time, so verify-cap.mjs still skips the round. - The upward walk's docstring states the code's bound, surplus // LISTING_PAGE_SIZE + 2 pages; two tests reach the climb and the ups == 0 cutoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No related issue: verify:unreadable when the artifact listing outgrows verify-cap's limit (#11903)
Summary
pr-feedback-dispatch run 41287325439 refused #11903 with
verify:unreadable. The verdict reader's cheap incremental listing (7 pages) missed GitHub's livetotal_countthree times, each time holding more artifacts than the count (13415 vs 13384, 13416 vs 13384, 13424 vs 13390). It then fell back to a full listing, which needed 134 pages and 141 s, more than verify-cap.mjs's 120 s limit.Cause: on 2026-11-01 GitHub deleted artifacts up to about an hour before their
expires_at, and not in expiry order. The carried base still held them, so the incremental result over-counted.#11926 (merged into this branch) handles the same over-count by taking one whole listing and then refusing, and notes the failure for the job. This PR runs a bounded re-read of the pages near expiry before that fallback, so the common case settles incrementally.
Artifact census (two full listings with duplicates removed: 13168 artifacts):
Options weighed:
nameparameter matches exact names only (https://docs.github.com/en/rest/actions/artifacts#list-artifacts-for-a-repository). FAILs carry other PR numbers, other heads and issue suffixes, so exact names cannot find every FAIL the cross-PRmax_failscount needs. Rejected.Fix
Changes to
babysit_verify_evidence.py, compared with main after #11926:total_count,Reader._incrementalfirst re-reads only the pages around kept entries that expire withinEARLY_DELETION_LEAD(2 h, twice the largest lead seen). Only when that cannot settle the count does it returnCOUNT_OVER, which goes to fix(source-control): stop the verdict reader's repeated cold artifact listings #11926's single whole listing. That listing refuses when its top-up misses the count.GET repos/{repo}/actions/artifacts/{id}answers 404 or an expired record. A live record keeps it. Any other answer (5xx, a timeout, a failed read, an odd body) leaves the listing unchanged, so it ends inCOUNT_OVER. Pages read one at a time skip the entry at a boundary when an artifact above it is deleted between reads (fix(source-control): the verdict reader's full artifact listing skips live artifacts at page boundaries, FAILs included #10486). Without this rule, a skipped live FAIL plus a leftover surplus entry could match the count. The rule sits in_near_expiry, so it covers both the incremental path and the whole-listing top-up path.NEAR_EXPIRY_PAGE_CAP = 24pages per listing, shared by the incremental read and the top-up. Basis: the re-read took 12 pages on the 2026-11-01 carry behind feat(pr-pipeline): name merge.rung values merge and merge-reviewed #11903's refusal, and the cap is twice that (judgment on that data).NEAR_EXPIRY_ID_READ_CAP = 34id reads per listing. Basis: the re-read drops at most the listing's surplus over the live count. That surplus was 31, 32 and 34 in the three over-counts behind feat(pr-pipeline): name merge.rung values merge and merge-reviewed #11903's refusal; the cap is the largest (judgment on that data).surplus // LISTING_PAGE_SIZE + 2pages above its start page. The docstring now says so, instead of "surpluspages".total_countreturnsCOUNT_OVER, whether it is above or below. NoINCREMENTAL_WAITSretry follows, and the listing never allows: it goes to the one whole listing, then refuses._over_countedlimits the whole-listing loop to one read when the pages over-counted. fix(source-control): stop the verdict reader's repeated cold artifact listings #11926 stopped after one only when the top-up over-counted; a top-up that came up short after an over-count still read up toLISTING_READS(3) whole listings..failednote..overcount-fallbackbesideBABYSIT_VERIFY_LISTINGis created (exclusively) only when the over-count's whole listing starts paging, after_complete_listing's budget and count checks pass. A refusal before paging leaves it free for the next Reader. Once it exists, a later Reader in the job whose pages over-count refusesverify:unreadablewithout another whole listing. That holds even when the first whole listing failed transiently or on a caller's limit, which.failedwould not note: an over-count is the exception to fix(source-control): stop the verdict reader's repeated cold artifact listings #11926's rule. When the first listing ran out of a caller's time, the marker records that, and the later refusal carries theDEADLINE_UNREADABLEprefix, so verify-cap.mjs still skips that round. The.failednote and its transient and caller-limit rules are otherwise unchanged..failedand.overcount-fallbacksidecars are shared by a job's Readers on purpose. That is what makes the rules per job, for example across pr-merge's merge-job candidates. Therm -f "$BABYSIT_VERIFY_LISTING"in pr-require-checks.yml leaves them in place. That step runs one reader per job, andBABYSIT_VERIFY_LISTINGis set only in that step (pr-require-checks.yml:6195), under the job's ownrunner.temp, so no earlier sidecar can exist there.babysit-listing-missline gainsregion_pages. Its readers (verify-cap.mjs, enable-auto-merge.mjs) match only the prefix.docs/conventions/pr-pipeline/README.mdand the module docstring now describe the order, the 404 rule and the marker.No reader rule changed, so resolve-pr.mjs and
test_verify_rules_sync.pyare untouched.Not done: the re-read still spends its page budget in group order rather than nearest the surplus first. Ordering by surplus needs a guess at which group holds the surplus, and the caps keep the result fail-closed either way.
Verification
Listing-only harness, run against the live API with the carried listing behind the refusal and verify-cap's 120 s limit, before #11926 merged and before the per-id reads were added. Calls are listing GET requests:
The harness was not re-run against main with #11926, or with the per-id reads.
API calls per pass (R34), relative to main with #11926:
cold_costin pr-require-checks.yml is now 3·FAILs + 3·13 + 1 + 3·(3·pages + 7) + 24 + 34. That moves 1,623 → 1,681 at 7,522 artifacts and 264 FAILs, and 2,021 → 2,079 at 10,450 and 300. The comment,TheColdReadHeadroomCoversTheListingCaps,scripts/ci-lanes-pr-hold.test.shand the pr-pipeline README cap list are updated to match.Tests in
test_babysit_verify_evidence.py, run against an earlier reader by restoring only the module (git restore --source=<ref>), then against the branch's reader:full!=incremental)full!=incremental)full!=incremental)LISTING_WAITS)verify:unreadable)verify:earlier-failafter the one whole listing)scripts/run-plugin-tests.sh plugins/source-control/skills/babysit-prs/scripts/engine.test.sh: 3831 tests OK, ruff clean, including thecold_costtest.scripts/ci-lanes-pr-hold.test.sh: PASS=295 FAIL=0.scripts/run-ruff.sh checkon both changed .py files: clean.UNVERIFIED observations from this work:
evidencerun for feat(pr-pipeline): name merge.rung values merge and merge-reviewed #11903 kept judging for about 19 minutes after its listing finished, until stopped.Related
🤖 Generated with Claude Code