Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
378783f
fix(server): isolate Antigravity runtime cleanup
Zheruel Oct 23, 2026
7921df2
test(server): reproduce Antigravity cleanup deadline failures
Zheruel Oct 23, 2026
e866772
test(server): use Windows temp environment in validation fixture
Zheruel Oct 23, 2026
fc0806a
fix(server): preserve Antigravity cleanup runtime environment
Zheruel Oct 23, 2026
bcbc927
test(server): exercise scheduled Antigravity cleanup retry
Zheruel Oct 23, 2026
f5f802d
Merge branch 'main' into fix/antigravity-cleanup-starvation-23877
Zheruel Oct 23, 2026
e1f4500
chore: merge main into antigravity cleanup fix
maria-rcks Oct 26, 2026
562f363
fix(server): avoid recompiling antigravity backlog predicate
maria-rcks Oct 26, 2026
23cccce
chore: merge main into antigravity cleanup fix
maria-rcks Oct 30, 2026
3524872
fix(server): merge main and preserve isolated antigravity cleanup
maria-rcks Nov 2, 2026
869886e
test(server): cover cleanup of retired antigravity directories
maria-rcks Nov 2, 2026
083950b
chore: merge main and preserve antigravity cleanup coverage
maria-rcks Nov 3, 2026
8dcfcae
fix(server): use current secure random service for cleanup
maria-rcks Nov 3, 2026
2efd191
chore: merge main into antigravity cleanup fix
maria-rcks Nov 3, 2026
8c3cd88
Merge remote-tracking branch 'origin/main' into fix/antigravity-clean…
juliusmarminge Nov 12, 2026
62937ee
refactor(server): drop the unused Antigravity runtime temp retire helper
juliusmarminge Nov 12, 2026
ae090d4
fix(server): Antigravity runs no longer stop on undeletable temp left…
juliusmarminge Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const permissionOptionIds = {
allowAlways: "allow-always",
rejectOnce: "reject-once",
};
const sessionId = "mock-session-1";
const sessionId = process.env.T3_ACP_SESSION_ID ?? "mock-session-1";

let currentModeId = antigravityProfile
? (process.env.T3_ACP_INITIAL_MODE ?? "default")
Expand Down
61 changes: 61 additions & 0 deletions apps/server/src/provider/AntigravityInstallation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as ChildProcess from "effect/process/ChildProcess";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";

import * as AntigravityInstallation from "./AntigravityInstallation.ts";
import * as AntigravityRuntimeTemp from "./AntigravityRuntimeTemp.ts";
import * as ModelManifest from "./ModelManifest.ts";
import { type AntigravityReleaseAsset } from "./antigravityRelease.ts";

Expand Down Expand Up @@ -175,6 +176,9 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* (
)
: installationFs.makeTempDirectoryScoped(settings),
});
const runtimeTemp = yield* AntigravityRuntimeTemp.make(path.join(baseDir, "antigravity-gc")).pipe(
Effect.provideService(FileSystem.FileSystem, trackedFs),
);
const installation = yield* AntigravityInstallation.makeAntigravityInstallation({
baseDir,
...(options.native ? {} : { releaseAsset: asset }),
Expand All @@ -194,6 +198,7 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* (
? Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, options.spawner)
: (effect) => effect,
Effect.provideService(FileSystem.FileSystem, trackedFs),
Effect.provideService(AntigravityRuntimeTemp.AntigravityRuntimeTemp, runtimeTemp),
Effect.provideService(HostProcess.Platform, platform),
Effect.provideService(HostProcess.Architecture, arch),
Effect.provideService(HostProcess.Environment, { PATH: options.path ?? "" }),
Expand Down Expand Up @@ -289,6 +294,62 @@ const expectPreviousRelease = Effect.fn("test.expectPreviousAntigravityRelease")
});

it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("Antigravity installation", (it) => {
it.effect("activates a validated runtime before profile cleanup finishes", () =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const node = yield* HostProcess.ExecutablePath;
const cleanupEntered = yield* Deferred.make<void>();
const validators: ChildProcessSpawner.ChildProcessHandle[] = [];
const initialized = {
protocolVersion: 1,
agentInfo: { name: "antigravity-acp", version: "fixture-new" },
agentCapabilities: {
loadSession: true,
sessionCapabilities: { resume: {} },
auth: { logout: {} },
},
authMethods: [{ id: "oauth-personal", name: "Log in with Google" }],
};
const script = [
'const fs = require("node:fs");',
'const path = require("node:path");',
'const unpack = path.join(process.env.TMPDIR ?? process.env.TEMP, "_MEI-fixture");',
"fs.mkdirSync(unpack, { recursive: true });",
'fs.writeFileSync(path.join(unpack, "payload.bin"), "fixture");',
'require("node:readline").createInterface({ input: process.stdin }).on("line", line => {',
" const request = JSON.parse(line);",
` const result = request.method === "initialize" ? ${JSON.stringify(initialized)} : {};`,
' if (request.id !== undefined) process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }) + "\\n");',
"});",
].join("\n");
const observedSpawner = ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
if (command._tag === "StandardCommand" && command.args[0] === "--eval") {
yield* Deferred.succeed(cleanupEntered, undefined);
return yield* Effect.never;
}
if (command._tag === "StandardCommand" && command.command.endsWith(executableName)) {
const handle = yield* spawner.spawn(
ChildProcess.make(node, ["-e", script], command.options),
);
validators.push(handle);
return handle;
}
return yield* spawner.spawn(command);
}),
);
const { installation } = yield* makeHarness({
useDefaultValidation: true,
}).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, observedSpawner));
yield* installation.start;
expect((yield* terminalState(installation)).phase).toBe("succeeded");
yield* Deferred.await(cleanupEntered);
expect((yield* installation.resolve()).version).toBe("fixture-new");
expect(validators).toHaveLength(1);
for (const handle of validators) expect(yield* handle.isRunning).toBe(false);
}).pipe(Effect.scoped),
);

it.effect.each([...new Set(["win32", hostPlatform])] as NodeJS.Platform[])(
"migrates the legacy managed runtime to the native CLI on %s and retains the old release",
(platform) =>
Expand Down
23 changes: 5 additions & 18 deletions apps/server/src/provider/AntigravityInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Schedule from "effect/Schedule";
import * as Scope from "effect/Scope";
import * as Semaphore from "effect/Semaphore";
import * as Stream from "effect/Stream";
Expand All @@ -41,6 +40,7 @@ import {
parseAntigravityCliRelease,
type AntigravityCliReleaseAsset,
} from "./antigravityCliRelease.ts";
import * as AntigravityRuntimeTemp from "./AntigravityRuntimeTemp.ts";
import { openZipArchive } from "../zipArchive.ts";
import { makeAntigravityAcpRuntime } from "./acp/AntigravityAcpSupport.ts";
import {
Expand Down Expand Up @@ -137,7 +137,7 @@ export class AntigravityInstallation extends Context.Service<
),
});
}),
);
).pipe(Layer.provideMerge(AntigravityRuntimeTemp.layer));
}

export interface AntigravityInstallationOptions {
Expand Down Expand Up @@ -188,6 +188,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
const random = yield* SecureRandom.SecureRandom;
const http = yield* HttpClient.HttpClient;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const runtimeTemp = yield* AntigravityRuntimeTemp.AntigravityRuntimeTemp;
const serviceScope = yield* Effect.scope;
const platform = yield* HostProcess.Platform;
const arch = yield* HostProcess.Architecture;
Expand Down Expand Up @@ -447,27 +448,13 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
Effect.fn("AntigravityInstallation.validate")(
function* (executable: AntigravityExecutable, expectedVersion: string) {
const validationScope = yield* Effect.scope;
const profileDirectory = yield* Effect.acquireRelease(
fs.makeTempDirectory({ prefix: "t3-antigravity-validate-" }),
(directory) =>
fs.remove(directory, { recursive: true, force: true }).pipe(
// Windows may retain DLL handles briefly after taskkill returns.
Effect.retry({
times: platform === "win32" ? 10 : 0,
schedule: Schedule.spaced("100 millis"),
while: (error) =>
error.reason._tag === "PermissionDenied" ||
error.reason._tag === "Busy" ||
error.reason._tag === "Unknown",
}),
Effect.orDie,
),
const profileDirectory = yield* runtimeTemp.makeDirectory.pipe(
Effect.provideService(HostProcess.Environment, environment),
);
const profile = yield* prepareAntigravityProfile({
profileDirectory,
platform,
baseEnv: environment,
// A shallow scoped profile cleans up unpacked files within Windows' path limit.
tempDirectory: profileDirectory,
});
const runtime = yield* makeAntigravityAcpRuntime({
Expand Down
205 changes: 205 additions & 0 deletions apps/server/src/provider/AntigravityRuntimeTemp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Queue from "effect/Queue";
import * as Scope from "effect/Scope";
import * as TestClock from "effect/testing/TestClock";
import * as ChildProcess from "effect/process/ChildProcess";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";

import * as AntigravityRuntimeTemp from "./AntigravityRuntimeTemp.ts";

it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("AntigravityRuntimeTemp", (it) => {
it.effect("cleans up when Node is available only in the provider environment", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const actualNode = yield* HostProcess.ExecutablePath;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-cleanup-node-" });
yield* Effect.gen(function* () {
const cleanup = yield* AntigravityRuntimeTemp.make(path.join(root, "gc"));
const pending = yield* cleanup.makeDirectory.pipe(
Effect.provideService(HostProcess.Environment, { PATH: path.dirname(actualNode) }),
Effect.scoped,
);
yield* cleanup.drain;
expect(yield* fs.exists(pending)).toBe(false);
}).pipe(
Effect.provideService(HostProcess.IsExecutable, true),
Effect.provideService(HostProcess.ExecutablePath, path.join(root, "t3-server")),
Effect.provideService(HostProcess.Environment, { PATH: "" }),
);
}).pipe(Effect.scoped),
);

it.effect("preserves the provider environment when spawning cleanup", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const environment = yield* HostProcess.Environment;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-cleanup-env-" });
const cleanup = yield* AntigravityRuntimeTemp.make(path.join(root, "gc")).pipe(
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) => {
if (command._tag !== "StandardCommand") return Effect.die("Unexpected pipeline");
return spawner.spawn(
ChildProcess.make(
command.command,
[
"--eval",
`if (process.env.T3_CLEANUP_RUNTIME_TEST !== "provider") process.exit(126); ${command.args[1]}`,
command.args[2]!,
],
command.options,
),
);
}),
),
);
const pending = yield* cleanup.makeDirectory.pipe(
Effect.provideService(HostProcess.Environment, {
...environment,
T3_CLEANUP_RUNTIME_TEST: "provider",
}),
Effect.scoped,
);
yield* cleanup.drain;
expect(yield* fs.exists(pending)).toBe(false);
}).pipe(Effect.scoped),
);

it.effect("keeps starting runs while cleanup stalls and preserves live siblings", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-cleanup-" });
const directory = path.join(root, "gc");
const entered = yield* Deferred.make<void>();
const release = yield* Deferred.make<void>();
let starts = 0;
const cleanup = yield* AntigravityRuntimeTemp.make(directory).pipe(
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
starts += 1;
yield* Deferred.succeed(entered, undefined);
yield* Deferred.await(release);
return yield* spawner.spawn(command);
}),
),
),
);
const liveScope = yield* Scope.make();
yield* Effect.addFinalizer((exit) => Scope.close(liveScope, exit));
const live = yield* cleanup.makeDirectory.pipe(Effect.provideService(Scope.Scope, liveScope));
yield* fs.writeFileString(path.join(live, "keep.txt"), "live");
yield* cleanup.makeDirectory.pipe(Effect.scoped);
yield* Deferred.await(entered);
for (let index = 0; index < 8; index += 1) {
yield* cleanup.makeDirectory.pipe(Effect.scoped);
}
expect(starts).toBe(1);
expect((yield* fs.readDirectory(directory)).length).toBe(10);
expect(yield* fs.readFileString(path.join(live, "keep.txt"))).toBe("live");
yield* Deferred.succeed(release, undefined);
yield* cleanup.drain;
expect(yield* fs.readDirectory(directory)).toEqual([path.basename(live)]);
yield* cleanup.makeDirectory.pipe(Effect.scoped);
yield* Scope.close(liveScope, Exit.void);
yield* cleanup.drain;
expect(yield* fs.readDirectory(directory)).toEqual([]);
}).pipe(Effect.scoped),
);

it.effect("retries failed cleanup without dropping pending directories", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-cleanup-retry-" });
const pending = path.join(root, "run-failed");
yield* fs.makeDirectory(pending);
const finished = yield* Queue.unbounded<void>();
let fail = true;
yield* AntigravityRuntimeTemp.make(root).pipe(
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Queue.offer(finished, undefined));
return yield* spawner.spawn(
fail && command._tag === "StandardCommand"
? ChildProcess.make(
command.command,
["--eval", "process.exit(1)"],
command.options,
)
: command,
);
}),
),
),
);
yield* Queue.take(finished);
expect(yield* fs.exists(pending)).toBe(true);
fail = false;
yield* TestClock.adjust("1 minute");
yield* Queue.take(finished);
expect(yield* fs.readDirectory(path.dirname(pending))).toEqual([]);
}).pipe(Effect.scoped),
);

it.effect("interrupts stalled cleanup on shutdown and recovers the directory on restart", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-cleanup-restart-" });
const directory = path.join(root, "gc");
const serviceScope = yield* Scope.make();
const entered = yield* Deferred.make<void>();
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const children: ChildProcessSpawner.ChildProcessHandle[] = [];
const first = yield* AntigravityRuntimeTemp.make(directory).pipe(
Effect.provideService(Scope.Scope, serviceScope),
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
if (command._tag !== "StandardCommand")
return yield* Effect.die("Unexpected pipeline");
const child = yield* spawner.spawn(
ChildProcess.make(command.command, ["--eval", "process.stdin.resume()"], {
...command.options,
stdin: "pipe",
}),
);
children.push(child);
yield* Deferred.succeed(entered, undefined);
return child;
}),
),
),
);
const pending = yield* first.makeDirectory.pipe(Effect.scoped);
yield* Deferred.await(entered);
yield* Scope.close(serviceScope, Exit.void);
expect(children).toHaveLength(1);
for (const child of children) expect(yield* child.isRunning).toBe(false);
expect(yield* fs.exists(pending)).toBe(true);
const second = yield* AntigravityRuntimeTemp.make(directory);
yield* second.drain;
expect(yield* fs.readDirectory(directory)).toEqual([]);
}).pipe(Effect.scoped),
);
});
Loading
Loading