Skip to content

Tuning cooldowns and usage caps

The re-review cooldown and the per-key usage cap are both database-only settings: .env.config is the source of truth for their values, but they are never a Render env var at all, unlike the rest of .env.config. They live only in the runtime_config table (the same table provider/key overrides write to), because the dispatcher needs to be able to change them with no redeploy.

uv run python -m bot.scripts.deploy --sync-config-db   # push .env.config's values into runtime_config

Needs only DATABASE_URL — no RENDER_API_KEY, no checklist, no redeploy. --sync-env also runs this same push as one of its own steps, so a normal full deploy keeps the database in sync too; --sync-config-db is only the fast, redeploy-free path for changing just this. Either way, the change takes effect on the next ticket the dispatcher claims.

The re-review cooldown

# .env.config
DISPATCHER_REREVIEW_COOLDOWN_SECONDS=30
DISPATCHER_REREVIEW_COOLDOWN_MAX_SECONDS=3600
DISPATCHER_REREVIEW_COOLDOWN_FACTOR=1.5

These three settings escalate the wait between re-reviews of the same PR: _SECONDS is the base, _FACTOR multiplies it on each successive push, and _MAX_SECONDS caps how high it can climb. --sync-config-db refuses the write (exit 2, nothing written) if the resolved base/cap/factor would be invalid (factor < 1.0, base > cap, or a non-positive base/cap) — that combination would otherwise write successfully but be silently discarded on every read. To go back to the built-in defaults (300s/3600s/2.0), remove the lines from .env.config and re-run --sync-config-db.

The per-key usage cap

# .env.config
KEY_USAGE_TOKEN_CAP=20000        # tokens/day for the ACTIVE key slot
KEY_USAGE_RESET_TIME_UTC=04:00   # when the day rolls over (default 04:00 UTC)

KEY_USAGE_TOKEN_CAP is unset by default — leave it unset and nothing is capped. When it's set, the dispatcher checks the currently-active (provider, key slot)'s usage so far today before starting a review; at or over the cap it defers the ticket to the next reset rather than making the call, and the PR gets a notice saying so, worded distinctly from a provider rate limit. This is the proactive counterpart to the reactive 429 backoff: it's what stops a bug or a PR spike from burning a free-tier credit before anyone notices. The reset time takes any HH:MM (or HH:MM:SS), not whole hours — set it a couple of minutes out to watch a cap reset during a demo instead of waiting for the next hour boundary.

Three things worth knowing:

  • The cap is per key slot, not global. Swapping slots with uv run python -m bot.scripts.set_override groq --index 1 immediately grants a fresh budget, exactly as key rotation already works — nothing auto-swaps on a breach; a human decides. That fresh budget applies to the next ticket claimed; a ticket already deferred by the cap still waits for its scheduled reset (raising or clearing the cap doesn't retroactively release it).
  • Usage survives restarts. It's summed from the persisted reviews history, not counted in memory, so a redeploy neither resets nor loses it.
  • A usage-check failure fails open. A broken usage query logs and lets the review proceed rather than blocking every review on it.

The cap is a ceiling on when the next review may start, not on the exact daily total: a review's real token usage is only known once it finishes, so the run that crosses the line is allowed to complete.

Reviewing draft PRs

# .env.config
REVIEW_DRAFT_PRS=false   # default: skip drafts entirely

Off by default. While a PR is a draft, opened/reopened/synchronize are all no-ops — no specialist call, no comment, no ticket left behind. ready_for_review still triggers a review even with zero new commits, since that's GitHub's dedicated signal for "this is now actionable" independent of any push. Converting a ready PR back to a draft needs no separate handling: the dispatcher checks the PR's current draft state at review time (piggy- backed on the diff fetch, no extra API call), so any ticket that gets re-armed by a later push while the PR is a draft is skipped there too, regardless of which webhook event produced it.

Set REVIEW_DRAFT_PRS=true to review drafts identically to ready-for-review PRs. Like the cooldown/cap settings above, this is database-only — --sync-config-db pushes it, and the change takes effect on the next ticket the dispatcher claims, no redeploy.

Next