Repository navigation
fix(rate-limits): read real Antigravity quota from the agy CLI, not the Gemini mirror - #24073
Conversation
Orca published a successful Gemini `retrieveUserQuota` read under the antigravity provider id. That reported Gemini CLI per-model buckets on a 60-minute window, so Antigravity's real pools were never shown, the weekly window was always null, and the segment depended on an installed @google/gemini-cli for token refresh that an Antigravity user has no reason to have. Read the quota from `agy -p "/usage" --output-format json` instead, which is the only caller that can authenticate it — agy keeps its credential in the OS keyring and mints its own token against daily-cloudcode-pa. Fixes #9122 Fixes #22511
Without the stub, each RateLimitService suite spawned the developer's real `agy` and resolved a login shell, which turned service-window-activation from 214 ms into 14 s and broke its fake-timer fetch counts.
…a read The flag stops agy expanding `/usage` as a command, so the text goes to the model as an ordinary prompt: the call starts a conversation, spends quota, and on an account near its limit answers RESOURCE_EXHAUSTED (429) instead of a reading. Adds an opt-in real-CLI suite that catches exactly this.
|
| session: reading.session, | ||
| weekly: reading.weekly, | ||
| buckets: reading.buckets.map(({ id: _id, ...bucket }) => bucket), |
There was a problem hiding this comment.
Weekly usage disappears When agy reports weekly-only pools, this publishes named buckets and a weekly reading but no session reading. The status bar’s verbose view filters out those bucket names and falls back only to session or monthly usage. As a result, the default Antigravity segment shows no number even when a weekly pool is exhausted.
| if (output.toLowerCase().includes(NOT_SIGNED_IN_MARKER)) { | ||
| return unavailable( | ||
| 'Antigravity usage is not available. Sign in with `agy` to report this account’s quota.', | ||
| 'missing-credentials', | ||
| now() | ||
| ) |
There was a problem hiding this comment.
| if (output.toLowerCase().includes(NOT_SIGNED_IN_MARKER)) { | ||
| return unavailable( | ||
| 'Antigravity usage is not available. Sign in with `agy` to report this account’s quota.', | ||
| 'missing-credentials', | ||
| now() | ||
| ) |
There was a problem hiding this comment.
Signed-out guidance stays hidden Antigravity no longer depends on Gemini OAuth, but a signed-out agy account returns
unavailable. The renderer still requires Gemini OAuth to display an unavailable Antigravity slot. With agy installed and Gemini OAuth off, the slot and its new sign-in guidance remain hidden.
There was a problem hiding this comment.
Important
This PR replaces the Antigravity Gemini mirror with a real agy fetch, but several renderer assumptions were only true because Antigravity was Gemini, and they were not updated. The most visible: the default status-bar segment no longer shows a number for Antigravity at all.
Reviewed changes
- New
agyread.antigravity-usage-command.ts/antigravity-usage-fetcher.tsresolveagyon the login-shell PATH, run-p "/usage" --output-format json --print-timeout 20sthroughrunProcess, and classify absent-CLI/signed-out asunavailable, timeout/parse aserror. - New parser.
antigravity-usage-response.tsreadscommand.data.groups[].buckets[](not the lossyresponsetext), mapsweekly→10080 /5h→300, dropsdisabledbuckets, names buckets by group, and summarises each window by its most-constrained group. - Shared summariser.
deriveMostConstrainedWindowmoves torate-limit-bucket-summary.ts; Gemini and Antigravity share it. - Service wiring.
antigravityResultPromiseis a sibling promise settled alongside grok/cursor/zcode; the mirror module and its test are deleted; the test harness and seven service suites stub the new fetcher.
⚠️ Antigravity's real pools never reach the default status-bar segment
The fetch now publishes group-named buckets ("Gemini Models", "Claude and GPT models") with session: null and weekly populated, but the verbose segment's bucket allowlist is Gemini's old model names, and its fallback window omits weekly. The result is that the default (verbose) Antigravity chip renders an icon and an unlabeled bar with no percentage — a regression, since the deleted mirror always carried Gemini's non-null session and Gemini-named buckets. The usage roster panel is fine; only the inline segment drops it.
Technical details
# Antigravity inline status-bar segment renders nothing
## Affected sites
- `src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx:181-205` — when `p.buckets` is non-empty it filters by `isVisibleStatusBarBucket` and falls back only to `p.session ?? p.monthly`. Antigravity's buckets are named by group and `session` is null, so `visibleBuckets` is empty and `fallbackWindow` is null.
- `src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx:166-172` — the allowlist is Gemini's `Flash`/`Pro`/`1.5 Pro` plus Cursor pools.
- `src/main/rate-limits/antigravity-usage-response.ts:90-95` — where the group name is minted (`formatBucketName`).
## Required outcome
- The default (verbose) Antigravity segment must show its real pool(s) or at least a labelled window, not an unlabelled bar.
## Suggested approach
- Add the Antigravity group names to `STATUS_BAR_BUCKET_NAMES`, or make the fallback `p.session ?? p.weekly ?? p.monthly`. Since the buckets *are* the whole meter for this provider, surfacing them is the intent.
## Open questions for the human
- With `usageTightestOnly`/compact the segment shows `tightest.label` (a group name), so is the intended inline presentation the two group pools, or a single "weekly" chip?⚠️ The renderer still gates Antigravity on the Gemini OAuth opt-in
Main no longer gates the Antigravity fetch on geminiCliOAuthEnabled, but the renderer still does. With the opt-in off, an unavailable Antigravity snapshot (no agy installed, or signed out) is hidden, so the new "Sign in with agy" / "CLI not found" guidance never appears; the durable-settings check also only counts Antigravity as configured via the Gemini term. The sign-in affordance additionally routes to the Gemini accounts section, which cannot sign a user into agy.
Technical details
# Renderer still keys Antigravity off Gemini OAuth
## Affected sites
- `src/renderer/src/components/status-bar/status-bar-provider-visibility.ts:116-121` — requires `antigravityUsageConfigured === true && geminiCliOAuthEnabled === true`.
- `src/renderer/src/components/status-bar/status-bar-provider-visibility.ts:82-86, 94-100` — `hasUsageProviderSettings` comment/term assume Antigravity is covered by the Gemini term.
- `src/renderer/src/components/status-bar/use-status-bar-controller.ts:99-102` — comment codifies the same coupling.
- `src/renderer/src/components/status-bar/usage-provider-settings-target.ts:11-14` — Antigravity → `accounts-gemini`.
## Required outcome
- An Antigravity-only user (agy installed, Gemini OAuth off) sees the bar and its actionable guidance, and the sign-in CTA points somewhere that can actually authenticate `agy`.
## Open questions for the human
- Is a dedicated Antigravity accounts section intended (cf. #17345/#19587), or should the CTA be suppressed until one exists?ℹ️ No minimum agy version gate or fallback for the print-mode read
The invocation is hardcoded with no capability probe or version gate, and the load-bearing print-mode behaviour is versioned. In the upstream google-antigravity/antigravity-cli CHANGELOG, --print-timeout predates this command, --output-format was added in 1.1.8, and the read-only print-mode slash commands (so -p "/usage" does not start a turn) were added in 1.1.11. On an older CLI the command has no defined outcome, and per the author's own note an unexpanded slash command is sent as an ordinary prompt — the exact billed-turn failure the comment warns about. Worth documenting the minimum supported version and defining the fallback.
Technical details
# Version compatibility of the agy print-mode read
## Affected sites
- `src/main/rate-limits/antigravity-usage-command.ts:14-27` — single hardcoded argv, no version/capability check.
## Evidence
- `--print-timeout`: present by agy 1.1.1 (superseded semantics in 1.1.28, 1.2.6, 1.2.9).
- `--output-format`: added in 1.1.8.
- read-only print-mode `/usage` (no agent turn, no quota): added in 1.1.11.
- Source: `google-antigravity/antigravity-cli` `CHANGELOG.md`.
## Required outcome
- Define behaviour for a CLI predating the read-only `/usage` contract so it cannot silently fall into a billed turn.
## Suggested approach (optional)
- Document the minimum supported `agy` version, and consider a capability probe (inspect `--help`/version) before treating absence as "not installed". Note the repo's `docs/reference/antigravity-readiness-evidence.md` warns that `agy --version` and the TUI banner disagree, so any numeric gate is unreliable.ℹ️ Nitpicks
src/main/rate-limits/antigravity-usage-command.ts:53—isPlainAntigravityExecutableis exported but never called; its doc comment describes a rule the fetcher (which always resolvesagydirectly) does not enforce. Remove it or wire it in.src/main/rate-limits/antigravity-usage-fetcher.ts:44-46— the comment says "'unavailable' and not 'error' for every failure", but the siblingfailed()helper returnsstatus: 'error'. Consider rewording so it describes only the absent-CLI/signed-out case.src/shared/rate-limit-types.ts:4— thewindowMinutesdoc still says "300 (5h) or 10080 (7d)", but the parser now emits0for an unrecognised agy window.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| * to that either runs the wrong program or feeds the slash command to the wrong argv slot. The quota | ||
| * read resolves the plain executable itself instead. | ||
| */ | ||
| export function isPlainAntigravityExecutable(command: string): boolean { |
There was a problem hiding this comment.
isPlainAntigravityExecutable is exported but never called (repo-wide grep finds only this definition). Its doc comment describes a rule the fetcher does not enforce — the fetcher always resolves the plain agy executable itself. Either delete it or use it where a configured launch command is considered.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe change adds an agy-based Antigravity quota parser and fetcher, then publishes its result independently from Gemini during rate-limit refresh. It also adds a shared helper for selecting the most constrained quota window and updates Gemini formatting to use it. Tests cover parsing, CLI outcomes, refresh behavior, and an opt-in real-CLI run. Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Antigravity usage now comes from agy independently of Gemini. The supplied evidence identifies no actionable merge-blocking risk. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The new background quota check uses fixed arguments and bounded execution. However, an incompatible CLI can turn the intended read-only check into a quota-consuming request before subsequent checks are disabled. Remote-host routing and account-switch behavior were not fully verified. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The PR satisfies the independent Antigravity source and CLI-decoupling requirements in [
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
In print mode an unrecognised slash command is not an error — agy sends the text to the model. On a build that does not know `/usage`, polling would start a conversation and spend the user's quota every cycle while Orca reported no quota. The envelope distinguishes the two: a command reply has an empty conversation_id and num_turns 0. A successful parse is checked first, so a real reading can never trip the latch.
There was a problem hiding this comment.
✅ No new issues found in this delta.
Reviewed changes
- Added an unsupported-command latch. A module-level
usageCommandUnsupportedflag short-circuits the fetch tounavailablewith an "updateagyand restart Orca" message once charged, with a test-onlyresetAntigravityUsageSupportForTeststo clear it. - Detects a model-turn reply. New
didRunModelTurn/stdoutShowsModelTurninantigravity-usage-response.tstreatnum_turns > 0or a non-emptyconversation_idas evidence agy ran the slash command as a prompt. - Latches only after a failed parse. The fetcher checks the successful-parse branch first, so a real reading can never trip the latch; transient/unparsable answers keep retrying.
- Tests. Covers the latch firing, single-spawn across repeated fetches, no-latch-when-parsed, and no-latch-on-transient-failure. All 35 rate-limit parser/fetcher tests pass locally.
The single production caller (service-full-cycle-preparation.ts:161) runs once per cycle on the local login-shell PATH, so the process-global latch does not leak across hosts or accounts, and the dedicated agy binary makes the "this version cannot answer" verdict a per-process property. Confirmed the latch cannot be set from the observed 1.2.11 command envelope (num_turns: 0, empty conversation_id) because a successful parse returns before the turn check.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Merge stablyai/orca main up to 78daf71 on top of the 2026-10-01 sync and fork/main 49d4264. - Antigravity quota: upstream stablyai#24073/stablyai#24074 land their own agy-CLI quota reader and model-group pools; the fork's earlier implementation, its configured-command override and its status-bar extras (grouped compact metrics, reset countdown, grouped tooltip/roster) are removed in favour of upstream's. - Agent exit confirmation: keep the fork rule that only a shell in the foreground closes chat, with Windows PTY job proof (07da011, 7a40146), on top of upstream's hook-presence verdicts; upstream's wsl.exe/tmux and owner-less silence cases are adapted to that rule. - Claude adapter: prompt suggestion bookkeeping folded into upstream's child-record observation. - Coordinator identity loader opens the orchestration database only if it exists; upstream's idle-edge mail lookup otherwise created it for every structured session. - Fork capabilities advertised via shared/agent-provider-runtime-capabilities.ts, prompt-suggestion reducer wrapper and frame-field alias keep upstream files under max-lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

ELI5
Orca's "Antigravity" usage number was never Antigravity's. It was the Gemini CLI's quota with the label changed. This asks the Antigravity CLI itself, which is the only thing that can answer.
What Changed
Before: the main process built the Antigravity reading out of the Gemini one — literally
{ ...gemini, provider: 'antigravity' }behind a small wrapper. The user saw Gemini CLI per-model buckets ("Pro", "Flash", …) on a 60-minute window, the weekly limit was always blank, and if@google/gemini-cliwas not installed the segment said "Token refresh failed" forever.After: Orca runs
agy -p "/usage" --output-format jsonon the machine that owns execution and publishes what agy reports — one named pool per model group, on the window agy actually meters.The mechanism:
antigravity-usage-response.tsparses agy's print-mode envelope (command.data.groups[].buckets[]). It readscommand.data, not the human-readableresponsetext, because the text rounds the fraction to a whole percent and drops both the bucket ids anddisabled.agy'swindowis mapped to Orca's minute counts (weekly→ 10080,5h→ 300). An unrecognised window stays a named bucket with no duration rather than being forced into one of the two, so a future agy window cannot silently be drawn as a weekly limit.disabledbucket is dropped instead of being drawn as 0% used — Antigravity (agy) quota shown incorrectly — weekly limit never displayed (reads a 60-min per-model window) #22511 hit an account whosegemini-5hpool was disabled whilegemini-weeklywas exhausted, and showing the disabled pool would report headroom the account does not have.antigravity-usage-fetcher.tsresolvesagywith the existingresolveCommandOnLocalPath(nowhich/wheresubprocess) against the login-shell PATH, spawns it throughrunProcess, and classifies the outcome: absent CLI and signed-out areunavailablewith an actionable message; timeout and unreadable payload areerror.deriveSessionSummarymoved out ofgemini-bucket-formatting.tsinto a provider-neutralrate-limit-bucket-summary.ts; both providers now share one implementation.Why
agykeeps its Google credential in the OS keyring (macOS: servicegemini, accountantigravity) and mints its own token againstdaily-cloudcode-pa. Nothing outside agy can authenticate its quota endpoint, so the CLI is the source of truth rather than a convenience.Alternatives I investigated and rejected:
Language server listening on random port at 60441 for HTTPS (gRPC)) but the port is random per run and there is no server while agy is not running, so a poll on a cadence has nothing to talk to most of the time.-p /usageto that runs the wrong program or puts the slash command in the wrong argv slot. The fetch resolves the plain executable itself.The quota-spend guard. In print mode an unrecognised slash command is not an error — agy sends the text to the model as an ordinary prompt. So on a build of agy that does not know
/usage, a 15-minute poll would quietly start a conversation and spend the user's quota every cycle, while Orca reported "did not report a quota" the whole time. The envelope distinguishes the two cases: a command reply carries an emptyconversation_idandnum_turns: 0; a prompt carries a conversation id and at least one turn. When Orca sees a turn, it latches Antigravity usage as unsupported and never probes again for the life of the process — because the evidence is the cost, and rediscovering it on a cadence would keep paying for the same answer. A successful parse is checked first, so a real reading can never trip the latch.I chose a runtime capability check over a version floor because I could not source when
/usagewas added — agy's changelog shows it being improved around 1.2.6 but not introduced. A guard that detects the actual failure needs no version table and cannot go stale. (#23426 took the version-gate route, which is what prompted this.)One thing worth flagging for reviewers: do not add
--disable-slash-commandsto this invocation. It looks like a hardening flag and it is the opposite — it stops agy expanding/usageas a command, so the text is sent to the model as an ordinary prompt. The call then starts a conversation, spends quota, and on an account near its limit returnsRESOURCE_EXHAUSTED (429)instead of a reading. I did this by accident while validating and burned a real weekly pool; the comment inantigravity-usage-command.tsand the opt-in real-CLI suite both exist to stop it happening again.Linked Issue
Fixes #9122
Fixes #22511
Fixes #14515
Unblocks the data source that #16704 and #21071 need. Related #17345, #19587 (Accounts surface, separate change).
Three earlier reports of the same root cause were closed without the data source being fixed, and this is what they were describing — worth reopening or closing as duplicates of the above: #19561 ("Usage unavailable"), #14227 ("Refresh failed — missing usage fetcher"), #7809 (segment disappears while the toggle is on).
Visual Proof
No renderer change — the status bar and Accounts pane render
ProviderRateLimitsas they already did; this PR changes what that record contains. The published record, read from a liveagy1.2.11 on macOS with one pool genuinely exhausted:{ "status": "ok", "session": null, "weekly": { "usedPercent": 100, "windowMinutes": 10080, "resetsAt": 1791361098000 }, "buckets": [ { "name": "Gemini Models", "usedPercent": 100, "windowMinutes": 10080 }, { "name": "Claude and GPT models", "usedPercent": 0, "windowMinutes": 10080 } ], "meta": { "source": "cli", "credentialSource": "antigravity-cli" } }That is the #22511 account state: one group out of weekly quota, the other untouched. Before this change the same account reported
weekly: nulland a list of Gemini CLI model buckets, so a user would route work to Antigravity and hit a 429 mid-task.Testing
Validated against the real CLI, not a remembered payload —
agy1.2.11, macOS arm64:agy -p "/usage" --output-format jsonandagy -p "/quota" …both answer ascommand.name: "usage"; the envelope reportsnum_turns: 0, every token counter0, and an emptyconversation_id, so a poll spends no quota and starts no conversation.remaining_fraction: 1) and an exhausted one (remaining_fraction: 0) and confirmed the published record for each.ORCA_REAL_AGY_CLI_TEST=1 pnpm test src/main/rate-limits/antigravity-usage-real-cli.test.ts— passes against the live CLI (opt-in, off by default, same shape as the existing real-Claude-CLI suite). This is the check that caught the--disable-slash-commandsmistake.pnpm test src/main/rate-limits/— all passing. 35 new tests across the parser and fetcher, including the verbatim envelope agy produced when it answered/usageas a prompt, including the verbatim agy 1.2.11 payload as a fixture.pnpm tc:node,oxlint,pnpm run check:code-quality:changed— clean.Host coverage via
orca host list:macOS (local, darwin) — signed in, fully exercised as above.
Linux (
ssh:openclaw) —agyis not installed, verified through anorca terminalon that host. That is thecli-unavailablepath, and it is why the absent-CLI case reportsunavailablewith "not found on this machine" instead of an error that keeps retrying.Windows — not exercised locally; the spawn goes through
runProcessand the lookup throughresolveCommandOnLocalPath, both of which already carry the Windows argument-encoding and PATHEXT rules. Windows verification is the main thing I would like help with.I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
SSH boundary: this fetch runs wherever the rate-limit service runs, which is the execution host for an
orca serveruntime and the desktop for an SSH-attached workspace. That matches every non-Claude/Codex provider today (Gemini, Cursor, Grok, Zcode all read local credentials), so this PR does not change the boundary — it does not fix it either. Making usage follow an SSH execution host is #19209 / #16466 and deliberately out of scope here.Performance: one extra ~2.5 s child process per refresh cycle, off the critical path. No polling change.
Backwards compatibility:
ProviderRateLimitsis unchanged — no new fields, no wire change, so a paired old client decodes this exactly as before and simply sees better numbers. What the host publishes changes, which reaches old clients; that is the intended fix and it degrades safely (a client that ignoredbucketsstill readsweekly).Security: no token material is read, logged, or returned. The fetch never touches the keychain; agy does.
Agent skill upstream boundary
Notes
This supersedes a long queue of PRs aimed at the same bug. I read all of them before building, and the design here takes the mechanism the
agy-CLI group converged on, plus specifics each of them contributed:agy -p "/usage"as the read, and that agy's keyring is why only the CLI can answer.--output-format jsonrather than parsing the tab-separated text.gemini-5h,gemini-weekly,3p-5h,3p-weekly).Approaches I did not adopt but which mapped the design space, and whose reasoning is in the Why section: #21999 (@Mivr), #20797 (@abti-ai), #22055 (@werlang), #18167 (@eojonathan) on keychain-and-API; #20933 (@lippdev), #23382 (@yuta27jojo-rgb), #14571 (@leomleao) on the local language server; #19209 (@abruption) on the SSH-host case; #20388 (@bluetomlee), #19588 (@artile), #7952 (@andrecristodev), #23761 (@Tai-DT) on the Accounts surface.
Issue reporters whose diagnosis this is built on: @liuyifeng92 (#9122, traced the mirror and the
@google/gemini-clirefresh dependency) and @rsuzukimktdigital (#22511, documented thegroups[].buckets[]contract includingdisabled, which is where the disabled-bucket handling comes from).Co-authored-by: baioccheg-bit baioccheg-bit@users.noreply.github.com
Co-authored-by: mikeascendx mikeascendx@users.noreply.github.com
Co-authored-by: isairz isairz@users.noreply.github.com
Co-authored-by: lurunzi lurunzi@users.noreply.github.com
Co-authored-by: brennanb2025 brennanb2025@users.noreply.github.com
Co-authored-by: liuyifeng92 liuyifeng92@users.noreply.github.com
Co-authored-by: rsuzukimktdigital rsuzukimktdigital@users.noreply.github.com
Checklist
oxlint,pnpm tc:node,pnpm test src/main/rate-limits/pass locally