fix(rate-limits): show agy's own error when Antigravity usage can't be read (#34106)

When agy refuses a usage read with its own error envelope (for example the location eligibility rejection), Orca now shows agy's text, redacted and bounded to one line, instead of the generic "did not report a quota" message. Existing failure precedence and the last good quota bars are unchanged.

Fixes #33426
This commit is contained in:
Jinwoo Hong
2026-10-26 20:14:01 -04:00
committed by GitHub
parent 19b930e8d8
commit fdd9151e17
5 changed files with 113 additions and 23 deletions
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
import { classifyAntigravityUsageFailure } from './antigravity-usage-error'
import {
classifyAntigravityUsageFailure,
formatAgyErrorForDisplay
} from './antigravity-usage-error'
/** Captured verbatim from agy 1.2.11 when the account had exhausted its weekly pool. */
const REAL_RESOURCE_EXHAUSTED =
@@ -74,3 +77,11 @@ describe('classifyAntigravityUsageFailure', () => {
expect(classifyAntigravityUsageFailure(output)).toBeNull()
})
})
it('bounds a large unbroken error without blocking the main process', () => {
const text = `${'!'.repeat(256 * 1024)}@invalid`
const started = performance.now()
expect(formatAgyErrorForDisplay(text)).toBe(`${'!'.repeat(159)}…`)
expect(performance.now() - started).toBeLessThan(2000)
})
@@ -1,4 +1,6 @@
import type { UsageRateLimitFailureKind } from '../../shared/rate-limit-types'
import { stripTerminalControl } from '../../shared/terminal-control-stripping'
import { redactString } from '../observability/redactor'
const AGY_ERROR_PREFIX = 'AGY_ERROR:'
@@ -17,6 +19,11 @@ const SIGNED_OUT_PHRASES = [
'authentication required'
] as const
const AGY_MESSAGE_MAX_LENGTH = 160
// Start at a boundary to avoid quadratic retries inside long, unbroken diagnostics.
const EMAIL_PATTERN = /(^|[\s@])[^\s@]+@[^\s@]+\.[^\s@]+/g
const TOKEN_LIKE_PATTERN = /[\w-]{32,}/g
export type AntigravityUsageFailure = {
failureKind: UsageRateLimitFailureKind
/** Missing authentication requires sign-in guidance. */
@@ -85,3 +92,21 @@ export function classifyAntigravityUsageFailure(output: string): AntigravityUsag
export function stderrShowsInteractiveSignIn(stderr: string): boolean {
return stderr.includes('Authentication required. Please visit the URL')
}
/** One bounded line of agy's error text, safe to show in the status bar. */
export function formatAgyErrorForDisplay(text: string): string | null {
const line = redactString(stripTerminalControl(text))
.replace(/\s+/g, ' ')
.trim()
.replace(EMAIL_PATTERN, '$1[email]')
.replace(TOKEN_LIKE_PATTERN, '[redacted]')
if (!line) {
return null
}
const chars = Array.from(line)
if (chars.length <= AGY_MESSAGE_MAX_LENGTH) {
return line
}
const kept = chars.slice(0, AGY_MESSAGE_MAX_LENGTH - 1).join('')
return `${kept.trimEnd()}…`
}
@@ -472,3 +472,53 @@ describe('quota read safety and diagnostic precedence', () => {
expect(runCommand).toHaveBeenCalledTimes(3)
})
})
describe("agy's own error envelope", () => {
beforeEach(() => resetAntigravityUsageSupportForTests())
const errorEnvelope = (error: string): string =>
`${JSON.stringify({ conversation_id: '', status: 'ERROR', response: '', error, num_turns: 0 })}\n`
const LOCATION_ERROR =
'Eligibility check failed: Your current account is not eligible for Antigravity, because it is not currently available in your location.'
it('shows agy’s text instead of the generic parse failure', async () => {
const result = await harness({
result: processResult({
code: 1,
stdout: errorEnvelope(LOCATION_ERROR),
stderr: `error: ${LOCATION_ERROR}\n`
})
}).fetch()
expect(result.status).toBe('error')
expect(result.usageMetadata?.failureKind).toBe('parse')
expect(result.error).toBe(`Antigravity usage is not available. agy reported: ${LOCATION_ERROR}`)
})
it('redacts identities and bounds agy’s text to one line', async () => {
const token = 'ya29.a0AfB_byC1234567890abcdefghijklmnopqrstuvwxyz'
const result = await harness({
result: processResult({
code: 1,
stdout: errorEnvelope(
`\u001b[31mDenied\u001b[0m for person@example.com\nAGY_SESSION=shortsecret\nwith ${token} ${'x '.repeat(200)}`
)
})
}).fetch()
const shown = result.error?.replace('Antigravity usage is not available. agy reported: ', '')
expect(shown).toMatch(/^Denied for \[email\] AGY_SESSION=\S+ with ya29\.\[redacted\] x x/)
expect(shown).not.toContain('shortsecret')
expect(Array.from(shown ?? '').length).toBeLessThanOrEqual(160)
expect(shown?.endsWith('…')).toBe(true)
})
it('keeps sign-in guidance ahead of agy’s text', async () => {
const result = await harness({
result: processResult({ code: 1, stdout: errorEnvelope('Not authenticated. Run agy login.') })
}).fetch()
expect(result.usageMetadata?.failureKind).toBe('missing-credentials')
expect(result.error).toContain('Sign in with `agy`')
})
})
@@ -14,10 +14,14 @@ import {
ANTIGRAVITY_VERSION_TIMEOUT_MS,
antigravityCommandName
} from './antigravity-usage-command'
import { parseAntigravityUsageStdout, stdoutShowsModelTurn } from './antigravity-usage-response'
import {
parseAntigravityUsageStdout,
readAntigravityErrorEnvelope,
stdoutShowsModelTurn
} from './antigravity-usage-response'
import {
classifyAntigravityUsageFailure,
formatAgyErrorForDisplay,
stderrShowsInteractiveSignIn
} from './antigravity-usage-error'
@@ -211,8 +215,10 @@ export async function fetchAntigravityRateLimits(
now()
)
}
}
if (!reading) {
const agyError = formatAgyErrorForDisplay(readAntigravityErrorEnvelope(result.stdout) ?? '')
if (agyError) {
return failed(`Antigravity usage is not available. agy reported: ${agyError}`, 'parse', now())
}
// runProcess returns nonzero exit as data; classify signed-out first and add exit detail only when payload is missing.
const exitDetail = result.code === 0 || result.code === null ? '' : ` (exit ${result.code})`
return failed(
@@ -156,16 +156,16 @@ export function parseAntigravityUsageEnvelope(value: unknown): AntigravityUsageR
}
/** Allow CLI log noise around the JSON envelope. */
export function parseAntigravityUsageStdout(stdout: string): AntigravityUsageReading | null {
function findInStdoutEnvelopes<T>(stdout: string, read: (value: unknown) => T | null): T | null {
for (const line of stdout.split('\n')) {
const trimmed = line.trim()
if (!trimmed.startsWith('{')) {
continue
}
try {
const reading = parseAntigravityUsageEnvelope(JSON.parse(trimmed))
if (reading) {
return reading
const found = read(JSON.parse(trimmed))
if (found !== null) {
return found
}
} catch {
continue
@@ -174,6 +174,17 @@ export function parseAntigravityUsageStdout(stdout: string): AntigravityUsageRea
return null
}
export function parseAntigravityUsageStdout(stdout: string): AntigravityUsageReading | null {
return findInStdoutEnvelopes(stdout, parseAntigravityUsageEnvelope)
}
/** agy's own error text from a `{"status":"ERROR","error":…}` print envelope. */
export function readAntigravityErrorEnvelope(stdout: string): string | null {
return findInStdoutEnvelopes(stdout, (value) =>
isRecord(value) && readString(value.status) === 'ERROR' ? readString(value.error) : null
)
}
/** Unknown slash commands can become billed model prompts; conversation/turn evidence must stop future polling. */
export function didRunModelTurn(value: unknown): boolean {
if (!isRecord(value)) {
@@ -187,18 +198,5 @@ export function didRunModelTurn(value: unknown): boolean {
}
export function stdoutShowsModelTurn(stdout: string): boolean {
for (const line of stdout.split('\n')) {
const trimmed = line.trim()
if (!trimmed.startsWith('{')) {
continue
}
try {
if (didRunModelTurn(JSON.parse(trimmed))) {
return true
}
} catch {
continue
}
}
return false
return findInStdoutEnvelopes(stdout, (value) => (didRunModelTurn(value) ? true : null)) !== null
}