Skip to content

fix(antigravity): surface Google 429 quota exhaustion instead of retrying it - #427

Merged
shuv1337 merged 4 commits into
integration-v2from
cursor/antigravity-quota-errors-c5e1
Oct 5, 2026
Merged

shuv1337 merged 4 commits into
integration-v2from
cursor/antigravity-quota-errors-c5e1

Conversation

@shuv1337

@shuv1337 shuv1337 commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Intent

The captain approved resuming the issue backlog (2026-10-04): "triage the ~22 Cursor draft PRs first, then small batches", and later answered yes that shuvbro triage review counts as the first review for these Cursor PRs. The read-only triage report at ~/repos/shuvbro/data/triage-cursor-shuvcode/report.md identifies #427 (fix(antigravity): surface Google 429 quota instead of retrying it, issue #404) as fix-then-land. P1: packages/antigravity-plugin/src/wire.ts:390-393 on the PR head lets fiveHourWindow fall back to ANY group 5h bucket when the failed model group is missing, so a Gemini RESOURCE_EXHAUSTED/RATE_LIMIT_EXCEEDED with only an exhausted Claude/GPT summary is classified QuotaExceeded, retryable=false - a transient throttle becomes terminal (reproduced with the real parser). Required: use only the quota group demonstrably belonging to the failed model; a missing/unrecognized group must not borrow another group exhaustion; keep explicit QUOTA_EXHAUSTED and affirmative long-window handling; add cross-group and missing-group regressions plus a response-hook integration regression proving the full body and classification reach the session. The referenced triage section says the full-body parsing and synthetic Google ErrorInfo are otherwise aligned with the quota-plugin group format; current default does not explain non-ok responses or classify quota_exhausted, so the issue fix remains needed after correcting model-group attribution.

What Changed

  • The Antigravity plugin's http.response hook now handles failed responses to rewritten Cloud Code requests. It parses the full error body, including nested Google errors and SSE data: payloads. On a 429 it also requests a best-effort quota readout from the retrieveUserQuotaSummary endpoint, which is new in oauth.ts. It then sends back a Google-format error with the Cloud Code status, the message, a summary of each quota window and a synthetic ErrorInfo reason.

  • A RESOURCE_EXHAUSTED error becomes non-retryable QUOTA_EXHAUSTED only in these cases:

    • The error carries an explicit QUOTA_EXHAUSTED reason.
    • The failed model's own quota group (Gemini, or Claude/GPT) has an exhausted 5h bucket. A missing or unrecognized group never takes another group's exhaustion.
    • The error message points to a long reset window (5h, or 1 hour or more).

    All other RESOURCE_EXHAUSTED errors remain retryable throttles. @opencode/ai provider-error classification now treats the quota_exhausted reason as a quota code.

  • Added tests in packages/antigravity-plugin/test/error.test.ts, packages/ai/test/provider-error.test.ts and the new packages/core/test/session-antigravity-error.test.ts:

    • parser and classification tests, including cross-group and missing-group regressions
    • a response-hook integration test that checks the full body and classification reach the session

    The plugin README also documents this behavior.

Risk Assessment

✅ Low: The 5h-window lookup now returns only the bucket for the failed model's group ("gemini:5h" or "3p:5h") and never falls back to another group. Explicit QUOTA_EXHAUSTED and long-window message handling are kept. Cross-group, missing-group, unknown-model, and response-hook regressions cover the required behavior, and the group naming matches packages/quota-plugin/src/google.ts.

Testing

After installing dependencies, I ran the targeted antigravity wire tests, the core session response-hook integration test and the ai classifier test; all pass. An A/B run against the pre-fix wire.ts shows the 6 new cross-group and missing-group regressions failing before the fix, which confirms they reproduce the P1. A temporary scratch test also drove the real plugin response hook with the quota-summary fetch path turned on. For a Gemini throttle, an exhausted Claude/GPT or unknown group stays RATE_LIMIT_EXCEEDED, and only an exhausted Gemini group becomes QUOTA_EXHAUSTED. These are unit and in-process integration results, not live results. Nothing ran against a real live Google 429, because that needs a real Antigravity OAuth account in a throttled or exhausted state against the hardcoded cloudcode-pa endpoint. There is no UI surface, so no screenshots were taken. The worktree is clean.

  • Live validation: ⚠️ inconclusive - 0 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Gemini 429 RATE_LIMIT_EXCEEDED with only an exhausted Claude/GPT 5h bucket stays a retryable throttle (cross-group adversarial) ⏸️ untested no The prior payload established no live result. Only the real parser and plugin hook were exercised, with a stubbed quota-summary fetch (error.test.ts, hook-crossgroup-scratch.log, A/B log); nothing ran…
Missing or unrecognized quota group, or unknown model, does not borrow another group's exhaustion ⏸️ untested no The prior payload established no live result. Only unit and in-process tests with stubbed quota summaries ran; there was no live Google Antigravity account to drive it.
Exhausted matching group (Gemini 5h for Gemini, 3p 5h for Claude/GPT) is classified QUOTA_EXHAUSTED ⏸️ untested no The prior payload established no live result. Only unit and in-process tests with stubbed quota summaries ran; a live run needs an Antigravity account whose quota is exhausted.
Explicit QUOTA_EXHAUSTED reason and affirmative 5h-reset messages still become non-retryable QuotaExceeded ⏸️ untested no The prior payload established no live result. Only unit and integration tests ran (error.test.ts, session-antigravity-error.test.ts, provider-error.test.ts); there was no live Google account.
Full >500-char 429 body passes through the http.response hook and its classification reaches the session (RequestExecutor.httpFailure) ⏸️ untested no The prior payload established no live result. The plugin hook ran in-process with fixture credentials (session-antigravity-error.test.ts), not against Google.
Real shuvcode session on a Google Antigravity account hitting a live 429 shows the quota message and stops retrying (or retries on a throttle) ⏸️ untested no Needs a real Google Antigravity OAuth subscription in a throttled or quota-exhausted state. The Cloud Code endpoint (cloudcode-pa) is hardcoded, so it can't be pointed at a local fake. To run it, log…
Evidence: Wire fix diff (fiveHourWindow no longer falls back to any group)
diff --git a/packages/antigravity-plugin/src/wire.ts b/packages/antigravity-plugin/src/wire.ts
index de052e029a..2a895bea23 100644
--- a/packages/antigravity-plugin/src/wire.ts
+++ b/packages/antigravity-plugin/src/wire.ts
@@ -389,8 +389,9 @@ function quotaWindowID(bucket: Record<string, unknown>) {
 
 function fiveHourWindow(windows: readonly QuotaWindow[], modelID: string | undefined) {
   const name = (modelID ?? "").toLowerCase()
-  const preferred = name.includes("claude") || name.includes("gpt") ? "3p:5h" : "gemini:5h"
-  return windows.find((window) => window.id === preferred) ?? windows.find((window) => window.id.endsWith(":5h"))
+  const preferred =
+    name.includes("claude") || name.includes("gpt") ? "3p:5h" : name.includes("gemini") ? "gemini:5h" : undefined
+  return windows.find((window) => window.id === preferred)
 }
 
 function messageIsLongQuota(message: string | undefined) {
Evidence: A/B: pre-fix wire.ts fails the 6 cross/missing-group regressions
(fail) Cloud Code 429 bodies > does not borrow quota from an unrelated or missing group: {"modelID":"gemini-3.8-flash-high","groups":[{"displayName":"Claude + GPT","buckets":[{"window":"5h","remainingFraction":0}]}]} [0.11ms]
(fail) Cloud Code 429 bodies > does not borrow quota from an unrelated or missing group: {"modelID":"claude-sonnet-4-6","groups":[{"displayName":"Gemini models","buckets":[{"window":"5h","remainingFraction":0,"resetTime":"2026-08-14T18:00:00Z"},{"window":"weekly","remainingFraction":0.4,"resetTime":"2026-08-20T00:00:00Z"}]}]} [0.02ms]
(fail) Cloud Code 429 bodies > does not borrow quota from an unrelated or missing group: {"modelID":"gpt-oss-120b","groups":[{"displayName":"Gemini models","buckets":[{"window":"5h","remainingFraction":0,"resetTime":"2026-08-14T18:00:00Z"},{"window":"weekly","remainingFraction":0.4,"resetTime":"2026-08-20T00:00:00Z"}]}]} [0.01ms]
(fail) Cloud Code 429 bodies > does not borrow quota from an unrelated or missing group: {"modelID":"gemini-3.8-flash-high","groups":[{"displayName":"Unknown models","buckets":[{"window":"5h","remainingFraction":0}]}]} [0.02ms]
(fail) Cloud Code 429 bodies > does not borrow quota from an unrelated or missing group: {"modelID":"unknown-model","groups":[{"displayName":"Gemini models","buckets":[{"window":"5h","remainingFraction":0,"resetTime":"2026-08-14T18:00:00Z"},{"window":"weekly","remainingFraction":0.4,"resetTime":"2026-08-20T00:00:00Z"}]}]} [0.02ms]
(fail) Cloud Code 429 bodies > does not borrow quota from an unrelated or missing group: {"groups":[{"displayName":"Gemini models","buckets":[{"window":"5h","remainingFraction":0,"resetTime":"2026-08-14T18:00:00Z"},{"window":"weekly","remainingFraction":0.4,"resetTime":"2026-08-20T00:00:00Z"}]}]} [0.01ms]
 10 pass
 6 fail
Evidence: Response hook with quota summary: rewritten bodies per summary group for a Gemini throttle

Claude + GPT exhausted -> reason RATE_LIMIT_EXCEEDED (retryable) Gemini models exhausted -> reason QUOTA_EXHAUSTED Unknown models exhausted -> reason RATE_LIMIT_EXCEEDED (retryable)

bun test v1.4.2 (744846f84)

test/zz-scratch-crossgroup.test.ts:
SCENARIO Claude + GPT -> {"error":{"code":429,"message":"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. check quota). 3p:5h remainingFraction=0 resetTime=2026-10-05T03:00:00Z","status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"RATE_LIMIT_EXCEEDED"}]}}
SCENARIO Gemini models -> {"error":{"code":429,"message":"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. check quota). gemini:5h remainingFraction=0 resetTime=2026-10-05T03:00:00Z","status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"QUOTA_EXHAUSTED"}]}}
SCENARIO Unknown models -> {"error":{"code":429,"message":"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. check quota). group-0:5h remainingFraction=0 resetTime=2026-10-05T03:00:00Z","status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"RATE_LIMIT_EXCEEDED"}]}}

 3 pass
 0 fail
 30 expect() calls
Ran 3 tests across 1 file. [470.00ms]
Evidence: Session integration test output
bun test v1.4.2 (744846f84)

 3 pass
 0 fail
 27 expect() calls
Ran 3 tests across 1 file. [375.00ms]
Evidence: Antigravity wire test output
bun test v1.4.2 (744846f84)

 16 pass
 0 fail
 34 expect() calls
Ran 16 tests across 1 file. [58.00ms]
- Outcome: ⚠️ 1 warning across 1 run (1m39s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped

Step was skipped.

✅ **Review** - passed

✅ No issues found.

⚠️ **Test** - 1 warning
  • ⚠️ live validation verdict: inconclusive (0 of 6 scenarios were driven live against the product); untested: Gemini 429 RATE_LIMIT_EXCEEDED with only an exhausted Claude/GPT 5h bucket stays a retryable throttle (cross-group adversarial), Missing or unrecognized quota group, or unknown model, does not borrow another group's exhaustion, Exhausted matching group (Gemini 5h for Gemini, 3p 5h for Claude/GPT) is classified QUOTA_EXHAUSTED, Explicit QUOTA_EXHAUSTED reason and affirmative 5h-reset messages still become non-retryable QuotaExceeded, Full >500-char 429 body passes through the http.response hook and its classification reaches the session (RequestExecutor.httpFailure), Real shuvcode session on a Google Antigravity account hitting a live 429 shows the quota message and stops retrying (or retries on a throttle)
  • Live validation: ⚠️ inconclusive - 0 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Gemini 429 RATE_LIMIT_EXCEEDED with only an exhausted Claude/GPT 5h bucket stays a retryable throttle (cross-group adversarial) ⏸️ untested no The prior payload established no live result. Only the real parser and plugin hook were exercised, with a stubbed quota-summary fetch (error.test.ts, hook-crossgroup-scratch.log, A/B log); nothing ran…
Missing or unrecognized quota group, or unknown model, does not borrow another group's exhaustion ⏸️ untested no The prior payload established no live result. Only unit and in-process tests with stubbed quota summaries ran; there was no live Google Antigravity account to drive it.
Exhausted matching group (Gemini 5h for Gemini, 3p 5h for Claude/GPT) is classified QUOTA_EXHAUSTED ⏸️ untested no The prior payload established no live result. Only unit and in-process tests with stubbed quota summaries ran; a live run needs an Antigravity account whose quota is exhausted.
Explicit QUOTA_EXHAUSTED reason and affirmative 5h-reset messages still become non-retryable QuotaExceeded ⏸️ untested no The prior payload established no live result. Only unit and integration tests ran (error.test.ts, session-antigravity-error.test.ts, provider-error.test.ts); there was no live Google account.
Full >500-char 429 body passes through the http.response hook and its classification reaches the session (RequestExecutor.httpFailure) ⏸️ untested no The prior payload established no live result. The plugin hook ran in-process with fixture credentials (session-antigravity-error.test.ts), not against Google.
Real shuvcode session on a Google Antigravity account hitting a live 429 shows the quota message and stops retrying (or retries on a throttle) ⏸️ untested no Needs a real Google Antigravity OAuth subscription in a throttled or quota-exhausted state. The Cloud Code endpoint (cloudcode-pa) is hardcoded, so it can't be pointed at a local fake. To run it, log…
  • bun install (worktree had no node_modules)
  • cd packages/antigravity-plugin &amp;&amp; bun test test/error.test.ts (16 pass)
  • cd packages/core &amp;&amp; bun test test/session-antigravity-error.test.ts (3 pass: QUOTA_EXHAUSTED→QuotaExceeded non-retryable, 5h-reset message→QuotaExceeded, plain throttle→RateLimit retryable, full >500-char body reaches the session)
  • cd packages/ai &amp;&amp; bun test test/provider-error.test.ts -t &#34;Google 5h&#34; (1 pass)
  • A/B: git show 0d0fc791b4:packages/antigravity-plugin/src/wire.ts swapped in, then error.test.ts reran (6 cross-/missing-group regressions fail before the fix), then restored with git checkout HEAD -- wire.ts
  • Temporary scratch variant of the session integration test (deleted afterwards): drives the real GoogleAntigravityPlugin http.response hook with the quota-summary fetch enabled and a stubbed retrieveUserQuotaSummary returning an exhausted Claude+GPT / Gemini / Unknown group for a Gemini model
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

cursoragent and others added 4 commits October 4, 2026 08:02
pc_b35cf668bc26

Parse the full Cloud Code body so a 500-character prefix cannot drop
the Google status and message. An exhausted 5h bucket from
retrieveUserQuotaSummary is non-retryable quota.
@shuv1337 shuv1337 changed the title fix(antigravity): surface Google 429 quota instead of retrying it fix(antigravity): surface Google 429 quota exhaustion instead of retrying it Oct 5, 2026
@shuv1337
shuv1337 marked this pull request as ready for review October 5, 2026 03:11
@shuv1337
shuv1337 merged commit a7e864f into integration-v2 Oct 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants