diff --git a/extensions/llama-cpp/src/inference-provider.test.ts b/extensions/llama-cpp/src/inference-provider.test.ts index 630e0a416738..35f9e22bb7ca 100644 --- a/extensions/llama-cpp/src/inference-provider.test.ts +++ b/extensions/llama-cpp/src/inference-provider.test.ts @@ -155,7 +155,7 @@ beforeEach(() => { }); afterEach(async () => { - await inferenceRuntime.dispose(); + await inferenceRuntime.dispose().catch(() => undefined); }); describe("llama.cpp inference provider", () => { @@ -942,16 +942,20 @@ describe("llama.cpp inference provider", () => { expect(mocks.llamaDispose).toHaveBeenCalledOnce(); }); - it("retains failed cleanup ownership so disposal can be retried", async () => { + it("keeps a failed best-effort cleanup as one disposal attempt", async () => { await collectTestEvents(); mocks.contextDispose.mockRejectedValueOnce(new Error("context cleanup failed")); - await expect(inferenceRuntime.dispose()).rejects.toThrow("context cleanup failed"); - expect([mocks.modelDispose.mock.calls.length, mocks.llamaDispose.mock.calls.length]).toEqual([ - 0, 0, - ]); - await inferenceRuntime.dispose(); - expect(mocks.contextDispose).toHaveBeenCalledTimes(2); + const firstDisposal = inferenceRuntime.dispose(); + await expect(firstDisposal).rejects.toThrow( + "llama.cpp runtime cleanup failed: Error: context cleanup failed", + ); + expect(mocks.modelDispose).toHaveBeenCalledOnce(); + expect(mocks.llamaDispose).toHaveBeenCalledOnce(); + const repeatedDisposal = inferenceRuntime.dispose(); + expect(repeatedDisposal).toBe(firstDisposal); + await expect(repeatedDisposal).rejects.toThrow("context cleanup failed"); + expect(mocks.contextDispose).toHaveBeenCalledOnce(); expect(mocks.modelDispose).toHaveBeenCalledOnce(); expect(mocks.llamaDispose).toHaveBeenCalledOnce(); }); diff --git a/extensions/llama-cpp/src/inference-provider.ts b/extensions/llama-cpp/src/inference-provider.ts index 80eb7138f3af..01e13899c3b1 100644 --- a/extensions/llama-cpp/src/inference-provider.ts +++ b/extensions/llama-cpp/src/inference-provider.ts @@ -305,33 +305,42 @@ async function serialize( } function disposeLlamaCppInferenceRuntime(state: LlamaCppInferenceRuntimeState): Promise { - if (state.lifecycle === "closed") { - return Promise.resolve(); - } if (state.disposePromise) { return state.disposePromise; } state.lifecycle = "closing"; - const attempt = serialize(state, async () => { - await disposeLoadedModel(state); + // Plugin services stop once, and node-llama-cpp disposers mark themselves + // disposed before awaiting cleanup. The first disposal attempt is authoritative. + state.disposePromise = serialize(state, async () => { + const errors: unknown[] = []; + const attemptDispose = async (dispose: () => Promise) => { + try { + await dispose(); + } catch (error) { + errors.push(error); + } + }; + if (state.loadedModel) { + const previous = state.loadedModel; + await attemptDispose(() => previous.context.dispose()); + await attemptDispose(() => previous.model.dispose()); + if (state.loadedModel === previous) { + state.loadedModel = undefined; + } + } if (state.llamaInstance) { const previous = state.llamaInstance; - await previous.dispose(); + await attemptDispose(() => previous.dispose()); if (state.llamaInstance === previous) { state.llamaInstance = undefined; } } + if (errors.length > 0) { + throw new AggregateError(errors, `llama.cpp runtime cleanup failed: ${String(errors[0])}`); + } + }).finally(() => { + state.lifecycle = "closed"; }); - state.disposePromise = attempt.then( - () => { - state.lifecycle = "closed"; - state.disposePromise = undefined; - }, - (error: unknown) => { - state.disposePromise = undefined; - throw error; - }, - ); return state.disposePromise; }