Symptom (2026-09-07/08, this machine)
Every gh GraphQL call on the account — gh pr view/list/checks, gh issue view, consult PR search, afx spawn's issue fetch — fails with API rate limit already exceeded for user ID … for most of every hour, while gh api rate_limit misleadingly reports 5000/5000 (the response headers show X-RateLimit-Resource: graphql, Used: 5000, Remaining: 0). Builders cannot run CMAP PR reviews, the architect cannot merge via gh pr merge, and the Tower dashboard/cloud UI "times out" because overview requests wait on hanging gh spawns.
Cause, measured
OverviewCache (servers/overview.ts, TTL 30 s) backs each workspace's overview with three GraphQL-backed CLI calls — gh issue list --limit 200 --json …, gh issue list --state closed --search closed:>…, gh pr list --state merged --search merged:>… — via executeForgeCommand. With 13 active workspaces polled (cloud UI + dashboard + VS Code extension every 60 s) the steady state is 13 × 3 × (3600/30) ≈ 4,700 GraphQL calls/hour: the whole 5,000-point budget, forever.
Two amplifiers once the limit is hit:
- Failures are not cached.
executeForgeCommand returns null on a non-zero exit (lib/forge.ts:336/352) and the cache only stores non-null (if (data !== null) cache.set(...)), so every overview request re-spawns all three commands immediately. Observed: 51 gh spawns from the Tower daemon in 40 s (≈4,600/h) — faster than the TTL allows.
- No rate-limit awareness. Nothing parses the rate-limit error or the reset header; Tower keeps hammering until the hour rolls over, and the first requests after reset re-exhaust it.
Cost per call is also higher than 1 point: --limit 200 with author/assignees/labels expansions is a multi-hundred-node GraphQL query.
Fix (BUGFIX, prescribed)
- Negative cache + backoff in
OverviewCache: a failed forge fetch stores a null result with its own TTL (start 60 s, double to 15 min) so a failing workspace costs one spawn per window, not one per request.
- Rate-limit awareness: detect the GraphQL rate-limit error (stderr
rate limit already exceeded or graphql_rate_limit), read X-RateLimit-Reset via a single gh api rate_limit --jq .resources.graphql, and suspend ALL GitHub-backed overview fetches until that instant. Surface it in the overview payload (forgeStatus: 'rate-limited', resetAt) so the dashboard shows why data is stale instead of timing out.
- Raise the positive TTL to 120 s and refresh only workspaces with a live SSE client or an overview request in the last TTL window; a workspace nobody is looking at must cost zero calls.
- Collapse the three calls into one GraphQL query (
gh api graphql) per workspace, or move the two --search calls to REST (gh api search/issues, separate 5,000 budget). One request per workspace per window, budgeted at ≤ 1/13 of the hourly quota for 13 workspaces.
- Doctor check:
codev doctor reports the GraphQL budget (used/limit, reset time) and the projected hourly spend N_workspaces × calls × 3600/TTL, warning when the projection exceeds 50 % of the limit.
- Acceptance: with 13 workspaces open and the dashboard polling, the Tower daemon spawns ≤ 60
gh processes per hour at steady state (count children of the Tower pid over 10 min); a forced rate-limit error produces zero further spawns until reset; full suite green.
Refs: Spec 0126 (overview cache), #1641 (consult also blocked by the exhausted quota), #1629 (same week's Tower incident).
Symptom (2026-09-07/08, this machine)
Every
ghGraphQL call on the account —gh pr view/list/checks,gh issue view,consultPR search,afx spawn's issue fetch — fails withAPI rate limit already exceeded for user ID …for most of every hour, whilegh api rate_limitmisleadingly reports 5000/5000 (the response headers showX-RateLimit-Resource: graphql,Used: 5000,Remaining: 0). Builders cannot run CMAP PR reviews, the architect cannot merge viagh pr merge, and the Tower dashboard/cloud UI "times out" because overview requests wait on hangingghspawns.Cause, measured
OverviewCache(servers/overview.ts, TTL 30 s) backs each workspace's overview with three GraphQL-backed CLI calls —gh issue list --limit 200 --json …,gh issue list --state closed --search closed:>…,gh pr list --state merged --search merged:>…— viaexecuteForgeCommand. With 13 active workspaces polled (cloud UI + dashboard + VS Code extension every 60 s) the steady state is13 × 3 × (3600/30) ≈ 4,700GraphQL calls/hour: the whole 5,000-point budget, forever.Two amplifiers once the limit is hit:
executeForgeCommandreturnsnullon a non-zero exit (lib/forge.ts:336/352) and the cache only stores non-null (if (data !== null) cache.set(...)), so every overview request re-spawns all three commands immediately. Observed: 51ghspawns from the Tower daemon in 40 s (≈4,600/h) — faster than the TTL allows.Cost per call is also higher than 1 point:
--limit 200with author/assignees/labels expansions is a multi-hundred-node GraphQL query.Fix (BUGFIX, prescribed)
OverviewCache: a failed forge fetch stores anullresult with its own TTL (start 60 s, double to 15 min) so a failing workspace costs one spawn per window, not one per request.rate limit already exceededorgraphql_rate_limit), readX-RateLimit-Resetvia a singlegh api rate_limit --jq .resources.graphql, and suspend ALL GitHub-backed overview fetches until that instant. Surface it in the overview payload (forgeStatus: 'rate-limited', resetAt) so the dashboard shows why data is stale instead of timing out.gh api graphql) per workspace, or move the two--searchcalls to REST (gh api search/issues, separate 5,000 budget). One request per workspace per window, budgeted at ≤ 1/13 of the hourly quota for 13 workspaces.codev doctorreports the GraphQL budget (used/limit, reset time) and the projected hourly spendN_workspaces × calls × 3600/TTL, warning when the projection exceeds 50 % of the limit.ghprocesses per hour at steady state (count children of the Tower pid over 10 min); a forced rate-limit error produces zero further spawns until reset; full suite green.Refs: Spec 0126 (overview cache), #1641 (consult also blocked by the exhausted quota), #1629 (same week's Tower incident).