acpx 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{agent-registry-DdKB-zia.js → agent-registry-DWlx5BQ4.js} +5 -3
  2. package/dist/agent-registry-DWlx5BQ4.js.map +1 -0
  3. package/dist/agent-registry.js +1 -1
  4. package/dist/{cli-8mVHeUXh.js → cli-BerngYEz.js} +5 -5
  5. package/dist/{cli-8mVHeUXh.js.map → cli-BerngYEz.js.map} +1 -1
  6. package/dist/cli.js +77 -53
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-DeNKZZcu.d.ts → client-BchvQRpz.d.ts} +17 -4
  9. package/dist/client-BchvQRpz.d.ts.map +1 -0
  10. package/dist/{flows-WOQNzCMC.js → flows-DimX0it5.js} +693 -475
  11. package/dist/flows-DimX0it5.js.map +1 -0
  12. package/dist/flows.d.ts +20 -7
  13. package/dist/flows.d.ts.map +1 -1
  14. package/dist/flows.js +1 -1
  15. package/dist/{invocation-options-B6d7TI-y.js → invocation-options-vobVApfO.js} +32 -11
  16. package/dist/invocation-options-vobVApfO.js.map +1 -0
  17. package/dist/{ipc-BJrvfNXr.js → ipc-DAApJ5mi.js} +1041 -263
  18. package/dist/ipc-DAApJ5mi.js.map +1 -0
  19. package/dist/{output-CL18YmsG.js → output-Cnuji8Bo.js} +41 -20
  20. package/dist/output-Cnuji8Bo.js.map +1 -0
  21. package/dist/{queue-owner-runtime-Cr0dWJuL.js → queue-owner-runtime-C4OOosbo.js} +732 -507
  22. package/dist/queue-owner-runtime-C4OOosbo.js.map +1 -0
  23. package/dist/runtime.d.ts +19 -1
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +48 -29
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/session-options-Dp6mzNSy.d.ts.map +1 -1
  28. package/dist/{session-watch-B8Q_TsGf.js → session-watch-Bj9LoYUq.js} +5 -5
  29. package/dist/session-watch-Bj9LoYUq.js.map +1 -0
  30. package/dist/{watch-Dp73ph8h.js → watch-Cz6jbrRs.js} +2 -2
  31. package/dist/{watch-Dp73ph8h.js.map → watch-Cz6jbrRs.js.map} +1 -1
  32. package/package.json +1 -1
  33. package/skills/acpx/SKILL.md +45 -23
  34. package/dist/agent-registry-DdKB-zia.js.map +0 -1
  35. package/dist/client-DeNKZZcu.d.ts.map +0 -1
  36. package/dist/flows-WOQNzCMC.js.map +0 -1
  37. package/dist/invocation-options-B6d7TI-y.js.map +0 -1
  38. package/dist/ipc-BJrvfNXr.js.map +0 -1
  39. package/dist/output-CL18YmsG.js.map +0 -1
  40. package/dist/queue-owner-runtime-Cr0dWJuL.js.map +0 -1
  41. package/dist/session-watch-B8Q_TsGf.js.map +0 -1
@@ -1,7 +1,7 @@
1
- import { t as createOutputFormatter } from "./output-CL18YmsG.js";
2
- import { C as promptToDisplayText, Dt as PERMISSION_MODES, I as resolveSessionRecord, ht as writePrivateJsonFile, kt as SESSION_RECORD_SCHEMA, lt as defaultSessionEventLog, mt as writePrivateFile, w as textPrompt } from "./ipc-BJrvfNXr.js";
3
- import { B as cloneSessionAcpxState, G as recordSessionUpdate, H as createSessionConversation, U as recordClientOperation, W as recordPromptSubmission, _ as cancelSessionPrompt, at as InterruptedError, ct as withInterrupt, i as runOnce, lt as withTimeout, m as createSessionWithClient, o as sendSessionDirect, ot as TimeoutError } from "./queue-owner-runtime-Cr0dWJuL.js";
4
- import { M as runTimedExecFile } from "./agent-registry-DdKB-zia.js";
1
+ import { t as createOutputFormatter } from "./output-Cnuji8Bo.js";
2
+ import { D as textPrompt, E as promptToDisplayText, _t as writePrivateFile, ft as defaultSessionEventLog, jt as SESSION_RECORD_SCHEMA, kt as PERMISSION_MODES, vt as writePrivateJsonFile, z as resolveSessionRecord } from "./ipc-DAApJ5mi.js";
3
+ import { G as recordPromptSubmission, K as recordSessionUpdate, U as createSessionConversation, V as cloneSessionAcpxState, W as recordClientOperation, i as runOnce, lt as withInterrupt, m as createSessionWithClient, o as sendSessionDirect, ot as InterruptedError, st as TimeoutError, ut as withTimeout } from "./queue-owner-runtime-C4OOosbo.js";
4
+ import { N as runTimedExecFile } from "./agent-registry-DWlx5BQ4.js";
5
5
  import path from "node:path";
6
6
  import { spawn } from "node:child_process";
7
7
  import fs from "node:fs/promises";
@@ -10,6 +10,131 @@ import { appendRegularFile } from "@openclaw/fs-safe/advanced";
10
10
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
11
11
  import { ZodError, z } from "zod";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
+ import { performance } from "node:perf_hooks";
14
+ //#region src/flows/attempt.ts
15
+ /** One node attempt owns admission and completion of its managed operations. */
16
+ var FlowAttempt = class {
17
+ signal;
18
+ nodeId;
19
+ attemptId;
20
+ startedAt;
21
+ terminationSignal = "SIGTERM";
22
+ controller = new AbortController();
23
+ pending = /* @__PURE__ */ new Set();
24
+ cancellations = /* @__PURE__ */ new Set();
25
+ cleanupFailures = [];
26
+ deadlineAt;
27
+ timeoutMs;
28
+ timer;
29
+ accepting = true;
30
+ finished = false;
31
+ constructor(options) {
32
+ this.nodeId = options.nodeId;
33
+ this.attemptId = options.attemptId;
34
+ this.startedAt = options.startedAt;
35
+ this.signal = this.controller.signal;
36
+ if (options.timeoutMs != null && options.timeoutMs > 0) {
37
+ this.timeoutMs = options.timeoutMs;
38
+ this.deadlineAt = performance.now() + options.timeoutMs;
39
+ this.timer = setTimeout(() => this.cancel(new TimeoutError(options.timeoutMs)), options.timeoutMs);
40
+ }
41
+ }
42
+ get active() {
43
+ return this.accepting && !this.signal.aborted;
44
+ }
45
+ assertActive() {
46
+ this.checkDeadline();
47
+ this.signal.throwIfAborted();
48
+ if (!this.accepting) throw new Error("Flow attempt has finished accepting work");
49
+ }
50
+ remainingTimeoutMs() {
51
+ this.assertActive();
52
+ return this.deadlineAt === void 0 ? void 0 : Math.max(1, Math.ceil(this.deadlineAt - performance.now()));
53
+ }
54
+ cancel(reason, signal = "SIGTERM") {
55
+ if (this.finished || this.signal.aborted) return;
56
+ this.accepting = false;
57
+ this.terminationSignal = signal;
58
+ this.controller.abort(reason);
59
+ for (const cancel of this.cancellations) this.trackCancellation(cancel);
60
+ }
61
+ registerCancellation(cancel) {
62
+ if (!this.active) {
63
+ this.trackCancellation(cancel);
64
+ return () => {};
65
+ }
66
+ this.cancellations.add(cancel);
67
+ return () => {
68
+ this.cancellations.delete(cancel);
69
+ };
70
+ }
71
+ /** Track runtime work, never an arbitrary user callback that may ignore cancellation. */
72
+ own(run, options = {}) {
73
+ this.assertActive();
74
+ const operation = run().then((value) => {
75
+ this.checkDeadline();
76
+ if (this.signal.aborted) this.signal.throwIfAborted();
77
+ return value;
78
+ });
79
+ const outcome = operation.then(() => ({ ok: true }), (error) => options.bestEffort ? { ok: true } : {
80
+ ok: false,
81
+ error
82
+ });
83
+ this.pending.add(outcome);
84
+ outcome.then(() => this.pending.delete(outcome));
85
+ return operation;
86
+ }
87
+ async run(run) {
88
+ let rejectAbort = () => {};
89
+ const aborted = new Promise((_resolve, reject) => {
90
+ rejectAbort = reject;
91
+ });
92
+ const onAbort = () => rejectAbort(this.signal.reason);
93
+ this.signal.addEventListener("abort", onAbort, { once: true });
94
+ try {
95
+ this.assertActive();
96
+ const value = await Promise.race([run(), aborted]);
97
+ this.accepting = false;
98
+ const errors = await this.drain();
99
+ if (errors.length > 0) throw this.cleanupError(errors);
100
+ this.checkDeadline();
101
+ this.signal.throwIfAborted();
102
+ return value;
103
+ } catch (error) {
104
+ this.cancel(error);
105
+ const errors = await this.drain();
106
+ if (errors.length > 0) throw this.cleanupError([error, ...errors]);
107
+ throw error;
108
+ } finally {
109
+ this.accepting = false;
110
+ this.finished = true;
111
+ clearTimeout(this.timer);
112
+ this.signal.removeEventListener("abort", onAbort);
113
+ }
114
+ }
115
+ checkDeadline() {
116
+ if (this.deadlineAt !== void 0 && performance.now() >= this.deadlineAt) this.cancel(new TimeoutError(this.timeoutMs));
117
+ }
118
+ async drain() {
119
+ const errors = [];
120
+ while (this.pending.size > 0) for (const outcome of await Promise.all(this.pending)) if (!outcome.ok && outcome.error !== this.signal.reason) errors.push(outcome.error);
121
+ return [.../* @__PURE__ */ new Set([...errors, ...this.cleanupFailures.splice(0)])];
122
+ }
123
+ trackCancellation(cancel) {
124
+ const outcome = Promise.resolve().then(() => cancel(this.terminationSignal)).then(() => ({ ok: true }), (error) => {
125
+ this.cleanupFailures.push(error);
126
+ return { ok: true };
127
+ });
128
+ this.pending.add(outcome);
129
+ outcome.then(() => this.pending.delete(outcome));
130
+ }
131
+ cleanupError(errors) {
132
+ const reason = this.signal.reason;
133
+ const failures = [.../* @__PURE__ */ new Set([...this.signal.aborted ? [reason] : [], ...errors])];
134
+ return new AggregateError(failures, "Flow attempt cleanup failed", { cause: failures[0] });
135
+ }
136
+ };
137
+ //#endregion
13
138
  //#region src/flows/authoring.ts
14
139
  const FLOW_DEFINITION_BRAND = Symbol.for("acpx.flow.definition");
15
140
  function markDefinedFlow(definition) {
@@ -109,7 +234,7 @@ const flowDefinitionSchema = extensibleObject({
109
234
  const flowNodeTypeSchema = z.object({ nodeType: z.enum(FLOW_NODE_TYPES) });
110
235
  function assertValidFlowDefinitionShape(flow) {
111
236
  const parsed = parseWithSchema("flow definition", flowDefinitionSchema, flow);
112
- for (const [nodeId, node] of Object.entries(parsed.nodes)) assertValidFlowNodeDefinitionShape(node, `flow node "${nodeId}"`);
237
+ for (const [nodeId, node] of Object.entries(flow.nodes)) assertValidFlowNodeDefinitionShape(node, `flow node "${nodeId}"`);
113
238
  parsed.edges.forEach((edge, index) => {
114
239
  assertValidFlowEdgeShape(edge, `flow definition: edges.${index}`);
115
240
  });
@@ -164,6 +289,8 @@ function assertValidFlowEdgeShape(edge, label) {
164
289
  return;
165
290
  }
166
291
  parseWithSchema(label, switchFlowEdgeSchema, edge);
292
+ const cases = edge.switch.cases;
293
+ for (const [key, target] of Object.entries(cases)) parseWithSchema(`${label}: switch.cases.${key}`, z.string(), target);
167
294
  }
168
295
  function parseWithSchema(label, schema, value) {
169
296
  try {
@@ -414,25 +541,11 @@ function resolveShellActionTimeoutMs(timeoutMs) {
414
541
  if (timeoutMs == null || !(timeoutMs > 0)) return;
415
542
  return timeoutMs;
416
543
  }
417
- async function withShellAbort(run, signal) {
418
- signal.throwIfAborted();
419
- let rejectAbort = () => {};
420
- const aborted = new Promise((_resolve, reject) => {
421
- rejectAbort = reject;
422
- });
423
- const onAbort = () => rejectAbort(signal.reason);
424
- signal.addEventListener("abort", onAbort, { once: true });
425
- try {
426
- return await Promise.race([run(), aborted]);
427
- } finally {
428
- signal.removeEventListener("abort", onAbort);
429
- }
430
- }
431
544
  function rejectIfShellFailed(spec, args, result, timedOut, timeoutMs) {
432
545
  if (timedOut) return new TimeoutError(timeoutMs ?? spec.timeoutMs ?? 0);
433
546
  if (((result.exitCode ?? 0) !== 0 || result.signal != null) && spec.allowNonZeroExit !== true) return createShellFailureError(spec, args, result.exitCode, result.signal, result.stderr);
434
547
  }
435
- function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, termination) {
548
+ function waitForShellResult(child, spec, args, cwd, startMs, termination, mode) {
436
549
  const stdoutStream = child.stdout;
437
550
  const stderrStream = child.stderr;
438
551
  if (!stdoutStream || !stderrStream) throw new Error("Shell action child is missing stdio pipes");
@@ -442,7 +555,8 @@ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, terminatio
442
555
  settled = true;
443
556
  reject(error);
444
557
  };
445
- const capture = createShellOutputCapture(spec.maxBufferBytes, () => settled || termination.timedOut(), (error) => {
558
+ const capture = createShellOutputCapture(spec.maxBufferBytes, () => settled || mode === "node" && termination.cancelled(), (error) => {
559
+ if (termination.cancelled()) return;
446
560
  fail(error);
447
561
  termination.cancel("SIGTERM").catch(fail);
448
562
  });
@@ -455,10 +569,10 @@ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, terminatio
455
569
  capture.append("stderr", chunk);
456
570
  });
457
571
  child.once("error", fail);
458
- child.once("exit", (exitCode, signal) => {
572
+ child.once(mode === "node" ? "exit" : "close", (exitCode, signal) => {
459
573
  settled = true;
460
574
  const { stdout, stderr } = capture.output;
461
- const result = {
575
+ resolve({
462
576
  command: spec.command,
463
577
  args,
464
578
  cwd,
@@ -467,14 +581,9 @@ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, terminatio
467
581
  combinedOutput: `${stdout}${stderr}`,
468
582
  exitCode,
469
583
  signal,
470
- durationMs: Date.now() - startMs
471
- };
472
- const error = rejectIfShellFailed(spec, args, result, termination.timedOut(), timeoutMs);
473
- if (error) {
474
- reject(error);
475
- return;
476
- }
477
- resolve(result);
584
+ durationMs: Date.now() - startMs,
585
+ timedOut: mode === "node" ? termination.cancelled() : termination.timedOut()
586
+ });
478
587
  });
479
588
  });
480
589
  }
@@ -486,6 +595,7 @@ function createShellTermination(child, timeoutMs, options) {
486
595
  });
487
596
  let termination;
488
597
  let timedOut = false;
598
+ let cancelled = false;
489
599
  let released = false;
490
600
  let deadline;
491
601
  let monitor;
@@ -504,7 +614,8 @@ function createShellTermination(child, timeoutMs, options) {
504
614
  const cancel = (signal) => {
505
615
  if (termination) return termination;
506
616
  if (released) return Promise.resolve();
507
- timedOut = true;
617
+ clearDeadline();
618
+ cancelled = true;
508
619
  termination = stopShellProcess(child, closed, signal).finally(release);
509
620
  termination.catch(rejectCleanup);
510
621
  return termination;
@@ -514,6 +625,7 @@ function createShellTermination(child, timeoutMs, options) {
514
625
  };
515
626
  options.signal?.addEventListener("abort", onAbort, { once: true });
516
627
  if (timeoutMs != null) deadline = setTimeout(() => {
628
+ timedOut = true;
517
629
  cancel("SIGTERM");
518
630
  }, timeoutMs);
519
631
  unregister = options.registerOwner?.({
@@ -532,6 +644,7 @@ function createShellTermination(child, timeoutMs, options) {
532
644
  return {
533
645
  cancel,
534
646
  timedOut: () => timedOut,
647
+ cancelled: () => cancelled,
535
648
  cleanupFailure,
536
649
  async dispose() {
537
650
  clearDeadline();
@@ -544,7 +657,16 @@ function createShellTermination(child, timeoutMs, options) {
544
657
  };
545
658
  }
546
659
  async function runShellAction(spec, options = {}) {
547
- if (options?.signal?.aborted) throw options.signal.reason;
660
+ const { timedOut, ...result } = await runShellProcess(spec, options, "node");
661
+ const failure = rejectIfShellFailed(spec, result.args, result, timedOut, resolveShellActionTimeoutMs(spec.timeoutMs));
662
+ if (failure) throw failure;
663
+ return result;
664
+ }
665
+ async function runShellCommand(spec, options) {
666
+ return await runShellProcess(spec, options, "command");
667
+ }
668
+ async function runShellProcess(spec, options, mode) {
669
+ options.signal?.throwIfAborted();
548
670
  const cwd = spec.cwd ?? process.cwd();
549
671
  const args = spec.args ?? [];
550
672
  const startMs = Date.now();
@@ -566,47 +688,57 @@ async function runShellAction(spec, options = {}) {
566
688
  detached: process.platform !== "win32"
567
689
  });
568
690
  const termination = createShellTermination(child, timeoutMs, options);
569
- const finish = waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, termination);
691
+ const finish = waitForShellResult(child, spec, args, cwd, startMs, termination, mode);
570
692
  writeShellStdin(child, spec.stdin);
571
693
  try {
572
- return await Promise.race([finish, termination.cleanupFailure]);
694
+ const result = await Promise.race([finish, termination.cleanupFailure]);
695
+ throwIfShellCancelled(options.signal, mode);
696
+ return result;
573
697
  } finally {
574
698
  await termination.dispose();
575
699
  }
576
700
  }
701
+ function throwIfShellCancelled(signal, mode) {
702
+ if (!signal?.aborted) return;
703
+ const reason = signal.reason;
704
+ if (mode === "command" || reason instanceof TimeoutError || reason instanceof InterruptedError) throw reason;
705
+ }
577
706
  //#endregion
578
707
  //#region src/flows/graph.ts
579
708
  function validateFlowDefinition(flow) {
580
709
  assertValidFlowDefinitionShape(flow);
581
- if (!flow.nodes[flow.startAt]) throw new Error(`Flow start node is missing: ${flow.startAt}`);
710
+ assertKnownFlowNode(flow, flow.startAt, "Flow start node is missing");
582
711
  const outgoingEdges = /* @__PURE__ */ new Set();
583
712
  for (const edge of flow.edges) validateFlowEdge(flow, edge, outgoingEdges);
584
713
  }
585
714
  function assertKnownFlowNode(flow, nodeId, description) {
586
- if (!flow.nodes[nodeId]) throw new Error(`${description}: ${nodeId}`);
715
+ if (!Object.hasOwn(flow.nodes, nodeId)) throw new Error(`${description}: ${nodeId}`);
716
+ }
717
+ function isDirectEdge(edge) {
718
+ return Object.hasOwn(edge, "to");
587
719
  }
588
720
  function validateFlowEdge(flow, edge, outgoingEdges) {
589
721
  assertKnownFlowNode(flow, edge.from, "Flow edge references unknown from-node");
590
722
  if (outgoingEdges.has(edge.from)) throw new Error(`Flow node must not declare multiple outgoing edges: ${edge.from}`);
591
723
  outgoingEdges.add(edge.from);
592
- if ("to" in edge) {
724
+ if (isDirectEdge(edge)) {
593
725
  assertKnownFlowNode(flow, edge.to, "Flow edge references unknown to-node");
594
726
  return;
595
727
  }
596
728
  for (const target of Object.values(edge.switch.cases)) assertKnownFlowNode(flow, target, "Flow switch references unknown to-node");
597
729
  }
598
730
  function canFollowEdge(edge, result) {
599
- return !result || result.outcome === "ok" || "switch" in edge && edge.switch.on.startsWith("$result.");
731
+ return !result || result.outcome === "ok" || !isDirectEdge(edge) && edge.switch.on.startsWith("$result.");
600
732
  }
601
733
  function resolveNext(edges, from, output, result) {
602
734
  const edge = edges.find((candidate) => candidate.from === from);
603
735
  if (!edge || !canFollowEdge(edge, result)) return null;
604
- if ("to" in edge) return edge.to;
736
+ if (isDirectEdge(edge)) return edge.to;
605
737
  const value = getBySwitchPath(output, result, edge.switch.on);
606
738
  if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") throw new Error(`Flow switch value must be scalar for ${edge.switch.on}`);
607
- const next = edge.switch.cases[String(value)];
608
- if (!next) throw new Error(`No flow switch case for ${edge.switch.on}=${JSON.stringify(value)}`);
609
- return next;
739
+ const key = String(value);
740
+ if (!Object.hasOwn(edge.switch.cases, key)) throw new Error(`No flow switch case for ${edge.switch.on}=${JSON.stringify(value)}`);
741
+ return edge.switch.cases[key];
610
742
  }
611
743
  function getBySwitchPath(output, result, jsonPath) {
612
744
  if (jsonPath.startsWith("$result.")) return getByPath(result, `$.${jsonPath.slice(8)}`);
@@ -707,11 +839,19 @@ function summarizePrompt(promptText, explicitDetail) {
707
839
  }
708
840
  function createQuietCaptureOutput() {
709
841
  const chunks = [];
842
+ let sessionId;
843
+ const formatter = createOutputFormatter("quiet", { stdout: { write(chunk) {
844
+ chunks.push(chunk);
845
+ } } });
846
+ const setContext = formatter.setContext.bind(formatter);
847
+ formatter.setContext = (context) => {
848
+ sessionId = context.sessionId ?? sessionId;
849
+ setContext(context);
850
+ };
710
851
  return {
711
- formatter: createOutputFormatter("quiet", { stdout: { write(chunk) {
712
- chunks.push(chunk);
713
- } } }),
714
- read: () => chunks.join("").trim()
852
+ formatter,
853
+ read: () => chunks.join("").trim(),
854
+ sessionId: () => sessionId
715
855
  };
716
856
  }
717
857
  async function resolveFlowRunTitle(flow, input, flowPath) {
@@ -730,8 +870,13 @@ function normalizeFlowRunTitle(value) {
730
870
  function createRunId(flowName) {
731
871
  return `${isoNow$1().replaceAll(":", "").replaceAll(".", "")}-${slugifyAsciiIdPart(flowName)}-${randomUUID().slice(0, 8)}`;
732
872
  }
733
- function createSessionBindingKey(agentCommand, cwd, handle) {
734
- return `${agentCommand}::${cwd}::${handle}`;
873
+ function createSessionBindingKey(agentCommand, cwd, handle, agentArgv) {
874
+ return JSON.stringify([
875
+ agentCommand,
876
+ agentArgv ?? null,
877
+ cwd,
878
+ handle
879
+ ]);
735
880
  }
736
881
  function createSessionName(flowName, handle, cwd, runId) {
737
882
  return `${flowName}-${handle}-${stableShortHash(cwd)}-${runId.slice(-8)}`;
@@ -909,10 +1054,15 @@ var FlowRunStore = class {
909
1054
  traceSeqByRun = /* @__PURE__ */ new Map();
910
1055
  sessionSeqByBundle = /* @__PURE__ */ new Map();
911
1056
  manifestByRun = /* @__PURE__ */ new Map();
912
- appendChainByPath = /* @__PURE__ */ new Map();
1057
+ writeChainByPath = /* @__PURE__ */ new Map();
913
1058
  constructor(outputRoot = flowRunsBaseDir()) {
914
1059
  this.outputRoot = outputRoot;
915
1060
  }
1061
+ releaseRun(runDir) {
1062
+ this.traceSeqByRun.delete(runDir);
1063
+ this.manifestByRun.delete(runDir);
1064
+ for (const key of this.sessionSeqByBundle.keys()) if (key.startsWith(`${runDir}::`)) this.sessionSeqByBundle.delete(key);
1065
+ }
916
1066
  async createRunDir(runId) {
917
1067
  const runDir = path.join(this.outputRoot, runId);
918
1068
  await fs.mkdir(path.join(runDir, PROJECTIONS_DIR), {
@@ -1002,57 +1152,65 @@ var FlowRunStore = class {
1002
1152
  });
1003
1153
  return artifact;
1004
1154
  }
1005
- async ensureSessionBundle(runDir, state, binding, record) {
1155
+ async ensureSessionBundle(runDir, state, inputBinding, record) {
1156
+ const binding = structuredClone(inputBinding);
1006
1157
  const sessionDir = this.resolveRunPath(runDir, sessionDirPath(binding.bundleId));
1007
- await fs.mkdir(sessionDir, {
1008
- recursive: true,
1009
- mode: 448
1010
- });
1011
- await writePrivateJsonFile(path.join(sessionDir, "binding.json"), binding);
1012
- await ensureFile(path.join(sessionDir, "events.ndjson"));
1013
- if (record) await this.writeSessionRecord(runDir, state, binding, record);
1014
- const manifest = this.getManifest(runDir, state);
1015
- const isNew = !manifest.sessions.find((entry) => entry.id === binding.bundleId);
1016
- if (isNew) {
1017
- const entry = {
1018
- id: binding.bundleId,
1019
- handle: binding.handle,
1020
- bindingPath: path.posix.join(sessionDirPath(binding.bundleId), "binding.json"),
1021
- recordPath: path.posix.join(sessionDirPath(binding.bundleId), "record.json"),
1022
- eventsPath: path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson")
1023
- };
1024
- manifest.sessions.push(entry);
1025
- await writePrivateJsonFile(this.resolveRunPath(runDir, MANIFEST_PATH), manifest);
1026
- }
1027
- if (isNew) await this.appendTrace(runDir, state, {
1028
- scope: "session",
1029
- type: "session_bound",
1030
- sessionId: binding.bundleId,
1031
- payload: {
1032
- sessionId: binding.bundleId,
1033
- handle: binding.handle,
1034
- bindingArtifact: {
1035
- path: path.posix.join(sessionDirPath(binding.bundleId), "binding.json"),
1158
+ await this.serializeWrite(path.join(sessionDir, "binding.json"), async () => {
1159
+ await fs.mkdir(sessionDir, {
1160
+ recursive: true,
1161
+ mode: 448
1162
+ });
1163
+ await writePrivateJsonFile(path.join(sessionDir, "binding.json"), binding);
1164
+ await ensureFile(path.join(sessionDir, "events.ndjson"));
1165
+ if (record) await this.writeSessionRecord(runDir, state, binding, record);
1166
+ const manifest = this.getManifest(runDir, state);
1167
+ if (!manifest.sessions.find((entry) => entry.id === binding.bundleId)) {
1168
+ const bindingArtifact = await this.writeArtifact(runDir, state, binding, {
1036
1169
  mediaType: "application/json",
1037
- sha256: await fileSha256(path.join(sessionDir, "binding.json"))
1038
- }
1170
+ extension: "json",
1171
+ emitTrace: false
1172
+ });
1173
+ const entry = {
1174
+ id: binding.bundleId,
1175
+ handle: binding.handle,
1176
+ bindingPath: path.posix.join(sessionDirPath(binding.bundleId), "binding.json"),
1177
+ recordPath: path.posix.join(sessionDirPath(binding.bundleId), "record.json"),
1178
+ eventsPath: path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson")
1179
+ };
1180
+ manifest.sessions.push(entry);
1181
+ await writePrivateJsonFile(this.resolveRunPath(runDir, MANIFEST_PATH), manifest);
1182
+ await this.appendTrace(runDir, state, {
1183
+ scope: "session",
1184
+ type: "session_bound",
1185
+ sessionId: binding.bundleId,
1186
+ payload: {
1187
+ sessionId: binding.bundleId,
1188
+ handle: binding.handle,
1189
+ bindingArtifact
1190
+ }
1191
+ });
1039
1192
  }
1040
1193
  });
1041
1194
  }
1042
1195
  async writeSessionRecord(runDir, _state, binding, record) {
1043
- const bundledRecord = createBundledSessionRecord(binding, record, this.sessionSeqByBundle.get(`${runDir}::${binding.bundleId}`) ?? 0);
1196
+ const bundledRecord = createBundledSessionRecord(binding, record, this.sessionSeqByBundle.get(`${runDir}::${binding.bundleId}`)?.persisted ?? 0);
1044
1197
  await writePrivateJsonFile(this.resolveRunPath(runDir, path.posix.join(sessionDirPath(binding.bundleId), "record.json")), bundledRecord);
1045
1198
  }
1046
1199
  async appendSessionEvent(runDir, binding, direction, message) {
1047
1200
  const sessionKey = `${runDir}::${binding.bundleId}`;
1048
- const seq = (this.sessionSeqByBundle.get(sessionKey) ?? 0) + 1;
1049
- this.sessionSeqByBundle.set(sessionKey, seq);
1201
+ const sequence = this.sessionSeqByBundle.get(sessionKey) ?? {
1202
+ allocated: 0,
1203
+ persisted: 0
1204
+ };
1205
+ this.sessionSeqByBundle.set(sessionKey, sequence);
1206
+ const seq = ++sequence.allocated;
1050
1207
  await this.appendJsonLine(this.resolveRunPath(runDir, path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson")), {
1051
1208
  seq,
1052
1209
  at: isoNow(),
1053
1210
  direction,
1054
1211
  message
1055
1212
  });
1213
+ sequence.persisted = Math.max(sequence.persisted, seq);
1056
1214
  return seq;
1057
1215
  }
1058
1216
  getManifest(runDir, state) {
@@ -1083,7 +1241,7 @@ var FlowRunStore = class {
1083
1241
  return path.join(runDir, ...relativePath.split("/"));
1084
1242
  }
1085
1243
  async appendJsonLine(filePath, value) {
1086
- const tracked = (this.appendChainByPath.get(filePath) ?? Promise.resolve()).then(async () => {
1244
+ await this.serializeWrite(filePath, async () => {
1087
1245
  await fs.mkdir(path.dirname(filePath), {
1088
1246
  recursive: true,
1089
1247
  mode: 448
@@ -1093,10 +1251,13 @@ var FlowRunStore = class {
1093
1251
  content: `${JSON.stringify(value)}\n`,
1094
1252
  mode: 384
1095
1253
  });
1096
- }).finally(() => {
1097
- if (this.appendChainByPath.get(filePath) === tracked) this.appendChainByPath.delete(filePath);
1098
1254
  });
1099
- this.appendChainByPath.set(filePath, tracked);
1255
+ }
1256
+ async serializeWrite(filePath, write) {
1257
+ const tracked = (this.writeChainByPath.get(filePath) ?? Promise.resolve()).then(write).finally(() => {
1258
+ if (this.writeChainByPath.get(filePath) === tracked) this.writeChainByPath.delete(filePath);
1259
+ });
1260
+ this.writeChainByPath.set(filePath, tracked);
1100
1261
  await tracked;
1101
1262
  }
1102
1263
  };
@@ -1229,10 +1390,6 @@ async function ensureFile(filePath) {
1229
1390
  mode: 384
1230
1391
  });
1231
1392
  }
1232
- async function fileSha256(filePath) {
1233
- const payload = await fs.readFile(filePath);
1234
- return createHash("sha256").update(payload).digest("hex");
1235
- }
1236
1393
  function toArtifactBuffer(content, mediaType) {
1237
1394
  if (typeof content === "string") return Buffer.from(content, "utf8");
1238
1395
  if (Buffer.isBuffer(content)) return content;
@@ -1254,6 +1411,14 @@ function isoNow() {
1254
1411
  //#region src/flows/runtime.ts
1255
1412
  const DEFAULT_FLOW_HEARTBEAT_MS = 5e3;
1256
1413
  const DEFAULT_FLOW_STEP_TIMEOUT_MS = 9e5;
1414
+ function setNodeValue(values, nodeId, value) {
1415
+ Object.defineProperty(values, nodeId, {
1416
+ value,
1417
+ enumerable: true,
1418
+ configurable: true,
1419
+ writable: true
1420
+ });
1421
+ }
1257
1422
  var FlowRunner = class {
1258
1423
  resolveAgent;
1259
1424
  defaultCwd;
@@ -1264,7 +1429,8 @@ var FlowRunner = class {
1264
1429
  services;
1265
1430
  store;
1266
1431
  pendingPersistentSessionClients = /* @__PURE__ */ new Map();
1267
- shellInterrupts = /* @__PURE__ */ new Map();
1432
+ attempts = /* @__PURE__ */ new Map();
1433
+ pendingClientReleases = /* @__PURE__ */ new WeakMap();
1268
1434
  runInterruptions = /* @__PURE__ */ new Map();
1269
1435
  shellOwners = /* @__PURE__ */ new Map();
1270
1436
  constructor(options) {
@@ -1305,23 +1471,27 @@ var FlowRunner = class {
1305
1471
  steps: [],
1306
1472
  sessionBindings: {}
1307
1473
  };
1308
- const inputArtifact = await this.store.writeArtifact(runDir, state, input, {
1309
- mediaType: "application/json",
1310
- extension: "json",
1311
- emitTrace: false
1312
- });
1313
- await this.store.initializeRunBundle(runDir, {
1314
- flow,
1315
- state,
1316
- inputArtifact
1317
- });
1318
1474
  try {
1319
- return await this.runWithShellOwnership(flow, input, runDir, state);
1475
+ const inputArtifact = await this.store.writeArtifact(runDir, state, input, {
1476
+ mediaType: "application/json",
1477
+ extension: "json",
1478
+ emitTrace: false
1479
+ });
1480
+ await this.store.initializeRunBundle(runDir, {
1481
+ flow,
1482
+ state,
1483
+ inputArtifact
1484
+ });
1485
+ return await this.runWithOwnership(flow, input, runDir, state);
1320
1486
  } finally {
1321
- await this.closePendingPersistentSessionClients();
1487
+ try {
1488
+ await this.closePendingPersistentSessionClients(runDir);
1489
+ } finally {
1490
+ this.store.releaseRun(runDir);
1491
+ }
1322
1492
  }
1323
1493
  }
1324
- async runWithShellOwnership(flow, input, runDir, state) {
1494
+ async runWithOwnership(flow, input, runDir, state) {
1325
1495
  let execution;
1326
1496
  let cancellation;
1327
1497
  let interruption;
@@ -1334,11 +1504,8 @@ var FlowRunner = class {
1334
1504
  this.runInterruptions.set(runDir, reason);
1335
1505
  cancellation ??= this.cancelShellOwners(runDir, signal);
1336
1506
  cancellation.catch(() => {});
1337
- const interruptShell = this.shellInterrupts.get(runDir);
1338
- if (interruptShell) {
1339
- interruptShell(reason, signal);
1340
- await execution?.catch(() => {});
1341
- }
1507
+ this.attempts.get(runDir)?.cancel(reason, signal);
1508
+ await execution?.catch(() => {});
1342
1509
  }).then((value) => ({
1343
1510
  ok: true,
1344
1511
  value
@@ -1390,7 +1557,7 @@ var FlowRunner = class {
1390
1557
  let current = flow.startAt;
1391
1558
  const attemptCounts = /* @__PURE__ */ new Map();
1392
1559
  try {
1393
- while (current) {
1560
+ while (current !== null) {
1394
1561
  this.throwIfRunInterrupted(runDir);
1395
1562
  const step = await this.executeFlowStep(flow, input, runDir, state, current, attemptCounts);
1396
1563
  this.throwIfRunInterrupted(runDir, step.executionError);
@@ -1412,21 +1579,33 @@ var FlowRunner = class {
1412
1579
  if (interrupted) throw error ?? interrupted;
1413
1580
  }
1414
1581
  async executeFlowStep(flow, input, runDir, state, nodeId, attemptCounts) {
1582
+ if (!Object.hasOwn(flow.nodes, nodeId)) throw new Error(`Unknown flow node: ${nodeId}`);
1415
1583
  const node = flow.nodes[nodeId];
1416
- if (!node) throw new Error(`Unknown flow node: ${nodeId}`);
1417
1584
  const attemptId = nextAttemptId(attemptCounts, nodeId);
1418
1585
  const startedAt = isoNow$1();
1419
1586
  markNodeStarted(state, nodeId, attemptId, node.nodeType, startedAt, node.statusDetail);
1420
- await this.writeNodeStartedSnapshot(runDir, state, nodeId, attemptId, node);
1421
- const context = makeFlowNodeContext(state, input, this.services);
1587
+ const attempt = new FlowAttempt({
1588
+ nodeId,
1589
+ attemptId,
1590
+ startedAt,
1591
+ timeoutMs: node.timeoutMs ?? this.defaultNodeTimeoutMs
1592
+ });
1593
+ this.attempts.set(runDir, attempt);
1594
+ const context = this.makeAttemptContext(runDir, state, input, node, attempt);
1595
+ let stopHeartbeat = () => {};
1422
1596
  let executed;
1423
1597
  let outcome = "ok";
1424
1598
  let executionError;
1425
1599
  try {
1426
1600
  this.throwIfRunInterrupted(runDir);
1427
- executed = await this.executeNode(runDir, state, flow, nodeId, node, context);
1601
+ executed = await attempt.run(async () => {
1602
+ await attempt.own(() => this.writeNodeStartedSnapshot(runDir, state, nodeId, attemptId, node));
1603
+ stopHeartbeat = this.startHeartbeat(runDir, state, node, attempt);
1604
+ const result = await this.executeNode(runDir, state, flow, nodeId, node, context);
1605
+ result.trace = await attempt.own(() => finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, result.output, result.trace));
1606
+ return result;
1607
+ });
1428
1608
  this.throwIfRunInterrupted(runDir);
1429
- executed.trace = await finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, executed.output, executed.trace);
1430
1609
  } catch (error) {
1431
1610
  outcome = outcomeForError(error);
1432
1611
  executionError = error;
@@ -1436,8 +1615,12 @@ var FlowRunner = class {
1436
1615
  rawText: null,
1437
1616
  sessionInfo: null,
1438
1617
  agentInfo: null,
1439
- trace: await finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, void 0, extractAttachedStepTrace(error) ?? null)
1618
+ ...context.acpResult,
1619
+ trace: await finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, void 0, context.acpResult ? context.acpResult.trace : extractAttachedStepTrace(error) ?? null)
1440
1620
  };
1621
+ } finally {
1622
+ stopHeartbeat();
1623
+ this.attempts.delete(runDir);
1441
1624
  }
1442
1625
  const nodeResult = createNodeResult({
1443
1626
  attemptId,
@@ -1448,7 +1631,7 @@ var FlowRunner = class {
1448
1631
  finishedAt: isoNow$1(),
1449
1632
  ...outcome === "ok" ? { output: executed.output } : { error: executionError instanceof Error ? executionError.message : String(executionError) }
1450
1633
  });
1451
- state.results[nodeId] = nodeResult;
1634
+ setNodeValue(state.results, nodeId, nodeResult);
1452
1635
  return {
1453
1636
  ...executed,
1454
1637
  nodeResult,
@@ -1460,6 +1643,17 @@ var FlowRunner = class {
1460
1643
  state
1461
1644
  };
1462
1645
  }
1646
+ makeAttemptContext(runDir, state, input, node, attempt) {
1647
+ const context = {
1648
+ nodeContext: {
1649
+ ...makeFlowNodeContext(state, input, this.services),
1650
+ signal: attempt.signal
1651
+ },
1652
+ attempt
1653
+ };
1654
+ if (node.nodeType === "action" && "run" in node) context.nodeContext.runShell = (execution) => this.runCallbackShell(runDir, attempt, execution);
1655
+ return context;
1656
+ }
1463
1657
  async writeNodeStartedSnapshot(runDir, state, nodeId, attemptId, node) {
1464
1658
  await this.store.writeSnapshot(runDir, state, {
1465
1659
  scope: "node",
@@ -1475,7 +1669,7 @@ var FlowRunner = class {
1475
1669
  }
1476
1670
  async maybeCompleteCheckpointStep(runDir, state, step) {
1477
1671
  if (step.nodeResult.outcome !== "ok" || step.node.nodeType !== "checkpoint") return;
1478
- state.outputs[step.nodeId] = step.output;
1672
+ setNodeValue(state.outputs, step.nodeId, step.output);
1479
1673
  state.waitingOn = step.nodeId;
1480
1674
  state.updatedAt = isoNow$1();
1481
1675
  state.status = "waiting";
@@ -1487,11 +1681,11 @@ var FlowRunner = class {
1487
1681
  }
1488
1682
  resolveNextNode(flow, step) {
1489
1683
  if (step.nodeResult.outcome === "ok") {
1490
- step.state.outputs[step.nodeId] = step.output;
1684
+ setNodeValue(step.state.outputs, step.nodeId, step.output);
1491
1685
  return resolveNext(flow.edges, step.nodeId, step.output, step.nodeResult);
1492
1686
  }
1493
1687
  const next = resolveNext(flow.edges, step.nodeId, void 0, step.nodeResult);
1494
- if (next) return next;
1688
+ if (next !== null) return next;
1495
1689
  throw step.executionError;
1496
1690
  }
1497
1691
  async completeFlowRun(runDir, state) {
@@ -1537,19 +1731,22 @@ var FlowRunner = class {
1537
1731
  }
1538
1732
  async executeNode(runDir, state, flow, nodeId, node, context) {
1539
1733
  switch (node.nodeType) {
1540
- case "compute": return await this.executeCallbackNode(runDir, state, nodeId, node, context);
1541
- case "action": return "run" in node ? await this.executeCallbackNode(runDir, state, nodeId, node, context) : await this.executeShellNode(runDir, state, node, context);
1542
- case "checkpoint": return await this.executeCallbackNode(runDir, state, nodeId, node, context);
1734
+ case "compute": return await this.executeCallbackNode(nodeId, node, context);
1735
+ case "action": return "run" in node ? await this.executeCallbackNode(nodeId, node, context) : await this.executeShellNode(runDir, state, node, context);
1736
+ case "checkpoint": return await this.executeCallbackNode(nodeId, node, context);
1543
1737
  case "acp": return await this.executeAcpNode(runDir, state, flow, node, context);
1544
1738
  default: throw new Error(`Unsupported flow node: ${String(node)}`);
1545
1739
  }
1546
1740
  }
1547
- async executeCallbackNode(runDir, state, nodeId, node, context) {
1741
+ async executeCallbackNode(nodeId, node, context) {
1742
+ context.attempt.assertActive();
1743
+ const output = node.nodeType === "checkpoint" && typeof node.run !== "function" ? {
1744
+ checkpoint: nodeId,
1745
+ summary: node.summary ?? nodeId
1746
+ } : await node.run?.(context.nodeContext);
1747
+ context.attempt.assertActive();
1548
1748
  return {
1549
- output: node.nodeType === "checkpoint" && typeof node.run !== "function" ? {
1550
- checkpoint: nodeId,
1551
- summary: node.summary ?? nodeId
1552
- } : await this.runWithHeartbeat(runDir, state, nodeId, node, node.timeoutMs ?? this.defaultNodeTimeoutMs, async () => await node.run?.(context)),
1749
+ output,
1553
1750
  promptText: null,
1554
1751
  rawText: null,
1555
1752
  sessionInfo: null,
@@ -1558,186 +1755,173 @@ var FlowRunner = class {
1558
1755
  };
1559
1756
  }
1560
1757
  async executeShellNode(runDir, state, node, context) {
1561
- const nodeTimeoutMs = node.timeoutMs ?? this.defaultNodeTimeoutMs;
1562
- const shellAbort = new AbortController();
1563
- let runningOwner;
1564
- const shellControl = {
1565
- signal: shellAbort.signal,
1566
- registerOwner: (owner) => {
1567
- runningOwner = owner;
1568
- return this.registerShellOwner(runDir, owner);
1569
- }
1758
+ const attempt = context.attempt;
1759
+ const execution = await node.exec(context.nodeContext);
1760
+ attempt.assertActive();
1761
+ const effectiveExecution = {
1762
+ ...execution,
1763
+ cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd),
1764
+ timeoutMs: resolveShellActionTimeoutMs(execution.timeoutMs ?? attempt.remainingTimeoutMs())
1570
1765
  };
1571
- this.shellInterrupts.set(runDir, (reason, signal) => {
1572
- shellControl.terminationSignal = signal;
1573
- shellAbort.abort(reason);
1574
- });
1575
- let runningShell;
1576
- const { output, rawText, trace } = await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, () => withShellAbort(async () => {
1577
- const execution = await Promise.resolve(node.exec(context));
1578
- shellAbort.signal.throwIfAborted();
1579
- const effectiveExecution = {
1580
- ...execution,
1581
- cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd),
1582
- timeoutMs: resolveShellActionTimeoutMs(execution.timeoutMs ?? nodeTimeoutMs)
1583
- };
1584
- updateStatusDetail(state, formatShellActionSummary(effectiveExecution));
1585
- await this.store.writeLive(runDir, state, {
1586
- scope: "node",
1587
- type: "node_heartbeat",
1588
- nodeId: state.currentNode,
1589
- attemptId: state.currentAttemptId,
1590
- payload: { statusDetail: state.statusDetail }
1591
- });
1592
- shellAbort.signal.throwIfAborted();
1593
- await this.store.appendTrace(runDir, state, {
1594
- scope: "action",
1595
- type: "action_prepared",
1596
- nodeId: state.currentNode,
1597
- attemptId: state.currentAttemptId,
1598
- payload: { action: {
1599
- actionType: "shell",
1600
- command: effectiveExecution.command,
1601
- args: effectiveExecution.args ?? [],
1602
- cwd: effectiveExecution.cwd
1603
- } }
1604
- });
1605
- runningShell = runShellAction(effectiveExecution, shellControl);
1606
- const result = await runningShell;
1607
- shellAbort.signal.throwIfAborted();
1608
- const stdoutArtifact = await this.store.writeArtifact(runDir, state, result.stdout, {
1609
- mediaType: "text/plain",
1610
- extension: "txt",
1611
- nodeId: state.currentNode,
1612
- attemptId: state.currentAttemptId
1613
- });
1614
- const stderrArtifact = await this.store.writeArtifact(runDir, state, result.stderr, {
1615
- mediaType: "text/plain",
1616
- extension: "txt",
1617
- nodeId: state.currentNode,
1618
- attemptId: state.currentAttemptId
1619
- });
1620
- shellAbort.signal.throwIfAborted();
1621
- await this.store.appendTrace(runDir, state, {
1622
- scope: "action",
1623
- type: "action_completed",
1624
- nodeId: state.currentNode,
1625
- attemptId: state.currentAttemptId,
1626
- payload: {
1627
- action: {
1628
- actionType: "shell",
1629
- command: result.command,
1630
- args: result.args,
1631
- cwd: result.cwd,
1632
- exitCode: result.exitCode,
1633
- signal: result.signal,
1634
- durationMs: result.durationMs
1635
- },
1636
- stdoutArtifact,
1637
- stderrArtifact
1638
- }
1639
- });
1640
- const trace = {
1641
- action: {
1642
- actionType: "shell",
1643
- command: result.command,
1644
- args: result.args,
1645
- cwd: result.cwd,
1646
- exitCode: result.exitCode,
1647
- signal: result.signal,
1648
- durationMs: result.durationMs
1649
- },
1766
+ updateStatusDetail(state, formatShellActionSummary(effectiveExecution));
1767
+ await attempt.own(() => this.store.writeLive(runDir, state, {
1768
+ scope: "node",
1769
+ type: "node_heartbeat",
1770
+ nodeId: attempt.nodeId,
1771
+ attemptId: attempt.attemptId,
1772
+ payload: { statusDetail: state.statusDetail }
1773
+ }));
1774
+ await attempt.own(() => this.store.appendTrace(runDir, state, {
1775
+ scope: "action",
1776
+ type: "action_prepared",
1777
+ nodeId: attempt.nodeId,
1778
+ attemptId: attempt.attemptId,
1779
+ payload: { action: {
1780
+ actionType: "shell",
1781
+ command: effectiveExecution.command,
1782
+ args: effectiveExecution.args ?? [],
1783
+ cwd: effectiveExecution.cwd
1784
+ } }
1785
+ }));
1786
+ const result = await attempt.own(() => runShellAction(effectiveExecution, this.shellControl(runDir, attempt)));
1787
+ const stdoutArtifact = await attempt.own(() => this.store.writeArtifact(runDir, state, result.stdout, {
1788
+ mediaType: "text/plain",
1789
+ extension: "txt",
1790
+ nodeId: attempt.nodeId,
1791
+ attemptId: attempt.attemptId
1792
+ }));
1793
+ const stderrArtifact = await attempt.own(() => this.store.writeArtifact(runDir, state, result.stderr, {
1794
+ mediaType: "text/plain",
1795
+ extension: "txt",
1796
+ nodeId: attempt.nodeId,
1797
+ attemptId: attempt.attemptId
1798
+ }));
1799
+ const actionTrace = {
1800
+ actionType: "shell",
1801
+ command: result.command,
1802
+ args: result.args,
1803
+ cwd: result.cwd,
1804
+ exitCode: result.exitCode,
1805
+ signal: result.signal,
1806
+ durationMs: result.durationMs
1807
+ };
1808
+ await attempt.own(() => this.store.appendTrace(runDir, state, {
1809
+ scope: "action",
1810
+ type: "action_completed",
1811
+ nodeId: attempt.nodeId,
1812
+ attemptId: attempt.attemptId,
1813
+ payload: {
1814
+ action: actionTrace,
1650
1815
  stdoutArtifact,
1651
1816
  stderrArtifact
1652
- };
1653
- shellAbort.signal.throwIfAborted();
1654
- let parsedOutput;
1655
- try {
1656
- parsedOutput = node.parse ? await node.parse(result, context) : result;
1657
- shellAbort.signal.throwIfAborted();
1658
- } catch (error) {
1659
- throw attachStepTrace(error, trace);
1660
- }
1661
- return {
1662
- output: parsedOutput,
1663
- rawText: result.combinedOutput,
1664
- trace
1665
- };
1666
- }, shellAbort.signal)).catch(async (error) => {
1667
- shellAbort.abort(error);
1668
- try {
1669
- await runningOwner?.cancel(shellControl.terminationSignal ?? "SIGTERM");
1670
- await runningShell;
1671
- } catch (cleanupError) {
1672
- if (!(cleanupError instanceof TimeoutError) && cleanupError !== error) throw new AggregateError([error, cleanupError], "Shell action cleanup failed", { cause: cleanupError });
1673
1817
  }
1674
- throw error;
1675
- }).finally(() => {
1676
- this.shellInterrupts.delete(runDir);
1677
- });
1818
+ }));
1819
+ const trace = {
1820
+ action: actionTrace,
1821
+ stdoutArtifact,
1822
+ stderrArtifact
1823
+ };
1824
+ let output;
1825
+ try {
1826
+ output = node.parse ? await node.parse(result, context.nodeContext) : result;
1827
+ attempt.assertActive();
1828
+ } catch (error) {
1829
+ throw attachStepTrace(error, trace);
1830
+ }
1678
1831
  return {
1679
1832
  output,
1680
1833
  promptText: null,
1681
- rawText,
1834
+ rawText: result.combinedOutput,
1682
1835
  sessionInfo: null,
1683
1836
  agentInfo: null,
1684
1837
  trace
1685
1838
  };
1686
1839
  }
1840
+ shellControl(runDir, attempt) {
1841
+ return {
1842
+ signal: attempt.signal,
1843
+ get terminationSignal() {
1844
+ return attempt.terminationSignal;
1845
+ },
1846
+ registerOwner: (owner) => {
1847
+ const releaseAttempt = attempt.registerCancellation((signal) => owner.cancel(signal));
1848
+ const releaseRun = this.registerShellOwner(runDir, owner);
1849
+ return () => {
1850
+ releaseAttempt();
1851
+ releaseRun();
1852
+ };
1853
+ }
1854
+ };
1855
+ }
1856
+ runCallbackShell(runDir, attempt, execution) {
1857
+ return attempt.own(() => runShellCommand({
1858
+ ...execution,
1859
+ cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd)
1860
+ }, this.shellControl(runDir, attempt)));
1861
+ }
1687
1862
  async executeAcpNode(runDir, state, flow, node, context) {
1688
- const nodeTimeoutMs = node.timeoutMs ?? this.defaultNodeTimeoutMs;
1689
- let boundSession = null;
1690
- return await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => {
1691
- const prepared = await this.prepareAcpPrompt(runDir, state, node, context, nodeTimeoutMs);
1692
- if (node.session?.isolated) return await this.executeIsolatedAcpPrompt(runDir, state, flow, node, context, prepared);
1693
- boundSession = await this.ensureSessionBinding(runDir, state, flow, node, prepared.agentInfo, nodeTimeoutMs);
1694
- return await this.executePersistentAcpPrompt(runDir, state, node, context, prepared, boundSession);
1695
- }, async () => {
1696
- if (!boundSession) return;
1697
- await cancelSessionPrompt({ sessionId: boundSession.acpxRecordId });
1698
- });
1863
+ const attempt = context.attempt;
1864
+ const prepared = await this.prepareAcpPrompt(runDir, state, node, context);
1865
+ if (node.session?.isolated) return await this.executeIsolatedAcpPrompt(runDir, state, flow, node, context, prepared);
1866
+ const boundSession = await this.ensureSessionBinding(runDir, state, flow, node, prepared.agentInfo, attempt);
1867
+ return await this.executePersistentAcpPrompt(runDir, state, node, context, prepared, boundSession);
1699
1868
  }
1700
- async prepareAcpPrompt(runDir, state, node, context, nodeTimeoutMs) {
1869
+ async prepareAcpPrompt(runDir, state, node, context) {
1701
1870
  const resolvedAgent = this.resolveAgent(node.profile);
1702
1871
  const agentInfo = {
1703
1872
  ...resolvedAgent,
1704
- cwd: await resolveNodeCwd(resolvedAgent.cwd, node.cwd, context)
1873
+ agentArgv: resolvedAgent.agentArgv?.slice(),
1874
+ cwd: await resolveNodeCwd(resolvedAgent.cwd, node.cwd, context.nodeContext)
1705
1875
  };
1706
- const prompt = normalizePromptInput(await Promise.resolve(node.prompt(context)));
1876
+ context.attempt.assertActive();
1877
+ const prompt = normalizePromptInput(await Promise.resolve(node.prompt(context.nodeContext)));
1878
+ context.attempt.assertActive();
1707
1879
  const promptText = promptToDisplayText(prompt);
1880
+ const result = {
1881
+ output: void 0,
1882
+ promptText,
1883
+ rawText: null,
1884
+ sessionInfo: null,
1885
+ agentInfo,
1886
+ trace: null
1887
+ };
1888
+ context.acpResult = result;
1708
1889
  updateStatusDetail(state, summarizePrompt(promptText, node.statusDetail));
1709
- await this.writeAcpPromptHeartbeat(runDir, state);
1890
+ await context.attempt.own(() => this.writeAcpPromptHeartbeat(runDir, state, context.attempt));
1891
+ const promptArtifact = await context.attempt.own(() => this.store.writeArtifact(runDir, state, promptText, {
1892
+ mediaType: "text/plain",
1893
+ extension: "txt",
1894
+ nodeId: context.attempt.nodeId,
1895
+ attemptId: context.attempt.attemptId
1896
+ }));
1897
+ result.trace = { promptArtifact };
1710
1898
  return {
1711
1899
  agentInfo,
1712
1900
  prompt,
1713
- promptText,
1714
- promptArtifact: await this.store.writeArtifact(runDir, state, promptText, {
1715
- mediaType: "text/plain",
1716
- extension: "txt",
1717
- nodeId: state.currentNode,
1718
- attemptId: state.currentAttemptId
1719
- }),
1720
- nodeTimeoutMs
1901
+ promptArtifact,
1902
+ attempt: context.attempt,
1903
+ result
1721
1904
  };
1722
1905
  }
1723
- async writeAcpPromptHeartbeat(runDir, state) {
1906
+ async writeAcpPromptHeartbeat(runDir, state, attempt) {
1724
1907
  await this.store.writeLive(runDir, state, {
1725
1908
  scope: "node",
1726
1909
  type: "node_heartbeat",
1727
- nodeId: state.currentNode,
1728
- attemptId: state.currentAttemptId,
1910
+ nodeId: attempt.nodeId,
1911
+ attemptId: attempt.attemptId,
1729
1912
  payload: { statusDetail: state.statusDetail }
1730
1913
  });
1731
1914
  }
1732
1915
  async executeIsolatedAcpPrompt(runDir, state, flow, node, context, prepared) {
1733
- const binding = createIsolatedSessionBinding(flow.name, state.runId, state.currentAttemptId ?? randomUUID(), node.profile, prepared.agentInfo);
1734
- await this.initializeIsolatedSessionBundle(runDir, state, binding);
1735
- await this.appendAcpPromptPreparedTrace(runDir, state, binding, prepared.promptArtifact);
1736
- const prompt = await this.runIsolatedPrompt(runDir, state, binding, prepared.agentInfo, prepared.prompt, prepared.nodeTimeoutMs);
1737
- return await this.finishAcpPrompt(runDir, state, node, context, prepared, prompt, binding);
1738
- }
1739
- async initializeIsolatedSessionBundle(runDir, state, binding) {
1740
- const timestamp = state.currentNodeStartedAt ?? isoNow$1();
1916
+ const binding = createIsolatedSessionBinding(flow.name, state.runId, prepared.attempt.attemptId, node.profile, prepared.agentInfo);
1917
+ prepared.result.sessionInfo = binding;
1918
+ await prepared.attempt.own(() => this.initializeIsolatedSessionBundle(runDir, state, binding, prepared.attempt));
1919
+ await prepared.attempt.own(() => this.appendAcpPromptPreparedTrace(runDir, state, binding, prepared.promptArtifact, prepared.attempt));
1920
+ const prompt = await prepared.attempt.own(() => this.runIsolatedPrompt(runDir, state, binding, prepared));
1921
+ return await this.finishAcpPrompt(runDir, state, node, context, prepared, prompt);
1922
+ }
1923
+ async initializeIsolatedSessionBundle(runDir, state, binding, attempt) {
1924
+ const timestamp = attempt.startedAt;
1741
1925
  const initialRecord = createSyntheticSessionRecord({
1742
1926
  binding,
1743
1927
  createdAt: timestamp,
@@ -1749,16 +1933,17 @@ var FlowRunner = class {
1749
1933
  await this.store.ensureSessionBundle(runDir, state, binding, initialRecord);
1750
1934
  }
1751
1935
  async executePersistentAcpPrompt(runDir, state, node, context, prepared, binding) {
1752
- await this.appendAcpPromptPreparedTrace(runDir, state, binding, prepared.promptArtifact);
1753
- const prompt = await this.runPersistentPrompt(runDir, state, binding, prepared.prompt, prepared.nodeTimeoutMs);
1754
- return await this.finishAcpPrompt(runDir, state, node, context, prepared, prompt, prompt.sessionInfo);
1936
+ prepared.result.sessionInfo = binding;
1937
+ await prepared.attempt.own(() => this.appendAcpPromptPreparedTrace(runDir, state, binding, prepared.promptArtifact, prepared.attempt));
1938
+ const prompt = await prepared.attempt.own(() => this.runPersistentPrompt(runDir, state, binding, prepared));
1939
+ return await this.finishAcpPrompt(runDir, state, node, context, prepared, prompt);
1755
1940
  }
1756
- async appendAcpPromptPreparedTrace(runDir, state, binding, promptArtifact) {
1941
+ async appendAcpPromptPreparedTrace(runDir, state, binding, promptArtifact, attempt) {
1757
1942
  await this.store.appendTrace(runDir, state, {
1758
1943
  scope: "acp",
1759
1944
  type: "acp_prompt_prepared",
1760
- nodeId: state.currentNode,
1761
- attemptId: state.currentAttemptId,
1945
+ nodeId: attempt.nodeId,
1946
+ attemptId: attempt.attemptId,
1762
1947
  sessionId: binding.bundleId,
1763
1948
  payload: {
1764
1949
  sessionId: binding.bundleId,
@@ -1766,107 +1951,121 @@ var FlowRunner = class {
1766
1951
  }
1767
1952
  });
1768
1953
  }
1769
- async finishAcpPrompt(runDir, state, node, context, prepared, prompt, sessionInfo) {
1770
- const rawResponseArtifact = await this.writeAcpRawResponseArtifact(runDir, state, prompt, sessionInfo);
1771
- await this.appendAcpResponseParsedTrace(runDir, state, prompt, sessionInfo, rawResponseArtifact);
1954
+ async finishAcpPrompt(runDir, state, node, context, prepared, prompt) {
1955
+ await prepared.attempt.own(() => this.appendAcpResponseParsedTrace(runDir, state, prompt, prepared.attempt));
1956
+ const output = await this.parseAcpOutput(node, context, prompt.rawText);
1957
+ return {
1958
+ ...prepared.result,
1959
+ output
1960
+ };
1961
+ }
1962
+ async publishAcpCapture(runDir, state, prepared, sessionInfo, record, messageStart, rawText, events) {
1963
+ const result = prepared.result;
1964
+ result.sessionInfo = sessionInfo;
1772
1965
  const trace = {
1773
1966
  sessionId: sessionInfo.bundleId,
1774
- promptArtifact: prepared.promptArtifact,
1775
- rawResponseArtifact,
1776
- conversation: prompt.conversation
1967
+ promptArtifact: prepared.promptArtifact
1777
1968
  };
1778
- return {
1779
- output: await this.parseAcpOutput(node, context, prompt.rawText, trace),
1780
- promptText: prepared.promptText,
1781
- rawText: prompt.rawText,
1782
- sessionInfo,
1783
- agentInfo: prepared.agentInfo,
1784
- trace
1969
+ result.trace = trace;
1970
+ await this.store.ensureSessionBundle(runDir, state, sessionInfo);
1971
+ await this.store.writeSessionRecord(runDir, state, sessionInfo, record);
1972
+ if (events) trace.conversation = {
1973
+ sessionId: sessionInfo.bundleId,
1974
+ messageStart,
1975
+ messageEnd: Math.max(messageStart, record.messages.length - 1),
1976
+ ...events
1785
1977
  };
1786
- }
1787
- async writeAcpRawResponseArtifact(runDir, state, prompt, sessionInfo) {
1788
- return await this.store.writeArtifact(runDir, state, prompt.rawText, {
1978
+ const rawResponseArtifact = await this.store.writeArtifact(runDir, state, rawText, {
1789
1979
  mediaType: "text/plain",
1790
1980
  extension: "txt",
1791
- nodeId: state.currentNode,
1792
- attemptId: state.currentAttemptId,
1981
+ nodeId: prepared.attempt.nodeId,
1982
+ attemptId: prepared.attempt.attemptId,
1793
1983
  sessionId: sessionInfo.bundleId
1794
1984
  });
1985
+ trace.rawResponseArtifact = rawResponseArtifact;
1986
+ return {
1987
+ rawText,
1988
+ sessionInfo,
1989
+ conversation: trace.conversation,
1990
+ rawResponseArtifact
1991
+ };
1795
1992
  }
1796
- async appendAcpResponseParsedTrace(runDir, state, prompt, sessionInfo, rawResponseArtifact) {
1993
+ async appendAcpResponseParsedTrace(runDir, state, prompt, attempt) {
1797
1994
  await this.store.appendTrace(runDir, state, {
1798
1995
  scope: "acp",
1799
1996
  type: "acp_response_parsed",
1800
- nodeId: state.currentNode,
1801
- attemptId: state.currentAttemptId,
1802
- sessionId: sessionInfo.bundleId,
1997
+ nodeId: attempt.nodeId,
1998
+ attemptId: attempt.attemptId,
1999
+ sessionId: prompt.sessionInfo.bundleId,
1803
2000
  payload: {
1804
- sessionId: sessionInfo.bundleId,
2001
+ sessionId: prompt.sessionInfo.bundleId,
1805
2002
  conversation: prompt.conversation,
1806
- rawResponseArtifact
2003
+ rawResponseArtifact: prompt.rawResponseArtifact
1807
2004
  }
1808
2005
  });
1809
2006
  }
1810
- async parseAcpOutput(node, context, rawText, trace) {
1811
- try {
1812
- return node.parse ? await node.parse(rawText, context) : rawText;
1813
- } catch (error) {
1814
- throw attachStepTrace(error, trace);
1815
- }
2007
+ async parseAcpOutput(node, context, rawText) {
2008
+ context.attempt.assertActive();
2009
+ const output = node.parse ? await node.parse(rawText, context.nodeContext) : rawText;
2010
+ context.attempt.assertActive();
2011
+ return output;
1816
2012
  }
1817
- async runWithHeartbeat(runDir, state, nodeId, node, timeoutMs, run, onTimeout) {
2013
+ startHeartbeat(runDir, state, node, attempt) {
1818
2014
  const heartbeatMs = Math.max(0, Math.round(node.heartbeatMs ?? DEFAULT_FLOW_HEARTBEAT_MS));
1819
- let timer;
1820
- let active = true;
1821
- let heartbeatPending = false;
2015
+ if (heartbeatMs === 0) return () => {};
2016
+ let pending = false;
1822
2017
  const heartbeat = async () => {
1823
- if (!active || heartbeatPending) return;
1824
- heartbeatPending = true;
2018
+ if (!attempt.active || pending) return;
2019
+ pending = true;
1825
2020
  try {
1826
- state.lastHeartbeatAt = isoNow$1();
1827
- state.updatedAt = state.lastHeartbeatAt;
1828
- await this.store.writeLive(runDir, state, {
1829
- scope: "node",
1830
- type: "node_heartbeat",
1831
- nodeId,
1832
- attemptId: state.currentAttemptId,
1833
- payload: { statusDetail: state.statusDetail }
1834
- });
1835
- } finally {
1836
- heartbeatPending = false;
2021
+ await attempt.own(async () => {
2022
+ state.lastHeartbeatAt = isoNow$1();
2023
+ state.updatedAt = state.lastHeartbeatAt;
2024
+ await this.store.writeLive(runDir, state, {
2025
+ scope: "node",
2026
+ type: "node_heartbeat",
2027
+ nodeId: attempt.nodeId,
2028
+ attemptId: attempt.attemptId,
2029
+ payload: { statusDetail: state.statusDetail }
2030
+ });
2031
+ }, { bestEffort: true });
2032
+ } catch {} finally {
2033
+ pending = false;
1837
2034
  }
1838
2035
  };
1839
- if (heartbeatMs > 0) timer = setInterval(() => {
1840
- heartbeat().catch(() => {});
2036
+ const timer = setInterval(() => {
2037
+ heartbeat();
1841
2038
  }, heartbeatMs);
1842
- try {
1843
- return await withTimeout(run(), timeoutMs);
1844
- } catch (error) {
1845
- if (error instanceof TimeoutError && onTimeout) await onTimeout().catch(() => {});
1846
- throw error;
1847
- } finally {
1848
- active = false;
1849
- if (timer) clearInterval(timer);
1850
- }
2039
+ return () => clearInterval(timer);
1851
2040
  }
1852
- async ensureSessionBinding(runDir, state, flow, node, agent, timeoutMs) {
2041
+ async ensureSessionBinding(runDir, state, flow, node, agent, attempt) {
1853
2042
  const handle = node.session?.handle ?? "main";
1854
- const key = createSessionBindingKey(agent.agentCommand, agent.cwd, handle);
2043
+ const key = createSessionBindingKey(agent.agentCommand, agent.cwd, handle, agent.agentArgv);
1855
2044
  const existing = state.sessionBindings[key];
1856
2045
  if (existing) {
1857
- await this.store.ensureSessionBundle(runDir, state, existing);
2046
+ await attempt.own(() => this.store.ensureSessionBundle(runDir, state, existing));
1858
2047
  return existing;
1859
2048
  }
1860
2049
  const name = createSessionName(flow.name, handle, agent.cwd, state.runId);
1861
- const created = await createSessionWithClient({
1862
- agentCommand: agent.agentCommand,
1863
- agentArgv: agent.agentArgv,
1864
- cwd: agent.cwd,
1865
- name,
1866
- ...this.connectionOptions,
1867
- timeoutMs,
1868
- sessionOptions: this.sessionOptions
2050
+ const created = await attempt.own(async () => {
2051
+ const acquired = await createSessionWithClient({
2052
+ agentCommand: agent.agentCommand,
2053
+ agentArgv: agent.agentArgv,
2054
+ cwd: agent.cwd,
2055
+ name,
2056
+ ...this.connectionOptions,
2057
+ signal: attempt.signal,
2058
+ handleProcessInterrupts: false,
2059
+ sessionOptions: this.sessionOptions
2060
+ });
2061
+ this.pendingClientReleases.set(acquired.client, attempt.registerCancellation(async () => {
2062
+ const clients = this.pendingPersistentSessionClients.get(runDir);
2063
+ if (clients?.get(key) === acquired.client) clients.delete(key);
2064
+ await acquired.client.close();
2065
+ }));
2066
+ return acquired;
1869
2067
  });
2068
+ attempt.assertActive();
1870
2069
  const binding = {
1871
2070
  key,
1872
2071
  handle,
@@ -1882,31 +2081,44 @@ var FlowRunner = class {
1882
2081
  agentSessionId: created.record.agentSessionId
1883
2082
  };
1884
2083
  state.sessionBindings[key] = binding;
1885
- this.pendingPersistentSessionClients.set(binding.key, created.client);
1886
- await this.store.ensureSessionBundle(runDir, state, binding, created.record);
2084
+ let clients = this.pendingPersistentSessionClients.get(runDir);
2085
+ if (!clients) {
2086
+ clients = /* @__PURE__ */ new Map();
2087
+ this.pendingPersistentSessionClients.set(runDir, clients);
2088
+ }
2089
+ clients.set(binding.key, created.client);
2090
+ await attempt.own(() => this.store.ensureSessionBundle(runDir, state, binding, created.record));
1887
2091
  return binding;
1888
2092
  }
1889
- async refreshSessionBinding(binding) {
1890
- const record = await resolveSessionRecord(binding.acpxRecordId);
1891
- return {
1892
- ...binding,
1893
- acpSessionId: record.acpSessionId,
1894
- agentSessionId: record.agentSessionId
1895
- };
1896
- }
1897
2093
  createPromptEventCapture(runDir, binding) {
1898
- const pending = [];
2094
+ const pending = /* @__PURE__ */ new Set();
2095
+ let ordinal = 0;
2096
+ let failure;
2097
+ let eventStartSeq;
2098
+ let eventEndSeq = 0;
2099
+ const snapshot = (outcome) => ({
2100
+ outcome,
2101
+ events: !failure && eventStartSeq !== void 0 ? {
2102
+ eventStartSeq,
2103
+ eventEndSeq
2104
+ } : void 0,
2105
+ lastSeq: eventEndSeq
2106
+ });
1899
2107
  return {
1900
2108
  onAcpMessage: (direction, message) => {
1901
- pending.push(this.store.appendSessionEvent(runDir, binding, direction, message).then((value) => ({
1902
- status: "fulfilled",
1903
- value
1904
- }), (reason) => ({
1905
- status: "rejected",
1906
- reason
1907
- })));
2109
+ const index = ordinal++;
2110
+ const write = this.store.appendSessionEvent(runDir, binding, direction, message).then((seq) => {
2111
+ eventStartSeq = Math.min(eventStartSeq ?? seq, seq);
2112
+ eventEndSeq = Math.max(eventEndSeq, seq);
2113
+ }, (reason) => {
2114
+ if (!failure || index < failure.ordinal) failure = {
2115
+ ordinal: index,
2116
+ reason
2117
+ };
2118
+ }).finally(() => pending.delete(write));
2119
+ pending.add(write);
1908
2120
  },
1909
- async run(operation) {
2121
+ async run(operation, finalize) {
1910
2122
  let result;
1911
2123
  try {
1912
2124
  result = {
@@ -1919,78 +2131,84 @@ var FlowRunner = class {
1919
2131
  reason
1920
2132
  };
1921
2133
  }
1922
- const writes = await Promise.all(pending);
1923
- if (result.status === "rejected") throw result.reason;
1924
- let eventStartSeq;
1925
- let eventEndSeq = 0;
1926
- for (const write of writes) {
1927
- if (write.status === "rejected") throw write.reason;
1928
- eventStartSeq = Math.min(eventStartSeq ?? write.value, write.value);
1929
- eventEndSeq = Math.max(eventEndSeq, write.value);
2134
+ await Promise.all(pending);
2135
+ let finalized;
2136
+ try {
2137
+ finalized = {
2138
+ status: "fulfilled",
2139
+ value: await finalize(snapshot(result))
2140
+ };
2141
+ } catch (reason) {
2142
+ finalized = {
2143
+ status: "rejected",
2144
+ reason
2145
+ };
1930
2146
  }
2147
+ if (result.status === "rejected") throw result.reason;
2148
+ if (failure) throw failure.reason;
1931
2149
  if (eventStartSeq === void 0) throw new Error(`Missing ACP event capture for session ${binding.bundleId}`);
1932
- return {
1933
- result: result.value,
1934
- eventStartSeq,
1935
- eventEndSeq
1936
- };
2150
+ if (finalized.status === "rejected") throw finalized.reason;
2151
+ return finalized.value;
1937
2152
  }
1938
2153
  };
1939
2154
  }
1940
- async runPersistentPrompt(runDir, state, binding, prompt, timeoutMs) {
2155
+ async runPersistentPrompt(runDir, state, binding, prepared) {
2156
+ const { attempt, prompt } = prepared;
1941
2157
  const capture = createQuietCaptureOutput();
1942
2158
  const beforeRecord = await resolveSessionRecord(binding.acpxRecordId);
2159
+ attempt.assertActive();
1943
2160
  const events = this.createPromptEventCapture(runDir, binding);
1944
- const initialClient = this.pendingPersistentSessionClients.get(binding.key);
1945
- if (initialClient) this.pendingPersistentSessionClients.delete(binding.key);
1946
- try {
1947
- const { eventStartSeq, eventEndSeq } = await events.run(() => sendSessionDirect({
1948
- sessionId: binding.acpxRecordId,
1949
- prompt,
1950
- resumePolicy: "same-session-only",
1951
- ...this.connectionOptions,
1952
- outputFormatter: capture.formatter,
1953
- errorEmissionPolicy: { queueErrorAlreadyEmitted: false },
1954
- onAcpMessage: events.onAcpMessage,
1955
- suppressSdkConsoleErrors: this.suppressSdkConsoleErrors,
1956
- timeoutMs,
1957
- client: initialClient
1958
- }));
1959
- const sessionInfo = await this.refreshSessionBinding(binding);
1960
- state.sessionBindings[sessionInfo.key] = sessionInfo;
1961
- await this.store.ensureSessionBundle(runDir, state, sessionInfo);
1962
- const afterRecord = await resolveSessionRecord(sessionInfo.acpxRecordId);
1963
- await this.store.writeSessionRecord(runDir, state, sessionInfo, afterRecord);
1964
- const messageStartResolved = findConversationDeltaStart(beforeRecord.messages, afterRecord.messages);
1965
- return {
1966
- rawText: capture.read(),
1967
- sessionInfo,
1968
- conversation: {
1969
- sessionId: sessionInfo.bundleId,
1970
- messageStart: messageStartResolved,
1971
- messageEnd: Math.max(messageStartResolved, afterRecord.messages.length - 1),
1972
- eventStartSeq,
1973
- eventEndSeq
1974
- }
1975
- };
1976
- } finally {
1977
- if (initialClient) await initialClient.close().catch(() => {});
2161
+ const clients = this.pendingPersistentSessionClients.get(runDir);
2162
+ const initialClient = clients?.get(binding.key);
2163
+ if (initialClient) {
2164
+ clients?.delete(binding.key);
2165
+ this.pendingClientReleases.get(initialClient)?.();
2166
+ this.pendingClientReleases.delete(initialClient);
1978
2167
  }
2168
+ return await events.run(() => sendSessionDirect({
2169
+ sessionId: binding.acpxRecordId,
2170
+ prompt,
2171
+ resumePolicy: "same-session-only",
2172
+ ...this.connectionOptions,
2173
+ outputFormatter: capture.formatter,
2174
+ errorEmissionPolicy: { queueErrorAlreadyEmitted: false },
2175
+ onAcpMessage: events.onAcpMessage,
2176
+ suppressSdkConsoleErrors: this.suppressSdkConsoleErrors,
2177
+ client: initialClient
2178
+ }, {
2179
+ signal: attempt.signal,
2180
+ handleProcessInterrupts: false
2181
+ }), async (receipt) => {
2182
+ const rawText = capture.read();
2183
+ prepared.result.rawText = rawText;
2184
+ const afterRecord = await resolveSessionRecord(binding.acpxRecordId);
2185
+ const sessionInfo = {
2186
+ ...binding,
2187
+ acpSessionId: afterRecord.acpSessionId,
2188
+ agentSessionId: afterRecord.agentSessionId
2189
+ };
2190
+ state.sessionBindings[sessionInfo.key] = sessionInfo;
2191
+ return await this.publishAcpCapture(runDir, state, prepared, sessionInfo, afterRecord, findConversationDeltaStart(beforeRecord.messages, afterRecord.messages), rawText, receipt.events);
2192
+ });
1979
2193
  }
1980
- async closePendingPersistentSessionClients() {
1981
- const pendingClients = [...this.pendingPersistentSessionClients.values()];
1982
- this.pendingPersistentSessionClients.clear();
1983
- await Promise.all(pendingClients.map(async (client) => {
1984
- await client.close().catch(() => {});
1985
- }));
1986
- }
1987
- async runIsolatedPrompt(runDir, state, binding, agent, prompt, timeoutMs) {
2194
+ async closePendingPersistentSessionClients(runDir) {
2195
+ const pendingClients = [...this.pendingPersistentSessionClients.get(runDir)?.values() ?? []];
2196
+ this.pendingPersistentSessionClients.delete(runDir);
2197
+ const failure = (await Promise.allSettled(pendingClients.map(async (client) => {
2198
+ this.pendingClientReleases.get(client)?.();
2199
+ this.pendingClientReleases.delete(client);
2200
+ await client.close();
2201
+ }))).find((result) => result.status === "rejected");
2202
+ if (failure?.status === "rejected") throw failure.reason;
2203
+ }
2204
+ async runIsolatedPrompt(runDir, state, binding, prepared) {
2205
+ const { agentInfo: agent, prompt, attempt } = prepared;
1988
2206
  const capture = createQuietCaptureOutput();
1989
- const conversation = createSessionConversation(state.currentNodeStartedAt ?? isoNow$1());
2207
+ const conversation = createSessionConversation(attempt.startedAt);
1990
2208
  let acpxState;
1991
- recordPromptSubmission(conversation, prompt, state.currentNodeStartedAt ?? isoNow$1());
2209
+ recordPromptSubmission(conversation, prompt, attempt.startedAt);
1992
2210
  const events = this.createPromptEventCapture(runDir, binding);
1993
- const { result, eventStartSeq, eventEndSeq } = await events.run(() => runOnce({
2211
+ return await events.run(() => runOnce({
1994
2212
  agentCommand: agent.agentCommand,
1995
2213
  agentArgv: agent.agentArgv,
1996
2214
  cwd: agent.cwd,
@@ -2006,35 +2224,29 @@ var FlowRunner = class {
2006
2224
  acpxState = recordClientOperation(conversation, acpxState, operation);
2007
2225
  },
2008
2226
  suppressSdkConsoleErrors: this.suppressSdkConsoleErrors,
2009
- timeoutMs,
2010
2227
  sessionOptions: this.sessionOptions
2011
- }));
2012
- const sessionInfo = {
2013
- ...binding,
2014
- acpxRecordId: result.sessionId,
2015
- acpSessionId: result.sessionId
2016
- };
2017
- await this.store.ensureSessionBundle(runDir, state, sessionInfo);
2018
- const syntheticRecord = createSyntheticSessionRecord({
2019
- binding: sessionInfo,
2020
- createdAt: state.currentNodeStartedAt ?? isoNow$1(),
2021
- updatedAt: conversation.updated_at,
2022
- conversation,
2023
- acpxState: cloneSessionAcpxState(acpxState),
2024
- lastSeq: eventEndSeq
2228
+ }, {
2229
+ signal: attempt.signal,
2230
+ handleProcessInterrupts: false
2231
+ }), async (receipt) => {
2232
+ const rawText = capture.read();
2233
+ prepared.result.rawText = rawText;
2234
+ const sessionId = receipt.outcome.status === "fulfilled" ? receipt.outcome.value.sessionId : capture.sessionId();
2235
+ const sessionInfo = sessionId === void 0 ? binding : {
2236
+ ...binding,
2237
+ acpxRecordId: sessionId,
2238
+ acpSessionId: sessionId
2239
+ };
2240
+ const record = createSyntheticSessionRecord({
2241
+ binding: sessionInfo,
2242
+ createdAt: attempt.startedAt,
2243
+ updatedAt: conversation.updated_at,
2244
+ conversation,
2245
+ acpxState: cloneSessionAcpxState(acpxState),
2246
+ lastSeq: receipt.lastSeq
2247
+ });
2248
+ return await this.publishAcpCapture(runDir, state, prepared, sessionInfo, record, 0, rawText, receipt.events);
2025
2249
  });
2026
- await this.store.writeSessionRecord(runDir, state, sessionInfo, syntheticRecord);
2027
- return {
2028
- rawText: capture.read(),
2029
- sessionInfo,
2030
- conversation: {
2031
- sessionId: sessionInfo.bundleId,
2032
- messageStart: 0,
2033
- messageEnd: Math.max(0, conversation.messages.length - 1),
2034
- eventStartSeq,
2035
- eventEndSeq
2036
- }
2037
- };
2038
2250
  }
2039
2251
  };
2040
2252
  //#endregion
@@ -2064,9 +2276,23 @@ function parseFencedJsonIfAllowed(text, mode) {
2064
2276
  const fencedText = extractFencedJsonText(text);
2065
2277
  return fencedText === null ? { ok: false } : tryParse(fencedText);
2066
2278
  }
2279
+ const COMPAT_JSON_WORK_MULTIPLIER = 8;
2280
+ function consumeRecoveryWork(budget, units) {
2281
+ if (units > budget.remaining) {
2282
+ budget.remaining = 0;
2283
+ return false;
2284
+ }
2285
+ budget.remaining -= units;
2286
+ return true;
2287
+ }
2067
2288
  function parseBalancedJsonCandidate(text) {
2068
- for (const candidate of extractBalancedJsonCandidates(text)) {
2069
- const parsed = tryParse(candidate);
2289
+ const budget = { remaining: text.length * COMPAT_JSON_WORK_MULTIPLIER };
2290
+ for (let index = 0; index < text.length && budget.remaining > 0; index += 1) {
2291
+ if (text[index] !== "{" && text[index] !== "[") continue;
2292
+ const end = scanBalanced(text, index, budget);
2293
+ if (end === null) continue;
2294
+ if (!consumeRecoveryWork(budget, end - index)) return { ok: false };
2295
+ const parsed = tryParse(text.slice(index, end));
2070
2296
  if (parsed.ok) return parsed;
2071
2297
  }
2072
2298
  return { ok: false };
@@ -2100,20 +2326,12 @@ function extractFencedJsonText(text) {
2100
2326
  function isFenceWhitespace(char) {
2101
2327
  return char === " " || char === "\n" || char === "\r" || char === " ";
2102
2328
  }
2103
- function extractBalancedJsonCandidates(text) {
2104
- const candidates = [];
2105
- for (let index = 0; index < text.length; index += 1) {
2106
- if (text[index] !== "{" && text[index] !== "[") continue;
2107
- const result = scanBalanced(text, index);
2108
- if (result) candidates.push(result);
2109
- }
2110
- return candidates;
2111
- }
2112
- function scanBalanced(text, startIndex) {
2329
+ function scanBalanced(text, startIndex, budget) {
2113
2330
  const stack = [];
2114
2331
  let inString = false;
2115
2332
  let escaped = false;
2116
2333
  for (let index = startIndex; index < text.length; index += 1) {
2334
+ if (!consumeRecoveryWork(budget, 1)) return null;
2117
2335
  const char = text[index];
2118
2336
  if (inString) {
2119
2337
  const next = scanStringChar(char, escaped);
@@ -2125,13 +2343,13 @@ function scanBalanced(text, startIndex) {
2125
2343
  inString = true;
2126
2344
  continue;
2127
2345
  }
2128
- const result = scanBalancedToken(text, startIndex, index, char, stack);
2346
+ const result = scanBalancedToken(index, char, stack);
2129
2347
  if (result !== SCAN_CONTINUE) return result;
2130
2348
  }
2131
2349
  return null;
2132
2350
  }
2133
2351
  const SCAN_CONTINUE = Symbol("scan-continue");
2134
- function scanBalancedToken(text, startIndex, index, char, stack) {
2352
+ function scanBalancedToken(index, char, stack) {
2135
2353
  if (char === "{" || char === "[") {
2136
2354
  stack.push(char);
2137
2355
  return SCAN_CONTINUE;
@@ -2139,7 +2357,7 @@ function scanBalancedToken(text, startIndex, index, char, stack) {
2139
2357
  if (char !== "}" && char !== "]") return SCAN_CONTINUE;
2140
2358
  if (!balancedClosingTokenMatches(stack.at(-1), char)) return null;
2141
2359
  stack.pop();
2142
- return stack.length === 0 ? text.slice(startIndex, index + 1) : SCAN_CONTINUE;
2360
+ return stack.length === 0 ? index + 1 : SCAN_CONTINUE;
2143
2361
  }
2144
2362
  function balancedClosingTokenMatches(open, close) {
2145
2363
  if (open === "{") return close === "}";
@@ -2229,4 +2447,4 @@ function formatDecisionPrompt(question, choices, field) {
2229
2447
  //#endregion
2230
2448
  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 };
2231
2449
 
2232
- //# sourceMappingURL=flows-WOQNzCMC.js.map
2450
+ //# sourceMappingURL=flows-DimX0it5.js.map