GitHub Traffic Control
Central gateway plan and current implementation for rate-limit-aware GitHub API traffic from Issue Reporter, Hub approvals, and Agent Orchestrator.
This page describes how Clapilot routes GitHub REST API traffic and how rate limits are handled. It is for developers touching Issue Reporter, Hub issue approval, or Agent Orchestrator GitHub calls. Part of this design is shipped (the shared githubTrafficFetch helper and the write queue table); the gateway budgets, queue drain worker, and shared webhook ingestion are still planned and are marked as such below.
Architecture Decision
Clapilot will route internal GitHub REST API calls through a central GitHub Traffic Control layer. The first implementation scope is intentionally narrow:
- normalize GitHub rate-limit detection for Issue Reporter and Hub approval writes
- log GitHub rate-limit headers from the shared web helper
- queue Hub approval issue-creation writes when GitHub returns a primary or secondary rate limit
- document the current GitHub call inventory and rollout path for Agent Orchestrator traffic
The long-term gateway owns request budgeting, read caching, write queues, and webhook ingestion. Existing Agent Orchestrator code has a local githubApiFetch wrapper that already implements the read side of this design for the poll loop: ETag conditional requests with a bounded LRU replay cache, per-token x-ratelimit-* tracking, budget-aware scan pacing, and rate-limit cooldowns (see bundled-modules/agent-orchestrator/api/github-api-cache.mjs and the Agent Orchestrator module page). When the shared gateway lands, that wrapper should be folded into it rather than kept as a second permanent abstraction.
Current Call Inventory
| Area | Current path | Traffic type | Notes |
|---|---|---|---|
| Issue Reporter | src/lib/issue-reporter.ts | POST /repos/{owner}/{repo}/issues | Now uses the shared githubTrafficFetch helper for header logging and rate-limit errors. |
| Hub Approval | src/app/api/hub/issues/[id]/route.ts -> createGithubIssueFromHubReport | user-triggered write | Rate-limited writes are inserted into github_traffic_control_queue and surfaced as queued instead of generic 502. |
| Agent Orchestrator repo list | bundled-modules/agent-orchestrator/api/handler.mjs | user-visible read | Uses local repo-list cache keyed by token hash. |
| PR Review automation | bundled-modules/agent-orchestrator/api/handler.mjs | background reads and review writes | Uses local githubApiFetch (ETag conditional GETs, budget pacing); should migrate to shared gateway in the next implementation slice. |
| Issue Observer | bundled-modules/agent-orchestrator/api/handler.mjs | background reads, comments, issue state writes | Per-tick poll caches for default branch, open issues, and open PRs, plus cross-tick ETag replay on 304. |
| Mention Observer | bundled-modules/agent-orchestrator/api/handler.mjs and github-mention-observer.mjs | background reads, reaction/comment writes | Candidate detection polls issues, PRs, comments, reviews, and check data; only threads updated since the previous scan are expanded. |
| Tracked PR follow-up | bundled-modules/agent-orchestrator/api/handler.mjs and tracked-pr-followup.mjs | background reads | Reads comments, reviews, check-runs, and PR state for follow-up decisions; unchanged resources come back as free 304s. |
| CI preview cleanup | scripts/ci/cleanup-pr-preview.sh | CI-only read | Stays outside product gateway unless moved into long-running app automation. |
Gateway Contract
Every product GitHub request should record:
- method and normalized endpoint
- actor, for example
issue_reporter,hub_approval,agent_orchestrator_pr_review, orsupervisor - token or installation identity without storing the secret
x-ratelimit-limit,x-ratelimit-remaining,x-ratelimit-reset,x-ratelimit-used,x-ratelimit-resourcex-github-request-id- response status and rate-limit classification
Rate-limit errors must map to a user-facing 429 or queued response, not a generic 502. The message should include the reset timestamp and GitHub request id when available.
Queue
github_traffic_control_queue stores write work that should not be dropped on rate limit. The first queued action is hub_issue_create; it records the Hub issue id and reviewer in the JSON payload with status waiting_for_rate_limit.
Queue states are:
pendingwaiting_for_rate_limitfailed_permanentcompleted
Status: enqueueing works today (enqueueGithubWrite in src/lib/github-traffic-control.ts), but the drain worker is not implemented yet. A follow-up worker should drain due rows by priority, call the same gateway, increment attempts, and mark permanent failures only after non-rate-limit errors or an attempt cap. Until that worker exists, queued rows stay in the table and need manual follow-up.
Request Budgets
Priority order:
- user-triggered writes
- active coding/debug jobs
- user-visible reads
- background sync and supervisor polling
When remaining budget is low, background polling should skip or extend its interval before user-triggered actions are affected. Agent Orchestrator implements this locally today: runSymphonyPollTick calls decideGithubScanForPollTick before touching GitHub, keeps a 15% reserve of the hourly limit (at least 100 requests) untouched by polling, stretches the GitHub scan interval up to 15 minutes when the remaining budget cannot sustain the configured poll cadence until the reset, and skips the GitHub section entirely while a token is in rate-limit cooldown. The shared gateway should take over this check with the same priorities.
Read Caching Strategy
Recommended cache windows:
- repo metadata: 10 to 60 minutes
- issue and PR metadata: 60 to 120 seconds
- check status: 30 to 90 seconds
- comments and reviews: 60 to 180 seconds
- workflow logs: explicit request only, or fetched when a failed check requires diagnostics
In-flight request coalescing should key by token identity, method, endpoint, and query params so parallel agents share the same read result.
Shipped in Agent Orchestrator: every GitHub GET carries If-None-Match from a bounded LRU keyed by token hash and full URL; 304 Not Modified replays the cached body and is not charged against the primary limit, so the time windows above only matter for endpoints whose payload actually changed. Per-tick poll caches additionally coalesce the open-PR, open-issue, and default-branch reads across the PR review, issue observer, and mention observer features.
Webhook Follow-up
Today, GitHub webhooks are received only by the Agent Orchestrator module (public module endpoint /api/modules/agent-orchestrator/webhook); there is no shared product-level webhook route yet. The planned shared gateway should add a GitHub webhook endpoint for:
issuesissue_commentpull_requestpull_request_reviewcheck_runcheck_suiteworkflow_run
Webhook events should invalidate gateway caches and enqueue agent work. Polling remains a fallback and supervisor recovery path, but it should not be the primary freshness mechanism for active repositories. Agent Orchestrator now also uses completed failing workflow_run, check_run, and check_suite webhook payloads to start a configured default-branch CI fix run that opens a PR, while PR-branch failures continue through tracked PR follow-up.
