Skip to content

fix: treat "Resource has been exhausted" as temporary capacity#194

Merged
tctinh merged 3 commits intoNoeFabris:devfrom
IsraelAraujo70:fix/treat-resource-exhausted-as-capacity
Jan 15, 2026
Merged

fix: treat "Resource has been exhausted" as temporary capacity#194
tctinh merged 3 commits intoNoeFabris:devfrom
IsraelAraujo70:fix/treat-resource-exhausted-as-capacity

Conversation

@IsraelAraujo70
Copy link
Contributor

@IsraelAraujo70 IsraelAraujo70 commented Jan 15, 2026

Summary

Fixes an issue where Resource has been exhausted (HTTP 429 without quotaResetTime) permanently marks all accounts as rate-limited on disk, even though it is a temporary backend capacity limitation.

Problem

When the Antigravity server returns HTTP 429 with the generic message "Resource has been exhausted" without quotaResetTime, the plugin treats it as account-level quota exhaustion and persists this state in antigravity-accounts.json. This causes:

  1. All accounts to become permanently blocked on disk
  2. After manually deleting the state and restarting, the server returns 429 again for all accounts (global capacity issue)
  3. The user loses access to the model even though accounts are available

Log example:

[RateLimit] message: Resource has been exhausted (e.g. check quota).
[RateLimit] reason: undefined (no quotaResetTime)

Solution

Differentiate between account-level quota exhaustion (persisted to disk) and global capacity exhaustion (in-memory cooldown, not persisted):

Service capacity (not persisted)

  • Detects Resource has been exhausted without quotaResetTime as capacity-related
  • Global cooldown per family/model kept in memory (capacityCooldownByKey)
  • Progressive backoff: 5s → 10s → 20s → 30s → 60s
  • State automatically resets after 2 minutes without errors

Real quota (unchanged)

  • 429 with quotaResetTime continues to be persisted on disk
  • QUOTA_EXHAUSTED or MODEL_CAPACITY_EXHAUSTED keeps the existing flow

Modified files

  • src/plugin.ts: 58 insertions, 10 deletions
    • Adds capacityCooldownByKey and capacityFailureStateByKey (global, in-memory)
    • Helpers: getCapacityKey, getCapacityCooldownRemainingMs, markCapacityCooldown, getCapacityBackoffForKey
    • Checks global cooldown before selecting an account (main loop)
    • Detects "resource has been exhausted" as capacity-related (without quotaResetTime)

Tests

  • ✅ All tests pass (bun run test: 558 passed)
  • ✅ No regressions in real quota flow

Problema: Quando o servidor retorna 429 com mensagem "Resource has been
exhausted" sem quotaResetTime, o plugin marca todas as contas como
rate-limited permanentemente no antigravity-accounts.json, mesmo sendo
um problema de capacidade temporária do backend.

Solução:
- Distinguir entre quota esgotada (persiste) e capacity exhausted (global)
- Capacity exhausted usa cooldown em memória por família/modelo (não persiste)
- Fluxo de quota real permanece inalterado (429 com quotaResetTime continua)

Arquivos:
- src/plugin.ts: adiciona capacidade global e helpers
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Refactors capacity exhaustion from per-account to a global, keyed (family/model) backoff. Adds constants and maps for global cooldown state plus helpers (getCapacityKey, getCapacityCooldownRemainingMs, markCapacityCooldown, getCapacityBackoffForKey). Extends capacity detection to more message variants and uses the global cooldown before account selection. Reworks 429 handling to employ the new global backoff and attempt counter, preserves sleep-and-retry behavior, and updates debug output and user toasts accordingly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: treating 'Resource has been exhausted' messages as temporary capacity issues rather than permanent quota exhaustion.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, clearly explaining the problem, solution, implementation details, and testing status.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 15, 2026

Greptile Summary

Differentiates between temporary backend capacity exhaustion and account-level quota exhaustion to prevent permanent account blocking.

Key Changes:

  • Detects HTTP 429 with "Resource has been exhausted" (without quotaResetTime) as SERVICE_CAPACITY_EXHAUSTED
  • Implements global, in-memory cooldown tracking per family/model using capacityCooldownByKey
  • Progressive backoff strategy: 5s → 10s → 20s → 30s → 60s with automatic reset after 2 minutes
  • Capacity state is NOT persisted to disk (unlike real quota exhaustion)
  • After cooldown, flow correctly restarts from account selection (line 971-980 check → line 982 account selection)

Issue Resolution:
Previously, generic 429 errors permanently marked all accounts as rate-limited in antigravity-accounts.json, requiring manual intervention. Now service capacity issues trigger temporary cooldowns without persisting state, allowing automatic recovery.

Confidence Score: 4/5

  • This PR is safe to merge with low risk - the changes are well-isolated and properly handle a specific edge case.
  • The implementation correctly differentiates between capacity types and uses appropriate recovery mechanisms. The logic flow properly restarts account selection after global cooldown. Tests pass (558/558). Minor consideration: the in-memory state will reset on process restart, but this is intentional and acceptable for transient capacity issues.
  • No files require special attention

Important Files Changed

Filename Overview
src/plugin.ts Adds global capacity cooldown tracking with progressive backoff (5s-60s) and in-memory state management. Logic correctly restarts account selection after cooldown.
src/plugin/accounts.ts Adds SERVICE_CAPACITY_EXHAUSTED reason with detection based on "resource has been exhausted" message without quotaResetTime. Proper backoff array configured.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Plugin as plugin.ts
    participant AccountMgr as AccountManager
    participant Server as Antigravity Server
    
    Client->>Plugin: Request with model
    
    loop Account Selection Loop
        Plugin->>Plugin: Check global capacity cooldown
        alt Capacity cooldown active
            Plugin-->>Client: Show toast "Server at capacity"
            Plugin->>Plugin: Sleep for cooldown duration
        else No cooldown
            Plugin->>AccountMgr: Get next available account
            AccountMgr-->>Plugin: Return account
            
            Plugin->>Server: Send request
            
            alt HTTP 429 with "Resource has been exhausted" (no quotaResetTime)
                Server-->>Plugin: 429 + message
                Plugin->>Plugin: Detect SERVICE_CAPACITY_EXHAUSTED
                Plugin->>Plugin: recordAndGetCapacityBackoff()
                Plugin->>Plugin: markCapacityCooldown(family, model)
                Plugin-->>Client: Show toast with backoff time
                Plugin->>Plugin: Sleep, then break to outer loop
                Note over Plugin: Does NOT persist to disk<br/>Does NOT mark account as rate-limited
            else HTTP 429 with quotaResetTime (real quota)
                Server-->>Plugin: 429 + quotaResetTime
                Plugin->>Plugin: Detect QUOTA_EXHAUSTED/MODEL_CAPACITY_EXHAUSTED
                Plugin->>AccountMgr: markRateLimitedWithReason()
                Note over Plugin,AccountMgr: Persists to disk
                Plugin->>Plugin: Switch to next account
            else Success (200)
                Server-->>Plugin: Response
                Plugin-->>Client: Return response
            end
        end
    end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

src/plugin.ts Outdated
Comment on lines 1291 to 1304
const { delayMs: capacityBackoffMs, attempt } = getCapacityBackoffForKey(family, model);
markCapacityCooldown(family, model, capacityBackoffMs);

const backoffFormatted = formatWaitTime(capacityBackoffMs);
pushDebug(`capacity exhausted on account ${account.index}, backoff=${capacityBackoffMs}ms (failure #${failures + 1})`);
pushDebug(`capacity exhausted (global) family=${family} model=${model ?? ""} backoff=${capacityBackoffMs}ms (attempt ${attempt})`);

await showToast(
`⏳ Server at capacity. Waiting ${backoffFormatted}... (attempt ${failures + 1})`,
"warning",
);
if (!quietMode) {
await showToast(
`⏳ Server at capacity. Waiting ${backoffFormatted}... (attempt ${attempt})`,
"warning",
);
}
await sleep(capacityBackoffMs, abortSignal);
continue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: After handling capacity exhaustion with a cooldown, the code continues the endpoint loop (line 1304). However, this doesn't reset the account selection - it retries with the same account and endpoint. Consider whether the outer while(true) loop should be continued instead to trigger fresh account selection after the cooldown expires.

Currently: cooldown → retry same endpoint with same account
Expected: cooldown → restart from account selection (line 926)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 1291:1304

Comment:
**logic:** After handling capacity exhaustion with a cooldown, the code continues the endpoint loop (line 1304). However, this doesn't reset the account selection - it retries with the same account and endpoint. Consider whether the outer `while(true)` loop should be continued instead to trigger fresh account selection after the cooldown expires.

Currently: cooldown → retry same endpoint with same account
Expected: cooldown → restart from account selection (line 926)

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

src/plugin.ts Outdated
Comment on lines 545 to 555
function getCapacityBackoffForKey(family: ModelFamily, model?: string | null): { delayMs: number; attempt: number } {
const key = getCapacityKey(family, model);
const now = Date.now();
const previous = capacityFailureStateByKey.get(key);
const failures = previous && (now - previous.lastAt < CAPACITY_STATE_RESET_MS)
? previous.consecutiveFailures
: 0;
const delayMs = getCapacityBackoffDelay(failures);
capacityFailureStateByKey.set(key, { consecutiveFailures: failures + 1, lastAt: now });
return { delayMs, attempt: failures + 1 };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The function getCapacityBackoffForKey updates state (line 553) even when just called to get the backoff delay. This creates a side effect during what appears to be a query operation.

Consider separating state mutation from state reading, or rename to clarify the side effect (e.g., incrementCapacityBackoffForKey).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 545:555

Comment:
**style:** The function `getCapacityBackoffForKey` updates state (line 553) even when just called to get the backoff delay. This creates a side effect during what appears to be a query operation.

Consider separating state mutation from state reading, or rename to clarify the side effect (e.g., `incrementCapacityBackoffForKey`).

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@IsraelAraujo70 IsraelAraujo70 changed the title fix: tratar Resource has been exhausted como capacity temporária fix: treat "Resource has been exhausted" as temporary capacity Jan 15, 2026
- Logic: capacity exhausted agora força nova seleção de conta (break ao invés de continue)
- Style: separar função de leitura de mutação (recordAndGetCapacityBackoff + calculateCapacityBackoffDelay)
- Docs: adicionar JSDoc para funções de capacity (5 funções)
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 15, 2026

Greptile found no issues!

From now on, if a review finishes and we haven't found any issues, we will not post anything, but you can confirm that we reviewed your changes in the status check section.

This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR".

@tctinh tctinh changed the base branch from main to dev January 15, 2026 18:43
@tctinh
Copy link
Collaborator

tctinh commented Jan 15, 2026

I just change merge target to dev, please fix conflict

- Added SERVICE_CAPACITY_EXHAUSTED rate limit reason type
- Updated parseRateLimitReason to detect "resource has been exhausted" without quotaResetTime as SERVICE_CAPACITY_EXHAUSTED
- Added progressive backoff tiers for service capacity exhaustion (5s, 10s, 20s, 30s, 60s)
- Integrated global capacity tracking with upstream/dev's rate limit system
- For SERVICE_CAPACITY_EXHAUSTED: uses global in-memory cooldown (not persisted)
- For other rate limits: uses upstream/dev's account-level tracking
@tctinh tctinh merged commit 4e39d98 into NoeFabris:dev Jan 15, 2026
2 checks passed
@IsraelAraujo70 IsraelAraujo70 deleted the fix/treat-resource-exhausted-as-capacity branch January 15, 2026 19:46
tctinh added a commit that referenced this pull request Jan 16, 2026
…e-exhausted-as-capacity"

This reverts commit 4e39d98, reversing
changes made to a0d9245.
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