diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts index 6a9a9b01c5aa5..3678b22061e9b 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts @@ -196,11 +196,85 @@ describe("AntigravityAdapterV2 session open", () => { cause: undefined, }), requiresSignIn: true, + failure: undefined, }, { name: "ACP authentication required", cause: EffectAcpErrors.AcpRequestError.authRequired(), requiresSignIn: true, + failure: undefined, + }, + { + name: "Gemini Enterprise licensing rate limit", + cause: EffectAcpErrors.AcpRequestError.fromProtocolError( + { + code: -32603, + message: "Gemini Enterprise license resolution failed (HTTP 429)", + }, + { method: "authenticate" }, + ), + requiresSignIn: undefined, + failure: { + class: "usage_limit", + message: + "Antigravity's Gemini Enterprise license check is rate limited. Wait before retrying; signing in again will not reset this limit.", + code: "RATE_LIMIT_EXCEEDED", + retryable: true, + }, + }, + { + name: "licensing rate limit reported as authentication required", + cause: EffectAcpErrors.AcpRequestError.fromProtocolError( + { + code: -32000, + message: "Gemini Enterprise license resolution failed (HTTP 429)", + }, + { method: "authenticate" }, + ), + requiresSignIn: undefined, + failure: { + class: "usage_limit", + message: + "Antigravity's Gemini Enterprise license check is rate limited. Wait before retrying; signing in again will not reset this limit.", + code: "RATE_LIMIT_EXCEEDED", + retryable: true, + }, + }, + { + name: "unrelated authentication rate limit", + cause: EffectAcpErrors.AcpRequestError.fromProtocolError( + { + code: -32000, + message: "Authentication failed (HTTP 429)", + }, + { method: "authenticate" }, + ), + requiresSignIn: true, + failure: undefined, + }, + { + name: "licensing service failure", + cause: EffectAcpErrors.AcpRequestError.fromProtocolError( + { + code: -32603, + message: "Gemini Enterprise license resolution failed (HTTP 500)", + }, + { method: "authenticate" }, + ), + requiresSignIn: undefined, + failure: undefined, + }, + { + name: "quota text outside authentication", + cause: EffectAcpErrors.AcpRequestError.fromProtocolError( + { + code: -32603, + message: "Gemini Enterprise license resolution failed (HTTP 429)", + }, + { method: "session/new" }, + ), + requiresSignIn: undefined, + failure: undefined, }, { name: "transport failure", @@ -209,8 +283,9 @@ describe("AntigravityAdapterV2 session open", () => { cause: undefined, }), requiresSignIn: undefined, + failure: undefined, }, - ] as const)("classifies $name during session open", ({ cause, requiresSignIn }) => + ] as const)("classifies $name during session open", ({ cause, requiresSignIn, failure }) => Effect.gen(function* () { const instanceId = ProviderInstanceId.make("antigravity-sign-in-test"); const adapter = yield* makeAntigravityAdapterV2({ @@ -239,6 +314,7 @@ describe("AntigravityAdapterV2 session open", () => { error._tag === "ProviderAdapterOpenSessionError" && error.requiresSignIn, requiresSignIn, ); + assert.deepEqual("failure" in error ? error.failure : undefined, failure); }).pipe(Effect.provide(layerSession), Effect.scoped), ); }); diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts index 1490140c1e6e2..debd390ffd2c1 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts @@ -43,7 +43,10 @@ import { makeAntigravityUserInputResponse, normalizeAntigravityToolCall, } from "../../provider/acp/AntigravityProtocol.ts"; -import { isAntigravitySignInRequiredError } from "../../provider/antigravityAuthSupport.ts"; +import { + antigravitySessionOpenFailure, + isAntigravitySignInRequiredError, +} from "../../provider/antigravityAuthSupport.ts"; import { AcpProviderCapabilitiesV2, makeAcpAdapterV2, @@ -273,6 +276,7 @@ export function makeAntigravityAcpAdapterFlavor( // ACP 1.3.0 treats /compact as ordinary model input. supportsCompaction: false, requiresSignIn: isAntigravitySignInRequiredError, + openSessionFailure: antigravitySessionOpenFailure, // Antigravity can leave a prompt open without updates; long tools and approvals pause this. promptInactivityTimeout: "15 minutes", // 1.4.5 returns the root trajectory's Gemini usageMetadata delta; 1.4.1 returns none. diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts index d1b5168258874..a0b98154ea2f8 100644 --- a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts +++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts @@ -1673,14 +1673,40 @@ effectIt.effect("leaves the run starting when a session-open failure will be ret }), ); -effectIt.effect("fails the run without a retry when the provider needs sign-in", () => +effectIt.effect.each([ + { + name: "needs sign-in", + sessionFailure: { requiresSignIn: true as const }, + failure: { + class: "provider_error", + message: "Failed to open antigravity provider session new-provider-session.", + }, + }, + { + name: "is rate limited while opening the session", + sessionFailure: { + failure: { + class: "usage_limit" as const, + message: "Antigravity's Gemini Enterprise license check is rate limited.", + code: "RATE_LIMIT_EXCEEDED", + retryable: true, + }, + }, + failure: { + class: "usage_limit", + message: "Antigravity's Gemini Enterprise license check is rate limited.", + code: "RATE_LIMIT_EXCEEDED", + retryable: true, + }, + }, +])("fails the run without a retry when the provider $name", ({ sessionFailure, failure }) => Effect.gen(function* () { const harness = makeLocalCommandHarness({ text: "Continue", openFailure: new ProviderAdapter.ProviderAdapterOpenSessionError({ driver: ProviderDriverKind.make("antigravity"), providerSessionId: ProviderSessionId.make("new-provider-session"), - requiresSignIn: true, + ...sessionFailure, }), }); @@ -1692,10 +1718,7 @@ effectIt.effect("fails the run without a retry when the provider needs sign-in", { type: "error", status: "failed", - failure: { - class: "provider_error", - message: "Failed to open antigravity provider session new-provider-session.", - }, + failure, }, ]); }), diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts index 54b16d7aa2aef..77b619ee93934 100644 --- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts +++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts @@ -6,6 +6,7 @@ import { type OrchestrationV2DomainEvent, type OrchestrationV2ContextHandoff, type OrchestrationV2ExecutionNode, + type OrchestrationV2ProviderFailure, type OrchestrationV2ProviderThread, type OrchestrationV2Run, type OrchestrationV2RunAttempt, @@ -521,6 +522,7 @@ export const layer: Layer.Layer< readonly title: string; readonly error: Error; readonly message?: string; + readonly failure?: OrchestrationV2ProviderFailure; }) { const nestedCause = "cause" in failed.error ? failed.error.cause : undefined; yield* settleRunBeforeStart({ @@ -532,18 +534,20 @@ export const layer: Layer.Layer< item: { type: "error", title: failed.title, - failure: makeProviderFailure({ - cause: failed.error, - message: - failed.message ?? - knownProviderFailureMessage(failed.error) ?? - (nestedCause instanceof Error - ? nestedCause.message - : typeof nestedCause === "string" - ? nestedCause - : failed.error.message), - class: "provider_error", - }), + failure: + failed.failure ?? + makeProviderFailure({ + cause: failed.error, + message: + failed.message ?? + knownProviderFailureMessage(failed.error) ?? + (nestedCause instanceof Error + ? nestedCause.message + : typeof nestedCause === "string" + ? nestedCause + : failed.error.message), + class: "provider_error", + }), }, }); }, @@ -808,15 +812,18 @@ export const layer: Layer.Layer< ); if (sessionResult._tag === "Failure") { const failure = sessionResult.failure; - const requiresSignIn = - "cause" in failure && - isProviderAdapterOpenSessionError(failure.cause) && - failure.cause.requiresSignIn === true; - if (input.willRetry === true && !requiresSignIn) return yield* failure; + const openFailure = + "cause" in failure && isProviderAdapterOpenSessionError(failure.cause) + ? failure.cause + : undefined; + const requiresSignIn = openFailure?.requiresSignIn === true; + const rateLimited = openFailure?.failure?.class === "usage_limit"; + if (input.willRetry === true && !requiresSignIn && !rateLimited) return yield* failure; yield* settleStartFailure({ signal: "provider-session-open-failure", title: "Provider session failed to open", error: sessionResult.failure, + ...(openFailure?.failure === undefined ? {} : { failure: openFailure.failure }), }); return; } diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index 32de13ef129e8..6825ef8ac82f0 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -1,7 +1,11 @@ // @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's symlink has no type argument, and Windows needs a junction to link without elevation. import * as NodeFSP from "node:fs/promises"; -import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts"; +import type { + AntigravityAuthMethod, + OrchestrationV2ProviderFailure, + ProviderInstanceId, +} from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; import * as Digest from "effect/crypto/Digest"; @@ -17,6 +21,7 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import * as AcpErrors from "effect-acp/errors"; import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText"; +import { makeProviderFailure } from "@t3tools/provider-core/server/failure"; import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; import { antigravityUserSkillDirectories, @@ -161,9 +166,32 @@ function authSupportError(detail: string) { return new AcpErrors.AcpTransportError({ detail, cause: undefined }); } +function isAntigravityLicensingRateLimit(error: unknown): boolean { + return ( + isAcpRequestError(error) && + error.method === "authenticate" && + error.errorMessage.startsWith("Gemini Enterprise license resolution failed (HTTP 429)") + ); +} + +export function antigravitySessionOpenFailure( + cause: unknown, +): OrchestrationV2ProviderFailure | undefined { + if (!isAntigravityLicensingRateLimit(cause)) return undefined; + return makeProviderFailure({ + class: "usage_limit", + message: + "Antigravity's Gemini Enterprise license check is rate limited. Wait before retrying; signing in again will not reset this limit.", + code: "RATE_LIMIT_EXCEEDED", + retryable: true, + }); +} + export function isAntigravitySignInRequiredError(error: unknown): boolean { return ( - (isAcpRequestError(error) && error.code === -32000) || + (isAcpRequestError(error) && + error.code === -32000 && + !isAntigravityLicensingRateLimit(error)) || (isAcpTransportError(error) && error.detail === ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE) ); } diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index f10be3c952dc8..7da8446b57aa4 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -318,6 +318,7 @@ export interface AcpAdapterV2Flavor { ) => Effect.Effect >; readonly requiresSignIn?: (cause: unknown) => boolean; + readonly openSessionFailure?: (cause: unknown) => OrchestrationV2ProviderFailure | undefined; /** * Runs before attachment paths, T3 instructions and native slash-command detection, * so a rewrite can turn the user's text into a native command. @@ -9407,15 +9408,16 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( }, (effect, input) => effect.pipe( - Effect.mapError( - (cause) => - new ProviderAdapter.ProviderAdapterOpenSessionError({ - driver, - providerSessionId: input.providerSessionId, - ...(flavor.requiresSignIn?.(cause) === true ? { requiresSignIn: true } : {}), - cause, - }), - ), + Effect.mapError((cause) => { + const failure = flavor.openSessionFailure?.(cause); + return new ProviderAdapter.ProviderAdapterOpenSessionError({ + driver, + providerSessionId: input.providerSessionId, + ...(flavor.requiresSignIn?.(cause) === true ? { requiresSignIn: true } : {}), + ...(failure === undefined ? {} : { failure }), + cause, + }); + }), ), ), }); diff --git a/packages/provider-core/src/server/ProviderAdapter.ts b/packages/provider-core/src/server/ProviderAdapter.ts index 9ea7597e6d087..04ebad91af048 100644 --- a/packages/provider-core/src/server/ProviderAdapter.ts +++ b/packages/provider-core/src/server/ProviderAdapter.ts @@ -200,6 +200,7 @@ export class ProviderAdapterOpenSessionError extends Schema.TaggedError