Skip to content

fix(antigravity): read native quota only on supported CLI versions - #21654

Merged
nwparker merged 1 commit into
nwparker/agy-manual-commandfrom
nwparker/agy-native-quota
Sep 21, 2026
Merged

nwparker merged 1 commit into
nwparker/agy-manual-commandfrom
nwparker/agy-native-quota

Conversation

@nwparker

@nwparker nwparker commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Latest verification — 2026-09-20: The actual production fetcher now returns two populated native quota buckets, and the rendered Usage menu shows Gemini Models weekly 10% and Claude/GPT weekly 0%. Live proof. This supersedes the earlier empty-groups-only limitation. #21678 adds custom executable support. Multiple accounts and Windows/WSL/SSH quota remain unverified. All checks at the unchanged PR head are green. Earlier screenshots and test notes below describe their original runs.

Files Added Deleted Net
Test 13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​895 $\color{#cf222e}{\Huge{\mathbf{−}}}$​95 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​800
Prod 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​652 $\color{#cf222e}{\Huge{\mathbf{−}}}$​117 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​535

ELI5

Antigravity and Gemini no longer share a quota meter in Orca. Orca asks Antigravity for its own numbers. If the CLI supplies no numbers, Orca says usage is unavailable. Older CLIs are refused before the quota command because they can treat it as a paid model request.

What Changed

Replaces the Gemini-derived Antigravity snapshot with bounded agy --version and agy --print /usage --output-format json calls. Only a recognized version at least 1.1.11 reaches /usage; cancellation stops the read. Shows independent Gemini-model and Claude/GPT quota groups, their session/weekly windows, and stale/error states. Group names remain in bucket labels for clients that ignore the new optional metadata.

Adopts the implementation from #20797 (03bce76ef0dca7f7309f1598953a4f45a92c595d), adding the version guard, legacy labels, test isolation, and style adjustments. Stacked on #21653.

Why

The Gemini quota endpoint can reject accounts while Antigravity has its own quota source. Using the CLI avoids extracting OAuth clients or reading credentials ourselves. Checking the executable version on every refresh avoids reusing a positive capability result after an executable downgrade; the existing service controls refresh frequency.

Linked Issue

Addresses #19561 and the quota portion of #19587; related #17345 and #9122. Does not add an Accounts login/card or close those broader issues.

Visual Proof

After, actual installed agy 1.2.7 on macOS: clicked Refresh rate limits in the rebuilt hidden Electron app, then opened the Antigravity usage panel. This API-key configuration returns no buckets.

Actual empty quota response

Populated display fixture, not live OAuth quota: temporary renderer state with four distinct percentages demonstrates group/window separation. Restored actual state after capture.

Quota groups fixture

No before screenshot captured. The previous Gemini-derived behavior is documented by the removed mirror implementation and its tests.

Testing

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

201 focused fetcher/service/status-bar tests pass. A separate live test passed against the guarded production fetcher and installed agy 1.2.7. Regression tests prove 1.1.10, 1.1.11 prerelease, and unknown versions never receive /usage; 1.1.11 and newer supported examples do. The older-version token-spend evidence is from the review on #20797, not a repeated old-version invocation here.

Full typecheck and changed-code quality gate pass. Dev main/preload/renderer and embedded web build launched successfully in background mode. Full release build and broad CI remain pending.

AI Disclosure

Review

Self-reviewed fetch bounds, cancellation, missing/malformed data, separate quota groups, stale results, and mixed-version labels. This remains a draft pending CI and further platform/account verification.

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

Verified on local macOS only. Live populated native quota now passes. Windows/WSL/SSH and paired-runtime quota UI remain unverified. The fetch runs on the service host with its installed CLI/auth; it does not select credentials by the viewed workspace. Custom launch-command overrides are not used by this fetcher. New bucket fields are optional, and legacy names retain group identity. No authentication or global hook settings changed.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • 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)

Adopts native usage parsing and display from PR #20797, adding a pre-invocation version guard and legacy group labels.

Co-authored-by: Abhishek Tiwari <69499+abti@users.noreply.github.com>
@vuthanhtrung2010

Copy link
Copy Markdown

That's a great feature, I woudl like to see it is merged

@Backhong3

Copy link
Copy Markdown

Windows data point for the version guard, since the checks so far were run against the installed macOS agy.

Environment: Orca 1.4.205, Windows 11 x64, agy 1.2.7, Antigravity IDE not running, no Gemini CLI credentials on disk.

agy --print /usage --output-format json returns the expected payload on the supported-version path, with num_turns: 0 and every token counter at 0 — no agent turn, no quota consumed:

{
  "status": "SUCCESS",
  "num_turns": 0,
  "usage": {"input_tokens":0,"output_tokens":0,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":0},
  "command": {
    "name": "usage",
    "data": {
      "groups": [
        {
          "name": "Gemini Models",
          "description": "Models within this group: Gemini Flash, Gemini Pro",
          "buckets": [
            {"id":"gemini-weekly","name":"Weekly Limit Remaining","window":"weekly",
             "remaining_fraction":0.8182741403579712,"reset_time":"2026-09-23T03:12:00Z"},
            {"id":"gemini-5h","name":"Five Hour Limit Remaining","window":"5h",
             "remaining_fraction":0,"reset_time":"2026-09-19T11:20:06Z"}
          ]
        },
        {
          "name": "Claude and GPT models",
          "description": "Models within this group: Claude Opus, Claude Sonnet, GPT-OSS",
          "buckets": [
            {"id":"3p-weekly","name":"Weekly Limit Remaining","window":"weekly",
             "remaining_fraction":1,"reset_time":"2026-09-26T10:57:33Z"},
            {"id":"3p-5h","name":"Five Hour Limit Remaining","window":"5h",
             "remaining_fraction":1,"reset_time":"2026-09-19T15:57:33Z"}
          ]
        }
      ]
    }
  }
}

Worth noting for the rendering side: buckets are per model pool, not per model. Gemini Flash and Gemini Pro share one pool, and Claude Opus / Claude Sonnet / GPT-OSS share another. There is no standalone Flash bucket anywhere in the payload, so a per-model label would promise a number that does not exist.

One Windows-specific gotcha a macOS run will not surface. Invoking agy --print /usage --output-format json from Git Bash (MSYS) with stdout piped returned an empty string with exit code 0. The identical invocation from PowerShell returned the full JSON.

I did not chase the cause, but the failure mode is the part that matters here: an empty read paired with a success exit code is indistinguishable from "the CLI reported no quota", so it would surface as usage unavailable rather than as a fetch error, and the version guard would not catch it. If the fetcher spawns through a shell on Windows, capturing the child process stdout directly avoids that path.

Happy to run a Windows build of this branch against agy 1.2.7 and report back.

@abruption

Copy link
Copy Markdown
Contributor

I also validated the current head (b21303ad) on macOS arm64 with agy 1.2.7.

  • agy --print /usage --output-format json returned SUCCESS, num_turns: 0, and zero for every token counter, while returning all four quota buckets (Gemini weekly/5h and Claude+GPT weekly/5h).
  • Replaying that live payload through this PR's parseAgyUsageResponse produced status: ok with the same four bucket IDs, windows, remaining percentages, and reset timestamps.

I did notice one integration gap with the stacked #21653. That PR allows agentCmdOverrides.antigravity when automatic detection fails, but this PR's getAntigravityUsageCommand() always calls resolveCliCommand('agy'). A direct custom agy path outside PATH can therefore launch successfully while quota refresh still reports the CLI as unavailable.

I have a narrow patch ready that resolves only a direct agy executable from the override, discards launch-only arguments for the fixed /usage invocation, rejects wrappers/shell expressions/relative paths, and re-reads the setting on every refresh. It passes 60 focused tests, 88 rate-limit service regression tests, and tc:node. Happy to send the commit if that fits the intended scope of this draft.

@AlexThunder1989

Copy link
Copy Markdown

Closed #19385, which read quota from the language server's loopback port: that path now returns 401 missing CSRF token and the CLI mints the token in-process, so it is not recoverable — details in #19385 (comment). A point in favour of the CLI route used here.

@nwparker

Copy link
Copy Markdown
Contributor Author

Independent verification update:

  • Installed agy 1.2.7 /usage call exited successfully with structured JSON status and usage metadata; no raw quota/account fields were published.
  • Native quota fetcher/service/status-bar suites: 42 passed in the focused run.
  • pnpm tc:node and pnpm tc:web passed.
  • Changed-code quality passed with 0 new findings.
  • CI run 35437195178 passed typecheck, static analysis, all 8 test shards, verify, and both packages.

Scoped verdict: Approve after stack order for version-gated native quota fetching, stacked on #21653. The populated live quota and rendered usage proof are documented in #21678. Windows/WSL/SSH, paired-runtime, and multiple-account quota remain unverified.

@nwparker

Copy link
Copy Markdown
Contributor Author

Independent verification (2026-09-20):

  • Quota fetcher/service/status-bar suites: 182/182 passed.
  • pnpm tc:node and pnpm tc:web passed.
  • pnpm run check:code-quality:changed origin/main passed with 0 new findings across 102 changed files.
  • Current authenticated agy 1.2.7 /usage print probe exited successfully; JSON output contained the expected command/response/status/usage envelope. Raw quota/account values are omitted.
  • Full CI run 35437195178 is green across all eight test shards, typecheck, static analysis, and both packages.

Verdict: approve after stack order, with #21678 for configured-executable/visibility behavior. This verifies the supported local CLI quota path; Windows/WSL/SSH/paired-runtime and multi-account quota remain outside the proof. No merge performed.

@nwparker

Copy link
Copy Markdown
Contributor Author

ELI5 / evidence / trade-offs audit

ELI5: Orca reads Antigravity's reported quota instead of guessing usage from chat text.

Proof: Authenticated agy /usage probe, focused tests, and CI pass; visual proof is not applicable to this main-process quota path.

User-facing trade-off: The quota is only as accurate and available as agy's endpoint; unsupported hosts show no invented numbers.

This note scopes the claim to the evidence above; it does not claim untested remote, SSH, WSL, or vendor combinations.

@nwparker
nwparker marked this pull request as ready for review September 21, 2026 01:04
@nwparker
nwparker merged commit 2cb3847 into nwparker/agy-manual-command Sep 21, 2026
29 checks passed

@pullfrog pullfrog 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.

ℹ️ No critical issues — a few minor suggestions inline, plus one latent stale route.

Reviewed changes

  • Independent Antigravity fetch replaces the Gemini mirror. antigravity-usage-mirror.ts (and its test) is deleted; a new antigravity-usage-fetcher.ts resolves agy, gates on hasReachedAppVersion(version, '1.1.11'), then reads agy --print /usage --output-format json, mapping groups/buckets into ProviderRateLimits.buckets.
  • Service wiring. Antigravity joins the Promise.allSettled batch and flows through applyStalePolicy; its result may now be error, so recent buckets are retained through transient failures instead of flapping to unavailable.
  • Shared types. RateLimitWindow.windowLabel and RateLimitBucket.{id,groupName,groupDescription} are added as optional fields, so mixed-version wire readers stay compatible.
  • Grouped renderer rendering. Antigravity windows are grouped by groupName in UsageRosterPanel, StatusBarProviderSegment, and ProviderPanel, with new antigravity-usage-format.ts and usage-section-selection.ts helpers and native 5h/wk/group labels.
  • Visibility is decoupled from Gemini OAuth. antigravityUsageConfigured (status item + detected CLI) is now the whole durable signal in status-bar-provider-visibility.ts and use-status-bar-controller.ts.

ℹ️ Antigravity still routes to Gemini's OAuth settings

getUsageProviderAccountsSectionId('antigravity') still returns accounts-gemini, guarded by a comment asserting "Antigravity usage currently shares Gemini's OAuth configuration" and by a passing test. After this PR the Gemini CLI OAuth opt-in has no effect on Antigravity usage, which is read from agy, so that pane no longer configures what the row represents. The route is unreachable through the roster today, so no user hits it yet, but the branch and its test encode the invariant this PR removed and will misroute Antigravity the first time the row's click path changes.

Technical details
# Antigravity accounts route points at Gemini OAuth

## Affected sites
- `src/renderer/src/components/status-bar/usage-provider-settings-target.ts:11-14` — `case 'antigravity'` falls through to `return 'accounts-gemini'` with the now-false comment.
- `src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts:9` — asserts `getUsageProviderAccountsSectionId('antigravity') === 'accounts-gemini'`.
- `src/renderer/src/components/status-bar/use-status-bar-controller.ts:217-225` — the route is consumed by `handleOpenProviderAccounts`.

## Why it is latent today
- `StatusBarSurface.tsx:173-212` always supplies `renderRow`, which wraps every non-Claude/Codex provider in `ProviderDetailsMenu`, so the default `onOpenProvider` row in `UsageRosterPanel.tsx:350` is not reached for Antigravity.
- The sign-in row path needs `state.kind === 'sign-in'`, which `usage-roster-row-state.ts:20-31` only returns for `failureKind === 'missing-credentials'` (or an error string match). Every Antigravity failure result sets a `failureKind`, so it never qualifies.

## Required outcome
- Decide the correct accounts destination for Antigravity now that it has no Gemini OAuth dependency (a settings section for the `agy` CLI, or `null` like Kimi), update the comment, and align the test.
Metadata

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

record.code === 'ETIMEDOUT' ||
record.code === 'ERR_CHILD_PROCESS_TIMEOUT' ||
record.killed === true ||
record.signal === 'SIGTERM'

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.

This treats any signal-killed process as a timeout. execFileCaptureToTermination sets killed for any non-null exit signal, so an agy that crashes or is stopped externally is surfaced to the user as "could not be refreshed before the agy CLI timed out". run-process.ts:346 documents this exact trap — a real deadline already arrives as code: 'ETIMEDOUT' from createTimeoutError. Consider keeping the timeout message for the timeout codes only and wording the signal case separately.

signal
})),
fetchGeminiRateLimits(geminiCliOAuthEnabled),
fetchAntigravityRateLimits(signal),

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.

fetchAntigravityRateLimits resolves agy from PATH and spawns --version plus /usage on every cycle, with no settings opt-in — unlike the Gemini fetcher, main has no knowledge of the Antigravity status-bar item, so a user with agy installed but the bar unchecked still gets both processes each refresh. Is that intended, or should the fetch be skipped unless the provider is actually surfaced? (The PR notes the CLI is now the user's opt-in signal, which main cannot currently see.)

// 'unavailable' until the user opts into Gemini CLI OAuth. Without that
// gate the default-on checked item would pin a permanently dead bar.
return settings.antigravityUsageConfigured === true && settings.geminiCliOAuthEnabled === true
return settings.antigravityUsageConfigured === true

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.

The UsageProviderSettings comment just above (lines 11-15) still says Antigravity durability "further requires geminiCliOAuthEnabled" and that its snapshot "mirrors the Gemini fetch". This line drops that gate, so the comment now documents removed behavior — worth updating so the type doesn't contradict the code.

// Why: stale-retention would otherwise show Gemini numbers as "Refresh failed" Antigravity usage.
expect(service.getState().antigravity?.status).toBe('unavailable')
expect(service.getState().antigravity?.session).toBeNull()
// Why: stale-retention must not substitute Gemini numbers for 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.

The test name (never leaves a cached Antigravity snapshot in the error retry lane, line 98) no longer matches what it asserts: lines 115-116 now require the 18% snapshot to be retained alongside status: 'error'. Renaming it (e.g. "retains the last Antigravity snapshot through a transient failure") would keep the intent readable.

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.

5 participants