Skip to content

Rate-limited PR reviews wait up to ~7h after the limit resets — the retry relies on GitHub's best-effort cron, which fires far less often than every 15 min #1994

Closed

Description

@don-petry

Summary

When pr-review defers a PR because an advisory reviewer is rate-limited, it writes:

<!-- pr-review-agent rate-limited v1 sha=<HEAD> status=rate-limited reset=<ISO> -->

from scripts/lib/advisory-review-gate.sh (~L340). The only thing that re-dispatches the review after reset is pr-review-sweep.yml / scripts/sweep-stuck-reviews.sh (~L300).

The sweep's comments call the cron the "guaranteed backstop (≤15 min)". It isn't: GitHub's schedule trigger is best-effort and gets dropped under load. Recent scheduled sweeps ran at 2026-09-30 21:22Z, then 2026-10-01 00:46Z, 06:36Z and 13:56Z, roughly every 3–7 hours against a 2,17,32,47 * * * * cron. The run history back to July shows about hourly at best.

The workflow_run fast path doesn't cover this case either: it is scoped to the PR whose CI just completed, so a PR waiting on a timer never gets one.

Observed on #1952

The 13:56Z sweep deferred #1952 because the reset (14:25:38Z) hadn't elapsed. No sweep has run since, so #1952 has not been re-reviewed. The reset was minutes away; the latency is governed by GitHub's cron, which is unbounded.

Goal

A time-bounded retry must fire close to its reset without depending on GitHub's scheduler. Target: re-dispatch within ~15 minutes after reset for resets up to ~60 minutes out. Longer resets may fall back to the sweep.

Acceptance criteria

  • When a PR is deferred on an un-elapsed rate-limit marker whose reset is within a bounded horizon (default ~60 min, configurable), a deterministic delayed retry is armed for that PR. It waits until reset (+ small buffer) and then re-dispatches the review through the same path the sweep uses.
    • Suggested approach: the sweep, or the marker writer, dispatches a scoped run (pr=<n>, not_before=<iso>) that sleeps until not_before, re-checks that the marker still applies (same head SHA, still not approved/reviewed at head), then dispatches.
    • Any approach is fine if it doesn't rely on schedule. State the choice and the runner-minute cost.
  • Concurrency: today pr-review-sweep.yml groups on github.event.workflow_run.head_branch || github.event_name with cancel-in-progress: true, so two workflow_dispatch runs would cancel each other. Delayed per-PR retries must not cancel each other or the full sweep (e.g. a per-PR group with cancel-in-progress: false, or a separate job/workflow).
  • Idempotent and bounded:
  • Correct the "≤15 min guaranteed backstop" comments in pr-review-sweep.yml to describe the real guarantee.
  • Tests:
    • a deferral with a near reset arms exactly one retry;
    • a far reset arms none and leaves it to the sweep;
    • the retry no-ops on a SHA change or an already-reviewed head;
    • two retries for different PRs don't cancel each other.
  • Keep it scoped to the rate-limit retry. Don't rework the sweep's other duties.

Related

Activity

  1. github-actions commented on Oct 1, 2026

    @github-actions
    Contributor

    Thanks for the detailed report and for the concrete evidence from the sweep run history and #1952, which makes the problem easy to follow. The proposed goal and acceptance criteria are clear. One question before this is picked up: for the bounded horizon, is ~60 minutes the right default for your workloads, or have you seen rate-limit resets in practice that are usually shorter or longer than that?

  2. don-petry commented on Oct 1, 2026

    @don-petry
    CollaboratorAuthor

    Plan — deterministic delayed retry for rate-limited PR reviews (#1994)

    Problem recap

    When pr-review defers a PR because an advisory reviewer is rate-limited, it writes
    <!-- pr-review-agent rate-limited v1 sha=<HEAD> status=rate-limited reset=<ISO> -->. The only thing that
    re-dispatches after reset is the scheduled pr-review-sweep.yml cron, which GitHub drops under load
    (observed 3–7 h instead of every 15 min — #1952). We need a retry that fires close to reset without
    depending on schedule
    .

    Approach (AC1)

    Arm a deterministic delayed retry from the sweep's defer branch (the sweep already holds the dispatch PAT;
    the marker-writer runs under GITHUB_TOKEN and cannot dispatch). When the sweep defers a PR on an un-elapsed
    rate-limit marker whose reset is within a bounded horizon (DELAYED_RETRY_HORIZON_SEC, default 3600 s),
    it dispatches a new pr-review-delayed-retry.yml workflow with pr_url, head_sha, not_before=<reset ISO>.
    That workflow runs scripts/delayed-rate-limit-retry.sh, which:

    1. sleeps until not_before + small buffer (DELAYED_RETRY_BUFFER_SEC, default 120 s), capped by a safety
      ceiling so a malformed not_before can never sleep unbounded;
    2. re-checks the current head SHA still equals the armed head_sha — if a newer push superseded it, logs a
      no-op and exits 0;
    3. otherwise delegates to the same path the sweep uses — invokes sweep-stuck-reviews.sh scoped to that one
      PR (SWEEP_PRS_FILE) with ARM_DELAYED_RETRY=false (no re-arm loop). The sweep re-validates the marker,
      standing verdict, CI and exclusions, then re-dispatches pr-review-trigger.yml without force.

    Resets beyond the horizon fall back to the existing cron sweep (unchanged behaviour).

    Runner-minute cost: one mostly-sleeping runner per armed (PR, head), bounded by the horizon + buffer
    (default ≤ ~62 min; hard-capped by the workflow timeout-minutes). Re-arming is idempotent (see AC3), so arms
    do not stack.

    Concurrency (AC2)

    pr-review-delayed-retry.yml uses a per-PR+SHA concurrency group (…-<pr_url>-<head_sha>). Different PRs
    (and different head SHAs) get distinct groups, so retries never cancel each other, and the group never matches
    the sweep's group, so a retry never cancels the full sweep.

    Idempotency / bounded (AC3)

    AC4

    Correct the "guaranteed ≤15 min backstop" wording in pr-review-sweep.yml (the cron is best-effort under load;
    the delayed retry is the near-reset path).

    Tests (AC5) — written first (TDD)

    • tests/test_sweep_stuck_reviews.bats: near-reset defer arms exactly one retry; far-reset defer arms
      none (preserves existing FUTURE_RESET=2999 tests); ARM_DELAYED_RETRY=false never arms.
    • new tests/test_delayed_rate_limit_retry.bats: retry no-ops on SHA change; no-ops on
      already-reviewed/standing-verdict head (via real-sweep delegation with mocked gh); two PRs' retries use
      distinct concurrency groups (parsed from the workflow) so they don't cancel each other.

    Change checklist

    • scripts/delayed-rate-limit-retry.sh (new)
    • .github/workflows/pr-review-delayed-retry.yml (new; Class 1, workflow_dispatch only)
    • scripts/sweep-stuck-reviews.sh — arm_delayed_retry() + call in defer branch; ARM_DELAYED_RETRY /
      DELAYED_RETRY_HORIZON_SEC / DELAYED_RETRY_WORKFLOW env
    • .github/workflows/pr-review-sweep.yml — AC4 comment fixes
    • docs/agentic-interaction-model.md — §4 row for the new workflow (Class 1, no timer_role: the in-run sleep
      is a deterministic armed delay, not a scheduled trigger)
    • interaction-contracts/pr-review.yml — add the workflow + workflow_dispatch:pr-review-delayed-retry emit
    • AGENTS.md — repo-specific-workflow exception for the new workflow
    • .github/workflows/lint.yml — register the new bats file

    Verification

    bash scripts/run-bats.sh tests/test_delayed_rate_limit_retry.bats tests/test_sweep_stuck_reviews.bats,
    full bats suite, bash .dev-lead/scripts/dev-lead-lint.sh, validate-interaction-model.sh, actionlint.

    Scope stays on the rate-limit retry (AC6) — the sweep's other duties are untouched.

  3. don-petry commented on Oct 1, 2026

    @don-petry
    CollaboratorAuthor

    On the horizon question: ~60 minutes (3600 s) is the right default.

    The reset that stranded #1952 was ~30 minutes out (deferred 13:56Z, reset 14:25:38Z), so it falls well inside it. Permanent quota exhaustion like Codex's (#1993) has no meaningful reset, and the advisory gate already drops such bots, so it shouldn't drive the horizon. Keep it configurable as planned.

    The plan above looks good to proceed with.


    Generated by Claude Code

  4. don-petry commented on Oct 1, 2026

    @don-petry
    CollaboratorAuthor

    Dev-Lead: Implementation Complete — PR #1999

    The implementation for issue #1994 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.

    Review happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.

  5. added a commit that references this issue on Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug reportsdev-leadFor dev-lead agent pickup

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions