Skip to content

feat(rate-limits): support Antigravity 5h and weekly quota via local probe - #22862

Closed
lurunzi wants to merge 1 commit into
stablyai:mainfrom
lurunzi:feat/antigravity-rate-limits
Closed

lurunzi wants to merge 1 commit into
stablyai:mainfrom
lurunzi:feat/antigravity-rate-limits

Conversation

@lurunzi

@lurunzi lurunzi commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Orca can now read Antigravity quota (both 5-hour rolling session and weekly limits) directly from the local Antigravity Language Server on the user's machine, falling back to shared Google Code Assist quota when Antigravity is not running.

What Changed

  • Before: Antigravity rate limits were derived strictly by mirroring Gemini CLI OAuth quota. This masked the 5-hour session window behind single-model buckets and lacked weekly quota information unless Gemini OAuth was active.
  • After: Antigravity queries the local Antigravity Language Server Connect-RPC endpoint on localhost (/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary and GetUserStatus) to obtain 5-hour session and weekly quota. If the local probe is unavailable, it gracefully mirrors shared Gemini quota as before.
  • Mechanism:
    • Detects active local Antigravity Language Server processes (language_server_windows_x64.exe, language_server_linux_x64, language_server_macos_*).
    • Verifies socket ownership to restrict requests exclusively to owned language server listeners.
    • Queries Connect-RPC endpoints with CSRF token protection over HTTP/1.1 or HTTPS.
    • Extracts 5-hour rolling session buckets (5h, 300) and weekly buckets (weekly, 10080), populating both session and weekly rate-limit windows.
    • Clears single-model buckets on Antigravity usage to present a clean dual-window interface.
    • Wires the probe into RateLimitServiceFullCyclePreparation and RateLimitServiceFullCycleApplication with fail-closed fallback to deriveAntigravityRateLimits(gemini).

Why

Addresses #16704. Antigravity users frequently work without Gemini CLI OAuth configured, or experience divergent quotas between local Antigravity sessions and shared cloud endpoints. Direct local probing provides real-time, accurate quota metrics without requiring external API tokens or browser logins.

Linked Issue

Fixes #16704

CC @nwparker as discussed in #20469.

Visual Proof

Live evidence captured directly from the status bar and flyout on Windows 11 Pro, demonstrating both single-window and dual-window Antigravity rate limit indicators (5h rolling session and weekly limits) and the · Pro plan badge:

Single-window mode

Antigravity single window quota

Dual-window mode (5h session + weekly)

Antigravity dual window quota

Testing

  • I manually tested these changes locally on Windows 11 Pro
  • Automated tests added/updated:
    • src/main/rate-limits/antigravity-local-probe.test.ts (port detection, process matching, CSRF authentication, error handling)
    • src/main/rate-limits/antigravity-local-probe-linux.test.ts (Linux socket inode resolution)
    • src/main/rate-limits/antigravity-local-quota-request.test.ts (Connect-RPC transport framing and abort signal handling)
    • src/main/rate-limits/antigravity-usage-mirror.test.ts (weekly and 5h session mirror formatting)
    • src/main/rate-limits/service-antigravity-usage.test.ts (full refresh cycle, probe precedence, and fallback)
    • Verified with pnpm tc:node and pnpm run check:code-quality:changed.

AI Disclosure

Antigravity 2.0 was used to refine the local probe implementation, test coverage, and service test harness mocks.

Review

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.

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)

…probe

- Probe local Antigravity Language Server Connect-RPC endpoint to retrieve quota summary and user plan
- Parse 5-hour session quota and weekly quota separately into dual-window presentation
- Update Gemini usage fetcher and mirror to preserve weekly quota and clear single-model masking
- Wire local probe into RateLimitService full cycle with graceful fallback to Gemini OAuth read

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

Important

The probe's transport matches a verified working request, but macOS detection does not: the real IDE bundle is Antigravity IDE.app (with a space), which the executable tokenizer truncates, so the probe returns null on macOS and silently falls back to Gemini. Two smaller findings inline.

Reviewed changes

  • Local probe — new antigravity-local-probe.ts locates the language-server process, verifies listener ownership through the port scanner, queries RetrieveUserQuotaSummary/GetUserStatus, and maps 5h/weekly buckets into session/weekly.
  • Transport — new antigravity-local-quota-request.ts sends the Connect-RPC POST with the CSRF header, a 2.5s timeout, abort propagation, and a 1 MB response cap.
  • Service wiring — the probe joins the provider Promise.allSettled; service-full-cycle-application.ts prefers the probe result and falls back to deriveAntigravityRateLimits(gemini).
  • Gemini mirror — deriveAntigravityRateLimits now clears model buckets, and gemini-usage-fetcher.ts attempts to read session/weekly from the cloud response.
  • Tests — new probe, transport, and mirror suites, plus probe mocks added to the seven service test files that use rate-limit-service-test-harness.

ℹ️ Nitpicks

  • antigravity-usage-mirror.ts:3-6 still opens with "Orca never actually queries Antigravity"; the local probe now does, so that rationale no longer holds.
  • status-bar-provider-visibility.ts:12-15 and :112-116 say Antigravity data is durable only with geminiCliOAuthEnabled. A local-probe ok snapshot bypasses hasUsageProviderSettingsForProvider via isProviderConfigured, so the comment now describes only the fallback path.

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

Comment on lines +151 to +155
const executable = row.command.match(/^(?:"([^"]+)"|([^\s]+))/)
const executablePath = executable?.[1] ?? executable?.[2] ?? ''
if (
!/[\\/](?:\.?antigravity(?:-cli)?|agy)[\\/]/i.test(executablePath) ||
!/[\\/]language_server[^\\/]*$/i.test(executablePath) ||

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.

On macOS the Antigravity IDE bundle is named Antigravity IDE.app (with a space) and ps prints the command line unquoted, so the [^\s]+ alternative captures only /Applications/Antigravity. Both path checks then run against that truncated string and fail, rejecting the real macOS process even though this PR claims macOS support and matches language_server_macos_*. Only Windows and Linux paths are covered by the new tests.

Technical details
# macOS executable tokenization drops the real Antigravity IDE path

## Affected sites
- src/main/rate-limits/antigravity-local-probe.ts:151 — `row.command.match(/^(?:"([^"]+)"|([^\s]+))/)` takes the first whitespace-delimited token when the command is unquoted.
- src/main/rate-limits/antigravity-local-probe.ts:154-155 — both the `antigravity` segment test and the `language_server…$` test then run against `/Applications/Antigravity`.

## Evidence
- macOS `ps` emits, unquoted: `88547 /Applications/Antigravity IDE.app/Contents/Resources/app/extensions/antigravity/bin/language_server_macos_arm --csrf_token=... --extension_server_port 56512 ...` (verified in https://github.com/kunchenguid/quota-axi/issues/164).
- The full path would satisfy both checks (segment `/antigravity/`, trailing `language_server_macos_arm`); only the tokenizer loses it.

## Required outcome
- A macOS language-server process whose executable path contains spaces must be recognized.

## Suggested approach
- Derive the executable from the whole command up to the first ` --` argument rather than the first whitespace token, then keep the existing `antigravity`-segment and trailing-`language_server` checks. Add a macOS `Antigravity IDE.app` case to `selectAntigravityLocalServer` tests.

...new Set(
ports
.filter(
(p) => p.pid === pid && p.port > 0 && (p.host === '127.0.0.1' || p.host === '0.0.0.0')

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 allowlist keeps only 127.0.0.1 and 0.0.0.0, but the scanner's macOS lsof parser reports a wildcard listener as * and IPv6 as ::/::1 (local-workspace-port-scanner.test.ts:44-49). connectHostForBindHost already normalizes exactly those values, so a wildcard-bound language server on macOS would be silently skipped. Conditional on the server binding wildcard rather than loopback.

Technical details
# macOS wildcard listener host is `*`, not `0.0.0.0`

## Affected sites
- src/main/rate-limits/antigravity-local-probe.ts:195 — host allowlist is `'127.0.0.1' || '0.0.0.0'`.
- src/main/ports/local-workspace-port-address.ts:3 — `connectHostForBindHost` maps `'*'`, `'0.0.0.0'`, `'::'` to a loopback host and exists for this normalization.

## Required outcome
- Ports the scanner attributes to the target pid but reports as a wildcard (`*`) should be probed via `127.0.0.1`, consistent with the rest of the scanner's consumers.

provider: 'gemini',
session: deriveSessionSummary(buckets),
weekly: null,
session: quotaSummary.session ?? deriveSessionSummary(buckets),

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.

parseAntigravityQuotaSummary (line 68) only reads a groups array (response.groups or top-level groups), but the cloud retrieveUserQuota response is an array or { buckets: [...] } — see parseQuotaResponse and the Gemini CLI RetrieveUserQuotaResponse type. So quotaSummary is always { session: null, weekly: null } here and lines 74-75 never change behavior; the mirrored weekly stays null. If weekly from the shared quota is intended, the cloud shape needs its own parser; otherwise this call is dead.

Technical details
# Cloud quota response has no `groups`, so the new parse is a no-op

## Affected sites
- src/main/rate-limits/gemini-usage-fetcher.ts:68 — `parseAntigravityQuotaSummary(data)`.
- src/main/rate-limits/gemini-usage-fetcher.ts:74-75 — `quotaSummary.session ?? deriveSessionSummary(buckets)` / `quotaSummary.weekly ?? null`.
- src/main/rate-limits/antigravity-local-probe.ts:87 — reads `response.groups ?? groups`, both absent in the cloud payload.

## Evidence
- `RetrieveUserQuotaResponse { buckets?: BucketInfo[] }` — https://raw.githubusercontent.com/google-gemini/gemini-cli/main/packages/core/src/code_assist/types.ts
- In-repo fixtures for that endpoint only ever use an array or `{ buckets }` (`gemini-usage-fetcher.test-fixtures.ts:40-43`, `gemini-usage-fetcher.test.ts:97-108,151`).

## Open questions for the human
- Is surfacing the cloud weekly window a goal, or was this added speculatively?

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change adds parsing for Antigravity quota and plan responses, plus a local language-server probe that discovers an eligible process and its listening ports. The rate-limit fetch cycle now includes the probe and uses its result when fulfilled, with Gemini-derived limits as a fallback. Gemini quota parsing can provide session and weekly windows, and Antigravity mapping omits model buckets.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 7ac0f

A slow local server can delay quota updates for every provider. The fallback can also show a model quota as Antigravity session usage. Bound the probe and correct the fallback before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 7ac0f

The new local quota probe has useful process, port, and response checks, but it can accept data from an endpoint whose ownership may change between discovery and request. Quota results also lack an account-identity check before they enter shared rate-limit state. The observed exposure is local quota information, not a demonstrated remote or privileged attack path.

Retained concerns

  • Medium · security · inferred: Process and socket ownership are checked during discovery, not when requests carrying the CSRF token are sent. If a local listener replaces the attributed socket, it could receive the token or return quota data that the service accepts; HTTPS does not authenticate that listener.
  • Low · security · inferred: A completed local quota read is applied without checking that the selected process or account still owns the result. A language-server or account transition during a refresh could leave quota from the earlier source visible in current state.
Security review details

Security Blast Radius

  • inferred — The directly observed attackable boundary is a local listener feeding Antigravity quota and plan state. The inspected flow shows no privileged command or remote service request driven by the returned values; downstream uses beyond the inspected rate-limit state remain less certain.

Security Findings and Attack Paths

  • inferred — A local actor able to replace a discovered listener before the request could receive its CSRF header or return plausible quota JSON. The source supports this conditional path, not a demonstrated token compromise or cross-user exploit.

Trust Boundaries and Controls

  • observed — PID-attributed port discovery, single-candidate selection, loopback requests, a CSRF header, bounded responses, and quota schema checks constrain ordinary misattribution. None ties the socket or returned quota to a still-current process and account at application time.

Resilience and Maintainability Implications

  • observed — Full-cycle results occupy corresponding tuple positions, aborts are checked, and an unavailable probe falls back. Unlike several other provider results, the Antigravity result is tracked and applied without a generation or provenance condition.

Hardening Proposals

  • proposed — Bind each request and accepted response to a current listener identity, and avoid treating unverified HTTPS as an endpoint-ownership check.
  • proposed — Carry process or account provenance with a local result and reject it at state application if that provenance has changed.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 18 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 Issue [#16704] asks for a practical solution to Antigravity’s unavailable quota display. This PR adds a local language-server probe that reports five-hour session and weekly quota data, plus Gemini qu…
Out of Scope Changes check ✅ Passed The probe, quota parsing, fallback, bucket clearing, service wiring, and related test changes support the Antigravity objective in issue [#16704]. The PR summary establishes no unrelated changes.
Title check ✅ Passed The title clearly summarizes the main change: adding local Antigravity probing for 5-hour and weekly quota support.
Description check ✅ Passed The description covers the required explanation, changes, rationale, linked issue, visual proof, testing, AI disclosure, and checklist. The Review and Notes sections are empty, and the listed validati…
  • Fix all pre-merge checks with AI

Warning

Review coverage is incomplete: 5 files could not be fully reviewed. Findings from completed review steps are included; see review info for details.


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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 40a0cf22-2458-4a2e-bddc-8d29f9926a8c

📥 Commits

Reviewing files that changed from the base of the PR and between 646e9a5 and 7ac0f0c.

📒 Files selected for processing (18)
  • src/main/rate-limits/antigravity-local-probe-linux.test.ts
  • src/main/rate-limits/antigravity-local-probe.test.ts
  • src/main/rate-limits/antigravity-local-probe.ts
  • src/main/rate-limits/antigravity-local-quota-request.test.ts
  • src/main/rate-limits/antigravity-local-quota-request.ts
  • src/main/rate-limits/antigravity-usage-mirror.test.ts
  • src/main/rate-limits/antigravity-usage-mirror.ts
  • src/main/rate-limits/gemini-usage-fetcher.ts
  • src/main/rate-limits/rate-limit-service-test-harness.ts
  • src/main/rate-limits/service-account-target-selection.test.ts
  • src/main/rate-limits/service-antigravity-usage.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-minimax-usage.test.ts
  • src/main/rate-limits/service-refresh-orchestration.test.ts
  • src/main/rate-limits/service-window-activation.test.ts
  • src/main/rate-limits/service/service-full-cycle-application.ts
  • src/main/rate-limits/service/service-full-cycle-preparation.ts
Files not reviewed due to moderation or processing errors (5)
  • src/main/rate-limits/antigravity-local-probe.ts
  • src/main/rate-limits/antigravity-local-quota-request.ts
  • src/main/rate-limits/antigravity-local-probe.test.ts
  • src/main/rate-limits/antigravity-local-quota-request.test.ts
  • src/main/rate-limits/antigravity-local-probe-linux.test.ts

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

provider: 'antigravity',
// Why: Antigravity distinguishes the 5-hour session and weekly windows cleanly,
// without masking the session window behind model-named buckets.
buckets: undefined

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '40,95p' src/main/rate-limits/gemini-usage-fetcher.ts
sed -n '1,105p' src/main/rate-limits/gemini-bucket-formatting.ts
sed -n '1,95p' src/main/rate-limits/antigravity-usage-mirror.ts
sed -n '70,100p' src/main/rate-limits/service/service-full-cycle-application.ts

Repository: stablyai/orca

Length of output: 7469


🏁 Script executed:

printf '%s\n' '--- Gemini parser and fetcher ---'
sed -n '1,65p' src/main/rate-limits/gemini-usage-fetcher.ts
printf '%s\n' '--- Shared rate-limit types ---'
cat -n src/shared/rate-limit-types.ts
printf '%s\n' '--- Session/window consumers and Antigravity labels ---'
rg -n -C 3 "windowMinutes|\.session|session.*5.hour|5.hour.*session|Antigravity" src
printf '%s\n' '--- PR change summary ---'
git diff --stat 646e9a5b02514795af5139961ccca225dfa01b12 7ac0f0cbfd0362787ec1091de461f5c5129e7433 -- src/main/rate-limits/antigravity-usage-mirror.ts

Repository: stablyai/orca

Length of output: 45659


🏁 Script executed:

printf '%s\n' '--- Quota summary parser ---'
rg -n -C 12 'parseAntigravityQuotaSummary|session:|weekly:' src/main/rate-limits/antigravity-local-probe.ts
printf '%s\n' '--- Rate-limit UI files ---'
git ls-files 'src/renderer/*' 'src/shared/*' | rg 'rate.?limit|usage'
printf '%s\n' '--- Focused duration and session display references ---'
rg -n -C 3 'windowMinutes|rateLimits\.session|\.session\?.*usedPercent|session.*label|Session' src/renderer --glob '*rate*' --glob '*usage*' --glob '*.tsx' | head -n 450

Repository: stablyai/orca

Length of output: 40912


🏁 Script executed:

printf '%s\n' '--- Recognized quota groups and duration construction ---'
sed -n '54,125p' src/main/rate-limits/antigravity-local-probe.ts
printf '%s\n' '--- Duration consumers ---'
rg -n -C 3 'windowMinutes' src/renderer/src src/shared
printf '%s\n' '--- Rate-limit session property consumers ---'
rg -n -C 2 'rateLimits\??\.session|rateLimits\.session|\.session\?\.usedPercent|session\.usedPercent' src/renderer/src/components src/renderer/src/lib

Repository: stablyai/orca

Length of output: 35972


Do not mirror a model bucket as Antigravity’s session.

When Gemini has only model buckets, deriveSessionSummary creates a 60-minute window. deriveAntigravityRateLimits preserves that window in session while removing buckets. The session contract requires a 5-hour window. Preserve the session only when windowMinutes === 300; otherwise set it to null.

🐛 Suggested fix
       ...gemini,
       provider: 'antigravity',
+      session: gemini.session?.windowMinutes === 300 ? gemini.session : null,
       // Why: Antigravity distinguishes the 5-hour session and weekly windows cleanly,
       // without masking the session window behind model-named buckets.
       buckets: undefined

apiKey: miniMaxApiKey
})
}),
probeLocalAntigravityLanguageServer({ 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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not make every provider wait for the local probe.

If an eligible local server accepts a connection but delays a quota or user-status response, Promise.allSettled cannot finish until that request settles. The probe can repeat this wait across ports and protocols. Claude, Codex, and the other completed results therefore remain in fetching instead of updating. Give the whole probe a short deadline, or apply the other provider results independently of it.

@lurunzi

lurunzi commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Known Limitation / Defect Supplement

Observed during manual testing on Windows 11:

When the user exits the Antigravity desktop application (\Antigravity.exe), the operating system terminates its child process tree, including the background \language_server.exe\ process.

  • Impact on Quota: The local Connect-RPC probe (\probeLocalAntigravityLanguageServer) no longer finds a candidate process. If the user does not have a separate Gemini CLI login, the fallback marks Antigravity rate limits as 'unavailable'\ and the status bar chip disappears.
  • Impact on Sessions: Any active Antigravity streaming turns drop with connection reset / token refresh errors (\error getting token source: You are not logged into Antigravity) because authentication and the Host Bridge reside with the desktop application.

Unlike Claude Code and Codex (which run as standalone CLI tools without external GUI app dependencies), Antigravity currently requires the desktop app to remain running in the background.

This follow-up defect and potential UX improvements (stale quota state caching, graceful exit messaging, headless daemon exploration) have been filed for tracking in #23008.

@lurunzi

lurunzi commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #23426, which reads Antigravity quota from agy --print /usage --output-format json (the maintainer-chosen approach from #21654/#21678). That works without the Antigravity app running, which this local language-server probe cannot do (see #23008). Closing in favour of it.

@lurunzi lurunzi closed this Sep 27, 2026
lurunzi added a commit to lurunzi/orca that referenced this pull request Sep 27, 2026
Replaces the fork's local language-server probe (superseded, stablyai#22862
closed) with the agy CLI /usage fetcher, so quota shows without the
Antigravity app running. Keeps the fork's Cursor mocks and win32 test
expectations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

[Feature]: Multi-tiered AI Usage Tracking Parity across Providers (Codex, Grok) and Practical Handling for AGY (--)

1 participant