Repository navigation
Read Antigravity usage from its language server - #19385
AlexThunder1989 wants to merge 5 commits into
Conversation
📝 WalkthroughWalkthroughThe 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 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)
✅ Passed checks (4 passed)
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 |
There was a problem hiding this comment.
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.tssrc/main/rate-limits/antigravity-usage-fetcher.tssrc/main/rate-limits/antigravity-usage-mirror.test.tssrc/main/rate-limits/antigravity-usage-mirror.tssrc/main/rate-limits/rate-limit-service-test-harness.tssrc/main/rate-limits/service-account-target-selection.test.tssrc/main/rate-limits/service-antigravity-usage.test.tssrc/main/rate-limits/service-inactive-account-previews.test.tssrc/main/rate-limits/service-live-claude-usage.test.tssrc/main/rate-limits/service-minimax-usage.test.tssrc/main/rate-limits/service-refresh-orchestration.test.tssrc/main/rate-limits/service-window-activation.test.tssrc/main/rate-limits/service/service-full-cycle-application.tssrc/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.
There was a problem hiding this comment.
ℹ️ 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 (GeminiandClaude and GPT) plus their5h/weeklybuckets intoProviderRateLimits. - Mirror removal — deletes
antigravity-usage-mirror, which previously republished a successful Gemini read under theantigravityprovider id. - Service integration — adds
fetchAntigravityRateLimits()to thePromise.allSettledfull-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.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
|
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 |
There was a problem hiding this comment.
ℹ️ No critical issues — one minor suggestion inline.
Reviewed changes
- Port discovery by mtime —
readLanguageServerPortnowstats eachcli-*.logand picks the most recently modified file, replacing the lexicographic.sort().pop(). - Cancellation —
fetchAntigravityRateLimitsaccepts anAbortSignaland forwards it to the HTTPS request; the full-cycleallSettledpasses the cycle signal. - Reachable-but-unreadable split — connection failures report
unavailable/NOT_RUNNING_REASON, while parse and empty-bucket failures reporterror/UNREADABLE_REASON, so schema drift is no longer disguised as "not running".
Both prior suggestions are addressed cleanly.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
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.
a78d231 to
d437492
Compare
|
Good catch on the non-200 path — a |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Non-200 quota responses now report unreadable, not "not running" —
fetchAntigravityRateLimitsnow resolves the response's status code together with its body and routes any non-200 status toerror/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.
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.
There was a problem hiding this comment.
✅ 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 withrejectUnauthorized: false, removing the certificate bypass while keeping the same loopback-only threat model. - Port-line disambiguation —
PORT_LINEnow matchesfor 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 fullProviderRateLimits, with the non-200 check performed before parsing. - Test coverage — fetcher tests call the new function and add an
it.eachcovering the unreadable cases (drifted endpoint, server error, malformed JSON, no groups, unknown windows); the now-unusedbucketIdfield was dropped from the bucket type.
The transport switch is well-reasoned and the prior feedback threads are all resolved.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: e832a6d3-b2a9-4ad7-a407-bba5177157e5
📒 Files selected for processing (2)
src/main/rate-limits/antigravity-usage-fetcher.test.tssrc/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))) |
There was a problem hiding this comment.
🩺 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)
NODERepository: 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 configRepository: 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.tsRepository: 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:
- 1: https://nodejs.org/api/http.html
- 2: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b4f65b920e76b97dc731ff8ebbcd2d6915998332/types/node/http.d.ts
- 3: https://nodejs.org/learn/http/anatomy-of-an-http-transaction
- 4: https://bun.sh/reference/node/http/IncomingMessage
- 5: https://stackoverflow.com/questions/20097255/node-js-http-incomingmessage-does-not-fire-close-event
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))
+ )
}|
Reviewed head P2 — the newest log can hide a still-running language server. 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:
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 |
|
Closing: the transport this PR relies on is gone. On 8 Sep the language server answered |


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.
What Changed
antigravity-usage-fetcher.ts, replacingantigravity-usage-mirror.ts.~/.gemini/antigravity-cli/log/cli-*.log, then asks it forRetrieveUserQuotaSummaryon 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.Gemini Models(Flash + Pro) andClaude and GPT models— each with a 5-hour and a weekly bucket.sessionandweeklyshow the tightest pool; all four buckets go inbucketsfor the tooltip.Why
The mirror copied Gemini Code Assist quota. For individual accounts
loadCodeAssistnow repliesUNSUPPORTED_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 anerror.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: okwith both pools and both windows, matching what Antigravity's own Model Quota screen showed.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,oxlintandoxfmt --checkare clean.pnpm test src/main/rate-limits/leaves only the 4codex-fetcher-pty-settlefailures, 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 withpath.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.mockis 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
PromiseSettledResulttuple type, the destructure inservice-full-cycle-application.ts, and theresults: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
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
node:httprather than Electron'snet.fetchso a configured proxy cannot intercept it.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)