diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index f1f39e104b44c..e2a988355867b 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -46,8 +46,8 @@ import { import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe"; import { - removeAntigravityRuntimeTempDirs, removeAntigravitySessionFiles, + retireAntigravityRuntimeTempDirs, } from "../acp/AntigravitySessionFiles.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; @@ -150,9 +150,11 @@ export const AntigravityDriver: ProviderDriver< directories.runtimeTemp, path.join(profileDirectory, "antigravity-acp", "tmp"), ]) { - yield* removeAntigravityRuntimeTempDirs(directory).pipe( + const removeRetired = yield* retireAntigravityRuntimeTempDirs(directory).pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), ); + yield* Effect.forkScoped(removeRetired); } const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER, diff --git a/apps/server/src/provider/acp/AntigravitySessionFiles.test.ts b/apps/server/src/provider/acp/AntigravitySessionFiles.test.ts new file mode 100644 index 0000000000000..795e80b861e84 --- /dev/null +++ b/apps/server/src/provider/acp/AntigravitySessionFiles.test.ts @@ -0,0 +1,64 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; + +import { retireAntigravityRuntimeTempDirs } from "./AntigravitySessionFiles.ts"; + +it.layer(NodeServices.layer)("retireAntigravityRuntimeTempDirs", (it) => { + it.effect("moves leftovers aside at once and deletes them, plus older retirees, later", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const parent = yield* fs.makeTempDirectoryScoped(); + const runtimeTemp = path.join(parent, "abc123"); + const orphan = path.join(runtimeTemp, "run-orphan", "_MEI123"); + yield* fs.makeDirectory(orphan, { recursive: true }); + yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale"); + yield* fs.makeDirectory(path.join(parent, "abc123.old-unfinished", "runtime"), { + recursive: true, + }); + yield* fs.makeDirectory(path.join(parent, "other-instance")); + + const removeRetired = yield* retireAntigravityRuntimeTempDirs(runtimeTemp); + expect(yield* fs.exists(runtimeTemp)).toBe(false); + const retired = (yield* fs.readDirectory(parent)).filter((entry) => + entry.startsWith("abc123.old-"), + ); + expect(retired).toHaveLength(2); + + yield* removeRetired; + expect((yield* fs.readDirectory(parent)).toSorted()).toEqual(["other-instance"]); + }).pipe(Effect.scoped), + ); + + it.effect( + "deletes leftovers inline when they cannot be moved aside, such as on a full disk", + () => + Effect.gen(function* () { + const removed: Array = []; + const fs = FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + makeTempDirectory: () => + Effect.fail( + PlatformError.systemError({ + _tag: "Unknown", + module: "FileSystem", + method: "makeTempDirectory", + description: "ENOSPC", + }), + ), + remove: (path) => Effect.sync(() => void removed.push(path)), + readDirectory: () => Effect.succeed([]), + }); + + const removeRetired = yield* retireAntigravityRuntimeTempDirs( + "/state/antigravity-tmp/abc123", + ).pipe(Effect.provideService(FileSystem.FileSystem, fs)); + yield* removeRetired; + expect(removed).toEqual(["/state/antigravity-tmp/abc123"]); + }), + ); +}); diff --git a/apps/server/src/provider/acp/AntigravitySessionFiles.ts b/apps/server/src/provider/acp/AntigravitySessionFiles.ts index 7a25c8323174d..819d79a05ed12 100644 --- a/apps/server/src/provider/acp/AntigravitySessionFiles.ts +++ b/apps/server/src/provider/acp/AntigravitySessionFiles.ts @@ -42,13 +42,35 @@ export const removeAntigravitySessionFiles = Effect.fn("removeAntigravitySession Effect.catch(() => Effect.logWarning("Could not remove temporary Antigravity session files.")), ); -/** Sweep only private runtimes before launch; Windows does not protect live extracted data files from deletion. */ -export const removeAntigravityRuntimeTempDirs = Effect.fn("removeAntigravityRuntimeTempDirs")( +const warnRuntimeTempLeftovers = Effect.logWarning( + "Could not remove leftover Antigravity runtime temp files.", +); + +/** + * Sweep only private runtimes before launch; Windows does not protect live extracted data files from deletion. + * Moves leftovers aside and returns their slow deletion (seconds on Windows) to run off the startup path. + */ +export const retireAntigravityRuntimeTempDirs = Effect.fn("retireAntigravityRuntimeTempDirs")( function* (tempDirectory: string) { const fs = yield* FileSystem.FileSystem; - yield* fs.remove(tempDirectory, { recursive: true, force: true }); + const path = yield* Path.Path; + const parent = path.dirname(tempDirectory); + const prefix = `${path.basename(tempDirectory)}.old-`; + if (yield* fs.exists(tempDirectory)) { + yield* fs.makeTempDirectory({ directory: parent, prefix }).pipe( + Effect.flatMap((retired) => fs.rename(tempDirectory, path.join(retired, "runtime"))), + Effect.catch(() => fs.remove(tempDirectory, { recursive: true, force: true })), + ); + } + const entries = yield* fs.readDirectory(parent).pipe(Effect.orElseSucceed(() => [])); + return Effect.forEach( + entries.filter((entry) => entry.startsWith(prefix)), + (entry) => + fs + .remove(path.join(parent, entry), { recursive: true, force: true }) + .pipe(Effect.catch(() => warnRuntimeTempLeftovers)), + { discard: true }, + ); }, - Effect.catch(() => - Effect.logWarning("Could not remove leftover Antigravity runtime temp files."), - ), + Effect.catch(() => warnRuntimeTempLeftovers.pipe(Effect.as(Effect.void))), );