diff --git a/apps/server/src/speech/native.test.ts b/apps/server/src/speech/native.test.ts index 8712c63ed347d..a41f917b2a682 100644 --- a/apps/server/src/speech/native.test.ts +++ b/apps/server/src/speech/native.test.ts @@ -1,4 +1,8 @@ // @effect-diagnostics nodeBuiltinImport:off - verifies isolated native worker processes and their exit events. +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; + import { expect, it } from "vite-plus/test"; import { loadNativeSpeechModel } from "./native.ts"; @@ -155,6 +159,37 @@ it("kills a hung streaming feed without waiting for native cleanup", async () => await model.dispose(); await rejected; }); + +it("skips process exit teardown when the backend dies during native inference", async () => { + const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-speech-orphan-")); + const marker = NodePath.join(directory, "exit-teardown"); + // The exit hook stands in for native atexit teardown; the interval for the threadpool call. + const model = await loadNativeSpeechModel( + "unused.gguf", + new AbortController().signal, + streamingFixture(`async () => { + const { writeFileSync } = process.getBuiltinModule("node:fs"); + process.once("exit", () => writeFileSync(${JSON.stringify(marker)}, "ran")); + setInterval(() => {}, 1 << 30); + process.disconnect(); + return await new Promise(() => {}); + }`), + ); + try { + await model.begin({ timestamps: "none" }, false); + await expect(model.feed(new Float32Array(512).fill(0.25))).rejects.toThrow( + "Speech process exited", + ); + expect( + NodeFS.existsSync(marker), + "orphaned worker ran exit teardown while native inference was in flight", + ).toBe(false); + } finally { + await model.dispose(); + NodeFS.rmSync(directory, { recursive: true, force: true }); + } +}); + const vadFixture = ({ streaming = false, failVad = false, diff --git a/apps/server/src/speech/native.ts b/apps/server/src/speech/native.ts index 8331e1b5e0ec1..70c5d21c6038d 100644 --- a/apps/server/src/speech/native.ts +++ b/apps/server/src/speech/native.ts @@ -182,7 +182,8 @@ process.on("message", async (message) => { error: error instanceof Error ? error.stack ?? error.message : String(error) }); } }); -process.on("disconnect", () => process.exit(0)); +// Exit teardown under in-flight native inference segfaults, so terminate without it. +process.on("disconnect", () => process.kill(process.pid, "SIGKILL")); `; type Reply = {