-
Notifications
You must be signed in to change notification settings - Fork 234
Expand file tree
/
Copy pathruntime-owner.ts
More file actions
91 lines (83 loc) · 3.42 KB
/
Copy pathruntime-owner.ts
File metadata and controls
91 lines (83 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
import { formatTerminalError } from "./utils.ts";
export interface McpRuntimeOwner {
readonly signal: AbortSignal;
isActive(): boolean;
addCleanup(cleanup: () => void | Promise<void>): void;
stop(reason?: string): Promise<void>;
throwIfInactive(): void;
}
export function createMcpRuntimeOwner(): McpRuntimeOwner {
const controller = new AbortController();
const cleanups: Array<() => void | Promise<void>> = [];
let stopPromise: Promise<void> | undefined;
const reportCleanupFailure = (error: unknown, late: boolean) => {
console.error(`MCP: ${late ? "late " : ""}runtime cleanup failed: ${formatTerminalError(error)}`);
};
return {
signal: controller.signal,
isActive: () => !controller.signal.aborted,
addCleanup: cleanup => {
if (controller.signal.aborted) {
void Promise.resolve().then(cleanup).catch(error => reportCleanupFailure(error, true));
return;
}
cleanups.push(cleanup);
},
stop: (reason = "MCP extension runtime stopped") => {
if (stopPromise) return stopPromise;
controller.abort(new Error(reason));
const pendingCleanups = cleanups.splice(0).reverse().map(cleanup =>
Promise.resolve().then(cleanup),
);
stopPromise = Promise.allSettled(pendingCleanups).then(results => {
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
if (failures.length > 0) {
const aggregate = new AggregateError(failures, "MCP runtime cleanup failed");
console.error(`MCP: runtime cleanup failed: ${formatTerminalError(aggregate)}`);
throw aggregate;
}
});
return stopPromise;
},
throwIfInactive: () => controller.signal.throwIfAborted(),
};
}
export function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
const active = signals.filter((signal): signal is AbortSignal => signal !== undefined);
if (active.length === 0) return undefined;
if (active.length === 1) return active[0];
return AbortSignal.any(active);
}
/** Fence session-bound UI calls after the owning extension runtime stops. */
export function createOwnedUi(ui: ExtensionUIContext, owner: McpRuntimeOwner): ExtensionUIContext {
const proxies = new WeakMap<object, object>();
const wrap = (value: unknown): unknown => {
if ((typeof value !== "object" || value === null) && typeof value !== "function") {
return value;
}
const object = value as object;
const existing = proxies.get(object);
if (existing) return existing;
const proxy = new Proxy(object, {
get(target, property, receiver) {
if (!owner.isActive()) return undefined;
const member = Reflect.get(target, property, receiver);
if (typeof member === "function") {
return (...args: unknown[]) => {
if (!owner.isActive()) return undefined;
return Reflect.apply(member, target, args);
};
}
return owner.isActive() ? wrap(member) : undefined;
},
});
proxies.set(object, proxy);
return proxy;
};
return wrap(ui) as ExtensionUIContext;
}
export function isAbortError(error: unknown, signal?: AbortSignal): boolean {
if (signal?.aborted) return true;
return error instanceof Error && (error.name === "AbortError" || error.message === "MCP extension runtime stopped");
}