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.
@@ -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-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
- function rejectIfShellFailed(spec, args, result, timedOut) {
245
- if (timedOut) return new TimeoutError(spec.timeoutMs ?? 0);
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
- 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
- });
402
+ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, timedOut) {
266
403
  let stdout = "";
267
404
  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) => {
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
- child.stderr.on("data", (chunk) => {
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
- if (spec.stdin != null) child.stdin.write(spec.stdin);
301
- child.stdin.end();
302
- if (spec.timeoutMs != null && spec.timeoutMs > 0) timeout = setTimeout(() => {
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.kill("SIGTERM");
305
- setTimeout(() => {
306
- child.kill("SIGKILL");
307
- }, 1e3).unref();
308
- }, spec.timeoutMs);
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
- if (timeout) clearTimeout(timeout);
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 withInterrupt(async () => await this.executeFlowRun(flow, input, runDir, state), async () => {
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 { output, rawText, trace } = await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => {
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
- const result = await runShellAction(effectiveExecution);
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
- 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
- });
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-BiRKgCnW.js.map
2262
+ //# sourceMappingURL=flows-DEook1GU.js.map