@zachwill/pi-orchestrate 0.7.2 → 0.9.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.
package/extension/host.ts CHANGED
@@ -1,27 +1,89 @@
1
- import { DeliveryCoordinator } from "./delivery.js";
1
+ import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect";
2
2
  import {
3
- createOrchestratorRuntime,
4
- type OrchestratorRuntime,
3
+ Delivery,
4
+ deliveryLayer,
5
+ type DeliveryService,
6
+ } from "./delivery.js";
7
+ import {
8
+ Orchestration,
9
+ orchestrationLayer,
10
+ type AbortTarget,
11
+ type AcceptedRun,
12
+ type CompletedRun,
13
+ type OrchestrationContext,
14
+ type OrchestrationService,
15
+ SHUTDOWN_CLEANUP_GRACE_MS,
16
+ type RuntimeSnapshot,
17
+ type SettlementListener,
18
+ type UnsubscribeSettlement,
5
19
  } from "./runtime.js";
6
- import { createWorkerSessionFactory } from "./worker-session.js";
20
+ import { createChildSessionsLayer } from "./worker-session.js";
21
+ import type { OrchestrateTaskInput, RunMode } from "./domain.js";
7
22
 
8
23
  const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v3");
9
24
 
25
+ type RunResult<M extends RunMode> = M extends "async"
26
+ ? AcceptedRun
27
+ : CompletedRun;
28
+
29
+ export interface OrchestratorRuntime {
30
+ orchestrate<M extends RunMode>(
31
+ context: OrchestrationContext,
32
+ task: OrchestrateTaskInput,
33
+ mode: M,
34
+ signal?: AbortSignal,
35
+ onSettlement?: SettlementListener,
36
+ ): Promise<RunResult<M>>;
37
+ sendInteractive<M extends RunMode>(
38
+ context: OrchestrationContext,
39
+ workerId: string,
40
+ instructions: string,
41
+ mode: M,
42
+ signal?: AbortSignal,
43
+ onSettlement?: SettlementListener,
44
+ ): Promise<RunResult<M>>;
45
+ abort(ownerSessionId: string, target: AbortTarget): Promise<void>;
46
+ closeInteractive(ownerSessionId: string, workerId: string): Promise<void>;
47
+ snapshot(ownerSessionId: string): Promise<RuntimeSnapshot>;
48
+ subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
49
+ subscribeState(
50
+ ownerSessionId: string,
51
+ listener: (snapshot: RuntimeSnapshot) => void,
52
+ ): () => void;
53
+ shutdown(): Promise<void>;
54
+ }
55
+
10
56
  export interface ProcessHost {
11
57
  readonly runtime: OrchestratorRuntime;
12
- readonly delivery: DeliveryCoordinator;
58
+ readonly delivery: DeliveryService;
13
59
  }
14
60
 
15
61
  export interface ProcessHostAttachment {
16
62
  readonly host: ProcessHost;
17
63
  }
18
64
 
65
+ export interface ProcessHostDestructionOptions {
66
+ /** Process-boundary deadline capability; production uses the orchestration cleanup grace. */
67
+ readonly awaitShutdown?: (
68
+ shutdown: Promise<void>,
69
+ graceMs: number,
70
+ ) => Promise<void>;
71
+ /** Process-boundary deadline capability; production uses the orchestration cleanup grace. */
72
+ readonly awaitRootDisposal?: (
73
+ disposal: Promise<void>,
74
+ graceMs: number,
75
+ ) => Promise<void>;
76
+ }
77
+
19
78
  interface AttachmentAwareProcessHost extends ProcessHost {
20
79
  attachments?: Set<ProcessHostAttachment>;
21
80
  }
22
81
 
82
+ type ProcessHostLifecycle = "active" | "destroying" | "destroyed";
83
+
23
84
  interface OwnedProcessHost extends AttachmentAwareProcessHost {
24
- readonly unsubscribeSettlement?: () => void;
85
+ readonly effectRuntime?: ManagedRuntime.ManagedRuntime<Orchestration | Delivery, never>;
86
+ lifecycle?: ProcessHostLifecycle;
25
87
  destroyPromise?: Promise<void>;
26
88
  }
27
89
 
@@ -37,32 +99,190 @@ export function getProcessHost(): ProcessHost | undefined {
37
99
  return processGlobal()[PROCESS_HOST_KEY];
38
100
  }
39
101
 
102
+ /** Pi-facing adapter. Every Promise operation executes one complete Orchestration Effect. */
103
+ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime {
104
+ constructor(
105
+ private readonly effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
106
+ private readonly orchestration: OrchestrationService,
107
+ ) {}
108
+
109
+ orchestrate(
110
+ context: OrchestrationContext,
111
+ task: OrchestrateTaskInput,
112
+ mode: "async",
113
+ signal?: AbortSignal,
114
+ onSettlement?: SettlementListener,
115
+ ): Promise<AcceptedRun>;
116
+ orchestrate(
117
+ context: OrchestrationContext,
118
+ task: OrchestrateTaskInput,
119
+ mode: "inline",
120
+ signal?: AbortSignal,
121
+ onSettlement?: SettlementListener,
122
+ ): Promise<CompletedRun>;
123
+ orchestrate(
124
+ context: OrchestrationContext,
125
+ task: OrchestrateTaskInput,
126
+ mode: RunMode,
127
+ signal?: AbortSignal,
128
+ onSettlement?: SettlementListener,
129
+ ): Promise<AcceptedRun | CompletedRun>;
130
+ orchestrate(
131
+ context: OrchestrationContext,
132
+ task: OrchestrateTaskInput,
133
+ mode: RunMode,
134
+ signal?: AbortSignal,
135
+ onSettlement?: SettlementListener,
136
+ ): Promise<AcceptedRun | CompletedRun> {
137
+ return this.run(
138
+ this.orchestration.orchestrate(context, task, mode, onSettlement),
139
+ signal,
140
+ );
141
+ }
142
+
143
+ sendInteractive(
144
+ context: OrchestrationContext,
145
+ workerId: string,
146
+ instructions: string,
147
+ mode: "async",
148
+ signal?: AbortSignal,
149
+ onSettlement?: SettlementListener,
150
+ ): Promise<AcceptedRun>;
151
+ sendInteractive(
152
+ context: OrchestrationContext,
153
+ workerId: string,
154
+ instructions: string,
155
+ mode: "inline",
156
+ signal?: AbortSignal,
157
+ onSettlement?: SettlementListener,
158
+ ): Promise<CompletedRun>;
159
+ sendInteractive(
160
+ context: OrchestrationContext,
161
+ workerId: string,
162
+ instructions: string,
163
+ mode: RunMode,
164
+ signal?: AbortSignal,
165
+ onSettlement?: SettlementListener,
166
+ ): Promise<AcceptedRun | CompletedRun>;
167
+ sendInteractive(
168
+ context: OrchestrationContext,
169
+ workerId: string,
170
+ instructions: string,
171
+ mode: RunMode,
172
+ signal?: AbortSignal,
173
+ onSettlement?: SettlementListener,
174
+ ): Promise<AcceptedRun | CompletedRun> {
175
+ return this.run(
176
+ this.orchestration.sendInteractive(
177
+ context,
178
+ workerId,
179
+ instructions,
180
+ mode,
181
+ onSettlement,
182
+ ),
183
+ signal,
184
+ );
185
+ }
186
+
187
+ abort(ownerSessionId: string, target: AbortTarget): Promise<void> {
188
+ return this.run(this.orchestration.abort(ownerSessionId, target));
189
+ }
190
+
191
+ closeInteractive(ownerSessionId: string, workerId: string): Promise<void> {
192
+ return this.run(this.orchestration.closeInteractive(ownerSessionId, workerId));
193
+ }
194
+
195
+ snapshot(ownerSessionId: string): Promise<RuntimeSnapshot> {
196
+ // Snapshot is dependency-free and remains readable from a retained host reference
197
+ // after the process root has been disposed.
198
+ return Effect.runPromise(this.orchestration.snapshot(ownerSessionId));
199
+ }
200
+
201
+ subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement {
202
+ return this.orchestration.subscribeSettlement(listener);
203
+ }
204
+
205
+ subscribeState(
206
+ ownerSessionId: string,
207
+ listener: (snapshot: RuntimeSnapshot) => void,
208
+ ): () => void {
209
+ return this.orchestration.subscribeState(ownerSessionId, listener);
210
+ }
211
+
212
+ shutdown(): Promise<void> {
213
+ return this.run(this.orchestration.shutdown());
214
+ }
215
+
216
+ private async run<A, E>(
217
+ effect: Effect.Effect<A, E>,
218
+ signal?: AbortSignal,
219
+ ): Promise<A> {
220
+ if (signal?.aborted) throw abortSignalReason(signal);
221
+ if (!signal) return this.effectRuntime.runPromise(effect);
222
+
223
+ const signalInterruption = {};
224
+ const exit = await this.effectRuntime.runPromiseExit(
225
+ Effect.raceFirst(effect, abortSignalEffect(signal, signalInterruption)),
226
+ );
227
+ if (Exit.isSuccess(exit)) return exit.value;
228
+
229
+ const error = Cause.squash(exit.cause);
230
+ if (error === signalInterruption) throw abortSignalReason(signal);
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ export function createProcessHostRuntimeAdapter<R>(
236
+ effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
237
+ ): OrchestratorRuntime {
238
+ // Orchestration acquisition is synchronous; subscriptions must remain reentrant.
239
+ const orchestration = effectRuntime.runSync(Orchestration);
240
+ return new ProcessHostRuntimeAdapter(effectRuntime, orchestration);
241
+ }
242
+
243
+ export function createProcessApplicationLayer(): Layer.Layer<Orchestration | Delivery> {
244
+ const orchestration = orchestrationLayer().pipe(
245
+ Layer.provide(createChildSessionsLayer()),
246
+ );
247
+ return deliveryLayer.pipe(Layer.provideMerge(orchestration));
248
+ }
249
+
40
250
  export function createProcessHost(): ProcessHost {
41
251
  const global = processGlobal();
42
252
  const existing = global[PROCESS_HOST_KEY];
43
- if (existing) return existing;
253
+ if (existing?.lifecycle === "destroying") {
254
+ throw new Error("Cannot create a process host while the current host is being destroyed");
255
+ }
256
+ if (existing?.lifecycle === "destroyed") {
257
+ delete global[PROCESS_HOST_KEY];
258
+ } else if (existing) {
259
+ existing.lifecycle = "active";
260
+ return existing;
261
+ }
44
262
 
45
- const runtime = createOrchestratorRuntime({
46
- workerSessionFactory: createWorkerSessionFactory(),
47
- });
48
- const delivery = new DeliveryCoordinator();
263
+ const effectRuntime = ManagedRuntime.make(createProcessApplicationLayer());
264
+ const runtime = createProcessHostRuntimeAdapter(effectRuntime);
265
+ const delivery = effectRuntime.runSync(Delivery);
49
266
  const host: OwnedProcessHost = {
50
267
  runtime,
51
268
  delivery,
269
+ effectRuntime,
52
270
  attachments: new Set(),
53
- unsubscribeSettlement: runtime.subscribeSettlement((settlement) => {
54
- delivery.accept(settlement);
55
- }),
271
+ lifecycle: "active",
56
272
  };
57
273
  global[PROCESS_HOST_KEY] = host;
58
274
  return host;
59
275
  }
60
276
 
61
277
  export function attachProcessHost(host: ProcessHost): ProcessHostAttachment {
278
+ const ownedHost = host as OwnedProcessHost;
279
+ if (ownedHost.lifecycle === "destroying" || ownedHost.lifecycle === "destroyed") {
280
+ throw new Error(`Cannot attach to a ${ownedHost.lifecycle} process host`);
281
+ }
282
+
62
283
  const attachment: ProcessHostAttachment = { host };
63
- const attachmentAwareHost = host as AttachmentAwareProcessHost;
64
- attachmentAwareHost.attachments ??= new Set();
65
- attachmentAwareHost.attachments.add(attachment);
284
+ ownedHost.attachments ??= new Set();
285
+ ownedHost.attachments.add(attachment);
66
286
  return attachment;
67
287
  }
68
288
 
@@ -77,27 +297,65 @@ export function detachProcessHost(
77
297
  return attachments.size === 0;
78
298
  }
79
299
 
80
- export async function destroyProcessHost(host: ProcessHost): Promise<void> {
300
+ export function destroyProcessHost(
301
+ host: ProcessHost,
302
+ options: ProcessHostDestructionOptions = {},
303
+ ): Promise<void> {
81
304
  const ownedHost = host as OwnedProcessHost;
82
- if ((ownedHost.attachments?.size ?? 0) > 0) return;
83
- if (ownedHost.destroyPromise) {
84
- await ownedHost.destroyPromise;
85
- return;
86
- }
305
+ if (ownedHost.destroyPromise) return ownedHost.destroyPromise;
306
+ if ((ownedHost.attachments?.size ?? 0) > 0) return Promise.resolve();
307
+ if (ownedHost.lifecycle === "destroyed") return Promise.resolve();
308
+
309
+ ownedHost.lifecycle = "destroying";
310
+ let resolveDestruction!: () => void;
311
+ let rejectDestruction!: (error: unknown) => void;
312
+ const destroyPromise = new Promise<void>((resolve, reject) => {
313
+ resolveDestruction = resolve;
314
+ rejectDestruction = reject;
315
+ });
316
+ // Publish the exact shared result before shutdown can synchronously reenter destruction.
317
+ ownedHost.destroyPromise = destroyPromise;
87
318
 
88
- ownedHost.destroyPromise = (async () => {
319
+ let shutdown: Promise<void>;
320
+ try {
321
+ // shutdown() closes Orchestration admission before returning its bounded teardown Promise.
322
+ shutdown = ownedHost.runtime.shutdown();
323
+ } catch (error) {
324
+ shutdown = Promise.reject(error);
325
+ }
326
+ const awaitShutdown =
327
+ options.awaitShutdown ?? awaitPromiseWithinCleanupGrace;
328
+ const awaitRootDisposal =
329
+ options.awaitRootDisposal ?? awaitPromiseWithinCleanupGrace;
330
+ const teardown = (async () => {
89
331
  try {
90
- await ownedHost.runtime.shutdown();
332
+ // A timed-out shutdown keeps physical best-effort finalizers alive. Observe
333
+ // either late outcome while proceeding to the separately bounded root disposal.
334
+ void shutdown.catch(() => {});
335
+ await awaitShutdown(shutdown, SHUTDOWN_CLEANUP_GRACE_MS);
91
336
  } finally {
92
- ownedHost.delivery.clear();
93
- ownedHost.unsubscribeSettlement?.();
94
- const global = processGlobal();
95
- if (global[PROCESS_HOST_KEY] === ownedHost) {
96
- delete global[PROCESS_HOST_KEY];
337
+ try {
338
+ const disposal = ownedHost.effectRuntime?.dispose();
339
+ if (disposal) {
340
+ // ManagedRuntime disposal keeps ownership of uninterruptible finalizers after timeout.
341
+ // Observe its eventual rejection while host destruction proceeds best-effort.
342
+ void disposal.catch(() => {});
343
+ await awaitRootDisposal(disposal, SHUTDOWN_CLEANUP_GRACE_MS);
344
+ }
345
+ } finally {
346
+ // After either deadline the host is logically destroyed and detached even
347
+ // though abandoned physical finalizers may still settle. A replacement may
348
+ // overlap only that cleanup; identity guards keep stale completion harmless.
349
+ ownedHost.lifecycle = "destroyed";
350
+ const global = processGlobal();
351
+ if (global[PROCESS_HOST_KEY] === ownedHost) {
352
+ delete global[PROCESS_HOST_KEY];
353
+ }
97
354
  }
98
355
  }
99
356
  })();
100
- await ownedHost.destroyPromise;
357
+ void teardown.then(resolveDestruction, rejectDestruction);
358
+ return destroyPromise;
101
359
  }
102
360
 
103
361
  export async function quitProcessHost(): Promise<void> {
@@ -105,3 +363,35 @@ export async function quitProcessHost(): Promise<void> {
105
363
  if (!host) return;
106
364
  await destroyProcessHost(host);
107
365
  }
366
+
367
+ function awaitPromiseWithinCleanupGrace(
368
+ promise: Promise<void>,
369
+ graceMs: number,
370
+ ): Promise<void> {
371
+ return Effect.runPromise(
372
+ Effect.promise(() => promise).pipe(
373
+ Effect.timeoutOption(graceMs),
374
+ Effect.asVoid,
375
+ ),
376
+ );
377
+ }
378
+
379
+ function abortSignalEffect(
380
+ signal: AbortSignal,
381
+ interruption: object,
382
+ ): Effect.Effect<never, object> {
383
+ return Effect.callback((resume) => {
384
+ const onAbort = () => resume(Effect.fail(interruption));
385
+ signal.addEventListener("abort", onAbort, { once: true });
386
+ if (signal.aborted) {
387
+ signal.removeEventListener("abort", onAbort);
388
+ onAbort();
389
+ }
390
+ return Effect.sync(() => signal.removeEventListener("abort", onAbort));
391
+ });
392
+ }
393
+
394
+ function abortSignalReason(signal: AbortSignal): unknown {
395
+ if ("reason" in signal) return signal.reason;
396
+ return new DOMException("This operation was aborted", "AbortError");
397
+ }
@@ -52,36 +52,31 @@ export function createOrchestrationExtension(
52
52
  dependencies: OrchestrationExtensionDependencies = {},
53
53
  ): ExtensionFactory {
54
54
  return (pi) => {
55
- const host = dependencies.getHost?.() ?? createProcessHost();
56
55
  const discoverCatalog = dependencies.discoverCatalog ?? discoverWorkerCatalog;
57
- const statusController =
58
- dependencies.createStatusController?.(host.runtime) ??
59
- createStatusController(host.runtime);
60
56
  const dispatchDecisions = new Map<string, StoredDispatchDecision>();
57
+ let host: ProcessHost | undefined;
61
58
  let hostAttachment: ProcessHostAttachment | undefined;
59
+ let statusController: StatusController | undefined;
60
+ let toolsRegistered = false;
62
61
  let activeBinding: OwnerBinding | undefined;
63
62
  let cachedCatalog: WorkerCatalog | undefined;
64
63
 
65
- const catalogFor = (ctx: ExtensionContext): WorkerCatalog => {
66
- if (cachedCatalog) return cachedCatalog;
67
- cachedCatalog = discoverCatalog({
64
+ const discoverCatalogFor = (ctx: ExtensionContext): WorkerCatalog =>
65
+ discoverCatalog({
68
66
  cwd: ctx.cwd,
69
67
  projectTrusted: ctx.isProjectTrusted(),
70
68
  });
69
+
70
+ const catalogFor = (ctx: ExtensionContext): WorkerCatalog => {
71
+ if (cachedCatalog) return cachedCatalog;
72
+ cachedCatalog = discoverCatalogFor(ctx);
71
73
  return cachedCatalog;
72
74
  };
73
75
 
74
- registerOrchestrationTools(pi, {
75
- runtime: host.runtime,
76
- getCatalog: catalogFor,
77
- getDispatchDecision: (toolCallId) =>
78
- dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
79
- });
80
76
  registerOrchestrationPresentation(pi);
81
77
 
82
78
  pi.on("session_start", (_event, ctx) => {
83
- hostAttachment ??= attachProcessHost(host);
84
- if (activeBinding) {
79
+ if (activeBinding && host && statusController) {
85
80
  host.delivery.unbind(
86
81
  activeBinding.ownerSessionId,
87
82
  activeBinding.generation,
@@ -89,6 +84,21 @@ export function createOrchestrationExtension(
89
84
  statusController.unbind(activeBinding.ownerSessionId);
90
85
  }
91
86
 
87
+ host ??= dependencies.getHost?.() ?? createProcessHost();
88
+ statusController ??=
89
+ dependencies.createStatusController?.(host.runtime) ??
90
+ createStatusController(host.runtime);
91
+ if (!toolsRegistered) {
92
+ registerOrchestrationTools(pi, {
93
+ runtime: host.runtime,
94
+ getCatalog: catalogFor,
95
+ getDispatchDecision: (toolCallId) =>
96
+ dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
97
+ });
98
+ toolsRegistered = true;
99
+ }
100
+ hostAttachment ??= attachProcessHost(host);
101
+
92
102
  dispatchDecisions.clear();
93
103
  cachedCatalog = undefined;
94
104
  const binding: OwnerBinding = {
@@ -106,10 +116,7 @@ export function createOrchestrationExtension(
106
116
  });
107
117
 
108
118
  pi.on("before_agent_start", (event, ctx) => {
109
- cachedCatalog = discoverCatalog({
110
- cwd: ctx.cwd,
111
- projectTrusted: ctx.isProjectTrusted(),
112
- });
119
+ cachedCatalog = discoverCatalogFor(ctx);
113
120
  return {
114
121
  systemPrompt: appendOrchestratorContract(
115
122
  event.systemPrompt,
@@ -125,16 +132,16 @@ export function createOrchestrationExtension(
125
132
  );
126
133
  const ownerSessionId = activeBinding?.ownerSessionId;
127
134
  if (!ownerSessionId) return;
128
- const groupedOrchestration =
135
+ const isOrchestrateGroup =
129
136
  toolCalls.length > 1 &&
130
137
  toolCalls.every((toolCall) => toolCall.name === "orchestrate");
131
- const synthesisGroup = groupedOrchestration
138
+ const synthesisGroup = isOrchestrateGroup
132
139
  ? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
133
140
  : undefined;
134
141
 
135
142
  for (const toolCall of toolCalls) {
136
143
  if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) continue;
137
- const mode = groupedOrchestration || toolCalls.length === 1
144
+ const mode = isOrchestrateGroup || toolCalls.length === 1
138
145
  ? "async"
139
146
  : "inline";
140
147
  dispatchDecisions.set(toolCall.id, {
@@ -151,7 +158,7 @@ export function createOrchestrationExtension(
151
158
  const decision = dispatchDecisions.get(event.toolCallId);
152
159
  dispatchDecisions.delete(event.toolCallId);
153
160
  if (!event.isError || !decision?.synthesisGroup) return;
154
- host.delivery.skipSynthesisGroupMember(
161
+ host?.delivery.skipSynthesisGroupMember(
155
162
  decision.ownerSessionId,
156
163
  decision.synthesisGroup.id,
157
164
  decision.synthesisGroup.size,
@@ -161,7 +168,7 @@ export function createOrchestrationExtension(
161
168
  pi.on("agent_start", () => {
162
169
  const binding = activeBinding;
163
170
  if (!binding) return;
164
- host.delivery.markAgentStarted(
171
+ host?.delivery.markAgentStarted(
165
172
  binding.ownerSessionId,
166
173
  binding.generation,
167
174
  );
@@ -170,7 +177,7 @@ export function createOrchestrationExtension(
170
177
  pi.on("agent_settled", () => {
171
178
  const binding = activeBinding;
172
179
  if (!binding) return;
173
- host.delivery.markAgentSettled(
180
+ host?.delivery.markAgentSettled(
174
181
  binding.ownerSessionId,
175
182
  binding.generation,
176
183
  );
@@ -182,17 +189,17 @@ export function createOrchestrationExtension(
182
189
  cachedCatalog = undefined;
183
190
  dispatchDecisions.clear();
184
191
 
185
- if (binding) {
192
+ if (binding && host) {
186
193
  host.delivery.unbind(binding.ownerSessionId, binding.generation);
187
194
  }
188
- statusController.dispose();
195
+ statusController?.dispose();
189
196
 
190
197
  const attachment = hostAttachment;
191
198
  hostAttachment = undefined;
192
- const wasLastAttachment = attachment
199
+ const wasLastAttachment = host && attachment
193
200
  ? detachProcessHost(host, attachment)
194
201
  : false;
195
- if (event.reason === "quit" && wasLastAttachment) {
202
+ if (event.reason === "quit" && wasLastAttachment && host) {
196
203
  await (dependencies.destroyHost ?? destroyProcessHost)(host);
197
204
  }
198
205
  });