@zachwill/pi-orchestrate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
1
+ import type { CompletedWave } from "./runtime.js";
2
+
3
+ export const MAX_DELIVERY_MARKDOWN_BYTES = 50 * 1024;
4
+ export const DELIVERY_TRUNCATION_MARKER =
5
+ "\n\n[Worker results truncated at 50KB. Full structured results remain available.]";
6
+ export const DELIVERY_PARENT_INSTRUCTIONS =
7
+ "Parent: Synthesize all results, resolve conflicts, review changes and evidence, run integration checks, and continue the user's task. Do not merely forward worker reports.";
8
+
9
+ export type ParentBindingGeneration = string | number | symbol;
10
+
11
+ export interface WaveDeliveryDetails {
12
+ readonly id: CompletedWave["id"];
13
+ readonly ownerSessionId: CompletedWave["ownerSessionId"];
14
+ readonly mode: CompletedWave["mode"];
15
+ readonly results: CompletedWave["results"];
16
+ }
17
+
18
+ export interface WaveDeliveryMessage {
19
+ readonly customType: "pi-orchestrate-wave";
20
+ readonly content: string;
21
+ readonly display: true;
22
+ readonly details: WaveDeliveryDetails;
23
+ }
24
+
25
+ export interface WaveDeliveryOptions {
26
+ readonly triggerTurn: boolean;
27
+ }
28
+
29
+ export interface ParentBinding {
30
+ readonly ownerSessionId: string;
31
+ readonly generation: ParentBindingGeneration;
32
+ isIdle(): boolean;
33
+ sendMessage(message: WaveDeliveryMessage, options: WaveDeliveryOptions): void;
34
+ }
35
+
36
+ interface BoundParent {
37
+ readonly binding: ParentBinding;
38
+ agentRunning: boolean;
39
+ }
40
+
41
+ export class DeliveryCoordinator {
42
+ private readonly boundParents = new Map<string, BoundParent>();
43
+ private readonly pendingWaves: CompletedWave[] = [];
44
+
45
+ bind(binding: ParentBinding): void {
46
+ this.boundParents.set(binding.ownerSessionId, {
47
+ binding,
48
+ agentRunning: !binding.isIdle(),
49
+ });
50
+ this.flush(binding.ownerSessionId, binding.generation);
51
+ }
52
+
53
+ unbind(ownerSessionId: string, generation: ParentBindingGeneration): void {
54
+ if (!this.matchesBinding(ownerSessionId, generation)) return;
55
+ this.boundParents.delete(ownerSessionId);
56
+ }
57
+
58
+ markAgentStarted(ownerSessionId: string, generation: ParentBindingGeneration): void {
59
+ if (!this.matchesBinding(ownerSessionId, generation)) return;
60
+ this.boundParents.get(ownerSessionId)!.agentRunning = true;
61
+ }
62
+
63
+ markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void {
64
+ if (!this.matchesBinding(ownerSessionId, generation)) return;
65
+ this.boundParents.get(ownerSessionId)!.agentRunning = false;
66
+ this.flush(ownerSessionId, generation);
67
+ }
68
+
69
+ accept(completedWave: CompletedWave): boolean {
70
+ if (completedWave.mode === "inline") return false;
71
+
72
+ this.pendingWaves.push(completedWave);
73
+ const generation = this.boundParents.get(completedWave.ownerSessionId)?.binding.generation;
74
+ if (generation !== undefined) {
75
+ this.flush(completedWave.ownerSessionId, generation);
76
+ }
77
+ return true;
78
+ }
79
+
80
+ pendingCount(ownerSessionId: string): number {
81
+ return this.pendingWaves.filter((wave) => wave.ownerSessionId === ownerSessionId).length;
82
+ }
83
+
84
+ clear(): void {
85
+ this.boundParents.clear();
86
+ this.pendingWaves.length = 0;
87
+ }
88
+
89
+ close(): void {
90
+ this.clear();
91
+ }
92
+
93
+ private matchesBinding(
94
+ ownerSessionId: string,
95
+ generation: ParentBindingGeneration,
96
+ ): boolean {
97
+ const binding = this.boundParents.get(ownerSessionId)?.binding;
98
+ return binding?.generation === generation;
99
+ }
100
+
101
+ private canDeliver(
102
+ ownerSessionId: string,
103
+ generation: ParentBindingGeneration,
104
+ ): boolean {
105
+ const parent = this.boundParents.get(ownerSessionId);
106
+ return (
107
+ parent !== undefined &&
108
+ parent.binding.generation === generation &&
109
+ !parent.agentRunning &&
110
+ parent.binding.isIdle()
111
+ );
112
+ }
113
+
114
+ private flush(
115
+ ownerSessionId: string,
116
+ generation: ParentBindingGeneration,
117
+ ): void {
118
+ if (!this.canDeliver(ownerSessionId, generation)) return;
119
+
120
+ const waves = this.pendingWaves.filter((wave) => wave.ownerSessionId === ownerSessionId);
121
+ for (let index = 0; index < waves.length; index += 1) {
122
+ if (!this.canDeliver(ownerSessionId, generation)) return;
123
+
124
+ const wave = waves[index];
125
+ if (!wave) return;
126
+
127
+ try {
128
+ // Pi 0.80.10 starts an idle turn only through triggerTurn. Omitting deliverAs keeps
129
+ // streaming races as steering; nextTurn would queue without starting a turn.
130
+ this.boundParents.get(ownerSessionId)!.binding.sendMessage(renderWaveMessage(wave), {
131
+ triggerTurn: index === waves.length - 1,
132
+ });
133
+ } catch {
134
+ return;
135
+ }
136
+
137
+ const pendingIndex = this.pendingWaves.indexOf(wave);
138
+ if (pendingIndex >= 0) this.pendingWaves.splice(pendingIndex, 1);
139
+ }
140
+ }
141
+ }
142
+
143
+ function renderWaveMessage(wave: CompletedWave): WaveDeliveryMessage {
144
+ const resultWord = wave.results.length === 1 ? "result" : "results";
145
+ const sections = wave.results.map((result) => {
146
+ const outcome = renderOutcome(result.outcome);
147
+ const heading = `### ${result.worker} — ${result.title}`;
148
+ const metadata = `Worker \`${result.workerId}\` · status \`${result.status}\``;
149
+ return outcome.length > 0 ? `${heading}\n${metadata}\n\n${outcome}` : `${heading}\n${metadata}`;
150
+ });
151
+ const summary = `## Worker results — wave \`${wave.id}\`\n\n${wave.results.length} ${resultWord}`;
152
+ const results = sections.length > 0 ? `${summary}\n\n${sections.join("\n\n")}` : summary;
153
+ const instructions = `\n\n---\n\n${DELIVERY_PARENT_INSTRUCTIONS}`;
154
+
155
+ return {
156
+ customType: "pi-orchestrate-wave",
157
+ content: capMarkdown(results, instructions),
158
+ display: true,
159
+ details: {
160
+ id: wave.id,
161
+ ownerSessionId: wave.ownerSessionId,
162
+ mode: wave.mode,
163
+ results: wave.results,
164
+ },
165
+ };
166
+ }
167
+
168
+ function capMarkdown(content: string, appendix: string): string {
169
+ const complete = `${content}${appendix}`;
170
+ const completeBytes = Buffer.from(complete, "utf8");
171
+ if (completeBytes.byteLength <= MAX_DELIVERY_MARKDOWN_BYTES) return complete;
172
+
173
+ const contentBytes = Buffer.from(content, "utf8");
174
+ const reservedBytes = Buffer.byteLength(`${DELIVERY_TRUNCATION_MARKER}${appendix}`, "utf8");
175
+ let prefixEnd = MAX_DELIVERY_MARKDOWN_BYTES - reservedBytes;
176
+ while (prefixEnd > 0 && (contentBytes[prefixEnd]! & 0xc0) === 0x80) prefixEnd -= 1;
177
+ return `${contentBytes.subarray(0, prefixEnd).toString("utf8")}${DELIVERY_TRUNCATION_MARKER}${appendix}`;
178
+ }
179
+
180
+ function renderOutcome(outcome: CompletedWave["results"][number]["outcome"]): string {
181
+ switch (outcome.status) {
182
+ case "completed":
183
+ case "ready":
184
+ return outcome.assistantText;
185
+ case "failed":
186
+ return outcome.assistantText
187
+ ? `Failed: ${outcome.message}\n\n${outcome.assistantText}`
188
+ : `Failed: ${outcome.message}`;
189
+ case "aborted": {
190
+ const reason = outcome.message ? `Aborted: ${outcome.message}` : "Aborted";
191
+ return outcome.assistantText ? `${reason}\n\n${outcome.assistantText}` : reason;
192
+ }
193
+ case "closed":
194
+ return "Closed";
195
+ }
196
+ }
@@ -0,0 +1,335 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+
3
+ export const SUPPORTED_TOOL_NAMES = [
4
+ "read",
5
+ "bash",
6
+ "edit",
7
+ "write",
8
+ "grep",
9
+ "find",
10
+ "ls",
11
+ ] as const;
12
+
13
+ export type SupportedToolName = (typeof SUPPORTED_TOOL_NAMES)[number];
14
+
15
+ const supportedToolNames: ReadonlySet<string> = new Set(SUPPORTED_TOOL_NAMES);
16
+
17
+ export function isSupportedToolName(value: unknown): value is SupportedToolName {
18
+ return typeof value === "string" && supportedToolNames.has(value);
19
+ }
20
+
21
+ export type WorkerSourceKind = "package" | "user" | "project";
22
+
23
+ export interface WorkerSource {
24
+ readonly kind: WorkerSourceKind;
25
+ readonly filePath: string;
26
+ }
27
+
28
+ export interface WorkerModel {
29
+ readonly provider: string;
30
+ readonly modelId: string;
31
+ }
32
+
33
+ export interface WorkerCompaction {
34
+ readonly enabled?: boolean;
35
+ readonly reserveTokens?: number;
36
+ readonly keepRecentTokens?: number;
37
+ }
38
+
39
+ export type WorkerLifecycle = "one-shot" | "reusable";
40
+
41
+ export interface WorkerDefinition {
42
+ readonly name: string;
43
+ readonly source: WorkerSource;
44
+ readonly description: string;
45
+ readonly systemPrompt: string;
46
+ readonly lifecycle: WorkerLifecycle;
47
+ readonly tools: readonly SupportedToolName[];
48
+ readonly skills: readonly string[];
49
+ readonly model?: WorkerModel;
50
+ readonly thinking?: ThinkingLevel;
51
+ readonly compaction?: WorkerCompaction;
52
+ }
53
+
54
+ export type CatalogDiagnosticSeverity = "warning" | "error";
55
+
56
+ export interface CatalogDiagnostic {
57
+ readonly severity: CatalogDiagnosticSeverity;
58
+ readonly source: WorkerSourceKind;
59
+ readonly message: string;
60
+ readonly filePath?: string;
61
+ }
62
+
63
+ export interface WorkerCatalog {
64
+ readonly workers: readonly WorkerDefinition[];
65
+ readonly diagnostics: readonly CatalogDiagnostic[];
66
+ }
67
+
68
+ export function createWorkerCatalog(
69
+ workers: readonly WorkerDefinition[],
70
+ diagnostics: readonly CatalogDiagnostic[] = [],
71
+ ): WorkerCatalog {
72
+ return {
73
+ workers: [...workers].sort(compareWorkersByName),
74
+ diagnostics: [...diagnostics],
75
+ };
76
+ }
77
+
78
+ export function findWorkerByName(
79
+ catalog: WorkerCatalog,
80
+ name: string,
81
+ ): WorkerDefinition | undefined {
82
+ return catalog.workers.find((worker) => worker.name === name);
83
+ }
84
+
85
+ function compareWorkersByName(left: WorkerDefinition, right: WorkerDefinition): number {
86
+ if (left.name < right.name) return -1;
87
+ if (left.name > right.name) return 1;
88
+ return 0;
89
+ }
90
+
91
+ export interface OrchestrateTaskInput {
92
+ readonly worker: string;
93
+ readonly title: string;
94
+ readonly instructions: string;
95
+ }
96
+
97
+ declare const workerIdBrand: unique symbol;
98
+ declare const waveIdBrand: unique symbol;
99
+
100
+ export type WorkerId = string & { readonly [workerIdBrand]: "WorkerId" };
101
+ export type WaveId = string & { readonly [waveIdBrand]: "WaveId" };
102
+
103
+ export type WorkerIdFactory = () => WorkerId;
104
+ export type WaveIdFactory = () => WaveId;
105
+
106
+ export interface OrchestrateIdFactories {
107
+ readonly workerId: WorkerIdFactory;
108
+ readonly waveId: WaveIdFactory;
109
+ }
110
+
111
+ export function createRandomWorkerIdFactory(
112
+ randomId: () => string = defaultRandomId,
113
+ ): WorkerIdFactory {
114
+ return () => `worker-${randomId()}` as WorkerId;
115
+ }
116
+
117
+ export function createRandomWaveIdFactory(
118
+ randomId: () => string = defaultRandomId,
119
+ ): WaveIdFactory {
120
+ return () => `wave-${randomId()}` as WaveId;
121
+ }
122
+
123
+ export function createRandomIdFactories(
124
+ randomId: () => string = defaultRandomId,
125
+ ): OrchestrateIdFactories {
126
+ return {
127
+ workerId: createRandomWorkerIdFactory(randomId),
128
+ waveId: createRandomWaveIdFactory(randomId),
129
+ };
130
+ }
131
+
132
+ export function createSequentialWorkerIdFactory(startAt = 1): WorkerIdFactory {
133
+ let next = startAt;
134
+ return () => `worker-${next++}` as WorkerId;
135
+ }
136
+
137
+ export function createSequentialWaveIdFactory(startAt = 1): WaveIdFactory {
138
+ let next = startAt;
139
+ return () => `wave-${next++}` as WaveId;
140
+ }
141
+
142
+ export function createSequentialIdFactories(startAt = 1): OrchestrateIdFactories {
143
+ return {
144
+ workerId: createSequentialWorkerIdFactory(startAt),
145
+ waveId: createSequentialWaveIdFactory(startAt),
146
+ };
147
+ }
148
+
149
+ function defaultRandomId(): string {
150
+ return globalThis.crypto.randomUUID();
151
+ }
152
+
153
+ export interface WorkerUsage {
154
+ readonly input: number;
155
+ readonly output: number;
156
+ readonly cacheRead: number;
157
+ readonly cacheWrite: number;
158
+ readonly cost: number;
159
+ readonly contextTokens: number;
160
+ readonly turns: number;
161
+ }
162
+
163
+ export const EMPTY_WORKER_USAGE: WorkerUsage = Object.freeze({
164
+ input: 0,
165
+ output: 0,
166
+ cacheRead: 0,
167
+ cacheWrite: 0,
168
+ cost: 0,
169
+ contextTokens: 0,
170
+ turns: 0,
171
+ });
172
+
173
+ export interface WorkerCompletedOutcome {
174
+ readonly status: "completed";
175
+ readonly assistantText: string;
176
+ }
177
+
178
+ export interface WorkerReadyOutcome {
179
+ readonly status: "ready";
180
+ readonly assistantText: string;
181
+ }
182
+
183
+ export interface WorkerFailedOutcome {
184
+ readonly status: "failed";
185
+ readonly message: string;
186
+ readonly assistantText?: string;
187
+ }
188
+
189
+ export interface WorkerAbortedOutcome {
190
+ readonly status: "aborted";
191
+ readonly message?: string;
192
+ readonly assistantText?: string;
193
+ }
194
+
195
+ export interface WorkerClosedOutcome {
196
+ readonly status: "closed";
197
+ }
198
+
199
+ export type WorkerOutcome =
200
+ | WorkerCompletedOutcome
201
+ | WorkerReadyOutcome
202
+ | WorkerFailedOutcome
203
+ | WorkerAbortedOutcome
204
+ | WorkerClosedOutcome;
205
+ export type TerminalWorkerOutcome = Exclude<WorkerOutcome, WorkerReadyOutcome>;
206
+ export type WorkerStatus =
207
+ | "starting"
208
+ | "running"
209
+ | "ready"
210
+ | "stopping"
211
+ | "completed"
212
+ | "failed"
213
+ | "aborted"
214
+ | "closed";
215
+ export type TerminalWorkerStatus = Extract<
216
+ WorkerStatus,
217
+ "completed" | "failed" | "aborted" | "closed"
218
+ >;
219
+ export type WaveCompleteWorkerStatus = TerminalWorkerStatus | "ready";
220
+
221
+ export interface WorkerRecord {
222
+ readonly id: WorkerId;
223
+ readonly worker: string;
224
+ readonly ownerSessionId: string;
225
+ readonly waveId: WaveId;
226
+ readonly title: string;
227
+ readonly instructions: string;
228
+ readonly lifecycle: WorkerLifecycle;
229
+ readonly status: WorkerStatus;
230
+ readonly usage: WorkerUsage;
231
+ readonly activity?: string;
232
+ readonly outcome?: WorkerOutcome;
233
+ readonly sessionFile?: string;
234
+ }
235
+
236
+ export type WaveMode = "async" | "inline";
237
+ export type WaveState = "running" | "complete";
238
+
239
+ export interface WaveRecord {
240
+ readonly id: WaveId;
241
+ readonly ownerSessionId: string;
242
+ readonly workerIds: readonly WorkerId[];
243
+ readonly mode: WaveMode;
244
+ readonly state: WaveState;
245
+ readonly createdAt: number;
246
+ }
247
+
248
+ export class InvalidTransitionError extends Error {
249
+ readonly from: WorkerStatus;
250
+ readonly to: WorkerStatus;
251
+
252
+ constructor(from: WorkerStatus, to: WorkerStatus) {
253
+ super(`Invalid worker status transition: ${from} -> ${to}`);
254
+ this.name = "InvalidTransitionError";
255
+ this.from = from;
256
+ this.to = to;
257
+ }
258
+ }
259
+
260
+ export function isTerminalWorkerStatus(status: WorkerStatus): status is TerminalWorkerStatus {
261
+ return status === "completed" || status === "failed" || status === "aborted" || status === "closed";
262
+ }
263
+
264
+ export function isTerminalWorkerOutcome(outcome: WorkerOutcome): outcome is TerminalWorkerOutcome {
265
+ return outcome.status !== "ready";
266
+ }
267
+
268
+ export function canTransitionWorkerStatus(
269
+ from: WorkerStatus,
270
+ to: WorkerStatus,
271
+ lifecycle: WorkerLifecycle,
272
+ ): boolean {
273
+ if (from === to || isTerminalWorkerStatus(from)) return false;
274
+
275
+ switch (from) {
276
+ case "starting":
277
+ return to === "running" || to === "stopping" || to === "failed" || to === "aborted";
278
+ case "running":
279
+ if (to === "ready") return lifecycle === "reusable";
280
+ if (to === "completed") return lifecycle === "one-shot";
281
+ return to === "stopping" || to === "failed" || to === "aborted";
282
+ case "ready":
283
+ if (lifecycle !== "reusable") return false;
284
+ return to === "running" || to === "stopping" || to === "closed";
285
+ case "stopping":
286
+ return to === "aborted" || to === "failed";
287
+ default:
288
+ return false;
289
+ }
290
+ }
291
+
292
+ export function transitionWorkerStatus(
293
+ worker: WorkerRecord,
294
+ status: WorkerStatus,
295
+ ): WorkerRecord {
296
+ if (!canTransitionWorkerStatus(worker.status, status, worker.lifecycle)) {
297
+ throw new InvalidTransitionError(worker.status, status);
298
+ }
299
+
300
+ return { ...worker, status, outcome: undefined };
301
+ }
302
+
303
+ export function isWorkerCompleteForWave(
304
+ status: WorkerStatus,
305
+ ): status is WaveCompleteWorkerStatus {
306
+ return status === "ready" || isTerminalWorkerStatus(status);
307
+ }
308
+
309
+ export function getWaveWorkersInOrder(
310
+ wave: WaveRecord,
311
+ workersById: ReadonlyMap<WorkerId, WorkerRecord>,
312
+ ): readonly WorkerRecord[] | undefined {
313
+ const workers: WorkerRecord[] = [];
314
+
315
+ for (const workerId of wave.workerIds) {
316
+ const worker = workersById.get(workerId);
317
+ if (!worker) return undefined;
318
+ workers.push(worker);
319
+ }
320
+
321
+ return workers;
322
+ }
323
+
324
+ export function isWaveComplete(
325
+ wave: WaveRecord,
326
+ workersById: ReadonlyMap<WorkerId, WorkerRecord>,
327
+ ): boolean {
328
+ const workers = getWaveWorkersInOrder(wave, workersById);
329
+ return workers !== undefined && workers.every((worker) => isWorkerCompleteForWave(worker.status));
330
+ }
331
+
332
+ export const MAX_TASKS_PER_WAVE = 12;
333
+ export const MAX_WORKER_TITLE_LENGTH = 200;
334
+ export const MAX_WORKER_INSTRUCTIONS_LENGTH = 100_000;
335
+ export const CANCELLATION_GRACE_MS = 5_000;
@@ -0,0 +1,107 @@
1
+ import { DeliveryCoordinator } from "./delivery.js";
2
+ import {
3
+ createOrchestratorRuntime,
4
+ type OrchestratorRuntime,
5
+ } from "./runtime.js";
6
+ import { createWorkerSessionFactory } from "./worker-session.js";
7
+
8
+ const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v1");
9
+
10
+ export interface ProcessHost {
11
+ readonly runtime: OrchestratorRuntime;
12
+ readonly delivery: DeliveryCoordinator;
13
+ }
14
+
15
+ export interface ProcessHostAttachment {
16
+ readonly host: ProcessHost;
17
+ }
18
+
19
+ interface AttachmentAwareProcessHost extends ProcessHost {
20
+ attachments?: Set<ProcessHostAttachment>;
21
+ }
22
+
23
+ interface OwnedProcessHost extends AttachmentAwareProcessHost {
24
+ readonly unsubscribeCompletion: () => void;
25
+ destroyPromise?: Promise<void>;
26
+ }
27
+
28
+ type ProcessGlobal = typeof globalThis & {
29
+ [PROCESS_HOST_KEY]?: OwnedProcessHost;
30
+ };
31
+
32
+ function processGlobal(): ProcessGlobal {
33
+ return globalThis as ProcessGlobal;
34
+ }
35
+
36
+ export function getProcessHost(): ProcessHost | undefined {
37
+ return processGlobal()[PROCESS_HOST_KEY];
38
+ }
39
+
40
+ export function createProcessHost(): ProcessHost {
41
+ const global = processGlobal();
42
+ const existing = global[PROCESS_HOST_KEY];
43
+ if (existing) return existing;
44
+
45
+ const runtime = createOrchestratorRuntime({
46
+ workerSessionFactory: createWorkerSessionFactory(),
47
+ });
48
+ const delivery = new DeliveryCoordinator();
49
+ const host: OwnedProcessHost = {
50
+ runtime,
51
+ delivery,
52
+ attachments: new Set(),
53
+ unsubscribeCompletion: runtime.subscribeCompletion((wave) => {
54
+ delivery.accept(wave);
55
+ }),
56
+ };
57
+ global[PROCESS_HOST_KEY] = host;
58
+ return host;
59
+ }
60
+
61
+ export function attachProcessHost(host: ProcessHost): ProcessHostAttachment {
62
+ const attachment: ProcessHostAttachment = { host };
63
+ const attachmentAwareHost = host as AttachmentAwareProcessHost;
64
+ attachmentAwareHost.attachments ??= new Set();
65
+ attachmentAwareHost.attachments.add(attachment);
66
+ return attachment;
67
+ }
68
+
69
+ export function detachProcessHost(
70
+ host: ProcessHost,
71
+ attachment: ProcessHostAttachment,
72
+ ): boolean {
73
+ if (attachment.host !== host) return false;
74
+
75
+ const attachments = (host as AttachmentAwareProcessHost).attachments;
76
+ if (!attachments?.delete(attachment)) return false;
77
+ return attachments.size === 0;
78
+ }
79
+
80
+ export async function destroyProcessHost(host: ProcessHost): Promise<void> {
81
+ const ownedHost = host as OwnedProcessHost;
82
+ if ((ownedHost.attachments?.size ?? 0) > 0) return;
83
+ if (ownedHost.destroyPromise) {
84
+ await ownedHost.destroyPromise;
85
+ return;
86
+ }
87
+
88
+ ownedHost.destroyPromise = (async () => {
89
+ try {
90
+ await ownedHost.runtime.shutdown();
91
+ } finally {
92
+ ownedHost.delivery.clear();
93
+ ownedHost.unsubscribeCompletion();
94
+ const global = processGlobal();
95
+ if (global[PROCESS_HOST_KEY] === ownedHost) {
96
+ delete global[PROCESS_HOST_KEY];
97
+ }
98
+ }
99
+ })();
100
+ await ownedHost.destroyPromise;
101
+ }
102
+
103
+ export async function quitProcessHost(): Promise<void> {
104
+ const host = getProcessHost();
105
+ if (!host) return;
106
+ await destroyProcessHost(host);
107
+ }