Skip to content

fix(rate-limits): stop showing Gemini failures as Antigravity "Refresh failed" - #15876

Merged
nwparker merged 2 commits into
mainfrom
nwparker/antigravity-usage-fetcher
Aug 23, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/antigravity-usage-fetcher

Conversation

@nwparker

@nwparker nwparker commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​170 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​170
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​32 $\color{#cf222e}{\Huge{\mathbf{−}}}$​5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​27

ELI5

The Antigravity chip in the status bar was showing a red "Refresh failed" for a request Orca never made. Orca doesn't actually talk to Antigravity at all — it was quietly copying whatever the Gemini usage fetch returned and relabelling it as Antigravity. So when a user's Gemini sign-in was broken, Antigravity "failed" too. Now Orca only borrows Gemini's numbers when they actually arrived; otherwise the chip says, honestly, that Antigravity usage isn't available.

What Changed

  • New src/main/rate-limits/antigravity-usage-mirror.ts with deriveAntigravityRateLimits(gemini): mirrors the Gemini snapshot only when gemini.status === 'ok', otherwise returns status: 'unavailable' with Antigravity-specific copy and no borrowed Gemini error text. The copy branches on the Gemini status so it does not misattribute the cause: a Gemini unavailable (opt-in off, or no credentials on disk) says a Gemini CLI sign-in must be connected, while a Gemini error (token refresh failed, project ID not found — the reported case, where the sign-in does exist) says the shared Code Assist quota could not be read right now. It reuses gemini.updatedAt so the window-activation freshness check isn't forced into a refetch every cycle.
  • src/main/rate-limits/service.ts: replaced the { ...gemini, provider: 'antigravity' } spread in runFetchAllCycle with a call to that function (net -5/+3 lines). The trackActiveFailureStreak and applyStalePolicy calls are untouched — both already behave correctly for an ok-or-unavailable snapshot (applyStalePolicy passes unavailable through verbatim).
  • Two new test files: a pure unit test for the derivation and a service-level regression test that reproduces the reported symptom.

No renderer change. The unavailable rendering path already exists and is covered: StatusBar.tsx:1288 renders a muted -- chip, tooltip.tsx:269 renders the reason as muted text, usage-roster-row-state.ts:61 labels the row "Usage unavailable".

Why

Root cause is not a missing fetcher registration (as the issue guessed) — it's a fake one. runFetchAllCycle never fetched Antigravity; it spread the Gemini result and relabelled it, so Gemini's status: 'error' and Gemini's raw error string ("Token refresh failed", "Gemini project ID not found") were republished under the Antigravity provider id. The renderer has no way to distinguish that from a real failure, so getProviderUsageStatusLabel fell through to "Refresh failed" (KO: "새로 고침 실패") — the exact reported symptom. The inverse was wrong too: a successful Gemini read was presented as Antigravity usage even for a different Google account.

I chose the honest unavailable state over building a real Antigravity fetcher, deliberately:

  • fetchGeminiRateLimits reads only ~/.gemini/oauth_creds.json and OpenCode's auth.json (gemini-oauth-sources.ts:8,29-52). The Antigravity CLI (agy) writes neither. On a machine with the Antigravity CLI installed and signed in, ~/.gemini/oauth_creds.json does not exist and ~/.gemini/google_accounts.json is {"active": null}; the CLI's own logs show ChainedAuth: authenticated via keyring, and the credential is an OS keyring item (service gemini, account antigravity).
  • A real fetcher would need a new cross-platform keyring dependency, reading another application's credential item (macOS prompts for access), a Code Assist host Orca has never called, and an OAuth refresh Orca cannot perform (no Antigravity client secret; the keyring token is short-lived and refreshed by the CLI itself). That's a feature, not a bug fix — and it collides with the SSH execution boundary, since those credentials live on the execution host, not the client. gemini-usage-fetcher.ts:207-210 already encodes exactly this caution for Gemini's own opt-in.
  • unavailable is already the codebase's word for this state, so no new vocabulary or wire surface is introduced.

No fetch-cadence change: Antigravity does drop out of getActiveWindowRefreshPlan's retryable-failure list, but gemini itself is still there and is likewise absent from INDIVIDUALLY_REFRESHABLE_PROVIDERS, so the plan still falls through canRefreshIndividually === false to a full fetchAll(). (An earlier draft of this description claimed a refresh-cadence win; it was wrong and has been corrected.)

Linked Issue

Fixes #14227

Visual Proof

N/A — no renderer code changed. The visible difference is produced entirely by the already-shipped unavailable branch. A reviewer can confirm by reading the two existing render paths: with a failing Gemini sign-in the Antigravity chip previously took the error branch (StatusBar.tsx:1297, warning styling, "Refresh failed" in the tooltip) and now takes the unavailable branch (StatusBar.tsx:1288, muted -- chip; tooltip shows the Antigravity reason sentence). Reproducing this live requires a Google account with no usable Code Assist project, which I could not stage.

Testing

Commands run (macOS 15, arm64):

npx vitest run --config config/vitest.config.ts \
  src/main/rate-limits/antigravity-usage-mirror.test.ts \
  src/main/rate-limits/service-antigravity-usage.test.ts
# 2 files, 8 tests passed

npx vitest run --config config/vitest.config.ts \
  src/main/rate-limits/service-refresh-orchestration.test.ts \
  src/main/rate-limits/service-minimax-usage.test.ts \
  src/main/rate-limits/service-account-target-selection.test.ts \
  src/main/rate-limits/service-inactive-account-previews.test.ts \
  src/main/rate-limits/service-live-claude-usage.test.ts \
  src/main/rate-limits/service-window-activation.test.ts
# all passed (30 and 58 tests across the two runs)

npx vitest run --config config/vitest.config.ts \
  src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts \
  src/renderer/src/components/status-bar/usage-error-copy.test.ts \
  src/renderer/src/components/status-bar/tooltip.test.ts \
  src/renderer/src/components/status-bar/usage-roster-row-state.test.ts
# 4 files, 80 tests passed

npx vitest run --config config/vitest.config.ts src/main/rate-limits/
# 40 files, 431 tests passed

npx vitest run --config config/vitest.config.ts src/renderer/src/components/status-bar/
# 42 files, 284 tests passed

npx oxlint <the 4 changed files>                                                                   # clean
npx oxlint --config config/oxlint-code-quality-native-plugins.json <the 4 changed files> --deny-warnings  # clean
npx oxfmt --check <the 4 changed files>                                                            # clean

Regression proof (mirror vs. service.ts): I stashed only service.ts (leaving the new tests and the new module in place) and re-ran service-antigravity-usage.test.ts — 2 of 3 tests failed with expected 'error' to be 'unavailable', and the mirrored-ok test still passed, confirming the happy path is unchanged. Restoring service.ts made all 3 pass.

Regression proof (cause-specific copy): I collapsed the gemini.status === 'unavailable' ? ... : ... ternary in antigravity-usage-mirror.ts back to the single sign-in sentence and re-ran the unit test — does not blame a missing sign-in when the quota read itself failed failed with expected 'Antigravity usage is not available. O…' to contain 'could not be read right now'. Restoring the branch made all 5 pass.

Platforms: tests executed on macOS only. Linux and Windows are covered by reasoning — the change is a pure function over an already-fetched snapshot, with no filesystem paths, no shell, no process spawn, and no platform branches. SSH/remote is covered by reasoning; no execution-host contact is added or removed.

I did not run pnpm typecheck / pnpm lint locally (they OOM in this environment); CI covers them.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Review

  • Cross-platform: no paths, no keyboard accelerators, no shell, no platform branches. Strictly less platform-specific than the alternative — a keyring-reading fetcher would have needed Keychain / libsecret / wincred backends.
  • SSH / remote execution boundary: no execution, no process spawn, no host contact added or removed; loss-of-contact vocabulary untouched. A real Antigravity fetcher would have read execution-host credentials, which is what the boundary forbids.
  • Remote wire compatibility: no new field, no new enum member, no new stream opcode. 'unavailable' is already in the shipped ProviderRateLimitStatus union (src/shared/rate-limit-types.ts:12) and in the mobile zod schema (mobile/src/components/accounts-snapshot.ts:54). This is the "changing what the host publishes reaches old clients even with no wire change" case from docs/reference/remote-wire-compatibility.md, and it is safe because old clients already render unavailable as a muted not-configured chip. No capability negotiation needed.
  • Agent / integration compatibility: no agent, CLI, or provider integration touched. Nothing persisted changes; the antigravity status-bar toggle and _antigravityStatusBarDefaultAdded keep their meaning. Users who currently see numbers keep seeing them.
  • Folder workspaces vs worktrees: rate limits are global/per-account, not workspace-scoped — unaffected.
  • Performance: neutral. The derivation is a pure function over an already-fetched snapshot; no extra fetch, and no change to the window-activation refresh cadence (see Why above).
  • UI quality: no renderer edits; the change routes into an existing, already-styled state. The chip goes from a false warning to a muted -- with an explanation.
  • Security: net reduction in surface — the change removes a code path that published one provider's credential-failure text under another provider's identity, and deliberately avoids adding any credential read. No new dependency, no network sink, no shell/eval. Note that the issue's "Proposed Solution" points at out/main/index.js, a build artifact; the fix belongs in src/main/rate-limits/.

Known limitations / follow-ups

Deliberately out of scope:

  1. hasUsageProviderSettingsForProvider (status-bar-provider-visibility.ts:107) still requires antigravityUsageConfigured && geminiCliOAuthEnabled, while the "Antigravity Usage" toggle is offered whenever agy is on PATH. A user with Antigravity installed and Gemini CLI OAuth off gets no chip at all. Relaxing that gate also means adding the term to hasUsageProviderSettings, which would suppress the setup CTA for Antigravity-only users — a deliberate UX call, not a drive-by. The reported symptom requires geminiCliOAuthEnabled on, so this fix is complete for the reported case.
  2. The reason sentence is not localized. It is emitted from main in English (same as 'Gemini CLI OAuth is disabled in settings' and the MiniMax credential-error path) and rendered raw by tooltip.tsx:269, so the detail line stays untranslated on a Korean UI even though the label localizes correctly. Fixing that properly means a provider-keyed unavailable message in usage-error-copy.ts driven by failureKind, applied to Gemini and MiniMax at the same time — not an Antigravity one-off. Reviewer-flagged and knowingly accepted here: the label above the sentence (Usage unavailable) does localize, so a non-English UI still gets a correct status, just an English detail line — the same gap the shipped Gemini and MiniMax unavailable strings already have.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Security, cross-platform support, remote SSH, mobile, backwards compatibility, and performance are all addressed in Review above. Mobile needs no edit: the schema already permits unavailable and the accounts view treats it as no active usage.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…h failed"

The Antigravity snapshot was synthesized as `{ ...gemini, provider: 'antigravity' }`,
so a Gemini fetch error was republished under the Antigravity provider id even though
Orca never queries Antigravity (its CLI keeps the token in the OS keyring). Derive the
snapshot instead: mirror only a successful Gemini read, otherwise report `unavailable`
with Antigravity-specific copy. This also drops Antigravity out of the error retry lane.

Fixes #14227
@coderabbitai

coderabbitai Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds deriveAntigravityRateLimits to mirror successful Gemini usage under the Antigravity provider. Failed Gemini reads now produce sanitized Antigravity results with null usage data, preserved timestamps, and status-specific messages. The rate-limit service uses this helper during refreshes. Tests cover successful mirroring, isolated failures, sign-in messaging, timestamp preservation, and clearing stale Antigravity state.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation satisfies issue #14227 by handling the missing Antigravity fetcher gracefully and reporting usage as unavailable instead of failed.
Out of Scope Changes check ✅ Passed The production changes and regression tests remain focused on Antigravity rate-limit derivation and Gemini failure handling.
Title check ✅ Passed The title clearly identifies the main fix: preventing Gemini failures from appearing as Antigravity refresh failures.
Description check ✅ Passed The description covers the required sections, linked issue, implementation, rationale, testing, compatibility impact, and known limitations.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e28c2b4-5377-4b38-a48c-1cad22d56513

📥 Commits

Reviewing files that changed from the base of the PR and between 2b1254d and 9350612.

📒 Files selected for processing (4)
  • src/main/rate-limits/antigravity-usage-mirror.test.ts
  • src/main/rate-limits/antigravity-usage-mirror.ts
  • src/main/rate-limits/service-antigravity-usage.test.ts
  • src/main/rate-limits/service.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Comment on lines +13 to +15
export function deriveAntigravityRateLimits(gemini: ProviderRateLimits): ProviderRateLimits {
if (gemini.status === 'ok') {
return { ...gemini, provider: 'antigravity' }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not copy Gemini-only buckets into Antigravity state.

Line 15 copies buckets, but ProviderRateLimits defines them as Gemini-only. Construct the Antigravity result from shared quota fields. Add a regression test with Gemini buckets and assert that Antigravity has no buckets.

Proposed fix
   if (gemini.status === 'ok') {
-    return { ...gemini, provider: 'antigravity' }
+    return {
+      provider: 'antigravity',
+      session: gemini.session,
+      weekly: gemini.weekly,
+      updatedAt: gemini.updatedAt,
+      error: null,
+      status: 'ok',
+      usageMetadata: gemini.usageMetadata
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function deriveAntigravityRateLimits(gemini: ProviderRateLimits): ProviderRateLimits {
if (gemini.status === 'ok') {
return { ...gemini, provider: 'antigravity' }
export function deriveAntigravityRateLimits(gemini: ProviderRateLimits): ProviderRateLimits {
if (gemini.status === 'ok') {
return {
provider: 'antigravity',
session: gemini.session,
weekly: gemini.weekly,
updatedAt: gemini.updatedAt,
error: null,
status: 'ok',
usageMetadata: gemini.usageMetadata
}

@nwparker
nwparker merged commit 636f428 into main Aug 23, 2026
47 checks passed
@nwparker
nwparker deleted the nwparker/antigravity-usage-fetcher branch August 23, 2026 04:24
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 23, 2026
CodeHourra pushed a commit to CodeHourra/orca that referenced this pull request Aug 23, 2026
dallascrilley pushed a commit to dallascrilley/orca that referenced this pull request Aug 27, 2026
@leomleao

Copy link
Copy Markdown

@nwparker Thanks for fixing the misleading “Refresh failed” state in #15876—that was a useful correctness improvement.

I think it addresses the visible symptom, but not the underlying provider issue in #14227. I’m hitting this concretely: I have a Google AI plan and use agy, but Gemini CLI no longer allows that consumer account to sign in. Because Antigravity usage still depends on a successful Gemini CLI quota read, Orca’s agy quota is currently broken for me even though Antigravity itself is authenticated and has valid quota data.

More generally, mirroring a successful Gemini result can also show quota from a different account than the active Antigravity session.

#14571 implements the missing native path: it queries the already-running host-local Antigravity service directly for all four quota buckets, supports Desktop and agy over HTTP/HTTPS, and does so without reading keyring credentials or taking ownership of OAuth refresh. It also preserves Gemini CLI independently for Enterprise, API-key, and Vertex AI users.

Could we treat #15876 as the interim error-state fix and #14571 as the follow-up that completes the actual provider support? I think reopening #14227—or linking a follow-up until the native fetcher lands—would better reflect the remaining gap.

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.

[Bug] Antigravity Usage Status Bar indicates Refresh Failed (Missing Usage Fetcher)

2 participants