pi-ultracode 0.3.2 → 0.5.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/README.md +32 -14
- package/README.zh-CN.md +32 -14
- package/extensions/ultracode.ts +11 -10
- package/package.json +17 -7
- package/src/commands.ts +21 -22
- package/src/depth.ts +38 -0
- package/src/index.ts +9 -1
- package/src/mode.ts +82 -58
- package/src/prompts.ts +65 -32
- package/src/thinking.ts +1 -1
- package/src/workflow/agent-runner.ts +23 -8
- package/src/workflow/agent-types.ts +19 -15
- package/src/workflow/hermetic-git.ts +31 -0
- package/src/workflow/journal.ts +134 -3
- package/src/workflow/parser.ts +254 -38
- package/src/workflow/registry.ts +44 -67
- package/src/workflow/repository-context.ts +76 -0
- package/src/workflow/run-artifacts.ts +60 -14
- package/src/workflow/runtime.ts +380 -180
- package/src/workflow/saved-workflow.ts +37 -0
- package/src/workflow/script-executor.ts +56 -10
- package/src/workflow/script-worker.mjs +65 -19
- package/src/workflow/tool.ts +108 -58
- package/src/workflow/workflow-overlay.ts +4 -4
- package/src/workflow/worktree.ts +1965 -244
- package/types/workflow.d.ts +2 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
artifactPathExists,
|
|
5
|
+
readContainedArtifactFile,
|
|
6
|
+
} from "./run-artifacts.ts";
|
|
7
|
+
|
|
8
|
+
export interface SavedWorkflowSource {
|
|
9
|
+
script: string;
|
|
10
|
+
sourcePath: string;
|
|
11
|
+
scope: "project" | "user";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Discover an ambient saved workflow without crossing the project-trust gate. */
|
|
15
|
+
export function readSavedWorkflowByName(
|
|
16
|
+
name: string,
|
|
17
|
+
cwd: string,
|
|
18
|
+
projectTrusted: boolean,
|
|
19
|
+
): SavedWorkflowSource | undefined {
|
|
20
|
+
const roots: Array<{ root: string; scope: "project" | "user" }> = [
|
|
21
|
+
...(projectTrusted ? [{ root: cwd, scope: "project" as const }] : []),
|
|
22
|
+
{ root: os.homedir(), scope: "user" },
|
|
23
|
+
];
|
|
24
|
+
for (const { root, scope } of roots) {
|
|
25
|
+
const directory = path.join(root, ".pi", "ultracode", "workflows");
|
|
26
|
+
for (const candidate of [`${name}.workflow.js`, `${name}.js`]) {
|
|
27
|
+
const sourcePath = path.join(directory, candidate);
|
|
28
|
+
if (!artifactPathExists(sourcePath)) continue;
|
|
29
|
+
return {
|
|
30
|
+
script: readContainedArtifactFile(root, sourcePath, "saved workflow", 16 * 1024 * 1024),
|
|
31
|
+
sourcePath,
|
|
32
|
+
scope,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
|
|
15
15
|
export interface ScriptExecutorHost {
|
|
16
16
|
agent(payload: any): Promise<unknown>;
|
|
17
|
+
/** Cancel child work after a fatal script policy without terminating branch finally blocks. */
|
|
18
|
+
abortChildren?(error: Error): void;
|
|
17
19
|
reservePanel(payload: {
|
|
18
20
|
callPath?: unknown;
|
|
19
21
|
reserveAgents: number;
|
|
@@ -49,6 +51,8 @@ export interface ScriptExecutorOptions {
|
|
|
49
51
|
hostCallLimit?: number;
|
|
50
52
|
/** Internal test seam for the Worker's V8 old-generation heap cap. */
|
|
51
53
|
workerMemoryLimitMb?: number;
|
|
54
|
+
/** Bound for fatal host-RPC draining before forced termination. */
|
|
55
|
+
fatalDrainTimeoutMs?: number;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
type TerminationCause = "abort" | "stall" | "fatal" | "error" | "cleanup";
|
|
@@ -59,6 +63,7 @@ export async function executeWorkflowScript(
|
|
|
59
63
|
options: ScriptExecutorOptions,
|
|
60
64
|
): Promise<unknown> {
|
|
61
65
|
if (options.signal.aborted) throw new WorkflowAbortError();
|
|
66
|
+
const fatalDrainTimeoutMs = normalizeFatalDrainTimeout(options.fatalDrainTimeoutMs);
|
|
62
67
|
const worker = new Worker(new URL("./script-worker.mjs", import.meta.url), {
|
|
63
68
|
workerData: {
|
|
64
69
|
body,
|
|
@@ -79,6 +84,7 @@ export async function executeWorkflowScript(
|
|
|
79
84
|
let terminationCause: TerminationCause | undefined;
|
|
80
85
|
let fatalError: Error | undefined;
|
|
81
86
|
let watchdog: NodeJS.Timeout | undefined;
|
|
87
|
+
let fatalDrainTimer: NodeJS.Timeout | undefined;
|
|
82
88
|
|
|
83
89
|
const terminate = (cause: TerminationCause): void => {
|
|
84
90
|
terminationCause ??= cause;
|
|
@@ -97,17 +103,40 @@ export async function executeWorkflowScript(
|
|
|
97
103
|
if (settled) return;
|
|
98
104
|
settled = true;
|
|
99
105
|
if (watchdog) clearInterval(watchdog);
|
|
106
|
+
if (fatalDrainTimer) clearTimeout(fatalDrainTimer);
|
|
100
107
|
if (ok) resolve(value);
|
|
101
108
|
else reject(value);
|
|
102
109
|
};
|
|
110
|
+
let workerReportedResult = false;
|
|
111
|
+
const maybeSettleFatal = (): void => {
|
|
112
|
+
if (!fatalError || !workerReportedResult || pendingRpcCount > 0) return;
|
|
113
|
+
settle(false, fatalError);
|
|
114
|
+
terminate("fatal");
|
|
115
|
+
};
|
|
103
116
|
const rejectAndTerminate = (error: unknown, cause: TerminationCause): void => {
|
|
104
117
|
if (cause === "fatal" && error instanceof Error) fatalError ??= error;
|
|
105
118
|
settle(false, error);
|
|
106
119
|
terminate(cause);
|
|
107
120
|
};
|
|
121
|
+
const beginFatalDrain = (error: Error): void => {
|
|
122
|
+
fatalError ??= error;
|
|
123
|
+
terminationCause ??= "fatal";
|
|
124
|
+
try {
|
|
125
|
+
host.abortChildren?.(fatalError);
|
|
126
|
+
} catch {
|
|
127
|
+
// Fatal draining remains authoritative even if cancellation notification fails.
|
|
128
|
+
}
|
|
129
|
+
if (!fatalDrainTimer) {
|
|
130
|
+
fatalDrainTimer = setTimeout(() => {
|
|
131
|
+
if (!settled && fatalError) rejectAndTerminate(fatalError, "fatal");
|
|
132
|
+
}, fatalDrainTimeoutMs);
|
|
133
|
+
fatalDrainTimer.unref?.();
|
|
134
|
+
}
|
|
135
|
+
maybeSettleFatal();
|
|
136
|
+
};
|
|
108
137
|
|
|
109
138
|
watchdog = setInterval(() => {
|
|
110
|
-
if (settled) return;
|
|
139
|
+
if (settled || fatalError) return;
|
|
111
140
|
const now = Date.now();
|
|
112
141
|
if (now - lastHeartbeat > stallTimeoutMs) {
|
|
113
142
|
if (options.signal.aborted) {
|
|
@@ -143,9 +172,7 @@ export async function executeWorkflowScript(
|
|
|
143
172
|
return;
|
|
144
173
|
}
|
|
145
174
|
if (message.type === "fatal") {
|
|
146
|
-
|
|
147
|
-
fatalError = error;
|
|
148
|
-
rejectAndTerminate(error, "fatal");
|
|
175
|
+
beginFatalDrain(reviveError(message.error));
|
|
149
176
|
return;
|
|
150
177
|
}
|
|
151
178
|
if (message.type === "event") {
|
|
@@ -164,15 +191,23 @@ export async function executeWorkflowScript(
|
|
|
164
191
|
void handleRpc(message).finally(() => {
|
|
165
192
|
pendingRpcCount = Math.max(0, pendingRpcCount - 1);
|
|
166
193
|
lastProgress = Date.now();
|
|
194
|
+
maybeSettleFatal();
|
|
167
195
|
});
|
|
168
196
|
return;
|
|
169
197
|
}
|
|
170
198
|
if (message.type === "result") {
|
|
171
|
-
|
|
172
|
-
|
|
199
|
+
workerReportedResult = true;
|
|
200
|
+
if (fatalError) {
|
|
201
|
+
maybeSettleFatal();
|
|
202
|
+
} else if (message.ok) {
|
|
203
|
+
settle(true, message.value);
|
|
204
|
+
} else {
|
|
173
205
|
const error = reviveError(message.error);
|
|
174
|
-
if (error instanceof WorkflowPolicyError)
|
|
175
|
-
|
|
206
|
+
if (error instanceof WorkflowPolicyError) {
|
|
207
|
+
beginFatalDrain(error);
|
|
208
|
+
} else {
|
|
209
|
+
settle(false, error);
|
|
210
|
+
}
|
|
176
211
|
}
|
|
177
212
|
}
|
|
178
213
|
});
|
|
@@ -180,7 +215,8 @@ export async function executeWorkflowScript(
|
|
|
180
215
|
worker.on("error", (error: NodeJS.ErrnoException) => {
|
|
181
216
|
if (settled) return;
|
|
182
217
|
if (error.code === "ERR_WORKER_OUT_OF_MEMORY") {
|
|
183
|
-
|
|
218
|
+
workerReportedResult = true;
|
|
219
|
+
beginFatalDrain(new WorkflowPolicyError("workflow script exceeded its worker memory limit"));
|
|
184
220
|
return;
|
|
185
221
|
}
|
|
186
222
|
settle(false, error);
|
|
@@ -189,7 +225,8 @@ export async function executeWorkflowScript(
|
|
|
189
225
|
worker.on("exit", (code) => {
|
|
190
226
|
if (settled) return;
|
|
191
227
|
if (fatalError) {
|
|
192
|
-
|
|
228
|
+
workerReportedResult = true;
|
|
229
|
+
maybeSettleFatal();
|
|
193
230
|
} else if (terminationCause === "abort" || options.signal.aborted) {
|
|
194
231
|
settle(false, new WorkflowAbortError());
|
|
195
232
|
} else if (terminationCause === "stall") {
|
|
@@ -240,6 +277,7 @@ export async function executeWorkflowScript(
|
|
|
240
277
|
} finally {
|
|
241
278
|
settled = true;
|
|
242
279
|
if (watchdog) clearInterval(watchdog);
|
|
280
|
+
if (fatalDrainTimer) clearTimeout(fatalDrainTimer);
|
|
243
281
|
options.signal.removeEventListener("abort", abortWorker);
|
|
244
282
|
terminate("cleanup");
|
|
245
283
|
await worker.terminate().catch(() => undefined);
|
|
@@ -262,6 +300,14 @@ function workerResourceLimits(value: number | undefined): {
|
|
|
262
300
|
};
|
|
263
301
|
}
|
|
264
302
|
|
|
303
|
+
function normalizeFatalDrainTimeout(value: number | undefined): number {
|
|
304
|
+
if (value === undefined) return 25_000;
|
|
305
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
|
|
306
|
+
throw new WorkflowPolicyError("workflow fatal drain timeout must be a positive integer");
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
|
|
265
311
|
function normalizeHostCallLimit(value: number | undefined): number {
|
|
266
312
|
if (value === undefined) return 10_000;
|
|
267
313
|
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > 1_000_000) {
|
|
@@ -77,7 +77,7 @@ async function executeBody(body, bodyArgs, depth, name, scopePath = "$") {
|
|
|
77
77
|
});
|
|
78
78
|
return executionContext.run(context, async () => {
|
|
79
79
|
const scriptContext = buildScriptContext(bodyArgs);
|
|
80
|
-
const wrapped = `(async ()
|
|
80
|
+
const wrapped = `(async function () {\n"use strict";\n${body}\n})()`;
|
|
81
81
|
return await new vm.Script(wrapped, { filename: `${name || "workflow"}.js` }).runInContext(scriptContext);
|
|
82
82
|
});
|
|
83
83
|
}
|
|
@@ -154,6 +154,15 @@ function buildScriptContext(bodyArgs) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
const sandboxLog = (value) => sync("log", [String(value)])
|
|
157
|
+
const safeArrayMap = Array.prototype.map
|
|
158
|
+
const safeArrayIsArray = Array.isArray
|
|
159
|
+
const safeReflectApply = Reflect.apply
|
|
160
|
+
const mapThunks = (receiver, mapper) => {
|
|
161
|
+
if (!safeArrayIsArray(receiver) || typeof mapper !== "function") {
|
|
162
|
+
throw new TypeError("parallel map requires a realm-local array and mapper")
|
|
163
|
+
}
|
|
164
|
+
return safeReflectApply(safeArrayMap, receiver, [mapper])
|
|
165
|
+
}
|
|
157
166
|
Object.defineProperties(globalThis, {
|
|
158
167
|
__ultracodeAgent: { value: (callSite, ...values) => orchestration("agent", callSite, values) },
|
|
159
168
|
__ultracodeParallel: { value: (callSite, ...values) => orchestration("parallel", callSite, values) },
|
|
@@ -161,11 +170,11 @@ function buildScriptContext(bodyArgs) {
|
|
|
161
170
|
__ultracodeWorkflow: { value: (callSite, ...values) => orchestration("workflow", callSite, values) },
|
|
162
171
|
__ultracodeLoop: { value: (site) => sync("loop", [site]) },
|
|
163
172
|
__ultracodeInvoke: { value: (callSite, fn, thisArg, ...values) => invoke(callSite, fn, thisArg, values) },
|
|
173
|
+
__ultracodeMapThunks: { value: mapThunks },
|
|
164
174
|
phase: { value: (value) => sync("phase", [value]) },
|
|
165
175
|
log: { value: sandboxLog },
|
|
166
176
|
args: { value: localize(rawArgs), enumerable: true },
|
|
167
177
|
cwd: { value: cwdValue, enumerable: true },
|
|
168
|
-
process: { value: Object.freeze({ cwd: () => cwdValue }) },
|
|
169
178
|
console: { value: Object.freeze({
|
|
170
179
|
log: sandboxLog,
|
|
171
180
|
info: sandboxLog,
|
|
@@ -175,6 +184,7 @@ function buildScriptContext(bodyArgs) {
|
|
|
175
184
|
structuredClone: { value: (value) => localize(value) },
|
|
176
185
|
Date: { value: undefined },
|
|
177
186
|
Function: { value: undefined },
|
|
187
|
+
Proxy: { value: undefined },
|
|
178
188
|
eval: { value: undefined },
|
|
179
189
|
require: { value: undefined },
|
|
180
190
|
performance: { value: undefined },
|
|
@@ -214,6 +224,8 @@ function buildScriptContext(bodyArgs) {
|
|
|
214
224
|
})
|
|
215
225
|
Object.freeze(Promise.prototype)
|
|
216
226
|
Object.freeze(Promise)
|
|
227
|
+
Object.freeze(Array.prototype)
|
|
228
|
+
Object.freeze(Array)
|
|
217
229
|
Object.defineProperty(Math, "random", {
|
|
218
230
|
value: () => { throw new Error("Math.random() is non-deterministic and forbidden in workflow scripts; vary randomness by agent index instead.") },
|
|
219
231
|
configurable: false,
|
|
@@ -279,35 +291,39 @@ async function parallelImpl(callSite, thunks, options = {}) {
|
|
|
279
291
|
if (thunks.length > MAX_ITEMS_PER_CALL) {
|
|
280
292
|
throw makePolicyError(`parallel() accepts at most ${MAX_ITEMS_PER_CALL} items (got ${thunks.length})`);
|
|
281
293
|
}
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
const plannedThunks = copyDenseDataArray(thunks, "parallel() thunk array");
|
|
295
|
+
for (const thunk of plannedThunks) {
|
|
296
|
+
if (typeof thunk !== "function") {
|
|
297
|
+
throw new TypeError("parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)");
|
|
298
|
+
}
|
|
284
299
|
}
|
|
285
300
|
const execution = executionContext.getStore() ?? createExecutionScope({ scopePath: "$" });
|
|
286
|
-
const reserveAgents = normalizeReserveAgents(options,
|
|
301
|
+
const reserveAgents = normalizeReserveAgents(options, plannedThunks.length);
|
|
287
302
|
const rawPanelSite = typeof callSite === "string" && callSite ? callSite : "parallel:dynamic";
|
|
288
303
|
const [panelSource, panelLoops = ""] = rawPanelSite.split("@", 2);
|
|
289
|
-
const panelSite = `${panelSource}#${
|
|
304
|
+
const panelSite = `${panelSource}#${plannedThunks.length}:${reserveAgents}${panelLoops ? `@${panelLoops}` : ""}`;
|
|
290
305
|
const panelCallPath = allocateCallPath(execution, "parallel", "p", panelSite);
|
|
291
|
-
if (reserveAgents <
|
|
292
|
-
throw makePolicyError(`parallel reserveAgents must be at least the number of thunks (${
|
|
306
|
+
if (reserveAgents < plannedThunks.length) {
|
|
307
|
+
throw makePolicyError(`parallel reserveAgents must be at least the number of thunks (${plannedThunks.length}); got ${reserveAgents}`);
|
|
293
308
|
}
|
|
294
309
|
const parentReservationIds = Array.isArray(execution.reservationIds) ? execution.reservationIds : [];
|
|
295
310
|
const panel = await rpc("reservePanel", {
|
|
296
311
|
callPath: panelCallPath,
|
|
297
312
|
reserveAgents,
|
|
298
|
-
branchCount:
|
|
313
|
+
branchCount: plannedThunks.length,
|
|
299
314
|
parentReservationIds,
|
|
300
315
|
});
|
|
301
316
|
const panelReservationId = requireString(panel?.panelReservationId, "panelReservationId");
|
|
302
317
|
const branchReservationIds = Array.isArray(panel?.branchReservationIds) ? panel.branchReservationIds : [];
|
|
303
|
-
if (branchReservationIds.length !==
|
|
318
|
+
if (branchReservationIds.length !== plannedThunks.length || branchReservationIds.some((id) => typeof id !== "string")) {
|
|
304
319
|
throw makePolicyError("workflow host returned an invalid parallel reservation panel");
|
|
305
320
|
}
|
|
306
321
|
|
|
307
322
|
let completed = false;
|
|
308
|
-
|
|
323
|
+
let firstFatal;
|
|
324
|
+
const branchOutcomes = Array.from({ length: plannedThunks.length }, () => "pending");
|
|
309
325
|
try {
|
|
310
|
-
const
|
|
326
|
+
const branchPromises = plannedThunks.map(async (thunk, index) => {
|
|
311
327
|
const branchReservationId = branchReservationIds[index];
|
|
312
328
|
const childContext = createExecutionScope({
|
|
313
329
|
workflowPath: [...(execution.workflowPath ?? [])],
|
|
@@ -329,7 +345,10 @@ async function parallelImpl(callSite, thunks, options = {}) {
|
|
|
329
345
|
});
|
|
330
346
|
return value;
|
|
331
347
|
} catch (error) {
|
|
332
|
-
if (isFatal(error))
|
|
348
|
+
if (isFatal(error)) {
|
|
349
|
+
firstFatal ??= error;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
333
352
|
branchOutcomes[index] = "failed";
|
|
334
353
|
await rpc("completePanelBranch", {
|
|
335
354
|
callPath: panelCallPath,
|
|
@@ -339,14 +358,19 @@ async function parallelImpl(callSite, thunks, options = {}) {
|
|
|
339
358
|
logLine(`parallel[${index}] failed: ${messageOf(error)}`);
|
|
340
359
|
return null;
|
|
341
360
|
} finally {
|
|
342
|
-
await
|
|
361
|
+
await cleanupRpc("releasePanel", { reservationId: branchReservationId }).catch(() => undefined);
|
|
343
362
|
}
|
|
344
363
|
});
|
|
345
|
-
})
|
|
364
|
+
});
|
|
365
|
+
const settled = await Promise.allSettled(branchPromises);
|
|
366
|
+
if (firstFatal) throw firstFatal;
|
|
367
|
+
const rejected = settled.find((entry) => entry.status === "rejected");
|
|
368
|
+
if (rejected) throw rejected.reason;
|
|
369
|
+
const results = settled.map((entry) => entry.value);
|
|
346
370
|
completed = true;
|
|
347
371
|
return results;
|
|
348
372
|
} finally {
|
|
349
|
-
const release =
|
|
373
|
+
const release = cleanupRpc("releasePanel", {
|
|
350
374
|
reservationId: panelReservationId,
|
|
351
375
|
callPath: panelCallPath,
|
|
352
376
|
completed,
|
|
@@ -367,12 +391,13 @@ async function pipelineImpl(callSite, items, ...stages) {
|
|
|
367
391
|
if (items.length > MAX_ITEMS_PER_CALL) {
|
|
368
392
|
throw makePolicyError(`pipeline() accepts at most ${MAX_ITEMS_PER_CALL} items (got ${items.length})`);
|
|
369
393
|
}
|
|
394
|
+
const plannedItems = copyDenseDataArray(items, "pipeline() item array");
|
|
370
395
|
if (stages.some((stage) => typeof stage !== "function")) {
|
|
371
396
|
throw new TypeError("pipeline() stages must be functions: pipeline(items, item => ..., result => ...)");
|
|
372
397
|
}
|
|
373
398
|
const execution = executionContext.getStore() ?? createExecutionScope({ scopePath: "$" });
|
|
374
399
|
const pipelineCallPath = allocateCallPath(execution, "pipeline", "l", callSite);
|
|
375
|
-
return await Promise.all(
|
|
400
|
+
return await Promise.all(plannedItems.map(async (item, index) => {
|
|
376
401
|
let value = item;
|
|
377
402
|
for (const [stageIndex, stage] of stages.entries()) {
|
|
378
403
|
try {
|
|
@@ -397,6 +422,18 @@ async function pipelineImpl(callSite, items, ...stages) {
|
|
|
397
422
|
}));
|
|
398
423
|
}
|
|
399
424
|
|
|
425
|
+
function copyDenseDataArray(value, label) {
|
|
426
|
+
const output = new Array(value.length);
|
|
427
|
+
for (let index = 0; index < value.length; index++) {
|
|
428
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
429
|
+
if (!descriptor || !Object.hasOwn(descriptor, "value")) {
|
|
430
|
+
throw new TypeError(`${label} must be dense and contain only data elements`);
|
|
431
|
+
}
|
|
432
|
+
output[index] = descriptor.value;
|
|
433
|
+
}
|
|
434
|
+
return output;
|
|
435
|
+
}
|
|
436
|
+
|
|
400
437
|
function workflow(callSite, nameOrRef, nestedArgs) {
|
|
401
438
|
return startOrchestration(() => workflowImpl(callSite, nameOrRef, nestedArgs));
|
|
402
439
|
}
|
|
@@ -520,6 +557,15 @@ function normalizeReserveAgents(options, fallback) {
|
|
|
520
557
|
function rpc(op, payload) {
|
|
521
558
|
throwIfAborted();
|
|
522
559
|
consumeHostCall();
|
|
560
|
+
return postRpc(op, payload);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Internal finally RPC that remains available after sticky fatal/abort state. */
|
|
564
|
+
function cleanupRpc(op, payload) {
|
|
565
|
+
return postRpc(op, payload);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function postRpc(op, payload) {
|
|
523
569
|
const id = ++nextRpcId;
|
|
524
570
|
return new Promise((resolve, reject) => {
|
|
525
571
|
rpcWaiters.set(id, { resolve, reject });
|
|
@@ -603,9 +649,9 @@ function makePolicyError(message) {
|
|
|
603
649
|
function markFatal(error) {
|
|
604
650
|
if (!isPolicy(error)) return;
|
|
605
651
|
stickyFatalError ??= error;
|
|
652
|
+
// Stop new orchestration, but let every already-dispatched RPC settle so
|
|
653
|
+
// branch and panel finally blocks reflect real sibling completion order.
|
|
606
654
|
aborted = true;
|
|
607
|
-
for (const waiter of rpcWaiters.values()) waiter.reject(stickyFatalError);
|
|
608
|
-
rpcWaiters.clear();
|
|
609
655
|
try {
|
|
610
656
|
parentPort.postMessage({ type: "fatal", error: serializeError(stickyFatalError) });
|
|
611
657
|
} catch {
|