acpx 0.13.2 → 0.15.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.
Files changed (34) hide show
  1. package/README.md +4 -3
  2. package/dist/{cli-DJT3MRRI.js → cli-Cen2Rb6S.js} +4 -4
  3. package/dist/{cli-DJT3MRRI.js.map → cli-Cen2Rb6S.js.map} +1 -1
  4. package/dist/cli.d.ts +5 -6
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +11 -8
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-CxNllqui.d.ts → client-Cvz6msGc.d.ts} +8 -3
  9. package/dist/client-Cvz6msGc.d.ts.map +1 -0
  10. package/dist/{flags-CM2rAdBo.js → flags-CRh8BJre.js} +21 -4
  11. package/dist/flags-CRh8BJre.js.map +1 -0
  12. package/dist/{flows-BiRKgCnW.js → flows-D-F3Y2o9.js} +436 -63
  13. package/dist/flows-D-F3Y2o9.js.map +1 -0
  14. package/dist/flows.d.ts +27 -17
  15. package/dist/flows.d.ts.map +1 -1
  16. package/dist/flows.js +1 -1
  17. package/dist/{live-checkpoint-Gw2oGjhe.js → live-checkpoint-CYRs-D9t.js} +312 -124
  18. package/dist/live-checkpoint-CYRs-D9t.js.map +1 -0
  19. package/dist/{output-DiPPprGk.js → output-CIVecnHU.js} +42 -9
  20. package/dist/output-CIVecnHU.js.map +1 -0
  21. package/dist/runtime.d.ts +22 -15
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +46 -8
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/{session-options-DwRDODlr.d.ts → session-options-DyIGRNfu.d.ts} +48 -2
  26. package/dist/session-options-DyIGRNfu.d.ts.map +1 -0
  27. package/package.json +10 -10
  28. package/skills/acpx/SKILL.md +5 -0
  29. package/dist/client-CxNllqui.d.ts.map +0 -1
  30. package/dist/flags-CM2rAdBo.js.map +0 -1
  31. package/dist/flows-BiRKgCnW.js.map +0 -1
  32. package/dist/live-checkpoint-Gw2oGjhe.js.map +0 -1
  33. package/dist/output-DiPPprGk.js.map +0 -1
  34. package/dist/session-options-DwRDODlr.d.ts.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-DiPPprGk.js";
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-Gw2oGjhe.js";
1
+ import { d as createSessionWithClient, f as cancelSessionPrompt, o as runOnce, s as sendSessionDirect, t as createOutputFormatter } from "./output-CIVecnHU.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-CYRs-D9t.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,167 @@ function checkpoint(definition = {}) {
228
229
  return node;
229
230
  }
230
231
  //#endregion
232
+ //#region src/flows/executors/shell-output.ts
233
+ function validateShellActionMaxBufferBytes(value) {
234
+ if (value === void 0) return;
235
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Shell action maxBufferBytes must be a non-negative safe integer");
236
+ }
237
+ function createShellOutputCapture(maxBufferBytes, inactive, overflow) {
238
+ const output = {
239
+ stdout: "",
240
+ stderr: ""
241
+ };
242
+ const bytes = {
243
+ stdout: 0,
244
+ stderr: 0
245
+ };
246
+ return {
247
+ output,
248
+ append(stream, chunk) {
249
+ if (inactive()) return;
250
+ if (maxBufferBytes !== void 0) {
251
+ bytes[stream] += Buffer.byteLength(chunk, "utf8");
252
+ if (bytes[stream] > maxBufferBytes) {
253
+ overflow(/* @__PURE__ */ new Error(`Shell action exceeded maxBuffer (${maxBufferBytes} bytes) on ${stream}`));
254
+ return;
255
+ }
256
+ }
257
+ output[stream] += chunk;
258
+ }
259
+ };
260
+ }
261
+ //#endregion
262
+ //#region src/flows/executors/shell-process.ts
263
+ const KILL_GRACE_MS = 1e3;
264
+ async function readPosixProcesses() {
265
+ return (await runTimedExecFile("ps", ["-eo", "pid=,ppid=,pgid=,stat="])).trim().split("\n").map((line) => {
266
+ const [pid, parent, group, state = ""] = line.trim().split(/\s+/u);
267
+ return {
268
+ pid: Number(pid),
269
+ parent: Number(parent),
270
+ group: Number(group),
271
+ state
272
+ };
273
+ }).filter((entry) => Number.isInteger(entry.pid) && entry.pid > 0);
274
+ }
275
+ function isLive(entry) {
276
+ return entry.state.length > 0 && !entry.state.startsWith("Z");
277
+ }
278
+ function rememberOwnedProcesses(rows, root, owned) {
279
+ for (const entry of rows) if (entry.group === root) owned.add(entry.pid);
280
+ for (const parent of owned) for (const entry of rows) if (entry.parent === parent) owned.add(entry.pid);
281
+ }
282
+ function signalPid(pid, signal) {
283
+ try {
284
+ process.kill(pid, signal);
285
+ } catch (error) {
286
+ if (error.code !== "ESRCH") throw error;
287
+ }
288
+ }
289
+ async function signalGroup(root, signal) {
290
+ try {
291
+ process.kill(-root, signal);
292
+ } catch (error) {
293
+ const code = error.code;
294
+ if (code === "ESRCH") return;
295
+ if (code === "EPERM" && !(await readPosixProcesses()).some((entry) => entry.group === root && isLive(entry))) return;
296
+ throw error;
297
+ }
298
+ }
299
+ async function signalOwned(root, owned, signal) {
300
+ const rows = await readPosixProcesses();
301
+ rememberOwnedProcesses(rows, root, owned);
302
+ await signalGroup(root, signal);
303
+ for (const entry of rows) if (owned.has(entry.pid) && entry.group !== root && isLive(entry)) signalPid(entry.pid, signal);
304
+ }
305
+ async function waitForOwned(root, owned) {
306
+ const deadline = Date.now() + KILL_GRACE_MS;
307
+ do {
308
+ const rows = await readPosixProcesses();
309
+ rememberOwnedProcesses(rows, root, owned);
310
+ if (!rows.some((entry) => owned.has(entry.pid) && isLive(entry))) return true;
311
+ await setTimeout$1(25);
312
+ } while (Date.now() < deadline);
313
+ return false;
314
+ }
315
+ async function forceKnownProcesses(root, owned) {
316
+ const results = await Promise.allSettled([signalGroup(root, "SIGKILL"), ...[...owned].map(async (pid) => signalPid(pid, "SIGKILL"))]);
317
+ const errors = [];
318
+ for (const result of results) if (result.status === "rejected") errors.push(result.reason);
319
+ if (errors.length > 0) throw new AggregateError(errors, "Known shell processes could not be stopped", { cause: errors[0] });
320
+ }
321
+ async function stopPosixTree(pid, signal) {
322
+ const owned = /* @__PURE__ */ new Set([pid]);
323
+ try {
324
+ await signalOwned(pid, owned, signal);
325
+ if (await waitForOwned(pid, owned)) return;
326
+ await signalOwned(pid, owned, "SIGKILL");
327
+ if (!await waitForOwned(pid, owned)) throw new Error("Shell process tree did not terminate after SIGKILL");
328
+ } catch (error) {
329
+ try {
330
+ await forceKnownProcesses(pid, owned);
331
+ } catch (cleanupError) {
332
+ throw new AggregateError([error, cleanupError], "Shell process cleanup failed", { cause: cleanupError });
333
+ }
334
+ throw error;
335
+ }
336
+ }
337
+ async function stopWindowsTree(child) {
338
+ if (child.pid == null || child.exitCode != null || child.signalCode != null) return;
339
+ try {
340
+ await runTimedExecFile("taskkill", [
341
+ "/pid",
342
+ String(child.pid),
343
+ "/t",
344
+ "/f"
345
+ ], { windowsHide: true });
346
+ } catch (error) {
347
+ child.kill("SIGKILL");
348
+ throw error;
349
+ }
350
+ }
351
+ async function waitForShellClose(closed) {
352
+ try {
353
+ await withTimeout(closed, KILL_GRACE_MS);
354
+ } catch (cause) {
355
+ throw new Error("Shell process streams did not close after termination", { cause });
356
+ }
357
+ }
358
+ async function stopShellProcess(child, closed, signal) {
359
+ try {
360
+ if (child.pid != null) {
361
+ if (process.platform === "win32") await stopWindowsTree(child);
362
+ else await stopPosixTree(child.pid, signal);
363
+ }
364
+ } catch (error) {
365
+ try {
366
+ await waitForShellClose(closed);
367
+ } catch (closeError) {
368
+ throw new AggregateError([error, closeError], "Shell process cleanup failed", { cause: closeError });
369
+ }
370
+ throw error;
371
+ }
372
+ await waitForShellClose(closed);
373
+ }
374
+ function hasShellProcesses(child) {
375
+ if (child.pid == null) return false;
376
+ if (process.platform === "win32") return child.exitCode == null && child.signalCode == null;
377
+ try {
378
+ process.kill(-child.pid, 0);
379
+ return true;
380
+ } catch (error) {
381
+ return error.code !== "ESRCH";
382
+ }
383
+ }
384
+ //#endregion
231
385
  //#region src/flows/executors/shell.ts
386
+ function writeShellStdin(child, stdin) {
387
+ const stream = child.stdin;
388
+ if (!stream) return;
389
+ stream.on("error", () => {});
390
+ if (stdin != null && stream.writable && !stream.writableEnded) stream.write(stdin);
391
+ if (stream.writable && !stream.writableEnded) stream.end();
392
+ }
232
393
  function formatShellActionSummary(spec) {
233
394
  return `shell: ${renderShellCommand(spec.command, spec.args ?? [])}`;
234
395
  }
@@ -241,43 +402,59 @@ function createShellFailureError(spec, args, exitCode, signal, stderr) {
241
402
  const details = stderr.length > 0 ? `\n${stderr.trim()}` : "";
242
403
  return /* @__PURE__ */ new Error(`Shell action failed (${renderShellCommand(spec.command, args)}): ${status}${details}`);
243
404
  }
244
- function rejectIfShellFailed(spec, args, result, timedOut) {
245
- if (timedOut) return new TimeoutError(spec.timeoutMs ?? 0);
405
+ /**
406
+ * Resolve a shell-action timeout.
407
+ * Non-positive values match withTimeout: no deadline (undefined).
408
+ * Positive values arm SIGTERM/SIGKILL after that many ms.
409
+ */
410
+ function resolveShellActionTimeoutMs(timeoutMs) {
411
+ if (timeoutMs == null || !(timeoutMs > 0)) return;
412
+ return timeoutMs;
413
+ }
414
+ async function withShellAbort(run, signal) {
415
+ signal.throwIfAborted();
416
+ let rejectAbort = () => {};
417
+ const aborted = new Promise((_resolve, reject) => {
418
+ rejectAbort = reject;
419
+ });
420
+ const onAbort = () => rejectAbort(signal.reason);
421
+ signal.addEventListener("abort", onAbort, { once: true });
422
+ try {
423
+ return await Promise.race([run(), aborted]);
424
+ } finally {
425
+ signal.removeEventListener("abort", onAbort);
426
+ }
427
+ }
428
+ function rejectIfShellFailed(spec, args, result, timedOut, timeoutMs) {
429
+ if (timedOut) return new TimeoutError(timeoutMs ?? spec.timeoutMs ?? 0);
246
430
  if (((result.exitCode ?? 0) !== 0 || result.signal != null) && spec.allowNonZeroExit !== true) return createShellFailureError(spec, args, result.exitCode, result.signal, result.stderr);
247
431
  }
248
- async function runShellAction(spec) {
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
- });
266
- let stdout = "";
267
- let stderr = "";
268
- let timedOut = false;
269
- let timeout;
270
- const finish = new Promise((resolve, reject) => {
271
- child.stdout.setEncoding("utf8");
272
- child.stderr.setEncoding("utf8");
273
- child.stdout.on("data", (chunk) => {
274
- stdout += chunk;
432
+ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, termination) {
433
+ const stdoutStream = child.stdout;
434
+ const stderrStream = child.stderr;
435
+ if (!stdoutStream || !stderrStream) throw new Error("Shell action child is missing stdio pipes");
436
+ return new Promise((resolve, reject) => {
437
+ let settled = false;
438
+ const fail = (error) => {
439
+ settled = true;
440
+ reject(error);
441
+ };
442
+ const capture = createShellOutputCapture(spec.maxBufferBytes, () => settled || termination.timedOut(), (error) => {
443
+ fail(error);
444
+ termination.cancel("SIGTERM").catch(fail);
445
+ });
446
+ stdoutStream.setEncoding("utf8");
447
+ stderrStream.setEncoding("utf8");
448
+ stdoutStream.on("data", (chunk) => {
449
+ capture.append("stdout", chunk);
275
450
  });
276
- child.stderr.on("data", (chunk) => {
277
- stderr += chunk;
451
+ stderrStream.on("data", (chunk) => {
452
+ capture.append("stderr", chunk);
278
453
  });
279
- child.once("error", reject);
454
+ child.once("error", fail);
280
455
  child.once("exit", (exitCode, signal) => {
456
+ settled = true;
457
+ const { stdout, stderr } = capture.output;
281
458
  const result = {
282
459
  command: spec.command,
283
460
  args,
@@ -289,7 +466,7 @@ async function runShellAction(spec) {
289
466
  signal,
290
467
  durationMs: Date.now() - startMs
291
468
  };
292
- const error = rejectIfShellFailed(spec, args, result, timedOut);
469
+ const error = rejectIfShellFailed(spec, args, result, termination.timedOut(), timeoutMs);
293
470
  if (error) {
294
471
  reject(error);
295
472
  return;
@@ -297,19 +474,101 @@ async function runShellAction(spec) {
297
474
  resolve(result);
298
475
  });
299
476
  });
300
- if (spec.stdin != null) child.stdin.write(spec.stdin);
301
- child.stdin.end();
302
- if (spec.timeoutMs != null && spec.timeoutMs > 0) timeout = setTimeout(() => {
477
+ }
478
+ function createShellTermination(child, timeoutMs, options) {
479
+ const closed = new Promise((resolve) => child.once("close", () => resolve()));
480
+ let rejectCleanup = () => {};
481
+ const cleanupFailure = new Promise((_resolve, reject) => {
482
+ rejectCleanup = reject;
483
+ });
484
+ let termination;
485
+ let timedOut = false;
486
+ let released = false;
487
+ let deadline;
488
+ let monitor;
489
+ let unregister;
490
+ const clearDeadline = () => {
491
+ if (deadline) clearTimeout(deadline);
492
+ };
493
+ const release = () => {
494
+ if (released) return;
495
+ released = true;
496
+ clearDeadline();
497
+ if (monitor) clearInterval(monitor);
498
+ options.signal?.removeEventListener("abort", onAbort);
499
+ unregister?.();
500
+ };
501
+ const cancel = (signal) => {
502
+ if (termination) return termination;
503
+ if (released) return Promise.resolve();
303
504
  timedOut = true;
304
- child.kill("SIGTERM");
305
- setTimeout(() => {
306
- child.kill("SIGKILL");
307
- }, 1e3).unref();
308
- }, spec.timeoutMs);
505
+ termination = stopShellProcess(child, closed, signal).finally(release);
506
+ termination.catch(rejectCleanup);
507
+ return termination;
508
+ };
509
+ const onAbort = () => {
510
+ cancel(options.terminationSignal ?? "SIGTERM");
511
+ };
512
+ options.signal?.addEventListener("abort", onAbort, { once: true });
513
+ if (timeoutMs != null) deadline = setTimeout(() => {
514
+ cancel("SIGTERM");
515
+ }, timeoutMs);
516
+ unregister = options.registerOwner?.({
517
+ cancel,
518
+ release
519
+ });
520
+ if (unregister) child.once("close", () => {
521
+ if (released || termination) return;
522
+ const prune = () => {
523
+ if (!termination && !hasShellProcesses(child)) release();
524
+ };
525
+ monitor = setInterval(prune, 100);
526
+ monitor.unref();
527
+ prune();
528
+ });
529
+ return {
530
+ cancel,
531
+ timedOut: () => timedOut,
532
+ cleanupFailure,
533
+ async dispose() {
534
+ clearDeadline();
535
+ try {
536
+ await termination;
537
+ } finally {
538
+ if (!unregister) release();
539
+ }
540
+ }
541
+ };
542
+ }
543
+ async function runShellAction(spec, options = {}) {
544
+ if (options?.signal?.aborted) throw options.signal.reason;
545
+ const cwd = spec.cwd ?? process.cwd();
546
+ const args = spec.args ?? [];
547
+ const startMs = Date.now();
548
+ const timeoutMs = resolveShellActionTimeoutMs(spec.timeoutMs);
549
+ validateShellActionMaxBufferBytes(spec.maxBufferBytes);
550
+ const child = spawn(spec.command, args, {
551
+ cwd,
552
+ env: {
553
+ ...process.env,
554
+ ...spec.env
555
+ },
556
+ shell: spec.shell,
557
+ stdio: [
558
+ "pipe",
559
+ "pipe",
560
+ "pipe"
561
+ ],
562
+ windowsHide: true,
563
+ detached: process.platform !== "win32"
564
+ });
565
+ const termination = createShellTermination(child, timeoutMs, options);
566
+ const finish = waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, termination);
567
+ writeShellStdin(child, spec.stdin);
309
568
  try {
310
- return await finish;
569
+ return await Promise.race([finish, termination.cleanupFailure]);
311
570
  } finally {
312
- if (timeout) clearTimeout(timeout);
571
+ await termination.dispose();
313
572
  }
314
573
  }
315
574
  //#endregion
@@ -1020,6 +1279,9 @@ var FlowRunner = class {
1020
1279
  services;
1021
1280
  store;
1022
1281
  pendingPersistentSessionClients = /* @__PURE__ */ new Map();
1282
+ shellInterrupts = /* @__PURE__ */ new Map();
1283
+ runInterruptions = /* @__PURE__ */ new Map();
1284
+ shellOwners = /* @__PURE__ */ new Map();
1023
1285
  constructor(options) {
1024
1286
  this.resolveAgent = options.resolveAgent;
1025
1287
  this.defaultCwd = options.resolveAgent(void 0).cwd;
@@ -1068,19 +1330,84 @@ var FlowRunner = class {
1068
1330
  inputArtifact
1069
1331
  });
1070
1332
  try {
1071
- return await withInterrupt(async () => await this.executeFlowRun(flow, input, runDir, state), async () => {
1072
- await persistRunFailure(this.store, runDir, state, new InterruptedError());
1073
- });
1333
+ return await this.runWithShellOwnership(flow, input, runDir, state);
1074
1334
  } finally {
1075
1335
  await this.closePendingPersistentSessionClients();
1076
1336
  }
1077
1337
  }
1338
+ async runWithShellOwnership(flow, input, runDir, state) {
1339
+ let execution;
1340
+ let cancellation;
1341
+ let interruption;
1342
+ const result = await withInterrupt(() => {
1343
+ execution = this.executeFlowRun(flow, input, runDir, state);
1344
+ return execution;
1345
+ }, async (signal) => {
1346
+ const reason = interruption ?? new InterruptedError();
1347
+ interruption = reason;
1348
+ this.runInterruptions.set(runDir, reason);
1349
+ cancellation ??= this.cancelShellOwners(runDir, signal);
1350
+ cancellation.catch(() => {});
1351
+ const interruptShell = this.shellInterrupts.get(runDir);
1352
+ if (interruptShell) {
1353
+ interruptShell(reason, signal);
1354
+ await execution?.catch(() => {});
1355
+ }
1356
+ }).then((value) => ({
1357
+ ok: true,
1358
+ value
1359
+ }), (error) => ({
1360
+ ok: false,
1361
+ error
1362
+ }));
1363
+ try {
1364
+ try {
1365
+ await cancellation;
1366
+ } catch (cleanupError) {
1367
+ const failure = result.ok || result.error === cleanupError ? cleanupError : new AggregateError([result.error, cleanupError], "Shell cleanup failed during interruption", { cause: cleanupError });
1368
+ await persistRunFailure(this.store, runDir, state, failure);
1369
+ throw failure;
1370
+ }
1371
+ if (result.ok && !interruption) return result.value;
1372
+ const failure = result.ok ? interruption : result.error;
1373
+ if (interruption) await persistRunFailure(this.store, runDir, state, failure);
1374
+ throw failure;
1375
+ } finally {
1376
+ this.releaseShellOwners(runDir);
1377
+ }
1378
+ }
1379
+ registerShellOwner(runDir, owner) {
1380
+ let owners = this.shellOwners.get(runDir);
1381
+ if (!owners) {
1382
+ owners = /* @__PURE__ */ new Set();
1383
+ this.shellOwners.set(runDir, owners);
1384
+ }
1385
+ owners.add(owner);
1386
+ const registered = owners;
1387
+ return () => {
1388
+ registered.delete(owner);
1389
+ if (registered.size === 0) this.shellOwners.delete(runDir);
1390
+ };
1391
+ }
1392
+ async cancelShellOwners(runDir, signal) {
1393
+ const owners = [...this.shellOwners.get(runDir) ?? []];
1394
+ const results = await Promise.allSettled(owners.map((owner) => owner.cancel(signal)));
1395
+ const errors = [];
1396
+ for (const result of results) if (result.status === "rejected") errors.push(result.reason);
1397
+ if (errors.length > 0) throw new AggregateError(errors, "Shell process cleanup failed", { cause: errors[0] });
1398
+ }
1399
+ releaseShellOwners(runDir) {
1400
+ for (const owner of this.shellOwners.get(runDir) ?? []) owner.release();
1401
+ this.shellOwners.delete(runDir);
1402
+ }
1078
1403
  async executeFlowRun(flow, input, runDir, state) {
1079
1404
  let current = flow.startAt;
1080
1405
  const attemptCounts = /* @__PURE__ */ new Map();
1081
1406
  try {
1082
1407
  while (current) {
1408
+ this.throwIfRunInterrupted(runDir);
1083
1409
  const step = await this.executeFlowStep(flow, input, runDir, state, current, attemptCounts);
1410
+ this.throwIfRunInterrupted(runDir, step.executionError);
1084
1411
  const waiting = await this.maybeCompleteCheckpointStep(runDir, state, step);
1085
1412
  if (waiting) return waiting;
1086
1413
  await this.recordFlowStepOutcome(runDir, state, step);
@@ -1088,10 +1415,16 @@ var FlowRunner = class {
1088
1415
  }
1089
1416
  return await this.completeFlowRun(runDir, state);
1090
1417
  } catch (error) {
1091
- await persistRunFailure(this.store, runDir, state, error);
1418
+ if (!this.runInterruptions.has(runDir)) await persistRunFailure(this.store, runDir, state, error);
1092
1419
  throw error;
1420
+ } finally {
1421
+ this.runInterruptions.delete(runDir);
1093
1422
  }
1094
1423
  }
1424
+ throwIfRunInterrupted(runDir, error) {
1425
+ const interrupted = this.runInterruptions.get(runDir);
1426
+ if (interrupted) throw error ?? interrupted;
1427
+ }
1095
1428
  async executeFlowStep(flow, input, runDir, state, nodeId, attemptCounts) {
1096
1429
  const node = flow.nodes[nodeId];
1097
1430
  if (!node) throw new Error(`Unknown flow node: ${nodeId}`);
@@ -1126,7 +1459,9 @@ var FlowRunner = class {
1126
1459
  async executeStartedFlowStep(params) {
1127
1460
  const context = makeFlowNodeContext(params.state, params.input, this.services);
1128
1461
  try {
1462
+ this.throwIfRunInterrupted(params.runDir);
1129
1463
  const executed = await this.executeNode(params.runDir, params.state, params.flow, params.nodeId, params.node, context);
1464
+ this.throwIfRunInterrupted(params.runDir);
1130
1465
  return await this.createSuccessfulFlowStep(params, executed);
1131
1466
  } catch (error) {
1132
1467
  return await this.createFailedFlowStep(params, error);
@@ -1279,12 +1614,27 @@ var FlowRunner = class {
1279
1614
  agentInfo: null,
1280
1615
  trace: { action: { actionType: "function" } }
1281
1616
  };
1282
- const { output, rawText, trace } = await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => {
1617
+ const shellAbort = new AbortController();
1618
+ let runningOwner;
1619
+ const shellControl = {
1620
+ signal: shellAbort.signal,
1621
+ registerOwner: (owner) => {
1622
+ runningOwner = owner;
1623
+ return this.registerShellOwner(runDir, owner);
1624
+ }
1625
+ };
1626
+ this.shellInterrupts.set(runDir, (reason, signal) => {
1627
+ shellControl.terminationSignal = signal;
1628
+ shellAbort.abort(reason);
1629
+ });
1630
+ let runningShell;
1631
+ const { output, rawText, trace } = await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, () => withShellAbort(async () => {
1283
1632
  const execution = await Promise.resolve(node.exec(context));
1633
+ shellAbort.signal.throwIfAborted();
1284
1634
  const effectiveExecution = {
1285
1635
  ...execution,
1286
1636
  cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd),
1287
- timeoutMs: execution.timeoutMs ?? nodeTimeoutMs
1637
+ timeoutMs: resolveShellActionTimeoutMs(execution.timeoutMs ?? nodeTimeoutMs)
1288
1638
  };
1289
1639
  updateStatusDetail(state, formatShellActionSummary(effectiveExecution));
1290
1640
  await this.store.writeLive(runDir, state, {
@@ -1294,6 +1644,7 @@ var FlowRunner = class {
1294
1644
  attemptId: state.currentAttemptId,
1295
1645
  payload: { statusDetail: state.statusDetail }
1296
1646
  });
1647
+ shellAbort.signal.throwIfAborted();
1297
1648
  await this.store.appendTrace(runDir, state, {
1298
1649
  scope: "action",
1299
1650
  type: "action_prepared",
@@ -1306,7 +1657,9 @@ var FlowRunner = class {
1306
1657
  cwd: effectiveExecution.cwd
1307
1658
  } }
1308
1659
  });
1309
- const result = await runShellAction(effectiveExecution);
1660
+ runningShell = runShellAction(effectiveExecution, shellControl);
1661
+ const result = await runningShell;
1662
+ shellAbort.signal.throwIfAborted();
1310
1663
  const stdoutArtifact = await this.store.writeArtifact(runDir, state, result.stdout, {
1311
1664
  mediaType: "text/plain",
1312
1665
  extension: "txt",
@@ -1319,6 +1672,7 @@ var FlowRunner = class {
1319
1672
  nodeId: state.currentNode,
1320
1673
  attemptId: state.currentAttemptId
1321
1674
  });
1675
+ shellAbort.signal.throwIfAborted();
1322
1676
  await this.store.appendTrace(runDir, state, {
1323
1677
  scope: "action",
1324
1678
  type: "action_completed",
@@ -1351,9 +1705,11 @@ var FlowRunner = class {
1351
1705
  stdoutArtifact,
1352
1706
  stderrArtifact
1353
1707
  };
1708
+ shellAbort.signal.throwIfAborted();
1354
1709
  let parsedOutput;
1355
1710
  try {
1356
1711
  parsedOutput = node.parse ? await node.parse(result, context) : result;
1712
+ shellAbort.signal.throwIfAborted();
1357
1713
  } catch (error) {
1358
1714
  throw attachStepTrace(error, trace);
1359
1715
  }
@@ -1362,6 +1718,17 @@ var FlowRunner = class {
1362
1718
  rawText: result.combinedOutput,
1363
1719
  trace
1364
1720
  };
1721
+ }, shellAbort.signal)).catch(async (error) => {
1722
+ shellAbort.abort(error);
1723
+ try {
1724
+ await runningOwner?.cancel(shellControl.terminationSignal ?? "SIGTERM");
1725
+ await runningShell;
1726
+ } catch (cleanupError) {
1727
+ if (!(cleanupError instanceof TimeoutError) && cleanupError !== error) throw new AggregateError([error, cleanupError], "Shell action cleanup failed", { cause: cleanupError });
1728
+ }
1729
+ throw error;
1730
+ }).finally(() => {
1731
+ this.shellInterrupts.delete(runDir);
1365
1732
  });
1366
1733
  return {
1367
1734
  output,
@@ -1520,17 +1887,23 @@ var FlowRunner = class {
1520
1887
  const heartbeatMs = Math.max(0, Math.round(node.heartbeatMs ?? DEFAULT_FLOW_HEARTBEAT_MS));
1521
1888
  let timer;
1522
1889
  let active = true;
1890
+ let heartbeatPending = false;
1523
1891
  const heartbeat = async () => {
1524
- if (!active) return;
1525
- state.lastHeartbeatAt = isoNow$1();
1526
- state.updatedAt = state.lastHeartbeatAt;
1527
- await this.store.writeLive(runDir, state, {
1528
- scope: "node",
1529
- type: "node_heartbeat",
1530
- nodeId,
1531
- attemptId: state.currentAttemptId,
1532
- payload: { statusDetail: state.statusDetail }
1533
- });
1892
+ if (!active || heartbeatPending) return;
1893
+ heartbeatPending = true;
1894
+ try {
1895
+ state.lastHeartbeatAt = isoNow$1();
1896
+ state.updatedAt = state.lastHeartbeatAt;
1897
+ await this.store.writeLive(runDir, state, {
1898
+ scope: "node",
1899
+ type: "node_heartbeat",
1900
+ nodeId,
1901
+ attemptId: state.currentAttemptId,
1902
+ payload: { statusDetail: state.statusDetail }
1903
+ });
1904
+ } finally {
1905
+ heartbeatPending = false;
1906
+ }
1534
1907
  };
1535
1908
  if (heartbeatMs > 0) timer = setInterval(() => {
1536
1909
  heartbeat().catch(() => {});
@@ -1927,4 +2300,4 @@ function formatDecisionPrompt(question, choices, field) {
1927
2300
  //#endregion
1928
2301
  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
2302
 
1930
- //# sourceMappingURL=flows-BiRKgCnW.js.map
2303
+ //# sourceMappingURL=flows-D-F3Y2o9.js.map