From a748ffb820fa1690e113ec2e9e7307eb998082a0 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:08:02 +1100 Subject: [PATCH] fix(antigravity): rate-limited turns keep their reset time Co-Authored-By: Claude Opus 5.5 --- .../acp/AntigravityTurnOutcome.test.ts | 20 +++++++++++++++++-- .../provider/acp/AntigravityTurnOutcome.ts | 5 ++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/acp/AntigravityTurnOutcome.test.ts b/apps/server/src/provider/acp/AntigravityTurnOutcome.test.ts index d3ca431c50794..09b8730bd6001 100644 --- a/apps/server/src/provider/acp/AntigravityTurnOutcome.test.ts +++ b/apps/server/src/provider/acp/AntigravityTurnOutcome.test.ts @@ -22,9 +22,12 @@ const errorPayload = (error: string) => `Agent execution terminated due to error.\u0012agent executor error: ${error}: ${error}\n(1) attached stack trace\n -- stack trace:`, ), ]); -// An unparsable reset timestamp must not hide the reset delay. +// An unparsable reset timestamp must not hide the reset delay, nor a retry delay outrank it. const QUOTA_ERROR = errorPayload( - "Error 429, Message: Quota exhausted for test model., Status: RESOURCE_EXHAUSTED, Details: [map[@type:type.googleapis.com/google.rpc.ErrorInfo domain:googleapis.com metadata:map[quotaResetDelay:3600s quotaResetTimeStamp:soon] reason:QUOTA_EXHAUSTED]]", + "Error 429, Message: Quota exhausted for test model., Status: RESOURCE_EXHAUSTED, Details: [map[@type:type.googleapis.com/google.rpc.RetryInfo retryDelay:42s] map[@type:type.googleapis.com/google.rpc.ErrorInfo domain:googleapis.com metadata:map[quotaResetDelay:3600s quotaResetTimeStamp:soon] reason:QUOTA_EXHAUSTED]]", +); +const RATE_LIMIT_ERROR = errorPayload( + "Error 429, Message: Resource has been exhausted (e.g. check quota)., Status: RESOURCE_EXHAUSTED, Details: [map[@type:type.googleapis.com/google.rpc.ErrorInfo domain:cloudcode-pa.googleapis.com reason:RATE_LIMIT_EXCEEDED] map[@type:type.googleapis.com/google.rpc.RetryInfo retryDelay:42s]]", ); const INVALID_ARGUMENT_ERROR = errorPayload( "Error 400, Message: Request contains an invalid argument., Status: INVALID_ARGUMENT, Details: []", @@ -101,6 +104,19 @@ describe("watchAntigravityPromptOutcome", () => { }, ), ); + assert.deepEqual( + yield* turn([ + [USER_INPUT, input], + [ERROR_MESSAGE, RATE_LIMIT_ERROR], + ]), + { + class: "usage_limit", + message: "Resource has been exhausted (e.g. check quota).", + code: "RESOURCE_EXHAUSTED", + retryable: true, + resetAt: "1970-01-01T00:00:42.000Z", + }, + ); assert.deepEqual( yield* turn([ [USER_INPUT, input], diff --git a/apps/server/src/provider/acp/AntigravityTurnOutcome.ts b/apps/server/src/provider/acp/AntigravityTurnOutcome.ts index 874c28eeb1537..00de1e836296e 100644 --- a/apps/server/src/provider/acp/AntigravityTurnOutcome.ts +++ b/apps/server/src/provider/acp/AntigravityTurnOutcome.ts @@ -21,6 +21,8 @@ const API_ERROR = /Error (\d{3}), Message: (.+?), Status: ([A-Z_]+)/s; const QUOTA_EXHAUSTED = /\breason:QUOTA_EXHAUSTED\b/; const RESET_TIMESTAMP = /\bquotaResetTimeStamp:([^\s\]]+)/; const RESET_DELAY = /\bquotaResetDelay:((?:\d+(?:\.\d+)?(?:ns|us|µs|μs|ms|h|m|s))+)(?=[\s\]])/; +// google.rpc.RetryInfo, which plain rate limits carry instead of a quota reset. +const RETRY_DELAY = /\bretryDelay:((?:\d+(?:\.\d+)?(?:ns|us|µs|μs|ms|h|m|s))+)(?=[\s\]])/; const GO_DURATION_PART = /(\d+(?:\.\d+)?)(ns|us|µs|μs|ms|h|m|s)/g; const GO_DURATION_UNIT_MS: Record = { ns: 1e-6, @@ -76,7 +78,7 @@ function resetAtFrom(text: string, now: DateTime.Utc): string | undefined { // makeProviderFailure drops a timestamp that does not parse, so fall back to the delay. const timestamp = RESET_TIMESTAMP.exec(text)?.[1]; if (timestamp !== undefined && Option.isSome(DateTime.make(timestamp))) return timestamp; - const delay = RESET_DELAY.exec(text)?.[1]; + const delay = RESET_DELAY.exec(text)?.[1] ?? RETRY_DELAY.exec(text)?.[1]; if (delay === undefined) return undefined; let delayMs = 0; for (const [, amount, unit] of delay.matchAll(GO_DURATION_PART)) { @@ -120,6 +122,7 @@ function turnFailure( message: message!.trim(), code: status ?? null, retryable: rateLimited || Number(httpStatus) >= 500, + resetAt: resetAtFrom(text, now) ?? null, }); }