diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index 24eb4e3d6df8..66094c7afa75 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -136,6 +136,33 @@ const installationError = (operation: string, detail: string, cause?: unknown) = const wrapFailure = (operation: string, detail: string) => (cause: unknown) => isInstallationError(cause) ? cause : installationError(operation, detail, cause); +const SIGNAL_INTERRUPT_PATTERN = /receipt of signal: '([A-Za-z0-9]+)'/; + +/** + * Walks a chained `cause` (as produced by effect/platform's child process + * spawner) looking for the signal that killed the process. Bounded so an + * unexpected cyclical cause cannot recurse forever. + * @internal + */ +export function findTerminatingSignal(cause: unknown, depth = 0): string | undefined { + if (depth > 8 || !(cause instanceof Error)) return undefined; + const match = SIGNAL_INTERRUPT_PATTERN.exec(cause.message); + if (match) return match[1]; + return findTerminatingSignal((cause as { cause?: unknown }).cause, depth + 1); +} + +const GENERIC_VALIDATION_FAILURE_DETAIL = + "The downloaded Antigravity runtime could not start in this environment."; +const SIGILL_VALIDATION_FAILURE_DETAIL = + "The downloaded Antigravity runtime crashed on startup with an illegal instruction (SIGILL). This usually means the CPU is missing instructions (such as AVX/AVX2) that the Antigravity runtime requires."; + +/** @internal */ +export function describeValidationFailure(cause: unknown): string { + return findTerminatingSignal(cause) === "SIGILL" + ? SIGILL_VALIDATION_FAILURE_DETAIL + : GENERIC_VALIDATION_FAILURE_DETAIL; +} + function executableNames(platform: NodeJS.Platform) { return platform === "win32" ? { executable: "agy_acp_server.exe", harness: "localharness_external.exe" } @@ -508,11 +535,10 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma Effect.provideService(Path.Path, path), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(Crypto.Crypto, crypto), - Effect.mapError( - wrapFailure( - "verify", - "The downloaded Antigravity runtime could not start in this environment.", - ), + Effect.mapError((cause) => + isInstallationError(cause) + ? cause + : installationError("verify", describeValidationFailure(cause), cause), ), ); diff --git a/apps/server/src/provider/AntigravityInstallation.validationFailure.test.ts b/apps/server/src/provider/AntigravityInstallation.validationFailure.test.ts new file mode 100644 index 000000000000..e489c3113111 --- /dev/null +++ b/apps/server/src/provider/AntigravityInstallation.validationFailure.test.ts @@ -0,0 +1,60 @@ +import * as NodeAssert from "node:assert/strict"; + +import { describe, it } from "vite-plus/test"; + +import { describeValidationFailure, findTerminatingSignal } from "./AntigravityInstallation.ts"; + +describe("findTerminatingSignal", () => { + it("extracts the signal name from a child-process interrupt error", () => { + const cause = new Error("Process interrupted due to receipt of signal: 'SIGILL'"); + NodeAssert.equal(findTerminatingSignal(cause), "SIGILL"); + }); + + it("walks a chain of wrapped causes to find the signal", () => { + const signalError = new Error("Process interrupted due to receipt of signal: 'SIGILL'"); + const platformError = new Error("Unknown: ChildProcess.exitCode (agy_acp_server.par)", { + cause: signalError, + }); + const transportError = new Error("ACP transport operation read-process-exit-status failed.", { + cause: platformError, + }); + NodeAssert.equal(findTerminatingSignal(transportError), "SIGILL"); + }); + + it("returns undefined when no signal is present in the chain", () => { + const cause = new Error("The archive could not be read.", { + cause: new Error("ENOENT: no such file or directory"), + }); + NodeAssert.equal(findTerminatingSignal(cause), undefined); + }); + + it("returns undefined for non-error causes", () => { + NodeAssert.equal(findTerminatingSignal(undefined), undefined); + NodeAssert.equal(findTerminatingSignal("SIGILL"), undefined); + }); + + it("does not recurse forever on a cyclical cause chain", () => { + const cause: Error & { cause?: unknown } = new Error("boom"); + cause.cause = cause; + NodeAssert.equal(findTerminatingSignal(cause), undefined); + }); +}); + +describe("describeValidationFailure", () => { + it("names AVX/AVX2 as the likely cause when the runtime is killed by SIGILL", () => { + const cause = new Error("ACP transport operation read-process-exit-status failed.", { + cause: new Error("Process interrupted due to receipt of signal: 'SIGILL'"), + }); + const detail = describeValidationFailure(cause); + NodeAssert.match(detail, /SIGILL/); + NodeAssert.match(detail, /AVX/); + }); + + it("falls back to the generic message for other failures", () => { + const cause = new Error("The downloaded runtime did not identify as the expected release."); + NodeAssert.equal( + describeValidationFailure(cause), + "The downloaded Antigravity runtime could not start in this environment.", + ); + }); +});