Skip to content

Commit a2be070

Browse files
fix(server): speech worker no longer segfaults when the backend dies mid-dictation (#33624)
1 parent 2dd207a commit a2be070

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

‎apps/server/src/speech/native.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
// @effect-diagnostics nodeBuiltinImport:off - verifies isolated native worker processes and their exit events.
2+
import * as NodeFS from "node:fs";
3+
import * as NodeOS from "node:os";
4+
import * as NodePath from "node:path";
5+
26
import { expect, it } from "vite-plus/test";
37

48
import { loadNativeSpeechModel } from "./native.ts";
@@ -155,6 +159,37 @@ it("kills a hung streaming feed without waiting for native cleanup", async () =>
155159
await model.dispose();
156160
await rejected;
157161
});
162+
163+
it("skips process exit teardown when the backend dies during native inference", async () => {
164+
const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-speech-orphan-"));
165+
const marker = NodePath.join(directory, "exit-teardown");
166+
// The exit hook stands in for native atexit teardown; the interval for the threadpool call.
167+
const model = await loadNativeSpeechModel(
168+
"unused.gguf",
169+
new AbortController().signal,
170+
streamingFixture(`async () => {
171+
const { writeFileSync } = process.getBuiltinModule("node:fs");
172+
process.once("exit", () => writeFileSync(${JSON.stringify(marker)}, "ran"));
173+
setInterval(() => {}, 1 << 30);
174+
process.disconnect();
175+
return await new Promise(() => {});
176+
}`),
177+
);
178+
try {
179+
await model.begin({ timestamps: "none" }, false);
180+
await expect(model.feed(new Float32Array(512).fill(0.25))).rejects.toThrow(
181+
"Speech process exited",
182+
);
183+
expect(
184+
NodeFS.existsSync(marker),
185+
"orphaned worker ran exit teardown while native inference was in flight",
186+
).toBe(false);
187+
} finally {
188+
await model.dispose();
189+
NodeFS.rmSync(directory, { recursive: true, force: true });
190+
}
191+
});
192+
158193
const vadFixture = ({
159194
streaming = false,
160195
failVad = false,

‎apps/server/src/speech/native.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ process.on("message", async (message) => {
182182
error: error instanceof Error ? error.stack ?? error.message : String(error) });
183183
}
184184
});
185-
process.on("disconnect", () => process.exit(0));
185+
// Exit teardown under in-flight native inference segfaults, so terminate without it.
186+
process.on("disconnect", () => process.kill(process.pid, "SIGKILL"));
186187
`;
187188

188189
type Reply = {

0 commit comments

Comments
 (0)