Diff
1diff --git a/index.ts b/index.ts
2index 9ebc7f44f6b35b249077a0589199fe9f0536dc55..fd5e90c88f54b57f01e84db4b00fd864ac05311f 100644
3--- a/index.ts
4+++ b/index.ts
5@@ -130,6 +130,13 @@ function applyTemplateThinkingSupport(model: MutableModelMetadata): void {
6 };
7 }
8
9+// Pi invalidates a captured ctx when the session is replaced (e.g. new_session in
10+// RPC mode). Any later ctx access then throws this error. Background work started
11+// before the replacement should treat it as "session gone" and stop quietly.
12+function isStaleContextError(error: unknown): boolean {
13+ return error instanceof Error && error.message.includes("stale after session replacement");
14+}
15+
16 export default async function (pi: ExtensionAPI) {
17 let currentModels: LlamaModel[] = [];
18
19@@ -365,9 +372,13 @@ export default async function (pi: ExtensionAPI) {
20 }
21 }
22 } catch (error) {
23- // Suppress errors from intentionally-aborted SSE connections.
24+ // Suppress errors from intentionally-aborted SSE connections and from a
25+ // stale ctx (session was replaced while streaming).
26 const msg = (error as Error).message;
27- if (signal.aborted && (error instanceof DOMException || msg === "terminated")) {
28+ if (
29+ isStaleContextError(error) ||
30+ (signal.aborted && (error instanceof DOMException || msg === "terminated"))
31+ ) {
32 return;
33 }
34 ctx?.ui.notify(`[llama-cpp] SSE error: ${msg}`, "warning");
35@@ -523,8 +534,9 @@ export default async function (pi: ExtensionAPI) {
36 });
37 } catch (error) {
38 const err = error as Error;
39- // Suppress notification for aborted requests (model was switched).
40- if (err.name !== "AbortError") {
41+ // Suppress notification for aborted requests (model was switched) and for a
42+ // stale ctx (session was replaced while awaiting) — both are expected.
43+ if (err.name !== "AbortError" && !isStaleContextError(err)) {
44 ctx?.ui.notify(`[llama-cpp] /props for ${modelId} failed: ${err.message}`, "error");
45 }
46 } finally {
47@@ -557,15 +569,25 @@ export default async function (pi: ExtensionAPI) {
48
49 // Discover /props for already-active models because re-selecting them does not emit model_select.
50 pi.on("before_provider_request", (event, ctx) => {
51- const modelId = (event.payload as { model?: unknown })?.model;
52- if (typeof modelId === "string") {
53- const activeModel =
54- ctx.model?.provider === PROVIDER_ID && ctx.model.id === modelId ? ctx.model : undefined;
55- void discoverModelMetadata(modelId, ctx, true, PROPS_TIMEOUT_MS, activeModel);
56+ try {
57+ const modelId = (event.payload as { model?: unknown })?.model;
58+ if (typeof modelId === "string") {
59+ const activeModel =
60+ ctx.model?.provider === PROVIDER_ID && ctx.model.id === modelId ? ctx.model : undefined;
61+ void discoverModelMetadata(modelId, ctx, true, PROPS_TIMEOUT_MS, activeModel);
62+ }
63+ } catch (error) {
64+ // Session was replaced as the request fired; nothing to discover.
65+ if (!isStaleContextError(error)) {
66+ throw error;
67+ }
68 }
69 });
70
71 pi.on("session_shutdown", () => {
72 clearFooterStatusTimeout();
73+ // Stop in-flight /props and SSE so they don't resume against a stale ctx.
74+ propsAbortController?.abort();
75+ sseAbortController?.abort();
76 });
77 }