acpx 0.13.2 → 0.14.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 +1 -0
- package/dist/{cli-DJT3MRRI.js → cli-DSwJ-0g-.js} +4 -4
- package/dist/{cli-DJT3MRRI.js.map → cli-DSwJ-0g-.js.map} +1 -1
- package/dist/cli.d.ts +4 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -7
- package/dist/cli.js.map +1 -1
- package/dist/{client-CxNllqui.d.ts → client-IfFkZz0S.d.ts} +3 -2
- package/dist/{client-CxNllqui.d.ts.map → client-IfFkZz0S.d.ts.map} +1 -1
- package/dist/{flags-CM2rAdBo.js → flags-ftdtbXWd.js} +21 -4
- package/dist/flags-ftdtbXWd.js.map +1 -0
- package/dist/{flows-BiRKgCnW.js → flows-DEook1GU.js} +390 -58
- package/dist/flows-DEook1GU.js.map +1 -0
- package/dist/flows.d.ts +24 -16
- package/dist/flows.d.ts.map +1 -1
- package/dist/flows.js +1 -1
- package/dist/{live-checkpoint-Gw2oGjhe.js → live-checkpoint-BW9JivEG.js} +53 -40
- package/dist/live-checkpoint-BW9JivEG.js.map +1 -0
- package/dist/{output-DiPPprGk.js → output-jb0298L8.js} +3 -2
- package/dist/output-jb0298L8.js.map +1 -0
- package/dist/runtime.d.ts +14 -14
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -2
- package/dist/runtime.js.map +1 -1
- package/package.json +10 -10
- package/skills/acpx/SKILL.md +3 -0
- package/dist/flags-CM2rAdBo.js.map +0 -1
- package/dist/flows-BiRKgCnW.js.map +0 -1
- package/dist/live-checkpoint-Gw2oGjhe.js.map +0 -1
- package/dist/output-DiPPprGk.js.map +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { d as createSessionWithClient, f as cancelSessionPrompt, o as runOnce, s as sendSessionDirect, t as createOutputFormatter } from "./output-
|
|
2
|
-
import { Dt as TimeoutError, Et as InterruptedError, Ot as withInterrupt, Tt as textPrompt, Y as resolveSessionRecord, Yt as PERMISSION_MODES, Zt as SESSION_RECORD_SCHEMA, _ as recordPromptSubmission, dt as defaultSessionEventLog, g as recordClientOperation, h as createSessionConversation, kt as withTimeout, p as cloneSessionAcpxState, v as recordSessionUpdate, wt as promptToDisplayText } from "./live-checkpoint-
|
|
1
|
+
import { d as createSessionWithClient, f as cancelSessionPrompt, o as runOnce, s as sendSessionDirect, t as createOutputFormatter } from "./output-jb0298L8.js";
|
|
2
|
+
import { Dt as TimeoutError, Et as InterruptedError, F as runTimedExecFile, Ot as withInterrupt, Tt as textPrompt, Y as resolveSessionRecord, Yt as PERMISSION_MODES, Zt as SESSION_RECORD_SCHEMA, _ as recordPromptSubmission, dt as defaultSessionEventLog, g as recordClientOperation, h as createSessionConversation, kt as withTimeout, p as cloneSessionAcpxState, v as recordSessionUpdate, wt as promptToDisplayText } from "./live-checkpoint-BW9JivEG.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { ZodError, z } from "zod";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
9
10
|
//#region src/flows/authoring.ts
|
|
10
11
|
const FLOW_DEFINITION_BRAND = Symbol.for("acpx.flow.definition");
|
|
11
12
|
function markDefinedFlow(definition) {
|
|
@@ -228,7 +229,137 @@ function checkpoint(definition = {}) {
|
|
|
228
229
|
return node;
|
|
229
230
|
}
|
|
230
231
|
//#endregion
|
|
232
|
+
//#region src/flows/executors/shell-process.ts
|
|
233
|
+
const KILL_GRACE_MS = 1e3;
|
|
234
|
+
async function readPosixProcesses() {
|
|
235
|
+
return (await runTimedExecFile("ps", ["-eo", "pid=,ppid=,pgid=,stat="])).trim().split("\n").map((line) => {
|
|
236
|
+
const [pid, parent, group, state = ""] = line.trim().split(/\s+/u);
|
|
237
|
+
return {
|
|
238
|
+
pid: Number(pid),
|
|
239
|
+
parent: Number(parent),
|
|
240
|
+
group: Number(group),
|
|
241
|
+
state
|
|
242
|
+
};
|
|
243
|
+
}).filter((entry) => Number.isInteger(entry.pid) && entry.pid > 0);
|
|
244
|
+
}
|
|
245
|
+
function isLive(entry) {
|
|
246
|
+
return entry.state.length > 0 && !entry.state.startsWith("Z");
|
|
247
|
+
}
|
|
248
|
+
function rememberOwnedProcesses(rows, root, owned) {
|
|
249
|
+
for (const entry of rows) if (entry.group === root) owned.add(entry.pid);
|
|
250
|
+
for (const parent of owned) for (const entry of rows) if (entry.parent === parent) owned.add(entry.pid);
|
|
251
|
+
}
|
|
252
|
+
function signalPid(pid, signal) {
|
|
253
|
+
try {
|
|
254
|
+
process.kill(pid, signal);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error.code !== "ESRCH") throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function signalGroup(root, signal) {
|
|
260
|
+
try {
|
|
261
|
+
process.kill(-root, signal);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const code = error.code;
|
|
264
|
+
if (code === "ESRCH") return;
|
|
265
|
+
if (code === "EPERM" && !(await readPosixProcesses()).some((entry) => entry.group === root && isLive(entry))) return;
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function signalOwned(root, owned, signal) {
|
|
270
|
+
const rows = await readPosixProcesses();
|
|
271
|
+
rememberOwnedProcesses(rows, root, owned);
|
|
272
|
+
await signalGroup(root, signal);
|
|
273
|
+
for (const entry of rows) if (owned.has(entry.pid) && entry.group !== root && isLive(entry)) signalPid(entry.pid, signal);
|
|
274
|
+
}
|
|
275
|
+
async function waitForOwned(root, owned) {
|
|
276
|
+
const deadline = Date.now() + KILL_GRACE_MS;
|
|
277
|
+
do {
|
|
278
|
+
const rows = await readPosixProcesses();
|
|
279
|
+
rememberOwnedProcesses(rows, root, owned);
|
|
280
|
+
if (!rows.some((entry) => owned.has(entry.pid) && isLive(entry))) return true;
|
|
281
|
+
await setTimeout$1(25);
|
|
282
|
+
} while (Date.now() < deadline);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
async function forceKnownProcesses(root, owned) {
|
|
286
|
+
const results = await Promise.allSettled([signalGroup(root, "SIGKILL"), ...[...owned].map(async (pid) => signalPid(pid, "SIGKILL"))]);
|
|
287
|
+
const errors = [];
|
|
288
|
+
for (const result of results) if (result.status === "rejected") errors.push(result.reason);
|
|
289
|
+
if (errors.length > 0) throw new AggregateError(errors, "Known shell processes could not be stopped", { cause: errors[0] });
|
|
290
|
+
}
|
|
291
|
+
async function stopPosixTree(pid, signal) {
|
|
292
|
+
const owned = /* @__PURE__ */ new Set([pid]);
|
|
293
|
+
try {
|
|
294
|
+
await signalOwned(pid, owned, signal);
|
|
295
|
+
if (await waitForOwned(pid, owned)) return;
|
|
296
|
+
await signalOwned(pid, owned, "SIGKILL");
|
|
297
|
+
if (!await waitForOwned(pid, owned)) throw new Error("Shell process tree did not terminate after SIGKILL");
|
|
298
|
+
} catch (error) {
|
|
299
|
+
try {
|
|
300
|
+
await forceKnownProcesses(pid, owned);
|
|
301
|
+
} catch (cleanupError) {
|
|
302
|
+
throw new AggregateError([error, cleanupError], "Shell process cleanup failed", { cause: cleanupError });
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function stopWindowsTree(child) {
|
|
308
|
+
if (child.pid == null || child.exitCode != null || child.signalCode != null) return;
|
|
309
|
+
try {
|
|
310
|
+
await runTimedExecFile("taskkill", [
|
|
311
|
+
"/pid",
|
|
312
|
+
String(child.pid),
|
|
313
|
+
"/t",
|
|
314
|
+
"/f"
|
|
315
|
+
], { windowsHide: true });
|
|
316
|
+
} catch (error) {
|
|
317
|
+
child.kill("SIGKILL");
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function waitForShellClose(closed) {
|
|
322
|
+
try {
|
|
323
|
+
await withTimeout(closed, KILL_GRACE_MS);
|
|
324
|
+
} catch (cause) {
|
|
325
|
+
throw new Error("Shell process streams did not close after termination", { cause });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function stopShellProcess(child, closed, signal) {
|
|
329
|
+
try {
|
|
330
|
+
if (child.pid != null) {
|
|
331
|
+
if (process.platform === "win32") await stopWindowsTree(child);
|
|
332
|
+
else await stopPosixTree(child.pid, signal);
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
try {
|
|
336
|
+
await waitForShellClose(closed);
|
|
337
|
+
} catch (closeError) {
|
|
338
|
+
throw new AggregateError([error, closeError], "Shell process cleanup failed", { cause: closeError });
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
await waitForShellClose(closed);
|
|
343
|
+
}
|
|
344
|
+
function hasShellProcesses(child) {
|
|
345
|
+
if (child.pid == null) return false;
|
|
346
|
+
if (process.platform === "win32") return child.exitCode == null && child.signalCode == null;
|
|
347
|
+
try {
|
|
348
|
+
process.kill(-child.pid, 0);
|
|
349
|
+
return true;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return error.code !== "ESRCH";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
231
355
|
//#region src/flows/executors/shell.ts
|
|
356
|
+
function writeShellStdin(child, stdin) {
|
|
357
|
+
const stream = child.stdin;
|
|
358
|
+
if (!stream) return;
|
|
359
|
+
stream.on("error", () => {});
|
|
360
|
+
if (stdin != null && stream.writable && !stream.writableEnded) stream.write(stdin);
|
|
361
|
+
if (stream.writable && !stream.writableEnded) stream.end();
|
|
362
|
+
}
|
|
232
363
|
function formatShellActionSummary(spec) {
|
|
233
364
|
return `shell: ${renderShellCommand(spec.command, spec.args ?? [])}`;
|
|
234
365
|
}
|
|
@@ -241,39 +372,46 @@ function createShellFailureError(spec, args, exitCode, signal, stderr) {
|
|
|
241
372
|
const details = stderr.length > 0 ? `\n${stderr.trim()}` : "";
|
|
242
373
|
return /* @__PURE__ */ new Error(`Shell action failed (${renderShellCommand(spec.command, args)}): ${status}${details}`);
|
|
243
374
|
}
|
|
244
|
-
|
|
245
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Resolve a shell-action timeout.
|
|
377
|
+
* Non-positive values match withTimeout: no deadline (undefined).
|
|
378
|
+
* Positive values arm SIGTERM/SIGKILL after that many ms.
|
|
379
|
+
*/
|
|
380
|
+
function resolveShellActionTimeoutMs(timeoutMs) {
|
|
381
|
+
if (timeoutMs == null || !(timeoutMs > 0)) return;
|
|
382
|
+
return timeoutMs;
|
|
383
|
+
}
|
|
384
|
+
async function withShellAbort(run, signal) {
|
|
385
|
+
signal.throwIfAborted();
|
|
386
|
+
let rejectAbort = () => {};
|
|
387
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
388
|
+
rejectAbort = reject;
|
|
389
|
+
});
|
|
390
|
+
const onAbort = () => rejectAbort(signal.reason);
|
|
391
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
392
|
+
try {
|
|
393
|
+
return await Promise.race([run(), aborted]);
|
|
394
|
+
} finally {
|
|
395
|
+
signal.removeEventListener("abort", onAbort);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function rejectIfShellFailed(spec, args, result, timedOut, timeoutMs) {
|
|
399
|
+
if (timedOut) return new TimeoutError(timeoutMs ?? spec.timeoutMs ?? 0);
|
|
246
400
|
if (((result.exitCode ?? 0) !== 0 || result.signal != null) && spec.allowNonZeroExit !== true) return createShellFailureError(spec, args, result.exitCode, result.signal, result.stderr);
|
|
247
401
|
}
|
|
248
|
-
|
|
249
|
-
const cwd = spec.cwd ?? process.cwd();
|
|
250
|
-
const args = spec.args ?? [];
|
|
251
|
-
const startMs = Date.now();
|
|
252
|
-
const child = spawn(spec.command, args, {
|
|
253
|
-
cwd,
|
|
254
|
-
env: {
|
|
255
|
-
...process.env,
|
|
256
|
-
...spec.env
|
|
257
|
-
},
|
|
258
|
-
shell: spec.shell,
|
|
259
|
-
stdio: [
|
|
260
|
-
"pipe",
|
|
261
|
-
"pipe",
|
|
262
|
-
"pipe"
|
|
263
|
-
],
|
|
264
|
-
windowsHide: true
|
|
265
|
-
});
|
|
402
|
+
function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, timedOut) {
|
|
266
403
|
let stdout = "";
|
|
267
404
|
let stderr = "";
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
405
|
+
const stdoutStream = child.stdout;
|
|
406
|
+
const stderrStream = child.stderr;
|
|
407
|
+
if (!stdoutStream || !stderrStream) throw new Error("Shell action child is missing stdio pipes");
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
stdoutStream.setEncoding("utf8");
|
|
410
|
+
stderrStream.setEncoding("utf8");
|
|
411
|
+
stdoutStream.on("data", (chunk) => {
|
|
274
412
|
stdout += chunk;
|
|
275
413
|
});
|
|
276
|
-
|
|
414
|
+
stderrStream.on("data", (chunk) => {
|
|
277
415
|
stderr += chunk;
|
|
278
416
|
});
|
|
279
417
|
child.once("error", reject);
|
|
@@ -289,7 +427,7 @@ async function runShellAction(spec) {
|
|
|
289
427
|
signal,
|
|
290
428
|
durationMs: Date.now() - startMs
|
|
291
429
|
};
|
|
292
|
-
const error = rejectIfShellFailed(spec, args, result, timedOut);
|
|
430
|
+
const error = rejectIfShellFailed(spec, args, result, timedOut(), timeoutMs);
|
|
293
431
|
if (error) {
|
|
294
432
|
reject(error);
|
|
295
433
|
return;
|
|
@@ -297,19 +435,99 @@ async function runShellAction(spec) {
|
|
|
297
435
|
resolve(result);
|
|
298
436
|
});
|
|
299
437
|
});
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
438
|
+
}
|
|
439
|
+
function createShellTermination(child, timeoutMs, options) {
|
|
440
|
+
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
441
|
+
let rejectCleanup = () => {};
|
|
442
|
+
const cleanupFailure = new Promise((_resolve, reject) => {
|
|
443
|
+
rejectCleanup = reject;
|
|
444
|
+
});
|
|
445
|
+
let termination;
|
|
446
|
+
let timedOut = false;
|
|
447
|
+
let released = false;
|
|
448
|
+
let deadline;
|
|
449
|
+
let monitor;
|
|
450
|
+
let unregister;
|
|
451
|
+
const clearDeadline = () => {
|
|
452
|
+
if (deadline) clearTimeout(deadline);
|
|
453
|
+
};
|
|
454
|
+
const release = () => {
|
|
455
|
+
if (released) return;
|
|
456
|
+
released = true;
|
|
457
|
+
clearDeadline();
|
|
458
|
+
if (monitor) clearInterval(monitor);
|
|
459
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
460
|
+
unregister?.();
|
|
461
|
+
};
|
|
462
|
+
const cancel = (signal) => {
|
|
463
|
+
if (termination) return termination;
|
|
464
|
+
if (released) return Promise.resolve();
|
|
303
465
|
timedOut = true;
|
|
304
|
-
child.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
466
|
+
termination = stopShellProcess(child, closed, signal).finally(release);
|
|
467
|
+
termination.catch(rejectCleanup);
|
|
468
|
+
return termination;
|
|
469
|
+
};
|
|
470
|
+
const onAbort = () => {
|
|
471
|
+
cancel(options.terminationSignal ?? "SIGTERM");
|
|
472
|
+
};
|
|
473
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
474
|
+
if (timeoutMs != null) deadline = setTimeout(() => {
|
|
475
|
+
cancel("SIGTERM");
|
|
476
|
+
}, timeoutMs);
|
|
477
|
+
unregister = options.registerOwner?.({
|
|
478
|
+
cancel,
|
|
479
|
+
release
|
|
480
|
+
});
|
|
481
|
+
if (unregister) child.once("close", () => {
|
|
482
|
+
if (released || termination) return;
|
|
483
|
+
const prune = () => {
|
|
484
|
+
if (!termination && !hasShellProcesses(child)) release();
|
|
485
|
+
};
|
|
486
|
+
monitor = setInterval(prune, 100);
|
|
487
|
+
monitor.unref();
|
|
488
|
+
prune();
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
timedOut: () => timedOut,
|
|
492
|
+
cleanupFailure,
|
|
493
|
+
async dispose() {
|
|
494
|
+
clearDeadline();
|
|
495
|
+
try {
|
|
496
|
+
await termination;
|
|
497
|
+
} finally {
|
|
498
|
+
if (!unregister) release();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async function runShellAction(spec, options = {}) {
|
|
504
|
+
if (options?.signal?.aborted) throw options.signal.reason;
|
|
505
|
+
const cwd = spec.cwd ?? process.cwd();
|
|
506
|
+
const args = spec.args ?? [];
|
|
507
|
+
const startMs = Date.now();
|
|
508
|
+
const timeoutMs = resolveShellActionTimeoutMs(spec.timeoutMs);
|
|
509
|
+
const child = spawn(spec.command, args, {
|
|
510
|
+
cwd,
|
|
511
|
+
env: {
|
|
512
|
+
...process.env,
|
|
513
|
+
...spec.env
|
|
514
|
+
},
|
|
515
|
+
shell: spec.shell,
|
|
516
|
+
stdio: [
|
|
517
|
+
"pipe",
|
|
518
|
+
"pipe",
|
|
519
|
+
"pipe"
|
|
520
|
+
],
|
|
521
|
+
windowsHide: true,
|
|
522
|
+
detached: process.platform !== "win32"
|
|
523
|
+
});
|
|
524
|
+
const termination = createShellTermination(child, timeoutMs, options);
|
|
525
|
+
const finish = waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, termination.timedOut);
|
|
526
|
+
writeShellStdin(child, spec.stdin);
|
|
309
527
|
try {
|
|
310
|
-
return await finish;
|
|
528
|
+
return await Promise.race([finish, termination.cleanupFailure]);
|
|
311
529
|
} finally {
|
|
312
|
-
|
|
530
|
+
await termination.dispose();
|
|
313
531
|
}
|
|
314
532
|
}
|
|
315
533
|
//#endregion
|
|
@@ -1020,6 +1238,9 @@ var FlowRunner = class {
|
|
|
1020
1238
|
services;
|
|
1021
1239
|
store;
|
|
1022
1240
|
pendingPersistentSessionClients = /* @__PURE__ */ new Map();
|
|
1241
|
+
shellInterrupts = /* @__PURE__ */ new Map();
|
|
1242
|
+
runInterruptions = /* @__PURE__ */ new Map();
|
|
1243
|
+
shellOwners = /* @__PURE__ */ new Map();
|
|
1023
1244
|
constructor(options) {
|
|
1024
1245
|
this.resolveAgent = options.resolveAgent;
|
|
1025
1246
|
this.defaultCwd = options.resolveAgent(void 0).cwd;
|
|
@@ -1068,19 +1289,84 @@ var FlowRunner = class {
|
|
|
1068
1289
|
inputArtifact
|
|
1069
1290
|
});
|
|
1070
1291
|
try {
|
|
1071
|
-
return await
|
|
1072
|
-
await persistRunFailure(this.store, runDir, state, new InterruptedError());
|
|
1073
|
-
});
|
|
1292
|
+
return await this.runWithShellOwnership(flow, input, runDir, state);
|
|
1074
1293
|
} finally {
|
|
1075
1294
|
await this.closePendingPersistentSessionClients();
|
|
1076
1295
|
}
|
|
1077
1296
|
}
|
|
1297
|
+
async runWithShellOwnership(flow, input, runDir, state) {
|
|
1298
|
+
let execution;
|
|
1299
|
+
let cancellation;
|
|
1300
|
+
let interruption;
|
|
1301
|
+
const result = await withInterrupt(() => {
|
|
1302
|
+
execution = this.executeFlowRun(flow, input, runDir, state);
|
|
1303
|
+
return execution;
|
|
1304
|
+
}, async (signal) => {
|
|
1305
|
+
const reason = interruption ?? new InterruptedError();
|
|
1306
|
+
interruption = reason;
|
|
1307
|
+
this.runInterruptions.set(runDir, reason);
|
|
1308
|
+
cancellation ??= this.cancelShellOwners(runDir, signal);
|
|
1309
|
+
cancellation.catch(() => {});
|
|
1310
|
+
const interruptShell = this.shellInterrupts.get(runDir);
|
|
1311
|
+
if (interruptShell) {
|
|
1312
|
+
interruptShell(reason, signal);
|
|
1313
|
+
await execution?.catch(() => {});
|
|
1314
|
+
}
|
|
1315
|
+
}).then((value) => ({
|
|
1316
|
+
ok: true,
|
|
1317
|
+
value
|
|
1318
|
+
}), (error) => ({
|
|
1319
|
+
ok: false,
|
|
1320
|
+
error
|
|
1321
|
+
}));
|
|
1322
|
+
try {
|
|
1323
|
+
try {
|
|
1324
|
+
await cancellation;
|
|
1325
|
+
} catch (cleanupError) {
|
|
1326
|
+
const failure = result.ok || result.error === cleanupError ? cleanupError : new AggregateError([result.error, cleanupError], "Shell cleanup failed during interruption", { cause: cleanupError });
|
|
1327
|
+
await persistRunFailure(this.store, runDir, state, failure);
|
|
1328
|
+
throw failure;
|
|
1329
|
+
}
|
|
1330
|
+
if (result.ok && !interruption) return result.value;
|
|
1331
|
+
const failure = result.ok ? interruption : result.error;
|
|
1332
|
+
if (interruption) await persistRunFailure(this.store, runDir, state, failure);
|
|
1333
|
+
throw failure;
|
|
1334
|
+
} finally {
|
|
1335
|
+
this.releaseShellOwners(runDir);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
registerShellOwner(runDir, owner) {
|
|
1339
|
+
let owners = this.shellOwners.get(runDir);
|
|
1340
|
+
if (!owners) {
|
|
1341
|
+
owners = /* @__PURE__ */ new Set();
|
|
1342
|
+
this.shellOwners.set(runDir, owners);
|
|
1343
|
+
}
|
|
1344
|
+
owners.add(owner);
|
|
1345
|
+
const registered = owners;
|
|
1346
|
+
return () => {
|
|
1347
|
+
registered.delete(owner);
|
|
1348
|
+
if (registered.size === 0) this.shellOwners.delete(runDir);
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
async cancelShellOwners(runDir, signal) {
|
|
1352
|
+
const owners = [...this.shellOwners.get(runDir) ?? []];
|
|
1353
|
+
const results = await Promise.allSettled(owners.map((owner) => owner.cancel(signal)));
|
|
1354
|
+
const errors = [];
|
|
1355
|
+
for (const result of results) if (result.status === "rejected") errors.push(result.reason);
|
|
1356
|
+
if (errors.length > 0) throw new AggregateError(errors, "Shell process cleanup failed", { cause: errors[0] });
|
|
1357
|
+
}
|
|
1358
|
+
releaseShellOwners(runDir) {
|
|
1359
|
+
for (const owner of this.shellOwners.get(runDir) ?? []) owner.release();
|
|
1360
|
+
this.shellOwners.delete(runDir);
|
|
1361
|
+
}
|
|
1078
1362
|
async executeFlowRun(flow, input, runDir, state) {
|
|
1079
1363
|
let current = flow.startAt;
|
|
1080
1364
|
const attemptCounts = /* @__PURE__ */ new Map();
|
|
1081
1365
|
try {
|
|
1082
1366
|
while (current) {
|
|
1367
|
+
this.throwIfRunInterrupted(runDir);
|
|
1083
1368
|
const step = await this.executeFlowStep(flow, input, runDir, state, current, attemptCounts);
|
|
1369
|
+
this.throwIfRunInterrupted(runDir, step.executionError);
|
|
1084
1370
|
const waiting = await this.maybeCompleteCheckpointStep(runDir, state, step);
|
|
1085
1371
|
if (waiting) return waiting;
|
|
1086
1372
|
await this.recordFlowStepOutcome(runDir, state, step);
|
|
@@ -1088,10 +1374,16 @@ var FlowRunner = class {
|
|
|
1088
1374
|
}
|
|
1089
1375
|
return await this.completeFlowRun(runDir, state);
|
|
1090
1376
|
} catch (error) {
|
|
1091
|
-
await persistRunFailure(this.store, runDir, state, error);
|
|
1377
|
+
if (!this.runInterruptions.has(runDir)) await persistRunFailure(this.store, runDir, state, error);
|
|
1092
1378
|
throw error;
|
|
1379
|
+
} finally {
|
|
1380
|
+
this.runInterruptions.delete(runDir);
|
|
1093
1381
|
}
|
|
1094
1382
|
}
|
|
1383
|
+
throwIfRunInterrupted(runDir, error) {
|
|
1384
|
+
const interrupted = this.runInterruptions.get(runDir);
|
|
1385
|
+
if (interrupted) throw error ?? interrupted;
|
|
1386
|
+
}
|
|
1095
1387
|
async executeFlowStep(flow, input, runDir, state, nodeId, attemptCounts) {
|
|
1096
1388
|
const node = flow.nodes[nodeId];
|
|
1097
1389
|
if (!node) throw new Error(`Unknown flow node: ${nodeId}`);
|
|
@@ -1126,7 +1418,9 @@ var FlowRunner = class {
|
|
|
1126
1418
|
async executeStartedFlowStep(params) {
|
|
1127
1419
|
const context = makeFlowNodeContext(params.state, params.input, this.services);
|
|
1128
1420
|
try {
|
|
1421
|
+
this.throwIfRunInterrupted(params.runDir);
|
|
1129
1422
|
const executed = await this.executeNode(params.runDir, params.state, params.flow, params.nodeId, params.node, context);
|
|
1423
|
+
this.throwIfRunInterrupted(params.runDir);
|
|
1130
1424
|
return await this.createSuccessfulFlowStep(params, executed);
|
|
1131
1425
|
} catch (error) {
|
|
1132
1426
|
return await this.createFailedFlowStep(params, error);
|
|
@@ -1279,12 +1573,27 @@ var FlowRunner = class {
|
|
|
1279
1573
|
agentInfo: null,
|
|
1280
1574
|
trace: { action: { actionType: "function" } }
|
|
1281
1575
|
};
|
|
1282
|
-
const
|
|
1576
|
+
const shellAbort = new AbortController();
|
|
1577
|
+
let runningOwner;
|
|
1578
|
+
const shellControl = {
|
|
1579
|
+
signal: shellAbort.signal,
|
|
1580
|
+
registerOwner: (owner) => {
|
|
1581
|
+
runningOwner = owner;
|
|
1582
|
+
return this.registerShellOwner(runDir, owner);
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
this.shellInterrupts.set(runDir, (reason, signal) => {
|
|
1586
|
+
shellControl.terminationSignal = signal;
|
|
1587
|
+
shellAbort.abort(reason);
|
|
1588
|
+
});
|
|
1589
|
+
let runningShell;
|
|
1590
|
+
const { output, rawText, trace } = await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, () => withShellAbort(async () => {
|
|
1283
1591
|
const execution = await Promise.resolve(node.exec(context));
|
|
1592
|
+
shellAbort.signal.throwIfAborted();
|
|
1284
1593
|
const effectiveExecution = {
|
|
1285
1594
|
...execution,
|
|
1286
1595
|
cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd),
|
|
1287
|
-
timeoutMs: execution.timeoutMs ?? nodeTimeoutMs
|
|
1596
|
+
timeoutMs: resolveShellActionTimeoutMs(execution.timeoutMs ?? nodeTimeoutMs)
|
|
1288
1597
|
};
|
|
1289
1598
|
updateStatusDetail(state, formatShellActionSummary(effectiveExecution));
|
|
1290
1599
|
await this.store.writeLive(runDir, state, {
|
|
@@ -1294,6 +1603,7 @@ var FlowRunner = class {
|
|
|
1294
1603
|
attemptId: state.currentAttemptId,
|
|
1295
1604
|
payload: { statusDetail: state.statusDetail }
|
|
1296
1605
|
});
|
|
1606
|
+
shellAbort.signal.throwIfAborted();
|
|
1297
1607
|
await this.store.appendTrace(runDir, state, {
|
|
1298
1608
|
scope: "action",
|
|
1299
1609
|
type: "action_prepared",
|
|
@@ -1306,7 +1616,9 @@ var FlowRunner = class {
|
|
|
1306
1616
|
cwd: effectiveExecution.cwd
|
|
1307
1617
|
} }
|
|
1308
1618
|
});
|
|
1309
|
-
|
|
1619
|
+
runningShell = runShellAction(effectiveExecution, shellControl);
|
|
1620
|
+
const result = await runningShell;
|
|
1621
|
+
shellAbort.signal.throwIfAborted();
|
|
1310
1622
|
const stdoutArtifact = await this.store.writeArtifact(runDir, state, result.stdout, {
|
|
1311
1623
|
mediaType: "text/plain",
|
|
1312
1624
|
extension: "txt",
|
|
@@ -1319,6 +1631,7 @@ var FlowRunner = class {
|
|
|
1319
1631
|
nodeId: state.currentNode,
|
|
1320
1632
|
attemptId: state.currentAttemptId
|
|
1321
1633
|
});
|
|
1634
|
+
shellAbort.signal.throwIfAborted();
|
|
1322
1635
|
await this.store.appendTrace(runDir, state, {
|
|
1323
1636
|
scope: "action",
|
|
1324
1637
|
type: "action_completed",
|
|
@@ -1351,9 +1664,11 @@ var FlowRunner = class {
|
|
|
1351
1664
|
stdoutArtifact,
|
|
1352
1665
|
stderrArtifact
|
|
1353
1666
|
};
|
|
1667
|
+
shellAbort.signal.throwIfAborted();
|
|
1354
1668
|
let parsedOutput;
|
|
1355
1669
|
try {
|
|
1356
1670
|
parsedOutput = node.parse ? await node.parse(result, context) : result;
|
|
1671
|
+
shellAbort.signal.throwIfAborted();
|
|
1357
1672
|
} catch (error) {
|
|
1358
1673
|
throw attachStepTrace(error, trace);
|
|
1359
1674
|
}
|
|
@@ -1362,6 +1677,17 @@ var FlowRunner = class {
|
|
|
1362
1677
|
rawText: result.combinedOutput,
|
|
1363
1678
|
trace
|
|
1364
1679
|
};
|
|
1680
|
+
}, shellAbort.signal)).catch(async (error) => {
|
|
1681
|
+
shellAbort.abort(error);
|
|
1682
|
+
try {
|
|
1683
|
+
await runningOwner?.cancel(shellControl.terminationSignal ?? "SIGTERM");
|
|
1684
|
+
await runningShell;
|
|
1685
|
+
} catch (cleanupError) {
|
|
1686
|
+
if (!(cleanupError instanceof TimeoutError) && cleanupError !== error) throw new AggregateError([error, cleanupError], "Shell action cleanup failed", { cause: cleanupError });
|
|
1687
|
+
}
|
|
1688
|
+
throw error;
|
|
1689
|
+
}).finally(() => {
|
|
1690
|
+
this.shellInterrupts.delete(runDir);
|
|
1365
1691
|
});
|
|
1366
1692
|
return {
|
|
1367
1693
|
output,
|
|
@@ -1520,17 +1846,23 @@ var FlowRunner = class {
|
|
|
1520
1846
|
const heartbeatMs = Math.max(0, Math.round(node.heartbeatMs ?? DEFAULT_FLOW_HEARTBEAT_MS));
|
|
1521
1847
|
let timer;
|
|
1522
1848
|
let active = true;
|
|
1849
|
+
let heartbeatPending = false;
|
|
1523
1850
|
const heartbeat = async () => {
|
|
1524
|
-
if (!active) return;
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1851
|
+
if (!active || heartbeatPending) return;
|
|
1852
|
+
heartbeatPending = true;
|
|
1853
|
+
try {
|
|
1854
|
+
state.lastHeartbeatAt = isoNow$1();
|
|
1855
|
+
state.updatedAt = state.lastHeartbeatAt;
|
|
1856
|
+
await this.store.writeLive(runDir, state, {
|
|
1857
|
+
scope: "node",
|
|
1858
|
+
type: "node_heartbeat",
|
|
1859
|
+
nodeId,
|
|
1860
|
+
attemptId: state.currentAttemptId,
|
|
1861
|
+
payload: { statusDetail: state.statusDetail }
|
|
1862
|
+
});
|
|
1863
|
+
} finally {
|
|
1864
|
+
heartbeatPending = false;
|
|
1865
|
+
}
|
|
1534
1866
|
};
|
|
1535
1867
|
if (heartbeatMs > 0) timer = setInterval(() => {
|
|
1536
1868
|
heartbeat().catch(() => {});
|
|
@@ -1927,4 +2259,4 @@ function formatDecisionPrompt(question, choices, field) {
|
|
|
1927
2259
|
//#endregion
|
|
1928
2260
|
export { parseStrictJsonObject as a, validateFlowDefinition as c, checkpoint as d, compute as f, isDefinedFlow as h, parseJsonObject as i, acp as l, shell as m, decisionEdge as n, FlowRunner as o, defineFlow as p, extractJsonObject as r, flowRunsBaseDir as s, decision as t, action as u };
|
|
1929
2261
|
|
|
1930
|
-
//# sourceMappingURL=flows-
|
|
2262
|
+
//# sourceMappingURL=flows-DEook1GU.js.map
|