Fix daemon startup blocking on model downloads

This commit is contained in:
Mohamed Boudra
2026-03-19 14:03:49 +07:00
parent 4ab307b10f
commit 9065dcef54
2 changed files with 46 additions and 31 deletions
@@ -3,13 +3,17 @@ import path from "node:path";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { Writable } from "node:stream";
import pino from "pino";
import { describe, expect, test } from "vitest";
import { afterEach, describe, expect, test, vi } from "vitest";
import { createPaseoDaemon, parseListenString, type PaseoDaemonConfig } from "./bootstrap.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
import { createTestAgentClients } from "./test-utils/fake-agent-client.js";
describe("paseo daemon bootstrap", () => {
afterEach(() => {
vi.restoreAllMocks();
});
test("starts and serves health endpoint", async () => {
const daemonHandle = await createTestPaseoDaemon({
openai: { apiKey: "test-openai-api-key" },
@@ -77,6 +81,47 @@ describe("paseo daemon bootstrap", () => {
}
});
test("does not block daemon start on local speech model downloads", async () => {
const originalFetch = globalThis.fetch;
let releaseFetch: ((value: Response) => void) | null = null;
const fetchGate = new Promise<Response>((resolve) => {
releaseFetch = resolve;
});
vi.stubGlobal("fetch", vi.fn(() => fetchGate));
const daemonHandle = await createTestPaseoDaemon({
speech: {
providers: {
dictationStt: { provider: "local", explicit: true, enabled: true },
voiceTurnDetection: { provider: "local", explicit: true, enabled: false },
voiceStt: { provider: "local", explicit: true, enabled: false },
voiceTts: { provider: "local", explicit: true, enabled: false },
},
local: {
modelsDir: path.join(os.tmpdir(), `paseo-missing-models-${Date.now()}`),
models: {
dictationStt: "parakeet-tdt-0.6b-v3-int8",
voiceStt: "parakeet-tdt-0.6b-v3-int8",
voiceTts: "kokoro-en-v0_19",
},
},
},
});
try {
const response = await originalFetch(`http://127.0.0.1:${daemonHandle.port}/api/health`);
expect(response.ok).toBe(true);
} finally {
releaseFetch?.(
new Response(null, {
status: 500,
statusText: "test cleanup",
})
);
await daemonHandle.close();
}
});
test("parses Windows named pipes as managed IPC listen targets", () => {
expect(parseListenString(String.raw`\\.\pipe\paseo-managed-test`)).toEqual({
type: "pipe",
@@ -6,7 +6,6 @@ import type { RequestedSpeechProviders } from "../../speech-types.js";
import type { TurnDetectionProvider } from "../../turn-detection-provider.js";
import { PocketTtsOnnxTTS } from "./pocket/pocket-tts-onnx.js";
import {
ensureLocalSpeechModels,
getLocalSpeechModelDir,
DEFAULT_LOCAL_STT_MODEL,
DEFAULT_LOCAL_TTS_MODEL,
@@ -203,35 +202,6 @@ export async function initializeLocalSpeechServices(params: {
models: localModels,
});
if (requiredLocalModelIds.length > 0 && localConfig) {
try {
logger.info(
{
modelsDir: localConfig.modelsDir,
modelIds: requiredLocalModelIds,
},
"Ensuring local speech models"
);
await ensureLocalSpeechModels({
modelsDir: localConfig.modelsDir,
modelIds: requiredLocalModelIds,
logger,
});
} catch (err) {
logger.warn(
{
err,
modelsDir: localConfig.modelsDir,
modelIds: requiredLocalModelIds,
hint:
"Use `paseo speech models` to inspect status and " +
"`paseo speech download --model <MODEL_ID>` to fetch missing models.",
},
"Local speech model bootstrap failed"
);
}
}
const localSttEngines = new Map<LocalSttModelId, LocalSttEngine>();
const getLocalSttEngine = async (