@zachwill/pi-orchestrate 0.1.0 → 0.1.1
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/README.md +5 -3
- package/extension/contract.ts +3 -1
- package/extension/delivery.ts +137 -89
- package/extension/domain.ts +2 -0
- package/extension/host.ts +41 -5
- package/extension/presentation.ts +304 -462
- package/extension/runtime.ts +190 -27
- package/extension/tools.ts +296 -19
- package/extension/worker-session.ts +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Pi Orchestrate
|
|
2
2
|
|
|
3
|
-
[`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) adds concurrent worker orchestration to [Pi](https://pi.dev). It helps a parent agent
|
|
3
|
+
[`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) adds concurrent worker orchestration to [Pi](https://pi.dev). It helps a parent agent delegate bounded work, message independent workers concurrently, and synthesize each response as it arrives.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -22,11 +22,13 @@ Pi Orchestrate adds exactly five tools:
|
|
|
22
22
|
|
|
23
23
|
An `orchestrate` wave first performs atomic input, catalog, and model preflight. No worker starts if any task fails that preflight. After acceptance, worker resources start independently: a resource startup failure becomes that worker's `failed` result and does not roll back or stop its peers.
|
|
24
24
|
|
|
25
|
-
All tasks in a wave execute concurrently, including a full 12-task wave. Pi Orchestrate applies no hidden concurrency throttle.
|
|
25
|
+
All tasks in a wave execute concurrently, including a full 12-task wave. Pi Orchestrate applies no hidden concurrency throttle. Exact unchanged worker instructions remain visible in each tool call: collapsed calls preview every message, and expanded calls show every full brief. Titles are labels, never replacements for instructions.
|
|
26
26
|
|
|
27
27
|
For asynchronous behavior, `orchestrate` or `worker_send` must be the sole tool call in its assistant message. Mixing either with any sibling tool call makes its wave inline and blocking. Inline work receives the parent turn's cancellation signal. Accepted async work does not retain that signal and continues independently.
|
|
28
28
|
|
|
29
|
-
A successful async call yields the parent turn
|
|
29
|
+
A successful async call yields the parent turn. Responses enter the transcript individually as workers settle, and the final response starts the parent's synthesis turn. The bottom widget is ephemeral and shows active work only; completed, failed, aborted, and reusable ready workers disappear immediately. Inline responses accumulate in the live tool output while the call blocks.
|
|
30
|
+
|
|
31
|
+
`orchestration_status` is for diagnostics and recovery, never a normal completion mechanism. Do not poll it for completion. If the owning session becomes inactive, its workers continue and completed results remain queued. Those results become available only when that exact owning session resumes; they are never delivered to another session.
|
|
30
32
|
|
|
31
33
|
Worker IDs identify live worker sessions. A reusable worker keeps the same worker ID across `worker_send` follow-ups, with each follow-up result belonging to a new wave. One-shot workers finish as `completed`. Reusable workers deliver as `ready` and wait for `worker_send` or `worker_close`. Use `worker_abort` only for active work, not to close a ready worker.
|
|
32
34
|
|
package/extension/contract.ts
CHANGED
|
@@ -34,7 +34,9 @@ You are the parent orchestrator and own the task end to end.
|
|
|
34
34
|
- Give every worker a full brief: objective; paths/scope; forbidden actions; context; constraints; observable success; checks; expected output.
|
|
35
35
|
- Input, catalog, and model preflight is atomic before any worker starts. Resource startup failures become per-worker failed results and do not roll back peers.
|
|
36
36
|
- For asynchronous behavior, \`orchestrate\` or \`worker_send\` must be the sole tool call in its assistant message. Mixing either with any sibling tool call makes its wave inline and blocking.
|
|
37
|
-
-
|
|
37
|
+
- Exact worker instructions remain visible in the tool call and can be expanded; titles are labels, not substitutes for complete messages.
|
|
38
|
+
- After an accepted async wave, yield the parent turn. Worker responses arrive individually as each worker settles, and the final response starts parent synthesis. Do not duplicate delegated work, poll \`orchestration_status\`, or use it as a normal completion mechanism.
|
|
39
|
+
- The active-work widget shows only workers currently starting, running, or stopping. Inline worker responses appear progressively in live tool output.
|
|
38
40
|
- The parent synthesizes worker results, reviews their evidence and changes, resolves conflicts, integrates the final result, and runs the relevant verification before declaring completion.
|
|
39
41
|
- Prefer one-shot workers. Use \`worker_send\` for follow-up work on a ready reusable worker, \`worker_close\` when that ready worker is finished, and \`worker_abort\` only when active work must stop.
|
|
40
42
|
- The public tools are \`orchestrate\`, \`orchestration_status\`, \`worker_send\`, \`worker_abort\`, and \`worker_close\`.
|
package/extension/delivery.ts
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WorkerSettlement } from "./runtime.js";
|
|
2
2
|
|
|
3
3
|
export const MAX_DELIVERY_MARKDOWN_BYTES = 50 * 1024;
|
|
4
|
+
export const MAX_WORKER_DELIVERY_MARKDOWN_BYTES = 16 * 1024;
|
|
4
5
|
export const DELIVERY_TRUNCATION_MARKER =
|
|
5
|
-
"\n\n[Worker
|
|
6
|
+
"\n\n[Worker result truncated for parent context. Full output remains in structured details.]";
|
|
6
7
|
export const DELIVERY_PARENT_INSTRUCTIONS =
|
|
7
8
|
"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
|
|
|
9
10
|
export type ParentBindingGeneration = string | number | symbol;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
readonly ownerSessionId: CompletedWave["ownerSessionId"];
|
|
14
|
-
readonly mode: CompletedWave["mode"];
|
|
15
|
-
readonly results: CompletedWave["results"];
|
|
16
|
-
}
|
|
12
|
+
/** Complete, immutable worker output for presentation and history consumers. */
|
|
13
|
+
export type WorkerDeliveryDetails = WorkerSettlement;
|
|
17
14
|
|
|
18
|
-
export interface
|
|
19
|
-
readonly customType: "pi-orchestrate-
|
|
15
|
+
export interface WorkerDeliveryMessage {
|
|
16
|
+
readonly customType: "pi-orchestrate-worker-result";
|
|
20
17
|
readonly content: string;
|
|
21
18
|
readonly display: true;
|
|
22
|
-
readonly details:
|
|
19
|
+
readonly details: WorkerDeliveryDetails;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
export interface
|
|
22
|
+
export interface WorkerDeliveryOptions {
|
|
26
23
|
readonly triggerTurn: boolean;
|
|
27
24
|
}
|
|
28
25
|
|
|
@@ -30,7 +27,7 @@ export interface ParentBinding {
|
|
|
30
27
|
readonly ownerSessionId: string;
|
|
31
28
|
readonly generation: ParentBindingGeneration;
|
|
32
29
|
isIdle(): boolean;
|
|
33
|
-
sendMessage(message:
|
|
30
|
+
sendMessage(message: WorkerDeliveryMessage, options: WorkerDeliveryOptions): void;
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
interface BoundParent {
|
|
@@ -40,7 +37,9 @@ interface BoundParent {
|
|
|
40
37
|
|
|
41
38
|
export class DeliveryCoordinator {
|
|
42
39
|
private readonly boundParents = new Map<string, BoundParent>();
|
|
43
|
-
private readonly
|
|
40
|
+
private readonly pendingSettlements: WorkerSettlement[] = [];
|
|
41
|
+
private readonly flushingOwners = new Set<string>();
|
|
42
|
+
private highestAcceptedSequence = 0;
|
|
44
43
|
|
|
45
44
|
bind(binding: ParentBinding): void {
|
|
46
45
|
this.boundParents.set(binding.ownerSessionId, {
|
|
@@ -57,33 +56,42 @@ export class DeliveryCoordinator {
|
|
|
57
56
|
|
|
58
57
|
markAgentStarted(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
59
58
|
if (!this.matchesBinding(ownerSessionId, generation)) return;
|
|
60
|
-
this.boundParents.get(ownerSessionId)
|
|
59
|
+
const parent = this.boundParents.get(ownerSessionId);
|
|
60
|
+
if (parent) parent.agentRunning = true;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
64
64
|
if (!this.matchesBinding(ownerSessionId, generation)) return;
|
|
65
|
-
this.boundParents.get(ownerSessionId)
|
|
65
|
+
const parent = this.boundParents.get(ownerSessionId);
|
|
66
|
+
if (!parent) return;
|
|
67
|
+
parent.agentRunning = false;
|
|
66
68
|
this.flush(ownerSessionId, generation);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
accept(
|
|
70
|
-
if (
|
|
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);
|
|
71
|
+
accept(settlement: WorkerSettlement): boolean {
|
|
72
|
+
if (settlement.mode === "inline" || settlement.sequence <= this.highestAcceptedSequence) {
|
|
73
|
+
return false;
|
|
76
74
|
}
|
|
75
|
+
|
|
76
|
+
this.highestAcceptedSequence = settlement.sequence;
|
|
77
|
+
this.pendingSettlements.push(settlement);
|
|
78
|
+
|
|
79
|
+
const parent = this.boundParents.get(settlement.ownerSessionId);
|
|
80
|
+
if (parent) this.flush(settlement.ownerSessionId, parent.binding.generation);
|
|
77
81
|
return true;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
pendingCount(ownerSessionId: string): number {
|
|
81
|
-
return this.
|
|
85
|
+
return this.pendingSettlements.filter(
|
|
86
|
+
(settlement) => settlement.ownerSessionId === ownerSessionId,
|
|
87
|
+
).length;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
clear(): void {
|
|
85
91
|
this.boundParents.clear();
|
|
86
|
-
this.
|
|
92
|
+
this.pendingSettlements.length = 0;
|
|
93
|
+
this.flushingOwners.clear();
|
|
94
|
+
this.highestAcceptedSequence = 0;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
close(): void {
|
|
@@ -94,14 +102,10 @@ export class DeliveryCoordinator {
|
|
|
94
102
|
ownerSessionId: string,
|
|
95
103
|
generation: ParentBindingGeneration,
|
|
96
104
|
): boolean {
|
|
97
|
-
|
|
98
|
-
return binding?.generation === generation;
|
|
105
|
+
return this.boundParents.get(ownerSessionId)?.binding.generation === generation;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
private canDeliver(
|
|
102
|
-
ownerSessionId: string,
|
|
103
|
-
generation: ParentBindingGeneration,
|
|
104
|
-
): boolean {
|
|
108
|
+
private canDeliver(ownerSessionId: string, generation: ParentBindingGeneration): boolean {
|
|
105
109
|
const parent = this.boundParents.get(ownerSessionId);
|
|
106
110
|
return (
|
|
107
111
|
parent !== undefined &&
|
|
@@ -111,73 +115,117 @@ export class DeliveryCoordinator {
|
|
|
111
115
|
);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
private flush(
|
|
115
|
-
ownerSessionId
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
}
|
|
118
|
+
private flush(ownerSessionId: string, generation: ParentBindingGeneration): void {
|
|
119
|
+
if (this.flushingOwners.has(ownerSessionId) || !this.canDeliver(ownerSessionId, generation)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
136
122
|
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
this.flushingOwners.add(ownerSessionId);
|
|
124
|
+
try {
|
|
125
|
+
const queued = this.pendingSettlements.filter(
|
|
126
|
+
(settlement) => settlement.ownerSessionId === ownerSessionId,
|
|
127
|
+
);
|
|
128
|
+
let latestFinalIndex = -1;
|
|
129
|
+
for (let index = 0; index < queued.length; index += 1) {
|
|
130
|
+
if (queued[index]?.waveComplete) latestFinalIndex = index;
|
|
131
|
+
}
|
|
132
|
+
const flushThrough = latestFinalIndex >= 0 ? latestFinalIndex : queued.length - 1;
|
|
133
|
+
let flushBytesRemaining = MAX_DELIVERY_MARKDOWN_BYTES;
|
|
134
|
+
|
|
135
|
+
for (let index = 0; index <= flushThrough; index += 1) {
|
|
136
|
+
if (!this.canDeliver(ownerSessionId, generation)) return;
|
|
137
|
+
const settlement = queued[index];
|
|
138
|
+
if (!settlement || !this.pendingSettlements.includes(settlement)) continue;
|
|
139
|
+
|
|
140
|
+
const messagesRemaining = flushThrough - index + 1;
|
|
141
|
+
const fairFlushBytes = Math.floor(flushBytesRemaining / messagesRemaining);
|
|
142
|
+
const fairWaveBytes = Math.floor(
|
|
143
|
+
MAX_DELIVERY_MARKDOWN_BYTES / Math.max(1, settlement.waveSize),
|
|
144
|
+
);
|
|
145
|
+
const byteLimit = Math.max(0, Math.min(
|
|
146
|
+
MAX_WORKER_DELIVERY_MARKDOWN_BYTES,
|
|
147
|
+
fairFlushBytes,
|
|
148
|
+
fairWaveBytes,
|
|
149
|
+
flushBytesRemaining,
|
|
150
|
+
));
|
|
151
|
+
const triggerTurn = latestFinalIndex >= 0 && index === flushThrough;
|
|
152
|
+
const message = this.renderWorkerMessage(settlement, byteLimit);
|
|
153
|
+
const parent = this.boundParents.get(ownerSessionId);
|
|
154
|
+
if (!parent || parent.binding.generation !== generation) return;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
parent.binding.sendMessage(message, { triggerTurn });
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const deliveredBytes = Buffer.byteLength(message.content, "utf8");
|
|
163
|
+
flushBytesRemaining = Math.max(0, flushBytesRemaining - deliveredBytes);
|
|
164
|
+
const pendingIndex = this.pendingSettlements.indexOf(settlement);
|
|
165
|
+
if (pendingIndex >= 0) this.pendingSettlements.splice(pendingIndex, 1);
|
|
166
|
+
|
|
167
|
+
if (triggerTurn) {
|
|
168
|
+
parent.agentRunning = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
this.flushingOwners.delete(ownerSessionId);
|
|
139
174
|
}
|
|
140
175
|
}
|
|
141
|
-
}
|
|
142
176
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const heading =
|
|
148
|
-
const metadata =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
private renderWorkerMessage(
|
|
178
|
+
settlement: WorkerSettlement,
|
|
179
|
+
byteLimit: number,
|
|
180
|
+
): WorkerDeliveryMessage {
|
|
181
|
+
const heading = `## Worker result — ${settlement.worker}`;
|
|
182
|
+
const metadata = [
|
|
183
|
+
`Worker \`${settlement.workerId}\``,
|
|
184
|
+
`wave \`${settlement.waveId}\``,
|
|
185
|
+
`status \`${settlement.status}\``,
|
|
186
|
+
].join(" · ");
|
|
187
|
+
const body = renderOutcome(settlement.outcome);
|
|
188
|
+
const content = body.length > 0
|
|
189
|
+
? `${heading}\n\n### ${settlement.title}\n${metadata}\n\n${body}`
|
|
190
|
+
: `${heading}\n\n### ${settlement.title}\n${metadata}`;
|
|
191
|
+
const appendix = settlement.waveComplete
|
|
192
|
+
? `\n\n---\n\n${DELIVERY_PARENT_INSTRUCTIONS}`
|
|
193
|
+
: "";
|
|
194
|
+
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
customType: "pi-orchestrate-worker-result",
|
|
197
|
+
content: capMarkdown(content, appendix, byteLimit),
|
|
198
|
+
display: true,
|
|
199
|
+
details: settlement,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
166
202
|
}
|
|
167
203
|
|
|
168
|
-
function capMarkdown(content: string, appendix: string): string {
|
|
204
|
+
function capMarkdown(content: string, appendix: string, byteLimit: number): string {
|
|
169
205
|
const complete = `${content}${appendix}`;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
206
|
+
if (Buffer.byteLength(complete, "utf8") <= byteLimit) return complete;
|
|
207
|
+
|
|
208
|
+
const suffix = `${DELIVERY_TRUNCATION_MARKER}${appendix}`;
|
|
209
|
+
const suffixBytes = Buffer.byteLength(suffix, "utf8");
|
|
210
|
+
if (suffixBytes >= byteLimit) return truncateUtf8(suffix, byteLimit);
|
|
211
|
+
const prefix = truncateUtf8(content, byteLimit - suffixBytes);
|
|
212
|
+
return `${prefix}${suffix}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function truncateUtf8(content: string, byteLimit: number): string {
|
|
216
|
+
if (byteLimit <= 0) return "";
|
|
217
|
+
const bytes = Buffer.from(content, "utf8");
|
|
218
|
+
if (bytes.byteLength <= byteLimit) return content;
|
|
219
|
+
let end = byteLimit;
|
|
220
|
+
while (end > 0) {
|
|
221
|
+
const byte = bytes[end];
|
|
222
|
+
if (byte === undefined || (byte & 0xc0) !== 0x80) break;
|
|
223
|
+
end -= 1;
|
|
224
|
+
}
|
|
225
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
178
226
|
}
|
|
179
227
|
|
|
180
|
-
function renderOutcome(outcome:
|
|
228
|
+
function renderOutcome(outcome: WorkerSettlement["outcome"]): string {
|
|
181
229
|
switch (outcome.status) {
|
|
182
230
|
case "completed":
|
|
183
231
|
case "ready":
|
package/extension/domain.ts
CHANGED
|
@@ -228,6 +228,8 @@ export interface WorkerRecord {
|
|
|
228
228
|
readonly lifecycle: WorkerLifecycle;
|
|
229
229
|
readonly status: WorkerStatus;
|
|
230
230
|
readonly usage: WorkerUsage;
|
|
231
|
+
readonly startedAt: number;
|
|
232
|
+
readonly settledAt?: number;
|
|
231
233
|
readonly activity?: string;
|
|
232
234
|
readonly outcome?: WorkerOutcome;
|
|
233
235
|
readonly sessionFile?: string;
|
package/extension/host.ts
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
} from "./runtime.js";
|
|
6
6
|
import { createWorkerSessionFactory } from "./worker-session.js";
|
|
7
7
|
|
|
8
|
-
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/
|
|
8
|
+
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v2");
|
|
9
|
+
const LEGACY_PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v1");
|
|
9
10
|
|
|
10
11
|
export interface ProcessHost {
|
|
11
12
|
readonly runtime: OrchestratorRuntime;
|
|
@@ -21,12 +22,18 @@ interface AttachmentAwareProcessHost extends ProcessHost {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface OwnedProcessHost extends AttachmentAwareProcessHost {
|
|
24
|
-
readonly
|
|
25
|
+
readonly unsubscribeSettlement: () => void;
|
|
25
26
|
destroyPromise?: Promise<void>;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
interface LegacyProcessHost extends ProcessHost {
|
|
30
|
+
unsubscribeCompletion?: () => void;
|
|
31
|
+
unsubscribeSettlement?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
type ProcessGlobal = typeof globalThis & {
|
|
29
35
|
[PROCESS_HOST_KEY]?: OwnedProcessHost;
|
|
36
|
+
[LEGACY_PROCESS_HOST_KEY]?: LegacyProcessHost;
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
function processGlobal(): ProcessGlobal {
|
|
@@ -42,6 +49,8 @@ export function createProcessHost(): ProcessHost {
|
|
|
42
49
|
const existing = global[PROCESS_HOST_KEY];
|
|
43
50
|
if (existing) return existing;
|
|
44
51
|
|
|
52
|
+
retireLegacyProcessHost(global);
|
|
53
|
+
|
|
45
54
|
const runtime = createOrchestratorRuntime({
|
|
46
55
|
workerSessionFactory: createWorkerSessionFactory(),
|
|
47
56
|
});
|
|
@@ -50,8 +59,8 @@ export function createProcessHost(): ProcessHost {
|
|
|
50
59
|
runtime,
|
|
51
60
|
delivery,
|
|
52
61
|
attachments: new Set(),
|
|
53
|
-
|
|
54
|
-
delivery.accept(
|
|
62
|
+
unsubscribeSettlement: runtime.subscribeSettlement((settlement) => {
|
|
63
|
+
delivery.accept(settlement);
|
|
55
64
|
}),
|
|
56
65
|
};
|
|
57
66
|
global[PROCESS_HOST_KEY] = host;
|
|
@@ -90,7 +99,7 @@ export async function destroyProcessHost(host: ProcessHost): Promise<void> {
|
|
|
90
99
|
await ownedHost.runtime.shutdown();
|
|
91
100
|
} finally {
|
|
92
101
|
ownedHost.delivery.clear();
|
|
93
|
-
ownedHost.
|
|
102
|
+
ownedHost.unsubscribeSettlement();
|
|
94
103
|
const global = processGlobal();
|
|
95
104
|
if (global[PROCESS_HOST_KEY] === ownedHost) {
|
|
96
105
|
delete global[PROCESS_HOST_KEY];
|
|
@@ -105,3 +114,30 @@ export async function quitProcessHost(): Promise<void> {
|
|
|
105
114
|
if (!host) return;
|
|
106
115
|
await destroyProcessHost(host);
|
|
107
116
|
}
|
|
117
|
+
|
|
118
|
+
function retireLegacyProcessHost(global: ProcessGlobal): void {
|
|
119
|
+
const legacy = global[LEGACY_PROCESS_HOST_KEY];
|
|
120
|
+
if (!legacy) return;
|
|
121
|
+
|
|
122
|
+
delete global[LEGACY_PROCESS_HOST_KEY];
|
|
123
|
+
try {
|
|
124
|
+
legacy.unsubscribeCompletion?.();
|
|
125
|
+
} catch {
|
|
126
|
+
// A stale subscription cannot prevent installation of the current host.
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
legacy.unsubscribeSettlement?.();
|
|
130
|
+
} catch {
|
|
131
|
+
// A stale subscription cannot prevent installation of the current host.
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
legacy.delivery.close();
|
|
135
|
+
} catch {
|
|
136
|
+
// Legacy delivery cleanup is best-effort during reload migration.
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
void legacy.runtime.shutdown().catch(() => undefined);
|
|
140
|
+
} catch {
|
|
141
|
+
// Legacy runtime shutdown is best-effort during reload migration.
|
|
142
|
+
}
|
|
143
|
+
}
|