fix(merge): classify a 429 secondary rate limit as infra-scoped - #9795
Conversation
`classifyMergeFailure` reached the self-healing `infra` scope for a rate-limit
window only via `status === 403`, but this repo's own GitHub client documents that
a secondary limit surfaces as 403 OR 429 (client.ts:422). A 429 matched no status
or message branch, so it fell through to the commit-scoped default; once the retry
cap was burned the failure went terminal with no expiry, and the head-scoped block
stranded a green, approved PR until the contributor pushed a commit they had no
reason to push. A fleet-wide 429 window catches every in-flight merge at once.
Add an exported `isRateLimitMessage` predicate and two branches, ahead of the
terminal `403`: a `429` and a rate-limited `403` both classify
`{ terminal: false, scope: "infra" }`, so the two spellings of one condition are
treated identically and the block expires on INFRA_MERGE_BLOCK_TTL_MS and is
re-probed autonomously — exactly what the module already documents. No other
status's `terminal` changes; the commit-scoped fall-through default is untouched.
Closes JSONbored#9693
|
🚨 Contributor flagged. Click here for more info: Superagent Dashboard |
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-29 09:42:50 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9795 +/- ##
==========================================
+ Coverage 76.69% 76.70% +0.01%
==========================================
Files 283 284 +1
Lines 59729 59761 +32
Branches 6557 6573 +16
==========================================
+ Hits 45808 45840 +32
Misses 13639 13639
Partials 282 282
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Held for manual review: the gate and required CI are green, but GitHub reports this pull request's mergeable state as |
What & why
classifyMergeFailure(src/services/merge-failure.ts) reached the self-healinginfrascope for a rate-limit window only viastatus === 403 && isConvergenceForbiddenMessage(...). But this repo's own GitHub client documents that a secondary rate limit surfaces as 403 or 429 (src/github/client.ts:422), and a sustained window exhausts the retries so the 429 reaches the classifier as a thrownRequestError.A 429 matched no status or message branch, so it fell through to
{ terminal: false, scope: "commit" }. Onceattempts >= MERGE_RETRY_CAPthe executor escalated it to terminal, andexpiresAtis set only forscope === "infra"— so the block had no expiry andisMergeBlockInEffectstayed true until the head SHA advanced. A green, approved PR was stranded until the contributor pushed a commit they had no reason to push. Because a 429 window is fleet-wide, one window stranded every in-flight merge it caught — the exact outcome the module doc says cannot happen.The fix
Add an exported
isRateLimitMessagepredicate (secondary rate limit,abuse,api rate limit exceeded) alongside the existing message helpers, and two branches placed before the terminal403:status === 429→{ terminal: false, scope: "infra" }status === 403 && isRateLimitMessage(message)→{ terminal: false, scope: "infra" }So the 403 and 429 spellings of one condition are treated identically and lapse on
INFRA_MERGE_BLOCK_TTL_MSwith an autonomous re-probe. No other status'sterminalchanges, and the commit-scoped fall-through default at line 132 is untouched (per the DoD's "do not default the fall-through to infra"). No import fromsrc/github/client.ts(that module is Response-shaped, not error-shaped).Tests
test/unit/merge-failure.test.ts:{ terminal: false, scope: "infra" }(429 fails onmain, which returnsscope: "commit").isRateLimitMessagematches only rate-limit text.test/unit/merge-block-recovery.test.ts:markPullRequestMergeBlockedcomposition (ashandleMergeFailuredoes) throughMERGE_RETRY_CAP429 failures persists a non-nullmergeBlockedUntilthat lapses atNOW + INFRA_MERGE_BLOCK_TTL_MS + 1.mergeBlockedUntil == null(unchanged).Validation
npm run typecheckgreen; both suites (41 tests) green.merge-failure.tsis 100% line and branch (429 arm + both rate-limited-403 arms); whole-file branch 100%.git diff --check <base> HEADclean; diff is three files, no route/schema/migration change.Closes #9693