From 864600fce37896fc5a00399004ca45f6140f828d Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sat, 29 Aug 2026 06:01:53 -0700 Subject: [PATCH 1/3] fix(rate-limits): a failed shared-quota read is an Antigravity error, not an absent reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Antigravity has no endpoint of its own: the Gemini read of shared Google Code Assist quota is Antigravity's read, which is why a successful one is mirrored verbatim. #15876 settled a *failed* one as `unavailable` to stop the row saying "Refresh failed" for a request Orca never sent. That fixed the wording, but `unavailable` is a retention verdict, not a label — `applyStalePolicy` discards on `unavailable` and retains on `error` — so one failure wiped the Antigravity row while sparing Gemini's, from the same read. Settle a failed quota read as `error` so both rows keep the last good reading. A Gemini `unavailable` (opt-in off) still maps to `unavailable`: nothing was read, which is genuinely absent. #15876's real protections are untouched and still pinned — the row never quotes Gemini's raw error and never blames a sign-in that exists, now including while it retains a reading. The two tests that pinned the old status are updated in place, not deleted. --- .../antigravity-usage-mirror.test.ts | 6 ++-- .../rate-limits/antigravity-usage-mirror.ts | 24 ++++++++------ .../service-antigravity-usage.test.ts | 32 +++++++++++++++---- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/main/rate-limits/antigravity-usage-mirror.test.ts b/src/main/rate-limits/antigravity-usage-mirror.test.ts index 84c92e1ad22..3367d7702e1 100644 --- a/src/main/rate-limits/antigravity-usage-mirror.test.ts +++ b/src/main/rate-limits/antigravity-usage-mirror.test.ts @@ -30,13 +30,15 @@ describe('deriveAntigravityRateLimits', () => { expect(antigravity.error).toBeNull() }) - it('reports unavailable without quoting the Gemini failure', () => { + it('settles a failed quota read as an error without quoting the Gemini failure', () => { const antigravity = deriveAntigravityRateLimits( geminiSnapshot('error', 'Gemini project ID not found') ) expect(antigravity.provider).toBe('antigravity') - expect(antigravity.status).toBe('unavailable') + // Why: the Gemini read is Antigravity's read of the shared quota, so its failure is a failed + // read, not an absent one — `unavailable` would discard the last good snapshot downstream. + expect(antigravity.status).toBe('error') expect(antigravity.error).not.toContain('Gemini project ID not found') expect(antigravity.error).toContain('Antigravity usage is not available') expect(antigravity.session).toBeNull() diff --git a/src/main/rate-limits/antigravity-usage-mirror.ts b/src/main/rate-limits/antigravity-usage-mirror.ts index 3cea6decc11..a7244a616f6 100644 --- a/src/main/rate-limits/antigravity-usage-mirror.ts +++ b/src/main/rate-limits/antigravity-usage-mirror.ts @@ -1,12 +1,16 @@ import type { ProviderRateLimits } from '../../shared/rate-limit-types' -// Why: the Antigravity CLI keeps its token in the OS keyring, not in the files the Gemini -// fetcher reads, so Orca never actually queries Antigravity. Only a *successful* Gemini read -// describes shared Google Code Assist quota; republishing a Gemini failure under the -// Antigravity provider id surfaced "Refresh failed" for a request that was never attempted. +// Why: Orca has no Antigravity endpoint — the Gemini read of shared Google Code Assist quota *is* +// Antigravity's read, which is why a successful one is mirrored verbatim. #15876 stopped this row +// quoting Gemini's raw error and blaming a sign-in that exists; both still hold. It also settled a +// failed read as `unavailable`, taken as a copy choice but really a retention verdict — +// `applyStalePolicy` discards on `unavailable`, retains on `error` — so one failure wiped this row +// and spared Gemini's. One read cannot be Antigravity's when it succeeds and nobody's when it +// fails, so a failed read settles `error` here too. That brings back the generic "Refresh failed" +// label deliberately: the reason below names the shared quota, so no Antigravity request is +// implied, and a bespoke label would suppress the tooltip's "showing cached data" suffix. const ANTIGRAVITY_NO_SIGN_IN_REASON = 'Antigravity usage is not available. Orca can only show shared Google Code Assist quota while a Gemini CLI sign-in is connected.' -// Why: a Gemini `error` means the sign-in exists and the quota read failed, so blaming a missing sign-in would misdirect the user. const ANTIGRAVITY_QUOTA_UNREADABLE_REASON = 'Antigravity usage is not available. Orca reads it from the shared Google Code Assist quota, which could not be read right now.' @@ -14,16 +18,16 @@ export function deriveAntigravityRateLimits(gemini: ProviderRateLimits): Provide if (gemini.status === 'ok') { return { ...gemini, provider: 'antigravity' } } + // Why: a Gemini `error` means the sign-in exists and the read failed; anything else means there + // was nothing to read, which is genuinely absent rather than failed. + const quotaReadFailed = gemini.status === 'error' return { provider: 'antigravity', session: null, weekly: null, // Why: reuse the Gemini timestamp so activation freshness checks don't force a refetch every cycle. updatedAt: gemini.updatedAt, - error: - gemini.status === 'unavailable' - ? ANTIGRAVITY_NO_SIGN_IN_REASON - : ANTIGRAVITY_QUOTA_UNREADABLE_REASON, - status: 'unavailable' + error: quotaReadFailed ? ANTIGRAVITY_QUOTA_UNREADABLE_REASON : ANTIGRAVITY_NO_SIGN_IN_REASON, + status: quotaReadFailed ? 'error' : 'unavailable' } } diff --git a/src/main/rate-limits/service-antigravity-usage.test.ts b/src/main/rate-limits/service-antigravity-usage.test.ts index b617d909944..83b68c9e049 100644 --- a/src/main/rate-limits/service-antigravity-usage.test.ts +++ b/src/main/rate-limits/service-antigravity-usage.test.ts @@ -54,7 +54,7 @@ describe('RateLimitService Antigravity usage', () => { vi.mocked(fetchCodexRateLimits).mockResolvedValue(okProvider('codex', 20)) }) - it('does not republish a Gemini failure as an Antigravity refresh failure', async () => { + it('does not republish the Gemini failure text under the Antigravity provider', async () => { vi.mocked(fetchGeminiRateLimits).mockResolvedValue( errorProvider('gemini', 'Gemini project ID not found') ) @@ -63,7 +63,8 @@ describe('RateLimitService Antigravity usage', () => { await service.refresh() const state = service.getState() - expect(state.antigravity?.status).toBe('unavailable') + // Why: a failed shared-quota read is a failed Antigravity read; only the wording is Antigravity's. + expect(state.antigravity?.status).toBe('error') expect(state.antigravity?.error).not.toContain('Gemini project ID not found') expect(state.antigravity?.session).toBeNull() // Why: the real Gemini failure must still surface under its own provider. @@ -83,7 +84,7 @@ describe('RateLimitService Antigravity usage', () => { expect(state.antigravity?.session?.usedPercent).toBe(42) }) - it('never leaves a cached Antigravity snapshot in the error retry lane', async () => { + it('keeps the last shared-quota reading when the quota read fails, exactly as Gemini does', async () => { vi.mocked(fetchGeminiRateLimits).mockResolvedValueOnce(okProvider('gemini', 42, Date.now())) const service = new RateLimitService() await service.refresh() @@ -93,8 +94,27 @@ describe('RateLimitService Antigravity usage', () => { ) await service.refresh() - // Why: stale-retention would otherwise show Gemini numbers as "Refresh failed" Antigravity usage. - expect(service.getState().antigravity?.status).toBe('unavailable') - expect(service.getState().antigravity?.session).toBeNull() + // Why: one read backs both rows, so one failure must not wipe one row and spare the other. + const state = service.getState() + expect(state.antigravity?.status).toBe('error') + expect(state.antigravity?.session?.usedPercent).toBe(42) + expect(state.gemini?.status).toBe('error') + expect(state.gemini?.session?.usedPercent).toBe(42) + }) + + it('does not quote the Gemini failure while showing a retained Antigravity reading', async () => { + vi.mocked(fetchGeminiRateLimits).mockResolvedValueOnce(okProvider('gemini', 42, Date.now())) + const service = new RateLimitService() + await service.refresh() + + vi.mocked(fetchGeminiRateLimits).mockResolvedValue( + errorProvider('gemini', 'Token refresh failed') + ) + await service.refresh() + + // Why: #15876's real protection — the Antigravity row owns its wording even in the failure lane. + const antigravity = service.getState().antigravity + expect(antigravity?.error).not.toContain('Token refresh failed') + expect(antigravity?.error).toContain('shared Google Code Assist quota') }) }) From 3169e05420ffdedb2f92a51e7f8d921c2c599c00 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sat, 29 Aug 2026 06:30:53 -0700 Subject: [PATCH 2/3] fix(rate-limits): stop the Antigravity failure reason claiming unavailable usage This lane now retains the last reading, so the tooltip prints the reason above a live meter; the old wording contradicted the numbers beside it. Also corrects the service comment that still stated the reversed rule. --- src/main/rate-limits/antigravity-usage-mirror.test.ts | 6 +++++- src/main/rate-limits/antigravity-usage-mirror.ts | 4 +++- src/main/rate-limits/service-antigravity-usage.test.ts | 5 +++++ src/main/rate-limits/service.ts | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/rate-limits/antigravity-usage-mirror.test.ts b/src/main/rate-limits/antigravity-usage-mirror.test.ts index 3367d7702e1..d4850223d83 100644 --- a/src/main/rate-limits/antigravity-usage-mirror.test.ts +++ b/src/main/rate-limits/antigravity-usage-mirror.test.ts @@ -40,7 +40,11 @@ describe('deriveAntigravityRateLimits', () => { // read, not an absent one — `unavailable` would discard the last good snapshot downstream. expect(antigravity.status).toBe('error') expect(antigravity.error).not.toContain('Gemini project ID not found') - expect(antigravity.error).toContain('Antigravity usage is not available') + // Why: still Orca's own Antigravity wording — but this lane retains a reading, so it may not + // say the usage is unavailable. The no-sign-in lane below keeps that phrasing. + expect(antigravity.error).toContain('Antigravity usage') + expect(antigravity.error).toContain('shared Google Code Assist quota') + expect(antigravity.error).not.toContain('not available') expect(antigravity.session).toBeNull() expect(antigravity.weekly).toBeNull() }) diff --git a/src/main/rate-limits/antigravity-usage-mirror.ts b/src/main/rate-limits/antigravity-usage-mirror.ts index a7244a616f6..cca60599e2a 100644 --- a/src/main/rate-limits/antigravity-usage-mirror.ts +++ b/src/main/rate-limits/antigravity-usage-mirror.ts @@ -11,8 +11,10 @@ import type { ProviderRateLimits } from '../../shared/rate-limit-types' // implied, and a bespoke label would suppress the tooltip's "showing cached data" suffix. const ANTIGRAVITY_NO_SIGN_IN_REASON = 'Antigravity usage is not available. Orca can only show shared Google Code Assist quota while a Gemini CLI sign-in is connected.' +// Why: this lane now retains the last reading, so the tooltip prints this line above a live +// meter — claiming the usage is unavailable would contradict the numbers beside it. const ANTIGRAVITY_QUOTA_UNREADABLE_REASON = - 'Antigravity usage is not available. Orca reads it from the shared Google Code Assist quota, which could not be read right now.' + 'Orca reads Antigravity usage from the shared Google Code Assist quota, which could not be read right now.' export function deriveAntigravityRateLimits(gemini: ProviderRateLimits): ProviderRateLimits { if (gemini.status === 'ok') { diff --git a/src/main/rate-limits/service-antigravity-usage.test.ts b/src/main/rate-limits/service-antigravity-usage.test.ts index 83b68c9e049..56f16d904c4 100644 --- a/src/main/rate-limits/service-antigravity-usage.test.ts +++ b/src/main/rate-limits/service-antigravity-usage.test.ts @@ -116,5 +116,10 @@ describe('RateLimitService Antigravity usage', () => { const antigravity = service.getState().antigravity expect(antigravity?.error).not.toContain('Token refresh failed') expect(antigravity?.error).toContain('shared Google Code Assist quota') + // Why: retention is what makes this lane new — the tooltip prints this reason under + // "Refresh failed — showing cached data" and directly above the retained meter, so a reason + // that calls the usage unavailable contradicts the numbers rendered next to it. + expect(antigravity?.session?.usedPercent).toBe(42) + expect(antigravity?.error).not.toContain('not available') }) }) diff --git a/src/main/rate-limits/service.ts b/src/main/rate-limits/service.ts index d6f291d6b38..864aed016de 100644 --- a/src/main/rate-limits/service.ts +++ b/src/main/rate-limits/service.ts @@ -1767,7 +1767,8 @@ export class RateLimitService { status: 'error' } satisfies ProviderRateLimits) - // Why: Antigravity can only borrow a *successful* Gemini read; a Gemini failure is not an Antigravity failure. + // Why: the Gemini read of shared Code Assist quota IS Antigravity's read, so its failure is + // Antigravity's failed read too — see antigravity-usage-mirror.ts for why that settles `error`. const antigravity = deriveAntigravityRateLimits(gemini) const opencodeGo = From a79a76f5b5dc23204dd91e4723e69fd0e9577d29 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Sat, 29 Aug 2026 14:42:56 -0700 Subject: [PATCH 3/3] fix(rate-limits): a malformed Gemini oauth_creds.json is a failed read, not absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit readGeminiCredentials() returned null both when the file was missing and when it was present but did not match the credential contract. The caller turns null into status 'unavailable' / "credentials not found", so a corrupt or wrong-typed oauth_creds.json was reported to the user as signed out — the failed read became an absent one before the shared-quota rule could see it, and absent takes the discard path. Throw on the present-but-invalid case so it reaches the caller's error path, and keep ENOENT returning null so an actually missing file still reads as absent. Both sides of that boundary are now pinned. --- src/main/rate-limits/gemini-oauth-sources.ts | 4 ++- .../gemini-usage-fetcher.fallback.test.ts | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/main/rate-limits/gemini-oauth-sources.ts b/src/main/rate-limits/gemini-oauth-sources.ts index 386c8b281f3..b4cf80da569 100644 --- a/src/main/rate-limits/gemini-oauth-sources.ts +++ b/src/main/rate-limits/gemini-oauth-sources.ts @@ -68,7 +68,9 @@ export async function readGeminiCredentials(): Promise ) { return parsed as GeminiCredentials } - return null + // A file that exists but does not match the credential contract is a failed + // read. Preserve that distinction so callers do not report it as signed out. + throw new Error('Gemini CLI credentials file is invalid') } catch (err) { if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') { return null diff --git a/src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts b/src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts index fddd0a77d59..eda72c607f7 100644 --- a/src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts +++ b/src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts @@ -153,6 +153,36 @@ describe('fetchGeminiRateLimits fallback oauth creds', () => { expect(result.weekly).toBeNull() }) + it('reports a present but malformed oauth_creds.json as a failed read', async () => { + readFileMock.mockImplementation(async (filePath: string) => { + if (filePath.includes('auth.json')) { + return JSON.stringify({}) + } + if (filePath.includes('oauth_creds.json')) { + return JSON.stringify({ + access_token: 42, + refresh_token: 'refresh', + expiry_date: Date.now() + }) + } + throw { code: 'ENOENT' } + }) + + const result = await fetchGeminiRateLimits(true) + + expect(result.status).toBe('error') + expect(result.error).toContain('credentials file is invalid') + }) + + it('keeps an actually missing oauth_creds.json as absent', async () => { + readFileMock.mockRejectedValue({ code: 'ENOENT' }) + + const result = await fetchGeminiRateLimits(true) + + expect(result.status).toBe('unavailable') + expect(result.error).toContain('credentials not found') + }) + it('returns error when loadCodeAssist cannot resolve a project for oauth_creds path', async () => { // Why: when the fallback (oauth_creds.json) path has no project embedded // and loadCodeAssist fails, we surface a clear "project ID not found"