Skip to content

fix(ci): hold rate-limit default changes for a person - #10304

Merged
kyle-sexton merged 4 commits into
mainfrom
fix/floor-rate-limit-defaults
Oct 26, 2026
Merged

kyle-sexton merged 4 commits into
mainfrom
fix/floor-rate-limit-defaults

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: rate-limit defaults could change without a person; owner treats rate-limit stops as safety

Summary

The limits.api defaults that stop a lane before GitHub rate-limits the account (each pool's plan, writer-pause, breaker, read-floor, github-token's limit-cap, content-creation's per-minute and per-hour) lived in docs/conventions/pr-pipeline/pr-pipeline.schema.json. That schema is not on the merge hold floor, so a pull request raising or removing those limits could merge on the two AI review lanes alone. Flooring the whole schema would hold every schema change for a person, and the schema changes often.

Fix

  • New file .github/actions/log-api-budget/rate-limits.json holds the defaults, with the same values. It is their only copy.
  • rate-guard.mjs apiLimits reads it, and checks it: every pool present, no unknown pool or field, shares within 0 to 1, counts as positive integers.
  • babysit_gh.py reads it for writer-pause, read-floor and github-token's limit-cap. The last replaces the GITHUB_TOKEN_LIMIT_CAP = 1000 constant, which held a second copy of the value.
  • The schema drops its default values for limits.api and its descriptions point at the file.
  • The file is on every hold list: HOLD_PATH_FLOOR (merge-holds.mjs), babysit_risk.HIGH_FLOOR, ci-status's hold_patterns (pr-require-checks.yml), and the hold-paths of pr-review-hosted.yml and pr-review-security-hosted.yml. This follows the shape #10252 uses for escalation-routes.json.
  • test_merge_holds_sync.py takes #10252's IMPORT pattern word for word. The pattern also follows a module-relative file a floored module reads (new URL("./…", import.meta.url)), so the floor test now reaches rate-limits.json through rate-guard.mjs.
  • intake-label-needs-triage.yml no longer checks out the schema. Only rate-guard.mjs needed it there, and the new file comes with the log-api-budget/ checkout.

Behaviour is unchanged:

  • apiLimits({resolvedPath: ""}) and babysit_gh.pool_limits give the same values for all five pools as before. Both were captured before the change and diffed after.
  • A missing or unreadable file makes apiLimits throw. The latch then warns that the read floor is unchecked, and stops still hold, as with a missing schema before. babysit_gh still refuses every call when the file is missing.
  • Tightened: a malformed value, such as text, a share past 1 or a cap of 0, now fails the same way. Before, it was taken or silently ignored.
  • A pr-pipeline.yaml override still wins field by field.

Verification

Tests that failed before the change and pass after:

  • test_merge_holds_sync.py: floor size 38, rate-limits.json and a case variant held on both sides, and the floor holds what its actions load.
  • test_babysit_risk.py: the floor lists.
  • scripts/ci-lanes-pr-hold.test.sh: the floor holds rate-limits.json.
  • rate-guard.test.mjs:
    • defaults come from rate-limits.json;
    • a missing or invalid file throws, across 13 malformed cases;
    • the schema states no limits.api default, and the file's pools and fields match the schema's.
  • test_babysit_gh_rate_guard.py:
    • missing defaults send nothing;
    • invalid defaults send nothing;
    • the github-token cap is read from the file;
    • the file default fills an unset share.

Suite results:

  • node --test .github/actions/log-api-budget/*.test.mjs .github/scripts/pr-merge/*.test.mjs .github/actions/check-born-triaged/*.test.mjs: 606 pass, 0 fail.
  • node --test .github/actions/resolve-config/*.test.mjs: 698 pass, 0 fail.
  • node --test over the check-trusted-trigger, pr-unblock, work-unblock, reconcile-lost-runs, check-wip-cap and report-check-run suites: 1587 pass, 0 fail.
  • python3 -m unittest discover -s tests -t . in babysit-prs/scripts: 3075 tests OK.
  • Shell suites:
    • scripts/ci-lanes-pr-hold.test.sh: 155 pass.
    • ci-review-lanes: 98 pass.
    • ci-hold-cost-decision: 141 pass.
    • ci-hold-merge-group: 66 pass.
    • post-merge-demotion-alert: 154 pass.
    • check-loop-lane-floor-drift: 28 pass.
    • plan-test-lanes: 45 pass.
    • check-lane-coverage.test.sh: 41 pass.
  • scripts/check-lane-coverage.sh --check passes.
  • actionlint is clean.
  • scripts/run-ruff.sh check and format --check pass.
  • check-changelog-fragments.sh --check and --check-required origin/main pass.
  • check-convention-yaml.sh --check passes.

Related

  • #10252 floors escalation-routes.json in the same shape. Whichever of the two merges second needs a rebase. After this change, #10252's OFF_FLOOR assertion in test_merge_holds_sync.py (set(OFF_FLOOR) <= set(reached)) fails, because rate-guard.mjs no longer reaches the schema. That entry and its comment should go when #10252 rebases. The floor count also moves (37 to 38 here, 39 there, 40 together).

  • pr-pipeline.yaml overrides can still raise a limit above its default:

    • limit-cap, per-minute and per-hour have no schema maximum.
    • read-floor: 0 turns the read-floor warning off.
    • writer-pause and breaker accept 0.

    docs/conventions/pr-pipeline.yaml is already on the floor, so such an override already waits for a person. Not changed here.

  • melodic-software/pr-pipeline-sandbox keeps its own copy of rate-guard.mjs and the schema (gh search code --owner melodic-software "api-content-pool"). When it is next staged from this repository, it needs rate-limits.json along with rate-guard.mjs. Without it, its guard warns that the read floor is unchecked while its stops still hold. Org searches for "writer-pause", "rate-guard.mjs" and "api-pool" read-floor found no other reader.

  • pr-require-checks.yml still sets API_BUDGET_READ_FLOOR: "0.1" and API_BUDGET_LIMIT_CAP: "1000" for ci-status, which has no checkout of the file. log-api-budget.test.mjs pins both to apiLimits, so they follow rate-limits.json, and the workflow is on the floor.

🤖 Generated with Claude Code

kyle-sexton and others added 2 commits October 26, 2026 11:53
The limits.api defaults that stop a lane before GitHub rate-limits the
account lived in pr-pipeline.schema.json, which is not on the merge hold
floor, so a pull request raising or removing them could merge on the AI
review lanes alone. Flooring the whole schema would hold most of its
frequent changes for a person.

Move the defaults, unchanged, into
.github/actions/log-api-budget/rate-limits.json, their only copy:
rate-guard.mjs and babysit_gh.py read it (babysit_gh.py's
GITHUB_TOKEN_LIMIT_CAP constant now comes from it too), the schema drops
its defaults, and every hold list floors the file (HOLD_PATH_FLOOR,
babysit_risk.HIGH_FLOOR, ci-status's hold_patterns, both review lanes'
hold-paths). A missing or invalid file still leaves rate-guard warning
that the read floor is unchecked while stops hold, and still refuses
every babysit_gh call; both now also reject a malformed value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T16:21:45.810395Z eb89f3b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 7e10b8bfdee2a21057e464e7abedc5060605030e: 0 finding(s), 0 as inline comments.

I found nothing to flag in this pull request. I read the whole diff and the PR context. I ran no tests, so the verification results in the PR description are author-claimed and unconfirmed by me.

What the change does. It moves the limits.api defaults out of pr-pipeline.schema.json into .github/actions/log-api-budget/rate-limits.json. It adds that file to all five hold lists:

  • HOLD_PATH_FLOOR in merge-holds.mjs
  • babysit_risk.HIGH_FLOOR
  • the hold_patterns in pr-require-checks.yml
  • the hold-paths in pr-review-hosted.yml
  • the hold-paths in pr-review-security-hosted.yml

What I checked.

  • Values and validation. The values in rate-limits.json match the schema defaults the diff removes: 0.7, 0.4, 0.2, 0.1, a limit-cap of 1000, per-minute 80 and per-hour 500. rate-guard.mjs readDefaults throws on a missing pool, an unknown pool or field, a share outside 0 to 1, and a non-integer count. A throw sends createRateLimitLatch to the "read floor is unchecked" warning, which is the same fallback a missing schema had.
  • Python port. In babysit_gh.py, _read_pool_limits checks the share types and the limit-cap. _below now takes the cap from limits.get("limit-cap"), so only github-token carries a cap, as before. Removing GITHUB_TOKEN_LIMIT_CAP leaves a single copy of the value.
  • Floors and checkouts. The five hold lists agree, and tests for them were added or updated. intake-label-needs-triage.yml still checks out /.github/actions/log-api-budget/, so dropping the schema line there is safe. pr-require-checks.yml sparse-checks out only rate-guard.mjs. The old code needed the schema there and the schema was not in that checkout either, so the fallback behaves as before.
  • Changelog fragments. Both are bump: patch, which fits a behaviour-preserving move.
  • Closing issue. There is none, and the PR says so. No acceptance criteria apply.
  • Plugin manifests. The diff changes none.
  • Embedded instructions. The PR text holds none.

Spend. This change spends nothing, and no hold applies.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 7e10b8bfdee2a21057e464e7abedc5060605030e: 0 finding(s), 0 as inline comments.

No security findings. critical-findings: 0

What I reviewed: the full 24-file diff and the PR context. Trusted-path files read in full: rate-guard.mjs, the new rate-limits.json, merge-holds.mjs, pr-require-checks.yml (hold patterns and comments), babysit_gh.py, babysit_risk.py, docs/conventions/pr-pipeline.yaml (comment-only change), resolve.mjs (comment-only change), and the two hosted review workflows' hold-paths. The instruction-exception register was not consulted. The diff deletes no standing instruction or rule, so it was not needed.

Trust-control analysis (judged from code, not PR text):

  • Merge holds widen, none narrow. .github/actions/log-api-budget/rate-limits.json is added to every hold list: HOLD_PATH_FLOOR in merge-holds.mjs, babysit_risk.HIGH_FLOOR, ci-status's hold_patterns in pr-require-checks.yml, and the hold-paths of both pr-review-hosted.yml and pr-review-security-hosted.yml. No existing entry is removed or changed.
  • Floor coverage test tightened. The IMPORT regex in test_merge_holds_sync.py now also follows new URL("./…", import.meta.url). The floor-closure test therefore reaches rate-limits.json through rate-guard.mjs.
  • Rate-limit defaults are now under the floor. The limits.api defaults moved out of the schema, which is not held, into a held file. Before, a change to those defaults could merge on the two AI review lanes alone.
  • Validation is stricter.
    • readDefaults throws on a missing pool, an unknown pool or field, a share outside 0–1, a non-integer or non-positive count, or plan of 0.
    • babysit_gh._read_pool_limits returns an error string for a missing, non-numeric or out-of-range value. That makes babysit_gh refuse every call (fail closed), as it did for a missing schema.
    • Only the Node guard's read-floor warning degrades, as before. Its rate-limit stops still hold.
  • No new input from the pull request. The new file is read from the checkout the lane already uses. In babysit_gh, limit-cap now comes only from the defaults file. No pr-pipeline.yaml override reaches it, which is unchanged from the old constant.
  • Hold-step checkout unchanged. The ci-status hold steps' base sparse checkout (pr-require-checks.yml ~L3753) lists rate-guard.mjs but not rate-limits.json. The schema was not checked out there either, so apiLimits already threw and the latch already warned that the floor was unchecked. No regression, and the stops still hold.
  • Other hardcoded values. API_BUDGET_READ_FLOOR and API_BUDGET_LIMIT_CAP in ci-status stay hardcoded. log-api-budget.test.mjs pins both to apiLimits, and the workflow is itself on the floor.

Manifests: no plugin.json or marketplace.json change, so no manifest <field> lines apply.

Cost: nothing here can cause a real charge. No hold.

@kyle-sexton
kyle-sexton enabled auto-merge October 26, 2026 18:45
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit fe6d49d Oct 26, 2026
47 checks passed
@kyle-sexton
kyle-sexton deleted the fix/floor-rate-limit-defaults branch October 26, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant