Skip to content

Read Antigravity usage from its language server - #19385

Closed
AlexThunder1989 wants to merge 5 commits into
stablyai:mainfrom
AlexThunder1989:antigravity-usage
Closed

AlexThunder1989 wants to merge 5 commits into
stablyai:mainfrom
AlexThunder1989:antigravity-usage

Conversation

@AlexThunder1989

@AlexThunder1989 AlexThunder1989 commented Sep 7, 2026 •

Copy link
Copy Markdown

ELI5

The Antigravity usage bar was always empty. It was reading Gemini's quota, and Google stopped answering that. Now it asks Antigravity for its own numbers.

image

What Changed

  • New antigravity-usage-fetcher.ts, replacing antigravity-usage-mirror.ts.
  • It finds the Antigravity language server's port in the newest ~/.gemini/antigravity-cli/log/cli-*.log, then asks it for RetrieveUserQuotaSummary on loopback. The server logs an HTTP port next to its HTTPS one, and the HTTP port needs neither a certificate nor a CSRF token, so there is no TLS to bypass.
  • Antigravity returns two pools — Gemini Models (Flash + Pro) and Claude and GPT models — each with a 5-hour and a weekly bucket. session and weekly show the tightest pool; all four buckets go in buckets for the tooltip.
  • It now runs as its own provider in the fetch cycle, so a Gemini failure no longer decides it.

Why

The mirror copied Gemini Code Assist quota. For individual accounts loadCodeAssist now replies UNSUPPORTED_CLIENT, so there is nothing left to copy and the bar reads "Usage unavailable" forever.

Antigravity has its own quota endpoint, and its language server already serves it locally. That means no cloud call from Orca and no credentials to handle.

Statuses stay meaningful: a server we cannot reach is unavailable (kept out of the retry lane), a server that answers with nothing readable is an error.

Linked Issue

Fixes #19561

Visual Proof

Screenshot above: the Antigravity segment showing real numbers where it previously read "Usage unavailable".

Testing

Ran against a live language server on a Google AI Pro account. Got status: ok with both pools and both windows, matching what Antigravity's own Model Quota screen showed.

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

New unit tests cover the bucket mapping, the tightest-window summary, and skipping unknown windows. The service test now checks that Antigravity is published independently of Gemini.

On Linux: pnpm build, pnpm tc, oxlint and oxfmt --check are clean. pnpm test src/main/rate-limits/ leaves only the 4 codex-fetcher-pty-settle failures, which also fail on an unmodified checkout.

The screenshot above is a packaged local build of this branch, so the numbers are the ones a real Orca window shows, not just a script's output.

Not tested on macOS or Windows. The log path is the same on all three (the Go CLI uses $HOME/.gemini), it is built with path.join, and the request is loopback-only — so there is no platform-specific branch. But I have only run it on Linux.

AI Disclosure

Written with Claude Code (Opus 5), including the reverse-engineering of the endpoint and bucket schema.

Review

Port discovery only looks at the CLI log. The IDE serves the same call and logs its port the same way in its own app-support folder; adding those paths would be a good follow-up.

One assumption worth naming: this reads the HTTP port line, which every CLI log I checked has alongside the HTTPS one. If a build ever stopped logging it, the provider would report "not running" rather than misbehave.

Why six unrelated test suites are touched

The service imports each provider fetcher as a module function and calls them all in one Promise.allSettled, so any test that constructs the service runs the real fetcher. vi.mock is per-file and hoisted, so it cannot live in the shared harness. Without a three-line mock in each of those suites they would read the log directory — and, on a machine with Antigravity running, make a loopback request — during tests that have nothing to do with usage. Those files already mock eight or nine modules each, so this follows existing practice rather than adding a new pattern.

Related friction, not something I think should change here: adding a provider means keeping three positional lists in step — the PromiseSettledResult tuple type, the destructure in service-full-cycle-application.ts, and the results: array — with no compiler check on their order. A mismatch would silently swap two providers' numbers rather than fail. I diffed the three against each other instead of trusting my eyes.

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: no credentials are read or stored, and no certificate check is skipped. The request is loopback-only, over node:http rather than Electron's net.fetch so a configured proxy cannot intercept it.
  • Compatibility: same provider id and shape, so saved UI state is unaffected.
  • Performance: one local request per cycle, cancelled with the cycle's abort signal.
  • SSH and mobile: unaffected. Like the other providers, this reads the local machine only.

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)

@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Walkthrough

Walkthrough

The change adds direct Antigravity quota fetching through local HTTP RPC. It maps quota groups into normalized buckets and session or weekly summaries. The refresh cycle fetches Antigravity data in parallel and publishes it independently from Gemini. Tests cover quota mapping, provider failures, unavailable results, and service mocks. The previous Gemini-to-Antigravity mirroring implementation and tests are removed.

Priority: ➖ Normal — Impact reflects medium issue severity.

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to c3f52

Direct Antigravity reporting can leave a usage refresh pending if the local server truncates its response. Response error and incomplete-close handling should be added before merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 12 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 addresses issue #19561 by querying Antigravity's own RetrieveUserQuotaSummary endpoint, mapping its quota buckets, and publishing Antigravity independently from Gemini. Error and un…
Out of Scope Changes check ✅ Passed The changes remain within scope. The fetcher, provider-cycle integration, test updates, and removal of Gemini mirroring directly support the linked issue and stated objectives.
Title check ✅ Passed The title clearly and concisely describes the main change: reading Antigravity usage from its language server.
Description check ✅ Passed The description includes all required sections, explains the change and rationale, links issue #19561, documents visual proof and testing, records platform limitations, and completes the checklist.
  • Fix all pre-merge checks with AI

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

Run ID: 7f9474f7-e328-41fb-bb54-54d1a8df7715

📥 Commits

Reviewing files that changed from the base of the PR and between 2265fce and 02032c78d7da8991b55fb8df50ad0b8d162795cd.

📒 Files selected for processing (14)
  • src/main/rate-limits/antigravity-usage-fetcher.test.ts
  • src/main/rate-limits/antigravity-usage-fetcher.ts
  • src/main/rate-limits/antigravity-usage-mirror.test.ts
  • src/main/rate-limits/antigravity-usage-mirror.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 with no reviewable changes (2)
  • src/main/rate-limits/antigravity-usage-mirror.ts
  • src/main/rate-limits/antigravity-usage-mirror.test.ts

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

Comment thread src/main/rate-limits/service/service-full-cycle-preparation.ts Outdated

@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 — two minor suggestions inline.

Reviewed changes

  • New antigravity-usage-fetcher — reads Antigravity quota directly from its loopback language server (RetrieveUserQuotaSummary) via the port discovered in ~/.gemini/antigravity-cli/log/cli-*.log, mapping the two pools (Gemini and Claude and GPT) plus their 5h/weekly buckets into ProviderRateLimits.
  • Mirror removal — deletes antigravity-usage-mirror, which previously republished a successful Gemini read under the antigravity provider id.
  • Service integration — adds fetchAntigravityRateLimits() to the Promise.allSettled full-cycle as an independent provider, with the same rejection fallback and stale/failure-streak handling as the other providers.
  • Tests — new fetcher tests (exact bucket mapping, tightest-window summary, unknown-window skip) and a rewritten service test asserting Antigravity no longer depends on Gemini; the test harness and all seven affected suites mock the new fetcher.

The change is well scoped, the deleted mirror has no dangling references, and the assertions are exact rather than lossy — the fetcher and service tests pass locally.

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

Comment thread src/main/rate-limits/antigravity-usage-fetcher.ts Outdated
Comment thread src/main/rate-limits/antigravity-usage-fetcher.ts Outdated
@AlexThunder1989

Copy link
Copy Markdown
Author

All three review findings fixed in a78d231: the cycle's abort signal is passed into the request, the log is picked by mtime rather than name, and a reachable-but-unreadable server now reports error with its own reason instead of claiming Antigravity isn't running. Template filled and issue linked.

@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 — one minor suggestion inline.

Reviewed changes

  • Port discovery by mtime — readLanguageServerPort now stats each cli-*.log and picks the most recently modified file, replacing the lexicographic .sort().pop().
  • Cancellation — fetchAntigravityRateLimits accepts an AbortSignal and forwards it to the HTTPS request; the full-cycle allSettled passes the cycle signal.
  • Reachable-but-unreadable split — connection failures report unavailable/NOT_RUNNING_REASON, while parse and empty-bucket failures report error/UNREADABLE_REASON, so schema drift is no longer disguised as "not running".

Both prior suggestions are addressed cleanly.

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

Comment thread src/main/rate-limits/antigravity-usage-fetcher.ts Outdated
@AlexThunder1989

Copy link
Copy Markdown
Author
image

The mirror borrowed Gemini Code Assist quota, which Google now refuses for
individual accounts. Fetch RetrieveUserQuotaSummary instead: a 5-hour and a
weekly bucket per pool, no cloud call of our own.
Pass the cycle's abort signal into the request, pick the log by mtime rather
than name, and report a reachable-but-unreadable server as an error instead of
claiming Antigravity is not running.
@AlexThunder1989

Copy link
Copy Markdown
Author

Good catch on the non-200 path — a 404 from a drifted endpoint was still landing in the "not running" bucket. Fixed in 341065c: the request now resolves with its status code, and anything other than 200 reports error/unreadable. Earlier threads resolved.

@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 new issues found.

Reviewed changes

  • Non-200 quota responses now report unreadable, not "not running" — fetchAntigravityRateLimits now resolves the response's status code together with its body and routes any non-200 status to error/UNREADABLE_REASON, so a reachable language server with a drifted endpoint is no longer reported as stopped. This closes the one open suggestion from the prior review.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

The server logs an HTTP port alongside the HTTPS one, and it needs neither a
certificate nor a CSRF token, so the self-signed-certificate bypass is gone.
Response interpretation moves into quotaRateLimitsFromResponse, which is where
the reachable-but-unreadable cases are now tested.

@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 new issues found.

Reviewed changes

  • Plain-HTTP transport — the quota read now goes to the language server's HTTP loopback port (node:http) instead of its HTTPS port with rejectUnauthorized: false, removing the certificate bypass while keeping the same loopback-only threat model.
  • Port-line disambiguation — PORT_LINE now matches for HTTP(?!S), so the HTTP port is picked even though the log also carries an HTTPS line (verified: the regex returns only the HTTP port).
  • Response mapping refactor — the reachable-but-unreadable routing moved into an exported, unit-testable quotaRateLimitsFromResponse(statusCode, body) that resolves the full ProviderRateLimits, with the non-200 check performed before parsing.
  • Test coverage — fetcher tests call the new function and add an it.each covering the unreadable cases (drifted endpoint, server error, malformed JSON, no groups, unknown windows); the now-unused bucketId field was dropped from the bucket type.

The transport switch is well-reasoned and the prior feedback threads are all resolved.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@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: Advanced

Run ID: e832a6d3-b2a9-4ad7-a407-bba5177157e5

📥 Commits

Reviewing files that changed from the base of the PR and between 341065c and c3f5245.

📒 Files selected for processing (2)
  • src/main/rate-limits/antigravity-usage-fetcher.test.ts
  • src/main/rate-limits/antigravity-usage-fetcher.ts

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

let chunks = ''
res.setEncoding('utf8')
res.on('data', (chunk: string) => (chunks += chunk))
res.on('end', () => resolve(quotaRateLimitsFromResponse(res.statusCode ?? 0, chunks)))

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 | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the repository-declared Node runtime before running this standalone probe.
for file in package.json .nvmrc .node-version .tool-versions; do
  [ -f "$file" ] && { echo "=== $file ==="; cat "$file"; }
done
node --version

# The probe must report a settled result before the 1-second guard expires.
node --input-type=module <<'NODE'
import http from 'node:http'

const server = http.createServer((_req, res) => {
  res.writeHead(200, { 'content-length': '100' })
  res.write('{"response":')
  res.socket.destroy()
})

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address()

let settled = false
const req = http.request({ host: '127.0.0.1', port }, (res) => {
  res.setEncoding('utf8')
  res.on('data', () => {})
  res.on('end', () => { settled = true })
  res.on('error', () => { settled = true })
  res.on('close', () => {
    if (!res.complete) settled = true
  })
})
req.on('error', () => { settled = true })
req.end()

setTimeout(() => {
  server.close()
  if (!settled) process.exitCode = 1
}, 1000)
NODE

Repository: stablyai/orca

Length of output: 25268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== target file ==='
sed -n '1,240p' src/main/rate-limits/antigravity-usage-fetcher.ts

printf '%s\n' '=== relevant symbols and callers ==='
rg -n -A12 -B8 'fetchAntigravityRateLimits|prepareFetchAllCycle|UNREADABLE_REASON|unusableResult|quotaRateLimitsFromResponse' src/main src/shared tests config

Repository: stablyai/orca

Length of output: 50369


🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings

Length of output: 10605


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '145,215p' src/main/rate-limits/service/service-full-cycle-preparation.ts

Repository: stablyai/orca

Length of output: 2312


🌐 Web query:

Node.js IncomingMessage response complete close error event incomplete HTTP message official documentation

💡 Result:

In Node.js, the http.IncomingMessage class represents an HTTP message (either a request or a response) and extends the readable stream interface [1][2][3]. Its lifecycle and status are managed through several key events and properties [1][4]. Events - 'close': This event is emitted when the underlying connection is closed [1]. Crucially, it indicates that the message is either fully completed or the connection was terminated prematurely before completion [1][5]. - 'error': As a readable stream, http.IncomingMessage emits an 'error' event if an error occurs during the stream's operation [3]. It is important to listen for this event, as unhandled stream errors can cause the Node.js process to crash [3]. - 'aborted': This event is emitted when the request has been aborted by the client [1]. Properties for Incomplete Messages - complete: The message.complete property is a boolean that indicates whether the HTTP message was received and successfully parsed in its entirety [4][2]. Checking this property is the standard way to determine if a message was incomplete when the connection closed (e.g., inside an 'end' event listener) [4][2]. Example usage When handling a stream, you can verify if a message was successfully received by checking the complete property after the stream reaches its end [4][2]: const req = http.request({... }, (res) => { res.resume; res.on('end', => { if (!res.complete) { console.error('The connection was terminated before the message was fully sent'); } }); });

Citations:


Settle incomplete response streams as unreadable errors.

If the server closes before end, the response handler does not resolve this Promise. Handle response error events and incomplete close events, then resolve with unusableResult('error', UNREADABLE_REASON).

Proposed fix
         (res) => {
           let chunks = ''
+          const unreadable = () => resolve(unusableResult('error', UNREADABLE_REASON))
           res.setEncoding('utf8')
           res.on('data', (chunk: string) => (chunks += chunk))
-          res.on('end', () => resolve(quotaRateLimitsFromResponse(res.statusCode ?? 0, chunks)))
+          res.once('error', unreadable)
+          res.once('close', () => {
+            if (!res.complete) unreadable()
+          })
+          res.once('end', () =>
+            resolve(quotaRateLimitsFromResponse(res.statusCode ?? 0, chunks))
+          )
         }

@nwparker

Copy link
Copy Markdown
Contributor

Reviewed head c3f52455e79d84ecec1c9c2442f59141416a181b while verifying the Gemini→Antigravity migration.

P2 — the newest log can hide a still-running language server. readLanguageServerPort() selects only the newest log by modification time (lines 110–116), and the fetcher returns unavailable if that one port is unreachable. With two agy sessions, closing the newer one can therefore remove the usage meter even though the older session still serves quota.

In plain language: Orca checks the last session’s address, finds it closed, and stops looking—even when another Antigravity session is still open.

I compiled this PR’s exact fetcher and exercised it with an isolated temporary log directory and a synthetic loopback quota server:

Case Result
Only the live server’s log ok, 25% used
Newer log points to a stopped server; older server remains listening unavailable; zero requests to the older live server
Newer log contains no port; older server remains listening unavailable; zero requests to the older live server

The test overrides only the home-directory lookup; the candidate’s discovery and HTTP code execute unchanged. All synthetic files and servers were removed. This is a reproduced discovery defect, not populated OAuth quota verification. A bounded inspection of the five latest installed-agy logs found their ports unreachable; it did not establish an alternative live quota source here.

Before adopting this path, discovery needs bounded live-server selection rather than newest-log selection, with an explicit account-selection policy if multiple authenticated servers are running. Also bound the log and response bytes: the current implementation reads the entire chosen log and accumulates the full HTTP body.

I’m retaining draft #21654’s version-guarded native /usage path for now. It has been verified against installed agy 1.2.7 for the empty-quota case; populated authenticated quota remains unverified on this machine. No claim that either approach completes Accounts sign-in or multi-account migration.

@AlexThunder1989

Copy link
Copy Markdown
Author

Closing: the transport this PR relies on is gone. On 8 Sep the language server answered RetrieveUserQuotaSummary unauthenticated; it now requires a CSRF token the CLI generates in-process and never exposes — not in argv, env, logs, files or children. Verified on two fresh 1.2.7 sessions. No small fix exists.

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.

Antigravity usage always reports "Usage unavailable"

2 participants