authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-15 19:02:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-15 19:02:12-07:00
log8c18b5bb21a5349fbadba8c77456ea34c41b7339
treef8bae6ddfd73c9dc9eb2f707d6b528d950aac567
parent935c55b3778fdeae0e25b250ce2a9fa7e79c0327
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): stop health checks from filling the disk with _MEI folders (#12008)

The health probe launched the PyInstaller ACP binary every minute and force killed it, leaving about 1 GB of _MEI files per run. The probe now resolves the install on disk without spawning. Each ACP process gets its own temp directory under the profile that is removed when the runtime closes, and the driver sweeps the profile temp root on create. Continues #11657 by Vita Skacel. Owned temp directory approach from #9626 by ariszz. Co-authored-by: Vita Skacel <skacel.vita@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

5 files changed, 244 insertions(+), 20 deletions(-)

apps/server/src/provider/Drivers/AntigravityDriver.test.ts+84-2
......@@ -31,6 +31,7 @@ import {
3131import {
3232 ANTIGRAVITY_AUTH_STDOUT_PREFIX,
3333 resolveAntigravityProfileDirectory,
34 resolveAntigravityRuntimeTempDirectory,
3435} from "../antigravityAuthSupport.ts";
3536import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
3637import * as ModelManifest from "../ModelManifest.ts";
......@@ -58,7 +59,7 @@ function shellQuote(value: string): string {
5859}
5960
6061const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
61 options: { readonly config?: Partial<AntigravitySettings> } = {},
62 options: { readonly config?: Partial<AntigravitySettings>; readonly enabled?: boolean } = {},
6263) {
6364 const fs = yield* FileSystem.FileSystem;
6465 const path = yield* Path.Path;
......@@ -124,11 +125,22 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
124125 forceFileStorage: string | undefined;
125126 credentialKeys: ReadonlyArray<string>;
126127 geminiApiKey: string | undefined;
128 tempDirectory: string | undefined;
127129 handle: ChildProcessSpawner.ChildProcessHandle;
128130 }> = [];
129131
130132 const installation = Layer.mock(AntigravityInstallation)({
131133 managedDirectory: root,
134 resolve: () =>
135 Effect.gen(function* () {
136 if (controls.failResolution) {
137 return yield* new AntigravityInstallationError({
138 operation: "resolve",
139 detail: "Fixture resolution failed.",
140 });
141 }
142 return controls.selected;
143 }),
132144 acquire: (binaryPath, environment) =>
133145 Effect.gen(function* () {
134146 acquisitions.push({ binaryPath, path: environment?.PATH });
......@@ -166,6 +178,10 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
166178 blockedCredentialKeys.has(key.toUpperCase()),
167179 ),
168180 geminiApiKey: environment.GEMINI_API_KEY,
181 // Only the agent gets a per-process temp directory. Other launches
182 // inherit the host TMPDIR.
183 tempDirectory:
184 environment.ANTIGRAVITY_HARNESS_PATH === undefined ? undefined : environment.TMPDIR,
169185 handle,
170186 });
171187 return handle;
......@@ -174,7 +190,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
174190 const instance = yield* AntigravityDriver.create({
175191 instanceId,
176192 displayName: "Google test account",
177 enabled: false,
193 enabled: options.enabled ?? false,
178194 config: { ...AntigravityDriver.defaultConfig(), ...options.config },
179195 environment: [
180196 { name: "PATH", value: instancePath },
......@@ -207,12 +223,14 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
207223 yield* launch.handle.exitCode.pipe(Effect.ignore);
208224 expect(yield* launch.handle.isRunning).toBe(false);
209225 if (launch.cwd) expect(yield* fs.exists(launch.cwd)).toBe(false);
226 if (launch.tempDirectory) expect(yield* fs.exists(launch.tempDirectory)).toBe(false);
210227 }
211228 });
212229 return {
213230 instance,
214231 refresh,
215232 fs,
233 path,
216234 profileDirectory,
217235 instancePath,
218236 first,
......@@ -435,4 +453,68 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
435453 yield* h.assertClosed;
436454 }).pipe(Effect.scoped),
437455 );
456
457 it.effect.skipIf(windowsHost)(
458 "gives each process its own temp directory and removes it when the process closes",
459 () =>
460 Effect.gen(function* () {
461 const h = yield* makeHarness();
462 const tempRoot = resolveAntigravityRuntimeTempDirectory(h.profileDirectory);
463 yield* h.refresh();
464 yield* h.refresh();
465 const directories = h.launches.flatMap((launch) =>
466 launch.tempDirectory === undefined ? [] : [launch.tempDirectory],
467 );
468 expect(directories).toHaveLength(2);
469 for (const directory of directories) {
470 expect(h.path.dirname(directory)).toBe(tempRoot);
471 }
472 expect(new Set(directories).size).toBe(2);
473 yield* h.assertClosed;
474 }).pipe(Effect.scoped),
475 );
476
477 it.effect.skipIf(windowsHost)(
478 "removes runtime temp directories left by a previous server on create",
479 () =>
480 Effect.gen(function* () {
481 const fs = yield* FileSystem.FileSystem;
482 const path = yield* Path.Path;
483 const config = yield* ServerConfig;
484 const instanceId = ProviderInstanceId.make("antigravity-orphan-sweep");
485 const tempRoot = resolveAntigravityRuntimeTempDirectory(
486 resolveAntigravityProfileDirectory(config.stateDir, instanceId),
487 );
488 const orphan = path.join(tempRoot, "run-orphan", "_MEI123", "google3");
489 yield* fs.makeDirectory(orphan, { recursive: true });
490 yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale");
491 yield* AntigravityDriver.create({
492 instanceId,
493 displayName: "Sweep",
494 enabled: false,
495 config: AntigravityDriver.defaultConfig(),
496 environment: [],
497 }).pipe(
498 Effect.provide(
499 Layer.mock(AntigravityInstallation)({
500 managedDirectory: config.stateDir,
501 resolve: () => Effect.die("unused"),
502 acquire: () => Effect.die("unused"),
503 }),
504 ),
505 );
506 expect(yield* fs.exists(tempRoot)).toBe(false);
507 }).pipe(Effect.scoped),
508 );
509
510 it.effect("probes through installation resolution without launching a process", () =>
511 Effect.gen(function* () {
512 const h = yield* makeHarness({ enabled: true });
513 const snapshot = yield* h.instance.snapshot.refresh;
514 expect(snapshot.installed).toBe(true);
515 expect(snapshot.version).toBe(h.first.version);
516 expect(h.launches).toEqual([]);
517 expect(h.acquisitions).toEqual([]);
518 }).pipe(Effect.scoped),
519 );
438520});
apps/server/src/provider/Drivers/AntigravityDriver.ts+78-17
......@@ -30,6 +30,7 @@ import {
3030 isAntigravitySignInRequiredError,
3131 prepareAntigravityProfile,
3232 resolveAntigravityProfileDirectory,
33 resolveAntigravityRuntimeTempDirectory,
3334 type AntigravityAuthConfig,
3435} from "../antigravityAuthSupport.ts";
3536import {
......@@ -38,7 +39,10 @@ import {
3839} from "../acp/AntigravityAcpSupport.ts";
3940import type { AcpSessionRuntime, AcpSessionRuntimeStartResult } from "../acp/AcpSessionRuntime.ts";
4041import type { ServerProviderDraft } from "../providerSnapshot.ts";
41import { removeAntigravitySessionFiles } from "../acp/AntigravitySessionFiles.ts";
42import {
43 removeAntigravityRuntimeTempDirs,
44 removeAntigravitySessionFiles,
45} from "../acp/AntigravitySessionFiles.ts";
4246import { ProviderDriverError } from "../Errors.ts";
4347import { makeAntigravityAdapter } from "../Layers/AntigravityAdapter.ts";
4448import { makeAntigravityProvider } from "../Layers/AntigravityProvider.ts";
......@@ -98,6 +102,11 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
98102 serverConfig.stateDir,
99103 instanceId,
100104 );
105 // No process of this instance exists yet, so every runtime temp
106 // directory left under the profile is an orphan from a killed server.
107 yield* removeAntigravityRuntimeTempDirs(
108 resolveAntigravityRuntimeTempDirectory(profileDirectory),
109 ).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem));
101110 const continuationIdentity = defaultProviderContinuationIdentity({
102111 driverKind: DRIVER,
103112 instanceId,
......@@ -142,6 +151,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
142151 instanceId,
143152 operation: "resolve",
144153 detail: cause.detail,
154 cause,
145155 }),
146156 ),
147157 );
......@@ -155,6 +165,32 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
155165 Effect.provideService(Path.Path, path),
156166 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
157167 );
168 // Each process unpacks into its own directory that dies with the
169 // runtime scope, after the child is killed. A shared directory would
170 // let one session's teardown delete files a sibling still reads.
171 // Removal is best effort: a handle can outlive the kill on Windows,
172 // and the sweep on the next driver start reclaims what is left.
173 const runtimeTempDirectory = yield* Effect.acquireRelease(
174 fileSystem.makeTempDirectory({ directory: profile.tempDirectory, prefix: "run-" }).pipe(
175 Effect.mapError(
176 (cause) =>
177 new ProviderSetupError({
178 instanceId,
179 operation: "start",
180 detail: "Could not create an Antigravity runtime temp directory.",
181 cause,
182 }),
183 ),
184 ),
185 (directory) =>
186 fileSystem
187 .remove(directory, { recursive: true, force: true })
188 .pipe(
189 Effect.catch(() =>
190 Effect.logWarning("Could not remove an Antigravity runtime temp directory."),
191 ),
192 ),
193 );
158194 const runtime = yield* makeAntigravityAcpRuntime({
159195 ...input,
160196 authMethod: auth.authMethod,
......@@ -165,6 +201,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
165201 cwd: input.cwd,
166202 baseEnv: withAgentDeviceEnvironment(processEnvironment, input),
167203 auth,
204 runtimeTempDirectory,
168205 }),
169206 }).pipe(Effect.provideService(Crypto.Crypto, crypto));
170207 return {
......@@ -258,24 +295,48 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
258295 // Kick the TTL-gated manifest refresh alongside the health check, as
259296 // Codex and Claude do. Without it an environment that only runs
260297 // Antigravity would keep classifying against a stale disk cache.
298 // The probe must not spawn. The agent is a PyInstaller one-file bundle
299 // that unpacks about 1 GB per launch, and the health check runs every
300 // minute. Resolving the install on disk is enough to report installed
301 // and version. The response below is synthetic: only agentInfo.version
302 // is read from it. Sessions and manual refreshes still spawn.
261303 const probe = Effect.gen(function* () {
262304 yield* modelManifest.refreshInBackground;
263 const processScope = yield* Scope.make();
264 yield* Effect.addFinalizer((exit) => Scope.close(processScope, exit));
265 return yield* authFlow
266 .withProcess(
267 Scope.close(processScope, Exit.void),
268 Effect.gen(function* () {
269 const runtime = yield* makeRuntime({
270 cwd: serverConfig.stateDir,
271 clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" },
272 mcpServers: [],
273 });
274 return yield* runtime.initialize();
275 }),
276 )
277 .pipe(Effect.provideService(Scope.Scope, processScope));
278 }).pipe(Effect.scoped);
305 if (authConfigIssue !== null) {
306 return yield* new ProviderSetupError({
307 instanceId,
308 operation: "configure",
309 detail: authConfigIssue,
310 });
311 }
312 const executable = yield* installation
313 .resolve(settings.binaryPath, processEnvironment)
314 .pipe(
315 Effect.mapError(
316 (cause) =>
317 new ProviderSetupError({
318 instanceId,
319 operation: "resolve",
320 detail: cause.detail,
321 cause,
322 }),
323 ),
324 );
325 return {
326 protocolVersion: 1,
327 agentCapabilities: {
328 loadSession: true,
329 promptCapabilities: { image: true, audio: true, embeddedContext: true },
330 sessionCapabilities: { list: {}, resume: {} },
331 },
332 authMethods: [{ id: "oauth-personal", name: "Log in with Google" }],
333 agentInfo: {
334 name: "antigravity-acp",
335 title: "Google Antigravity",
336 version: executable.version ?? "unknown",
337 },
338 };
339 });
279340
280341 const provider = yield* makeAntigravityProvider(settings, {
281342 stampIdentity: classifyModels,
apps/server/src/provider/acp/AntigravitySessionFiles.ts+18
......@@ -41,3 +41,21 @@ export const removeAntigravitySessionFiles = Effect.fn("removeAntigravitySession
4141 },
4242 Effect.catch(() => Effect.logWarning("Could not remove temporary Antigravity session files.")),
4343);
44
45/**
46 * Removes every per-process runtime temp directory under the profile. Call
47 * once when the driver starts, before it launches any process, so a previous
48 * server that was killed mid-session cannot leave unpacked runtimes behind.
49 * Only the profile-owned directory is touched. The system temp directory
50 * belongs to other programs and Windows does not lock data files, so sweeping
51 * it could gut a live extraction.
52 */
53export const removeAntigravityRuntimeTempDirs = Effect.fn("removeAntigravityRuntimeTempDirs")(
54 function* (tempDirectory: string) {
55 const fs = yield* FileSystem.FileSystem;
56 yield* fs.remove(tempDirectory, { recursive: true, force: true });
57 },
58 Effect.catch(() =>
59 Effect.logWarning("Could not remove leftover Antigravity runtime temp files."),
60 ),
61);
apps/server/src/provider/antigravityAuthSupport.test.ts+43
......@@ -51,6 +51,7 @@ describe("Antigravity process environment", () => {
5151 geminiHome: "/t3/userdata/providers/antigravity/profile",
5252 acpDirectory: "/t3/userdata/providers/antigravity/profile/antigravity-acp",
5353 tokenPath: "/t3/userdata/providers/antigravity/profile/antigravity-acp/acp_token.json",
54 tempDirectory: "/t3/userdata/providers/antigravity/profile/antigravity-acp/tmp",
5455 browserCommand: "managed-browser-helper",
5556 };
5657
......@@ -101,6 +102,7 @@ describe("Antigravity process environment", () => {
101102 BROWSER: profile.browserCommand,
102103 PYTHONUNBUFFERED: "1",
103104 ELECTRON_RUN_AS_NODE: "1",
105 TMPDIR: profile.tempDirectory,
104106 },
105107 });
106108 });
......@@ -187,6 +189,47 @@ describe("Antigravity process environment", () => {
187189 ).toBeNull();
188190 });
189191
192 it("isolates TEMP and TMP to the profile directory on Windows", () => {
193 const windowsProfile: AntigravityProfile = {
194 platform: "win32",
195 geminiHome: "C:\\state\\providers\\antigravity\\profile",
196 acpDirectory: "C:\\state\\providers\\antigravity\\profile\\antigravity-acp",
197 tokenPath: "C:\\state\\providers\\antigravity\\profile\\antigravity-acp\\acp_token.json",
198 tempDirectory: "C:\\state\\providers\\antigravity\\profile\\antigravity-acp\\tmp",
199 browserCommand: "managed-browser-helper",
200 };
201 const input = {
202 installation: {
203 executablePath: "C:\\release\\agy_acp_server.exe",
204 harnessPath: "C:\\release\\localharness_external.exe",
205 },
206 profile: windowsProfile,
207 cwd: "C:\\project",
208 baseEnv: { PATH: "C:\\Windows\\system32", TEMP: "C:\\Users\\user\\AppData\\Local\\Temp" },
209 };
210 const shared = buildAntigravityAcpSpawnInput(input);
211 expect(shared.env?.TEMP).toBe(windowsProfile.tempDirectory);
212 expect(shared.env?.TMP).toBe(windowsProfile.tempDirectory);
213 const perRun = buildAntigravityAcpSpawnInput({
214 ...input,
215 runtimeTempDirectory: `${windowsProfile.tempDirectory}\\run-1`,
216 });
217 expect(perRun.env?.TEMP).toBe(`${windowsProfile.tempDirectory}\\run-1`);
218 expect(perRun.env?.TMP).toBe(`${windowsProfile.tempDirectory}\\run-1`);
219 });
220
221 it("isolates TMPDIR to the profile directory on POSIX hosts", () => {
222 const spawn = buildAntigravityAcpSpawnInput({
223 installation: { executablePath: "/release/acp", harnessPath: "/release/harness" },
224 profile,
225 cwd: "/project",
226 baseEnv: { TMPDIR: "/tmp" },
227 runtimeTempDirectory: `${profile.tempDirectory}/run-1`,
228 });
229 expect(spawn.env?.TMPDIR).toBe(`${profile.tempDirectory}/run-1`);
230 expect(spawn.env?.TEMP).toBeUndefined();
231 });
232
190233 it("uses the registry launch arguments for each supported host platform", () => {
191234 for (const platform of ["linux", "darwin", "win32"] as const) {
192235 const spawn = buildAntigravityAcpSpawnInput({
apps/server/src/provider/antigravityAuthSupport.ts+21-1
......@@ -85,6 +85,8 @@ export interface AntigravityProfile {
8585 readonly geminiHome: string;
8686 readonly acpDirectory: string;
8787 readonly tokenPath: string;
88 /** Parent of the per-process temp directories PyInstaller unpacks into. */
89 readonly tempDirectory: string;
8890 readonly browserCommand: string;
8991}
9092
......@@ -191,6 +193,11 @@ export function resolveAntigravityProfileDirectory(
191193 return NodePath.join(stateDir, "providers", "antigravity", directoryName);
192194}
193195
196/** Parent of the per-process runtime temp directories inside a profile. */
197export function resolveAntigravityRuntimeTempDirectory(profileDirectory: string): string {
198 return NodePath.join(profileDirectory, "antigravity-acp", "tmp");
199}
200
194201function quoteBrowserArgument(value: string): string {
195202 return `'${value.replaceAll("'", `'"'"'`)}'`;
196203}
......@@ -199,6 +206,7 @@ function antigravityEnvironment(
199206 profile: AntigravityProfile,
200207 baseEnv: NodeJS.ProcessEnv,
201208 auth: AntigravityAuthConfig,
209 runtimeTempDirectory?: string,
202210) {
203211 const environment: NodeJS.ProcessEnv = {};
204212 for (const [key, value] of Object.entries(baseEnv)) {
......@@ -214,6 +222,10 @@ function antigravityEnvironment(
214222 : auth.authMethod === "agent-platform" && auth.apiKey
215223 ? { GOOGLE_API_KEY: auth.apiKey }
216224 : {};
225 // The agent is a PyInstaller one-file bundle. It unpacks about 1 GB into
226 // the system temp directory per launch and a force kill leaves that behind.
227 // Point it at a T3-owned directory so the driver can reclaim the space.
228 const tempDirectory = runtimeTempDirectory ?? profile.tempDirectory;
217229 return {
218230 ...environment,
219231 ...credential,
......@@ -222,6 +234,9 @@ function antigravityEnvironment(
222234 BROWSER: profile.browserCommand,
223235 PYTHONUNBUFFERED: "1",
224236 ELECTRON_RUN_AS_NODE: "1",
237 ...(profile.platform === "win32"
238 ? { TEMP: tempDirectory, TMP: tempDirectory }
239 : { TMPDIR: tempDirectory }),
225240 };
226241}
227242
......@@ -311,11 +326,13 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
311326
312327 const geminiHome = path.resolve(input.profileDirectory);
313328 const acpDirectory = path.join(geminiHome, "antigravity-acp");
329 const tempDirectory = resolveAntigravityRuntimeTempDirectory(geminiHome);
314330 const profile: AntigravityProfile = {
315331 platform,
316332 geminiHome,
317333 acpDirectory,
318334 tokenPath: path.join(acpDirectory, "acp_token.json"),
335 tempDirectory,
319336 browserCommand,
320337 };
321338 const environment = antigravityEnvironment(profile, input.baseEnv ?? process.env, auth);
......@@ -358,7 +375,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
358375 ),
359376 );
360377
361 for (const directory of [geminiHome, acpDirectory]) {
378 for (const directory of [geminiHome, acpDirectory, tempDirectory]) {
362379 yield* fs
363380 .makeDirectory(directory, { recursive: true, mode: 0o700 })
364381 .pipe(
......@@ -400,6 +417,8 @@ export function buildAntigravityAcpSpawnInput(input: {
400417 readonly cwd: string;
401418 readonly baseEnv?: NodeJS.ProcessEnv;
402419 readonly auth?: AntigravityAuthConfig;
420 /** Per-process temp directory. Defaults to the profile's shared temp directory. */
421 readonly runtimeTempDirectory?: string;
403422}): AcpSpawnInput {
404423 return {
405424 command: input.installation.executablePath,
......@@ -410,6 +429,7 @@ export function buildAntigravityAcpSpawnInput(input: {
410429 input.profile,
411430 input.baseEnv ?? process.env,
412431 input.auth ?? ANTIGRAVITY_PERSONAL_AUTH,
432 input.runtimeTempDirectory,
413433 ),
414434 ANTIGRAVITY_HARNESS_PATH: input.installation.harnessPath,
415435 },