Repository navigation
perf(pr-merge): list only new artifacts on top of a trusted carried listing - #10488
Conversation
…isting The pr-verify verdict reader paged every repository artifact on each cold read (69 pages on 2026-10-27) on the job's GITHUB_TOKEN. It now builds on the newest whole listing begun within three hours: its own, the run's saved one, or the pr-merge-verify-listing artifact the last scheduled or hand-started pr-merge run on main uploaded (listing-carry.mjs, found as the head-ref map is). It reads pages newest id first down past that listing's highest id until an artifact created two hours before it was read, drops entries the pages no longer hold or that expired by the first page's Date, and keeps the result only when it holds exactly the first page's live total_count. Any other count, a missing, older or malformed base, or a failed read lists every page as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… count A whole artifact listing can skip the artifact at a page boundary when an earlier one expires while it pages; on 2026-10-27 two of three whole listings skipped live artifacts, FAIL verdicts among them. Each whole listing is now deduplicated by id, added to the ones read before it and kept only when a top-up from the first page shows it holds the live total_count. After three whole listings that never hold it the pull request refuses verify:unreadable. Closes #10486 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
683b42a to
fb1ca60
Compare
The listing-count test stubbed _read_listing, so the new completeness top-up read a real first page. It now stubs _complete_listing, with no base to build on. 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-security-review reviewed No security issues found in this change. Review was read-only: I had no shell and ran no tests. critical-findings: 0 Trusted paths read. I read the whole change of Carried listing as a trust boundary. A forged or stale carried listing could hide a FAIL from the verdict reader, but nothing untrusted reaches it.
Live-count guard fails closed.
Gate-file registration. Token and permissions. I saw no token scope or Spend, not a hold. The cold-read headroom check in |
|
claude-review reviewed No findings on this head (dfa762c). I read the full diff and the surrounding code it depends on. I did not run any tests: this lane has no shell, so every test count and live-probe result in the PR body is author-claimed and unverified by me. Scope
Closing issue #10486 has no separate acceptance-criteria list. Its Scope paragraph is met in code, as far as reading shows:
Trusted-path scrutiny (
Spend. This uses only the job's GITHUB_TOKEN request budget and a 1-day, roughly megabyte-sized artifact per scheduled pr-merge run. It enables no paid plan, billing setting or billed resource, so there is no cost hold. Left for a person. Nothing blocks this review. The size and age constants ( Manifests. The diff changes no |
The 900-request headroom per queue entry came from one 69-page whole listing. A cold reader now reads up to LISTING_READS (3) whole listings, each up to 1 + len(LISTING_WAITS) (3) attempts, with a top-up after each, after an incremental try on a carried listing. Its worst case is 793 per-FAIL reads + 13 + 3 x (3 x 77 + 7) = 1,520 requests, so ci-status reserves 1,520 per entry, and a test pins that figure to the caps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The pr-unblock lane stopped on this pull request and needs a person: the merge stages a change under .github/workflows/, which the lane does not push.
To resume, deal with the reason above, remove the gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=10488 -f activity=resolve-conflicts |
…al-listing-resign # Conflicts: # plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_verify_evidence.py
|
The pr-unblock lane escalated this pull request as |
|
The pr-unblock lane stopped on this pull request and needs a person: a conflicted path is a workflow, submodule config or a settings file, which the lane does not resolve.
To resume, deal with the reason above, remove the gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=10488 -f activity=resolve-conflicts |
…al-listing-resign # Conflicts: # .github/scripts/pr-merge/workflow.test.mjs # .github/workflows/pr-require-checks.yml
|
The pr-unblock lane escalated this pull request as |
…al-listing-resign
GitHub's artifact schema marks created_at and expires_at nullable, but the reader refused any listing holding such an entry, so one undated artifact made every whole listing's top-up fail and every candidate refuse verify:unreadable. An entry may now carry null dates: it is never pruned by date, and paging stops only on a dated entry, so the result is still checked against the live total_count. A missing date key or a malformed date still lists every page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al-listing-resign
Main's #10470 sets findings-* retention to 1 day; this branch chose 3 days after that change, so a manual re-run of the file job still finds the findings over a weekend (claude-review thread on artifact-retention.json). Keep 3 in the workflow and the table. Main's #10488 adds the pr-merge-verify-listing upload (1 day, read by the next run through listing-carry.mjs); add its row so the retention checker passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #10486
Summary
The pr-verify verdict reader (
babysit_verify_evidence.py) paged every repository artifact on each cold read: 69 pages on 2026-10-27, on the job's GITHUB_TOKEN. pr-merge's merge job, enable-auto-merge and ci-status's merge-queue step each list cold, at about 5 to 12 listings an hour. That is 380 to 910 of the token's 1,000 requests an hour, and the count grows with the artifacts.The reader now builds each listing on a trusted earlier one. It reads only the pages since that listing, and keeps the result only when it holds GitHub's live
total_countexactly. Otherwise it lists every page.It also closes #10486, a HIGH gap in today's code: a whole listing can skip a live artifact at a page boundary when an earlier one expires while it pages, and on 2026-10-27 two of three whole listings skipped live FAILs, so the gate could allow past a FAIL. Now no judgment uses a listing, whole or incremental, whose unique ids are not exactly the live
total_count.Fix
Carry. The merge job of a scheduled or hand-started pr-merge run uploads its last saved listing as
pr-merge-verify-listing(one day)..github/scripts/pr-merge/listing-carry.mjsfetches it the way the head-ref map and the run cache are fetched (fix(pr-merge): carry the run cache through a trusted artifact, not an Actions cache #10292, fix(pr-merge): carry the verifier reads' run cache through its own trusted artifact #10327):rotation.mjsnewestTrustedRunplusonMain, never actions/cache. It writes the file toBABYSIT_VERIFY_LISTING_CARRYin the merge job, enable-auto-merge and ci-status.Increment (
Reader._incremental). The base is the newest whole listing begun withinLISTING_CARRY_MAX_AGE(3 h): this Reader's own (sorefresh()before an allow is incremental too), the run's saved one, or the carry. The listing comes newest id first, but ids are not ordered bycreated_at. Pages are read down past the base's highest id until an artifact createdLISTING_OVERLAP(2 h) before the base was read. Those pages decide every id down to the lowest one they hold, so an overwrite's deleted copy drops out. Duplicates are merged by id.Prune. A base entry below that point is dropped when it is marked expired or its
expires_athas passed by the first page'sDateheader.babysit_gh.gh_api_with_headersreturns the headers.Live completeness guard. The result must hold exactly the first page's
total_count. It lists every page as today on any of these:DateA forged carry that drops a FAIL must also add a fabricated entry to match the live count, and forging it needs code running in main's pr-merge run. This rule now sits in the module docstring ("Carried reads") as the one exception to "a carried read only counts a FAIL". The
_run_statecomment points there.Whole listings held to the live count (
Reader._complete_listing, fix(source-control): the verdict reader's full artifact listing skips live artifacts at page boundaries, FAILs included #10486). A whole listing is deduplicated by id and added to the whole listings read before it, then checked by the same top-up from page 1 (fresh pages decide the newest ids,expires_atis pruned by the page'sDate, and the total must equaltotal_count). A mismatch reads the whole listing again and unions it. AfterLISTING_READS= 3 whole listings that never hold the count, the candidate refusesverify:unreadable; it never allows. Basis for 3: of three whole listings on 2026-10-27, two skipped live artifacts (1 and 3), and every skipped id was in every other whole listing, so a second read restores a skip and a third is margin (judgment on that data). The reads and top-ups share the oneLISTING_TIMEOUT_SECONDS. A listing entry with no id now refuses too, since it cannot be counted. The refresh before an allow (babysit_merge.pyrefresh()) reads through the same path.Consumers. The saved listing keeps its shape (
fetchedAt,artifacts), so these work unchanged:babysit_merge.pyrefresh,babysit_merge_skip.py, ci-status's PASS-name loop,head-ref-map.mjsandlane-records.mjspruning. Page calls show in the existingbabysit-gh-callslines asGET repos/{}/{}/actions/artifacts.Gate-file lists.
listing-carry.mjsis added to the gate-file lists:babysit_risk.py,merge-holds.mjs, ci-statushold_patterns, both hosted review lanes, and ci-status's sparse checkout.Bases.
LISTING_OVERLAP= 2 h. Across 6,835 artifacts listed on 2026-10-27 (created 10-16 to 10-27), the largest lead of a lower id'screated_atover a higher id's was 58m49s. Each day's largest lead was 34 to 59 min. 2 h is twice the largest (judgment on that data).LISTING_CARRY_MAX_AGE= 3 h:pr-merge.ymlcron).newestTrustedRunasks at most 10 trusted runs (MAX_RUNS), about 2.5 h of runs.total_countandDate) 17 times, about 49 s apart, from 15:36 to 15:50 UTC. Every time,total_countequalled the last reading, plus the new uploads on page 1, less the artifacts whoseexpires_athad passed byDate. Five artifacts expiring at 15:40:47-55 left the count by the next reading. The per-id GET of one artifact answered 404 a second before itsexpires_at, while the count still held it until the next reading.Verification
Live probe (read-only, my token). The real reader ran an incremental read on a listing I took at 15:32, chained twice, and each run was followed by a full listing:
The four skipped ids sit at page boundaries, and a GET after each probe confirmed every one is live. Each skipped id was present in every other whole listing. This is #10486, now closed here: a whole listing is kept only when it holds the live count.
Tests.
TheListingBuildsOnACarriedOne(12 tests) andlisting-carry.test.mjs(4 tests). Each fallback has its own test, and each was proven to fail when the matching mechanism is removed:test_a_carry_past_its_age_bound_lists_every_page(3 h kept, 3 h 1 s lists every page)test_a_carry_that_does_not_parse_lists_every_page,test_a_page_with_no_date_or_an_odd_entry_lists_every_pagetest_the_overlap_reads_a_lower_id_created_after_the_base,test_a_fail_uploaded_since_is_read_from_the_first_page_alonetest_a_count_other_than_githubs_lists_every_page(a forged carry drops an old FAIL),test_a_live_count_no_listing_can_hold_refusesDatetest_an_expired_carried_entry_is_dropped_by_githubs_datetest_a_carried_entry_the_pages_no_longer_hold_is_droppedAWholeListingMustHoldTheLiveCount(4 tests, each failing with the check removed): a whole listing that skips a FAIL is restored by the next read and the FAIL refuses (verify:earlier-fail, where the first listing alone would allow); one that never holds the live count refusesverify:unreadableafter 3 reads and saves nothing; a duplicate at a page boundary is counted once; the refresh before an allow is held to the same rule. A listing entry with no id refuses (test_a_listing_with_an_artifact_of_no_id_refuses).Two fallbacks have their own tests: a failed page read (502 and 404), in
test_a_failed_page_read_lists_every_page, and an untrusted or off-main run, inlisting-carry.test.mjs. A module-level patch fails any other test that reads a page.Suites.
python3 -m unittest(babysit-prs scripts): 3,214 tests with realghblocked on PATH;test_babysit_verify_evidence(305) andtest_verify_rules_syncOK.node --testin.github/scripts/pr-merge: 530 pass. Inpr-verify: 158 pass.scripts/ci-lanes-pr-hold.test.sh: 216 pass.scripts/ci-review-lanes.test.sh: 98 pass.actionlinton the changed workflows: clean.scripts/check-affected.sh: 14 passed.Related
🤖 Generated with Claude Code