authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 16:30:31-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 16:30:31-07:00
logf25e4428961982e3935300cfd3c78f7d0fac4ee9
tree687a5f4e7de18a420cb93486f62abc1339f9ff51
parent498ab9c399d5e8c3097a286be14d03238e071ac1
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): allow slow runtime startup during setup (#9510)


7 files changed, 107 insertions(+), 8 deletions(-)

apps/server/src/provider/AntigravityAuth.test.ts+42-1
...@@ -56,6 +56,7 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (...@@ -56,6 +56,7 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
56 readonly interactive?: boolean;56 readonly interactive?: boolean;
57 readonly authorizationUrls?: ReadonlyArray<string>;57 readonly authorizationUrls?: ReadonlyArray<string>;
58 readonly supportsLogout?: boolean;58 readonly supportsLogout?: boolean;
59 readonly beforeInitialize?: Effect.Effect<void>;
59 readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>;60 readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>;
60 } = {},61 } = {},
61) {62) {
...@@ -82,8 +83,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (...@@ -82,8 +83,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
82 );83 );
83 return {84 return {
84 initialize: () =>85 initialize: () =>
85 Effect.sync(() => {86 Effect.gen(function* () {
86 events.push("initialize");87 events.push("initialize");
88 yield* options.beforeInitialize ?? Effect.void;
87 return options.supportsLogout === false89 return options.supportsLogout === false
88 ? { ...initialized, agentCapabilities: {} }90 ? { ...initialized, agentCapabilities: {} }
89 : initialized;91 : initialized;
...@@ -394,6 +396,45 @@ it.layer(NodeServices.layer)("AntigravityAuth", (it) => {...@@ -394,6 +396,45 @@ it.layer(NodeServices.layer)("AntigravityAuth", (it) => {
394 }),396 }),
395 );397 );
396398
399 it.effect("signs out after a slow packaged runtime starts", () =>
400 Effect.gen(function* () {
401 const entered = yield* Deferred.make<void>();
402 const initialized = yield* Deferred.make<void>();
403 const harness = yield* makeHarness({
404 beforeInitialize: Deferred.succeed(entered, undefined).pipe(
405 Effect.andThen(Deferred.await(initialized)),
406 ),
407 });
408 const logout = yield* harness.auth.controller.logout(Effect.void).pipe(Effect.forkScoped);
409 yield* Deferred.await(entered);
410 yield* TestClock.adjust("47 seconds");
411 yield* Deferred.succeed(initialized, undefined);
412 assert.equal((yield* Fiber.join(logout)).phase, "idle");
413 assert.include(harness.events, "logout");
414 assert.deepEqual(harness.catalog(), []);
415 yield* Deferred.await(harness.closed);
416 }),
417 );
418
419 it.effect("closes a stalled sign-out process without clearing its account catalog", () =>
420 Effect.gen(function* () {
421 const entered = yield* Deferred.make<void>();
422 const harness = yield* makeHarness({
423 beforeInitialize: Deferred.succeed(entered, undefined).pipe(Effect.andThen(Effect.never)),
424 });
425 const logout = yield* harness.auth.controller
426 .logout(Effect.void)
427 .pipe(Effect.exit, Effect.forkScoped);
428 yield* Deferred.await(entered);
429 yield* TestClock.adjust("90 seconds");
430 assert.isTrue(Exit.isFailure(yield* Fiber.join(logout)));
431 yield* Deferred.await(harness.closed);
432 assert.notInclude(harness.events, "logout");
433 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
434 yield* harness.auth.withProcess(Effect.void, Effect.void).pipe(Effect.scoped);
435 }),
436 );
437
397 it.effect(438 it.effect(
398 "sign-out interrupts startup without interrupting its caller after startup returns",439 "sign-out interrupts startup without interrupting its caller after startup returns",
399 () =>440 () =>
apps/server/src/provider/AntigravityAuth.ts+1-1
...@@ -480,7 +480,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <...@@ -480,7 +480,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
480 }).pipe(480 }).pipe(
481 Effect.scoped,481 Effect.scoped,
482 Effect.timeoutOrElse({482 Effect.timeoutOrElse({
483 duration: "30 seconds",483 duration: "90 seconds",
484 orElse: () => Effect.fail(setupError("logout", "Antigravity sign-out timed out.")),484 orElse: () => Effect.fail(setupError("logout", "Antigravity sign-out timed out.")),
485 }),485 }),
486 ),486 ),
apps/server/src/provider/Drivers/AntigravityDriver.test.ts+25-1
...@@ -11,10 +11,13 @@ import {...@@ -11,10 +11,13 @@ import {
11 HostProcessPlatform,11 HostProcessPlatform,
12} from "@t3tools/shared/hostProcess";12} from "@t3tools/shared/hostProcess";
13import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
14import * as Deferred from "effect/Deferred";
15import * as Fiber from "effect/Fiber";
14import * as FileSystem from "effect/FileSystem";16import * as FileSystem from "effect/FileSystem";
15import * as Layer from "effect/Layer";17import * as Layer from "effect/Layer";
16import * as Path from "effect/Path";18import * as Path from "effect/Path";
17import * as Schema from "effect/Schema";19import * as Schema from "effect/Schema";
20import * as TestClock from "effect/testing/TestClock";
18import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";21import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
1922
20import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";23import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
...@@ -108,7 +111,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (...@@ -108,7 +111,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
108 const first = yield* makeExecutable("runtime 'one");111 const first = yield* makeExecutable("runtime 'one");
109 const second = yield* makeExecutable("runtime two");112 const second = yield* makeExecutable("runtime two");
110 const signedOut = yield* makeExecutable("runtime signed-out", true);113 const signedOut = yield* makeExecutable("runtime signed-out", true);
111 const controls = { selected: first, failResolution: false };114 const controls = { selected: first, failResolution: false, beforeAcquire: Effect.void };
112 const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = [];115 const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = [];
113 const releases: Array<string | null> = [];116 const releases: Array<string | null> = [];
114 const launches: Array<{117 const launches: Array<{
...@@ -129,6 +132,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (...@@ -129,6 +132,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
129 acquire: (binaryPath, environment) =>132 acquire: (binaryPath, environment) =>
130 Effect.gen(function* () {133 Effect.gen(function* () {
131 acquisitions.push({ binaryPath, path: environment?.PATH });134 acquisitions.push({ binaryPath, path: environment?.PATH });
135 yield* controls.beforeAcquire;
132 if (controls.failResolution) {136 if (controls.failResolution) {
133 return yield* new AntigravityInstallationError({137 return yield* new AntigravityInstallationError({
134 operation: "resolve",138 operation: "resolve",
...@@ -249,6 +253,26 @@ it.layer(testLayer)("AntigravityDriver", (it) => {...@@ -249,6 +253,26 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
249 }).pipe(Effect.scoped),253 }).pipe(Effect.scoped),
250 );254 );
251255
256 it.effect.skipIf(windowsHost)("refreshes models after slow process startup", () =>
257 Effect.gen(function* () {
258 const h = yield* makeHarness();
259 const entered = yield* Deferred.make<void>();
260 const ready = yield* Deferred.make<void>();
261 h.controls.beforeAcquire = Deferred.succeed(entered, undefined).pipe(
262 Effect.andThen(Deferred.await(ready)),
263 );
264 const refresh = yield* h.refresh().pipe(Effect.forkScoped);
265 yield* Deferred.await(entered);
266 yield* TestClock.adjust("70 seconds");
267 yield* Deferred.succeed(ready, undefined);
268 yield* Fiber.join(refresh);
269 const snapshot = yield* h.instance.snapshot.getSnapshot;
270 expect(snapshot.auth.status).toBe("authenticated");
271 expect(snapshot.models.length).toBeGreaterThan(0);
272 yield* h.assertClosed;
273 }).pipe(Effect.scoped),
274 );
275
252 it.effect.skipIf(windowsHost)(276 it.effect.skipIf(windowsHost)(
253 "refreshes a disabled instance through the selected executable and personal Google ACP",277 "refreshes a disabled instance through the selected executable and personal Google ACP",
254 () =>278 () =>
apps/server/src/provider/Drivers/AntigravityDriver.ts+1-1
...@@ -332,7 +332,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD...@@ -332,7 +332,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
332 },332 },
333 Effect.scoped,333 Effect.scoped,
334 Effect.timeoutOrElse({334 Effect.timeoutOrElse({
335 duration: "60 seconds",335 duration: "90 seconds",
336 orElse: () =>336 orElse: () =>
337 Effect.fail(337 Effect.fail(
338 new ProviderDriverError({338 new ProviderDriverError({
apps/server/src/provider/Layers/AntigravityProvider.test.ts+34-3
...@@ -417,11 +417,17 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {...@@ -417,11 +417,17 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
417 Effect.gen(function* () {417 Effect.gen(function* () {
418 const harness = yield* makeHarness();418 const harness = yield* makeHarness();
419 yield* harness.initialize;419 yield* harness.initialize;
420 yield* Ref.set(harness.probe, Effect.sleep("20 seconds").pipe(Effect.as(initializeResult)));420 const entered = yield* Deferred.make<void>();
421 const initialized = yield* Deferred.make<EffectAcpSchema.InitializeResponse>();
422 yield* Ref.set(
423 harness.probe,
424 Deferred.succeed(entered, undefined).pipe(Effect.andThen(Deferred.await(initialized))),
425 );
421426
422 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);427 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
423 yield* Effect.yieldNow;428 yield* Deferred.await(entered);
424 yield* TestClock.adjust("20 seconds");429 yield* TestClock.adjust("47 seconds");
430 yield* Deferred.succeed(initialized, initializeResult);
425 const snapshot = yield* Fiber.join(refresh);431 const snapshot = yield* Fiber.join(refresh);
426432
427 expect(snapshot).toMatchObject({433 expect(snapshot).toMatchObject({
...@@ -433,6 +439,31 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {...@@ -433,6 +439,31 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
433 ),439 ),
434 );440 );
435441
442 it.effect("closes a stalled health probe at its deadline", () =>
443 Effect.scoped(
444 Effect.gen(function* () {
445 const harness = yield* makeHarness();
446 yield* harness.initialize;
447 const entered = yield* Deferred.make<void>();
448 const closed = yield* Deferred.make<void>();
449 yield* Ref.set(
450 harness.probe,
451 Deferred.succeed(entered, undefined).pipe(
452 Effect.andThen(Effect.never),
453 Effect.ensuring(Deferred.succeed(closed, undefined)),
454 ),
455 );
456 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
457 yield* Deferred.await(entered);
458 yield* TestClock.adjust("90 seconds");
459 const snapshot = yield* Fiber.join(refresh);
460 yield* Deferred.await(closed);
461 expect(snapshot.status).toBe("error");
462 expect(snapshot.message).toContain("90 seconds");
463 }),
464 ),
465 );
466
436 it.effect("distinguishes missing executables from a failed installed executable", () =>467 it.effect("distinguishes missing executables from a failed installed executable", () =>
437 Effect.scoped(468 Effect.scoped(
438 Effect.gen(function* () {469 Effect.gen(function* () {
apps/server/src/provider/Layers/AntigravityProvider.ts+1-1
...@@ -32,7 +32,7 @@ import {...@@ -32,7 +32,7 @@ import {
3232
33const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });33const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
34const MAX_WORKSPACE_SNAPSHOTS = 32;34const MAX_WORKSPACE_SNAPSHOTS = 32;
35const HEALTH_CHECK_TIMEOUT = "45 seconds";35const HEALTH_CHECK_TIMEOUT = "90 seconds";
36const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";36const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";
37const AUTH_UNCHECKED_MESSAGE =37const AUTH_UNCHECKED_MESSAGE =
38 "Antigravity is installed. Google account access is not checked yet.";38 "Antigravity is installed. Google account access is not checked yet.";
docs/user/providers-antigravity.md+3
...@@ -169,6 +169,9 @@ in web or desktop provider settings, or **Refresh models** in the mobile model p...@@ -169,6 +169,9 @@ in web or desktop provider settings, or **Refresh models** in the mobile model p
169uses saved Google sign-in and does not open a login page. If sign-in is required, use the169uses saved Google sign-in and does not open a login page. If sign-in is required, use the
170provider's setup controls. Automatic status checks verify the installation only.170provider's setup controls. Automatic status checks verify the installation only.
171171
172The packaged runtime can be slow to start, especially on Windows. Health checks, model refresh,
173and sign-out each allow up to 90 seconds before reporting a timeout.
174
172If Google reports `SUBSCRIPTION_REQUIRED`, an account restriction, or a usage limit, read the175If Google reports `SUBSCRIPTION_REQUIRED`, an account restriction, or a usage limit, read the
173provider's message. A finished turn can contain an upstream error instead of completed work.176provider's message. A finished turn can contain an upstream error instead of completed work.
174Use any retry time Google supplies. T3 Code does not switch to an API key to get past the limit.177Use any retry time Google supplies. T3 Code does not switch to an API key to get past the limit.