acpx 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{agent-registry-evUoJINz.js → agent-registry-Ct2yWPW7.js} +18 -18
  2. package/dist/agent-registry-Ct2yWPW7.js.map +1 -0
  3. package/dist/agent-registry.d.ts.map +1 -1
  4. package/dist/agent-registry.js +1 -1
  5. package/dist/{cli-B2ycKzaO.js → cli-Bmki9cj5.js} +5 -5
  6. package/dist/{cli-B2ycKzaO.js.map → cli-Bmki9cj5.js.map} +1 -1
  7. package/dist/cli.js +167 -131
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{client-CkbBoSGd.d.ts → client-DzaUKazo.d.ts} +23 -15
  10. package/dist/client-DzaUKazo.d.ts.map +1 -0
  11. package/dist/{flows-xGyhmoHO.js → flows--Ewxn_fQ.js} +665 -460
  12. package/dist/flows--Ewxn_fQ.js.map +1 -0
  13. package/dist/flows.d.ts +19 -7
  14. package/dist/flows.d.ts.map +1 -1
  15. package/dist/flows.js +1 -1
  16. package/dist/{invocation-options-eZEBh4vd.js → invocation-options-DRzxHZmr.js} +34 -12
  17. package/dist/invocation-options-DRzxHZmr.js.map +1 -0
  18. package/dist/{ipc-D2rstexT.js → ipc-fMgj5vrO.js} +818 -302
  19. package/dist/ipc-fMgj5vrO.js.map +1 -0
  20. package/dist/{output-D2xL6wdp.js → output-D6kDs8BN.js} +30 -14
  21. package/dist/output-D6kDs8BN.js.map +1 -0
  22. package/dist/{queue-owner-runtime-Dgv3e2Tp.js → queue-owner-runtime-CLwjMYNL.js} +1448 -717
  23. package/dist/queue-owner-runtime-CLwjMYNL.js.map +1 -0
  24. package/dist/runtime.d.ts +57 -3
  25. package/dist/runtime.d.ts.map +1 -1
  26. package/dist/runtime.js +303 -166
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/session-options-Dp6mzNSy.d.ts.map +1 -1
  29. package/dist/{session-watch-CTe9Bnbc.js → session-watch-BfdIo-n4.js} +5 -5
  30. package/dist/session-watch-BfdIo-n4.js.map +1 -0
  31. package/dist/{watch-DxuXEc8n.js → watch-DH-zXQH2.js} +2 -2
  32. package/dist/{watch-DxuXEc8n.js.map → watch-DH-zXQH2.js.map} +1 -1
  33. package/package.json +1 -1
  34. package/skills/acpx/SKILL.md +69 -30
  35. package/dist/agent-registry-evUoJINz.js.map +0 -1
  36. package/dist/client-CkbBoSGd.d.ts.map +0 -1
  37. package/dist/flows-xGyhmoHO.js.map +0 -1
  38. package/dist/invocation-options-eZEBh4vd.js.map +0 -1
  39. package/dist/ipc-D2rstexT.js.map +0 -1
  40. package/dist/output-D2xL6wdp.js.map +0 -1
  41. package/dist/queue-owner-runtime-Dgv3e2Tp.js.map +0 -1
  42. package/dist/session-watch-CTe9Bnbc.js.map +0 -1
@@ -1,7 +1,7 @@
1
- import { t as createOutputFormatter } from "./output-D2xL6wdp.js";
2
- import { C as textPrompt, Dt as SESSION_RECORD_SCHEMA, F as resolveSessionRecord, S as promptToDisplayText, Tt as PERMISSION_MODES, ct as defaultSessionEventLog, mt as writePrivateJsonFile, pt as writePrivateFile } from "./ipc-D2rstexT.js";
3
- import { B as recordClientOperation, H as recordSessionUpdate, L as cloneSessionAcpxState, V as recordPromptSubmission, _ as cancelSessionPrompt, at as withInterrupt, i as runOnce, m as createSessionWithClient, nt as InterruptedError, o as sendSessionDirect, ot as withTimeout, rt as TimeoutError, z as createSessionConversation } from "./queue-owner-runtime-Dgv3e2Tp.js";
4
- import { M as runTimedExecFile } from "./agent-registry-evUoJINz.js";
1
+ import { t as createOutputFormatter } from "./output-D6kDs8BN.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-fMgj5vrO.js";
3
+ import { B as cloneSessionAcpxState, G as recordSessionUpdate, H as createSessionConversation, U as recordClientOperation, W as recordPromptSubmission, 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-CLwjMYNL.js";
4
+ import { M as runTimedExecFile } from "./agent-registry-Ct2yWPW7.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) {
@@ -414,25 +539,11 @@ function resolveShellActionTimeoutMs(timeoutMs) {
414
539
  if (timeoutMs == null || !(timeoutMs > 0)) return;
415
540
  return timeoutMs;
416
541
  }
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
542
  function rejectIfShellFailed(spec, args, result, timedOut, timeoutMs) {
432
543
  if (timedOut) return new TimeoutError(timeoutMs ?? spec.timeoutMs ?? 0);
433
544
  if (((result.exitCode ?? 0) !== 0 || result.signal != null) && spec.allowNonZeroExit !== true) return createShellFailureError(spec, args, result.exitCode, result.signal, result.stderr);
434
545
  }
435
- function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, termination) {
546
+ function waitForShellResult(child, spec, args, cwd, startMs, termination, mode) {
436
547
  const stdoutStream = child.stdout;
437
548
  const stderrStream = child.stderr;
438
549
  if (!stdoutStream || !stderrStream) throw new Error("Shell action child is missing stdio pipes");
@@ -442,7 +553,8 @@ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, terminatio
442
553
  settled = true;
443
554
  reject(error);
444
555
  };
445
- const capture = createShellOutputCapture(spec.maxBufferBytes, () => settled || termination.timedOut(), (error) => {
556
+ const capture = createShellOutputCapture(spec.maxBufferBytes, () => settled || mode === "node" && termination.cancelled(), (error) => {
557
+ if (termination.cancelled()) return;
446
558
  fail(error);
447
559
  termination.cancel("SIGTERM").catch(fail);
448
560
  });
@@ -455,10 +567,10 @@ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, terminatio
455
567
  capture.append("stderr", chunk);
456
568
  });
457
569
  child.once("error", fail);
458
- child.once("exit", (exitCode, signal) => {
570
+ child.once(mode === "node" ? "exit" : "close", (exitCode, signal) => {
459
571
  settled = true;
460
572
  const { stdout, stderr } = capture.output;
461
- const result = {
573
+ resolve({
462
574
  command: spec.command,
463
575
  args,
464
576
  cwd,
@@ -467,14 +579,9 @@ function waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, terminatio
467
579
  combinedOutput: `${stdout}${stderr}`,
468
580
  exitCode,
469
581
  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);
582
+ durationMs: Date.now() - startMs,
583
+ timedOut: mode === "node" ? termination.cancelled() : termination.timedOut()
584
+ });
478
585
  });
479
586
  });
480
587
  }
@@ -486,6 +593,7 @@ function createShellTermination(child, timeoutMs, options) {
486
593
  });
487
594
  let termination;
488
595
  let timedOut = false;
596
+ let cancelled = false;
489
597
  let released = false;
490
598
  let deadline;
491
599
  let monitor;
@@ -504,7 +612,8 @@ function createShellTermination(child, timeoutMs, options) {
504
612
  const cancel = (signal) => {
505
613
  if (termination) return termination;
506
614
  if (released) return Promise.resolve();
507
- timedOut = true;
615
+ clearDeadline();
616
+ cancelled = true;
508
617
  termination = stopShellProcess(child, closed, signal).finally(release);
509
618
  termination.catch(rejectCleanup);
510
619
  return termination;
@@ -514,6 +623,7 @@ function createShellTermination(child, timeoutMs, options) {
514
623
  };
515
624
  options.signal?.addEventListener("abort", onAbort, { once: true });
516
625
  if (timeoutMs != null) deadline = setTimeout(() => {
626
+ timedOut = true;
517
627
  cancel("SIGTERM");
518
628
  }, timeoutMs);
519
629
  unregister = options.registerOwner?.({
@@ -532,6 +642,7 @@ function createShellTermination(child, timeoutMs, options) {
532
642
  return {
533
643
  cancel,
534
644
  timedOut: () => timedOut,
645
+ cancelled: () => cancelled,
535
646
  cleanupFailure,
536
647
  async dispose() {
537
648
  clearDeadline();
@@ -544,7 +655,16 @@ function createShellTermination(child, timeoutMs, options) {
544
655
  };
545
656
  }
546
657
  async function runShellAction(spec, options = {}) {
547
- if (options?.signal?.aborted) throw options.signal.reason;
658
+ const { timedOut, ...result } = await runShellProcess(spec, options, "node");
659
+ const failure = rejectIfShellFailed(spec, result.args, result, timedOut, resolveShellActionTimeoutMs(spec.timeoutMs));
660
+ if (failure) throw failure;
661
+ return result;
662
+ }
663
+ async function runShellCommand(spec, options) {
664
+ return await runShellProcess(spec, options, "command");
665
+ }
666
+ async function runShellProcess(spec, options, mode) {
667
+ options.signal?.throwIfAborted();
548
668
  const cwd = spec.cwd ?? process.cwd();
549
669
  const args = spec.args ?? [];
550
670
  const startMs = Date.now();
@@ -566,14 +686,21 @@ async function runShellAction(spec, options = {}) {
566
686
  detached: process.platform !== "win32"
567
687
  });
568
688
  const termination = createShellTermination(child, timeoutMs, options);
569
- const finish = waitForShellExit(child, spec, args, cwd, startMs, timeoutMs, termination);
689
+ const finish = waitForShellResult(child, spec, args, cwd, startMs, termination, mode);
570
690
  writeShellStdin(child, spec.stdin);
571
691
  try {
572
- return await Promise.race([finish, termination.cleanupFailure]);
692
+ const result = await Promise.race([finish, termination.cleanupFailure]);
693
+ throwIfShellCancelled(options.signal, mode);
694
+ return result;
573
695
  } finally {
574
696
  await termination.dispose();
575
697
  }
576
698
  }
699
+ function throwIfShellCancelled(signal, mode) {
700
+ if (!signal?.aborted) return;
701
+ const reason = signal.reason;
702
+ if (mode === "command" || reason instanceof TimeoutError || reason instanceof InterruptedError) throw reason;
703
+ }
577
704
  //#endregion
578
705
  //#region src/flows/graph.ts
579
706
  function validateFlowDefinition(flow) {
@@ -707,11 +834,19 @@ function summarizePrompt(promptText, explicitDetail) {
707
834
  }
708
835
  function createQuietCaptureOutput() {
709
836
  const chunks = [];
837
+ let sessionId;
838
+ const formatter = createOutputFormatter("quiet", { stdout: { write(chunk) {
839
+ chunks.push(chunk);
840
+ } } });
841
+ const setContext = formatter.setContext.bind(formatter);
842
+ formatter.setContext = (context) => {
843
+ sessionId = context.sessionId ?? sessionId;
844
+ setContext(context);
845
+ };
710
846
  return {
711
- formatter: createOutputFormatter("quiet", { stdout: { write(chunk) {
712
- chunks.push(chunk);
713
- } } }),
714
- read: () => chunks.join("").trim()
847
+ formatter,
848
+ read: () => chunks.join("").trim(),
849
+ sessionId: () => sessionId
715
850
  };
716
851
  }
717
852
  async function resolveFlowRunTitle(flow, input, flowPath) {
@@ -730,8 +865,13 @@ function normalizeFlowRunTitle(value) {
730
865
  function createRunId(flowName) {
731
866
  return `${isoNow$1().replaceAll(":", "").replaceAll(".", "")}-${slugifyAsciiIdPart(flowName)}-${randomUUID().slice(0, 8)}`;
732
867
  }
733
- function createSessionBindingKey(agentCommand, cwd, handle) {
734
- return `${agentCommand}::${cwd}::${handle}`;
868
+ function createSessionBindingKey(agentCommand, cwd, handle, agentArgv) {
869
+ return JSON.stringify([
870
+ agentCommand,
871
+ agentArgv ?? null,
872
+ cwd,
873
+ handle
874
+ ]);
735
875
  }
736
876
  function createSessionName(flowName, handle, cwd, runId) {
737
877
  return `${flowName}-${handle}-${stableShortHash(cwd)}-${runId.slice(-8)}`;
@@ -909,10 +1049,15 @@ var FlowRunStore = class {
909
1049
  traceSeqByRun = /* @__PURE__ */ new Map();
910
1050
  sessionSeqByBundle = /* @__PURE__ */ new Map();
911
1051
  manifestByRun = /* @__PURE__ */ new Map();
912
- appendChainByPath = /* @__PURE__ */ new Map();
1052
+ writeChainByPath = /* @__PURE__ */ new Map();
913
1053
  constructor(outputRoot = flowRunsBaseDir()) {
914
1054
  this.outputRoot = outputRoot;
915
1055
  }
1056
+ releaseRun(runDir) {
1057
+ this.traceSeqByRun.delete(runDir);
1058
+ this.manifestByRun.delete(runDir);
1059
+ for (const key of this.sessionSeqByBundle.keys()) if (key.startsWith(`${runDir}::`)) this.sessionSeqByBundle.delete(key);
1060
+ }
916
1061
  async createRunDir(runId) {
917
1062
  const runDir = path.join(this.outputRoot, runId);
918
1063
  await fs.mkdir(path.join(runDir, PROJECTIONS_DIR), {
@@ -1002,57 +1147,65 @@ var FlowRunStore = class {
1002
1147
  });
1003
1148
  return artifact;
1004
1149
  }
1005
- async ensureSessionBundle(runDir, state, binding, record) {
1150
+ async ensureSessionBundle(runDir, state, inputBinding, record) {
1151
+ const binding = structuredClone(inputBinding);
1006
1152
  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"),
1153
+ await this.serializeWrite(path.join(sessionDir, "binding.json"), async () => {
1154
+ await fs.mkdir(sessionDir, {
1155
+ recursive: true,
1156
+ mode: 448
1157
+ });
1158
+ await writePrivateJsonFile(path.join(sessionDir, "binding.json"), binding);
1159
+ await ensureFile(path.join(sessionDir, "events.ndjson"));
1160
+ if (record) await this.writeSessionRecord(runDir, state, binding, record);
1161
+ const manifest = this.getManifest(runDir, state);
1162
+ if (!manifest.sessions.find((entry) => entry.id === binding.bundleId)) {
1163
+ const bindingArtifact = await this.writeArtifact(runDir, state, binding, {
1036
1164
  mediaType: "application/json",
1037
- sha256: await fileSha256(path.join(sessionDir, "binding.json"))
1038
- }
1165
+ extension: "json",
1166
+ emitTrace: false
1167
+ });
1168
+ const entry = {
1169
+ id: binding.bundleId,
1170
+ handle: binding.handle,
1171
+ bindingPath: path.posix.join(sessionDirPath(binding.bundleId), "binding.json"),
1172
+ recordPath: path.posix.join(sessionDirPath(binding.bundleId), "record.json"),
1173
+ eventsPath: path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson")
1174
+ };
1175
+ manifest.sessions.push(entry);
1176
+ await writePrivateJsonFile(this.resolveRunPath(runDir, MANIFEST_PATH), manifest);
1177
+ await this.appendTrace(runDir, state, {
1178
+ scope: "session",
1179
+ type: "session_bound",
1180
+ sessionId: binding.bundleId,
1181
+ payload: {
1182
+ sessionId: binding.bundleId,
1183
+ handle: binding.handle,
1184
+ bindingArtifact
1185
+ }
1186
+ });
1039
1187
  }
1040
1188
  });
1041
1189
  }
1042
1190
  async writeSessionRecord(runDir, _state, binding, record) {
1043
- const bundledRecord = createBundledSessionRecord(binding, record, this.sessionSeqByBundle.get(`${runDir}::${binding.bundleId}`) ?? 0);
1191
+ const bundledRecord = createBundledSessionRecord(binding, record, this.sessionSeqByBundle.get(`${runDir}::${binding.bundleId}`)?.persisted ?? 0);
1044
1192
  await writePrivateJsonFile(this.resolveRunPath(runDir, path.posix.join(sessionDirPath(binding.bundleId), "record.json")), bundledRecord);
1045
1193
  }
1046
1194
  async appendSessionEvent(runDir, binding, direction, message) {
1047
1195
  const sessionKey = `${runDir}::${binding.bundleId}`;
1048
- const seq = (this.sessionSeqByBundle.get(sessionKey) ?? 0) + 1;
1049
- this.sessionSeqByBundle.set(sessionKey, seq);
1196
+ const sequence = this.sessionSeqByBundle.get(sessionKey) ?? {
1197
+ allocated: 0,
1198
+ persisted: 0
1199
+ };
1200
+ this.sessionSeqByBundle.set(sessionKey, sequence);
1201
+ const seq = ++sequence.allocated;
1050
1202
  await this.appendJsonLine(this.resolveRunPath(runDir, path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson")), {
1051
1203
  seq,
1052
1204
  at: isoNow(),
1053
1205
  direction,
1054
1206
  message
1055
1207
  });
1208
+ sequence.persisted = Math.max(sequence.persisted, seq);
1056
1209
  return seq;
1057
1210
  }
1058
1211
  getManifest(runDir, state) {
@@ -1083,7 +1236,7 @@ var FlowRunStore = class {
1083
1236
  return path.join(runDir, ...relativePath.split("/"));
1084
1237
  }
1085
1238
  async appendJsonLine(filePath, value) {
1086
- const tracked = (this.appendChainByPath.get(filePath) ?? Promise.resolve()).then(async () => {
1239
+ await this.serializeWrite(filePath, async () => {
1087
1240
  await fs.mkdir(path.dirname(filePath), {
1088
1241
  recursive: true,
1089
1242
  mode: 448
@@ -1093,10 +1246,13 @@ var FlowRunStore = class {
1093
1246
  content: `${JSON.stringify(value)}\n`,
1094
1247
  mode: 384
1095
1248
  });
1096
- }).finally(() => {
1097
- if (this.appendChainByPath.get(filePath) === tracked) this.appendChainByPath.delete(filePath);
1098
1249
  });
1099
- this.appendChainByPath.set(filePath, tracked);
1250
+ }
1251
+ async serializeWrite(filePath, write) {
1252
+ const tracked = (this.writeChainByPath.get(filePath) ?? Promise.resolve()).then(write).finally(() => {
1253
+ if (this.writeChainByPath.get(filePath) === tracked) this.writeChainByPath.delete(filePath);
1254
+ });
1255
+ this.writeChainByPath.set(filePath, tracked);
1100
1256
  await tracked;
1101
1257
  }
1102
1258
  };
@@ -1229,10 +1385,6 @@ async function ensureFile(filePath) {
1229
1385
  mode: 384
1230
1386
  });
1231
1387
  }
1232
- async function fileSha256(filePath) {
1233
- const payload = await fs.readFile(filePath);
1234
- return createHash("sha256").update(payload).digest("hex");
1235
- }
1236
1388
  function toArtifactBuffer(content, mediaType) {
1237
1389
  if (typeof content === "string") return Buffer.from(content, "utf8");
1238
1390
  if (Buffer.isBuffer(content)) return content;
@@ -1264,7 +1416,8 @@ var FlowRunner = class {
1264
1416
  services;
1265
1417
  store;
1266
1418
  pendingPersistentSessionClients = /* @__PURE__ */ new Map();
1267
- shellInterrupts = /* @__PURE__ */ new Map();
1419
+ attempts = /* @__PURE__ */ new Map();
1420
+ pendingClientReleases = /* @__PURE__ */ new WeakMap();
1268
1421
  runInterruptions = /* @__PURE__ */ new Map();
1269
1422
  shellOwners = /* @__PURE__ */ new Map();
1270
1423
  constructor(options) {
@@ -1305,23 +1458,27 @@ var FlowRunner = class {
1305
1458
  steps: [],
1306
1459
  sessionBindings: {}
1307
1460
  };
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
1461
  try {
1319
- return await this.runWithShellOwnership(flow, input, runDir, state);
1462
+ const inputArtifact = await this.store.writeArtifact(runDir, state, input, {
1463
+ mediaType: "application/json",
1464
+ extension: "json",
1465
+ emitTrace: false
1466
+ });
1467
+ await this.store.initializeRunBundle(runDir, {
1468
+ flow,
1469
+ state,
1470
+ inputArtifact
1471
+ });
1472
+ return await this.runWithOwnership(flow, input, runDir, state);
1320
1473
  } finally {
1321
- await this.closePendingPersistentSessionClients();
1474
+ try {
1475
+ await this.closePendingPersistentSessionClients(runDir);
1476
+ } finally {
1477
+ this.store.releaseRun(runDir);
1478
+ }
1322
1479
  }
1323
1480
  }
1324
- async runWithShellOwnership(flow, input, runDir, state) {
1481
+ async runWithOwnership(flow, input, runDir, state) {
1325
1482
  let execution;
1326
1483
  let cancellation;
1327
1484
  let interruption;
@@ -1334,11 +1491,8 @@ var FlowRunner = class {
1334
1491
  this.runInterruptions.set(runDir, reason);
1335
1492
  cancellation ??= this.cancelShellOwners(runDir, signal);
1336
1493
  cancellation.catch(() => {});
1337
- const interruptShell = this.shellInterrupts.get(runDir);
1338
- if (interruptShell) {
1339
- interruptShell(reason, signal);
1340
- await execution?.catch(() => {});
1341
- }
1494
+ this.attempts.get(runDir)?.cancel(reason, signal);
1495
+ await execution?.catch(() => {});
1342
1496
  }).then((value) => ({
1343
1497
  ok: true,
1344
1498
  value
@@ -1417,16 +1571,28 @@ var FlowRunner = class {
1417
1571
  const attemptId = nextAttemptId(attemptCounts, nodeId);
1418
1572
  const startedAt = isoNow$1();
1419
1573
  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);
1574
+ const attempt = new FlowAttempt({
1575
+ nodeId,
1576
+ attemptId,
1577
+ startedAt,
1578
+ timeoutMs: node.timeoutMs ?? this.defaultNodeTimeoutMs
1579
+ });
1580
+ this.attempts.set(runDir, attempt);
1581
+ const context = this.makeAttemptContext(runDir, state, input, node, attempt);
1582
+ let stopHeartbeat = () => {};
1422
1583
  let executed;
1423
1584
  let outcome = "ok";
1424
1585
  let executionError;
1425
1586
  try {
1426
1587
  this.throwIfRunInterrupted(runDir);
1427
- executed = await this.executeNode(runDir, state, flow, nodeId, node, context);
1588
+ executed = await attempt.run(async () => {
1589
+ await attempt.own(() => this.writeNodeStartedSnapshot(runDir, state, nodeId, attemptId, node));
1590
+ stopHeartbeat = this.startHeartbeat(runDir, state, node, attempt);
1591
+ const result = await this.executeNode(runDir, state, flow, nodeId, node, context);
1592
+ result.trace = await attempt.own(() => finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, result.output, result.trace));
1593
+ return result;
1594
+ });
1428
1595
  this.throwIfRunInterrupted(runDir);
1429
- executed.trace = await finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, executed.output, executed.trace);
1430
1596
  } catch (error) {
1431
1597
  outcome = outcomeForError(error);
1432
1598
  executionError = error;
@@ -1436,8 +1602,12 @@ var FlowRunner = class {
1436
1602
  rawText: null,
1437
1603
  sessionInfo: null,
1438
1604
  agentInfo: null,
1439
- trace: await finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, void 0, extractAttachedStepTrace(error) ?? null)
1605
+ ...context.acpResult,
1606
+ trace: await finalizeStepTrace(this.store, runDir, state, nodeId, attemptId, void 0, context.acpResult ? context.acpResult.trace : extractAttachedStepTrace(error) ?? null)
1440
1607
  };
1608
+ } finally {
1609
+ stopHeartbeat();
1610
+ this.attempts.delete(runDir);
1441
1611
  }
1442
1612
  const nodeResult = createNodeResult({
1443
1613
  attemptId,
@@ -1460,6 +1630,17 @@ var FlowRunner = class {
1460
1630
  state
1461
1631
  };
1462
1632
  }
1633
+ makeAttemptContext(runDir, state, input, node, attempt) {
1634
+ const context = {
1635
+ nodeContext: {
1636
+ ...makeFlowNodeContext(state, input, this.services),
1637
+ signal: attempt.signal
1638
+ },
1639
+ attempt
1640
+ };
1641
+ if (node.nodeType === "action" && "run" in node) context.nodeContext.runShell = (execution) => this.runCallbackShell(runDir, attempt, execution);
1642
+ return context;
1643
+ }
1463
1644
  async writeNodeStartedSnapshot(runDir, state, nodeId, attemptId, node) {
1464
1645
  await this.store.writeSnapshot(runDir, state, {
1465
1646
  scope: "node",
@@ -1537,19 +1718,22 @@ var FlowRunner = class {
1537
1718
  }
1538
1719
  async executeNode(runDir, state, flow, nodeId, node, context) {
1539
1720
  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);
1721
+ case "compute": return await this.executeCallbackNode(nodeId, node, context);
1722
+ case "action": return "run" in node ? await this.executeCallbackNode(nodeId, node, context) : await this.executeShellNode(runDir, state, node, context);
1723
+ case "checkpoint": return await this.executeCallbackNode(nodeId, node, context);
1543
1724
  case "acp": return await this.executeAcpNode(runDir, state, flow, node, context);
1544
1725
  default: throw new Error(`Unsupported flow node: ${String(node)}`);
1545
1726
  }
1546
1727
  }
1547
- async executeCallbackNode(runDir, state, nodeId, node, context) {
1728
+ async executeCallbackNode(nodeId, node, context) {
1729
+ context.attempt.assertActive();
1730
+ const output = node.nodeType === "checkpoint" && typeof node.run !== "function" ? {
1731
+ checkpoint: nodeId,
1732
+ summary: node.summary ?? nodeId
1733
+ } : await node.run?.(context.nodeContext);
1734
+ context.attempt.assertActive();
1548
1735
  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)),
1736
+ output,
1553
1737
  promptText: null,
1554
1738
  rawText: null,
1555
1739
  sessionInfo: null,
@@ -1558,186 +1742,173 @@ var FlowRunner = class {
1558
1742
  };
1559
1743
  }
1560
1744
  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
- }
1745
+ const attempt = context.attempt;
1746
+ const execution = await node.exec(context.nodeContext);
1747
+ attempt.assertActive();
1748
+ const effectiveExecution = {
1749
+ ...execution,
1750
+ cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd),
1751
+ timeoutMs: resolveShellActionTimeoutMs(execution.timeoutMs ?? attempt.remainingTimeoutMs())
1570
1752
  };
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
- },
1753
+ updateStatusDetail(state, formatShellActionSummary(effectiveExecution));
1754
+ await attempt.own(() => this.store.writeLive(runDir, state, {
1755
+ scope: "node",
1756
+ type: "node_heartbeat",
1757
+ nodeId: attempt.nodeId,
1758
+ attemptId: attempt.attemptId,
1759
+ payload: { statusDetail: state.statusDetail }
1760
+ }));
1761
+ await attempt.own(() => this.store.appendTrace(runDir, state, {
1762
+ scope: "action",
1763
+ type: "action_prepared",
1764
+ nodeId: attempt.nodeId,
1765
+ attemptId: attempt.attemptId,
1766
+ payload: { action: {
1767
+ actionType: "shell",
1768
+ command: effectiveExecution.command,
1769
+ args: effectiveExecution.args ?? [],
1770
+ cwd: effectiveExecution.cwd
1771
+ } }
1772
+ }));
1773
+ const result = await attempt.own(() => runShellAction(effectiveExecution, this.shellControl(runDir, attempt)));
1774
+ const stdoutArtifact = await attempt.own(() => this.store.writeArtifact(runDir, state, result.stdout, {
1775
+ mediaType: "text/plain",
1776
+ extension: "txt",
1777
+ nodeId: attempt.nodeId,
1778
+ attemptId: attempt.attemptId
1779
+ }));
1780
+ const stderrArtifact = await attempt.own(() => this.store.writeArtifact(runDir, state, result.stderr, {
1781
+ mediaType: "text/plain",
1782
+ extension: "txt",
1783
+ nodeId: attempt.nodeId,
1784
+ attemptId: attempt.attemptId
1785
+ }));
1786
+ const actionTrace = {
1787
+ actionType: "shell",
1788
+ command: result.command,
1789
+ args: result.args,
1790
+ cwd: result.cwd,
1791
+ exitCode: result.exitCode,
1792
+ signal: result.signal,
1793
+ durationMs: result.durationMs
1794
+ };
1795
+ await attempt.own(() => this.store.appendTrace(runDir, state, {
1796
+ scope: "action",
1797
+ type: "action_completed",
1798
+ nodeId: attempt.nodeId,
1799
+ attemptId: attempt.attemptId,
1800
+ payload: {
1801
+ action: actionTrace,
1650
1802
  stdoutArtifact,
1651
1803
  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
1804
  }
1674
- throw error;
1675
- }).finally(() => {
1676
- this.shellInterrupts.delete(runDir);
1677
- });
1805
+ }));
1806
+ const trace = {
1807
+ action: actionTrace,
1808
+ stdoutArtifact,
1809
+ stderrArtifact
1810
+ };
1811
+ let output;
1812
+ try {
1813
+ output = node.parse ? await node.parse(result, context.nodeContext) : result;
1814
+ attempt.assertActive();
1815
+ } catch (error) {
1816
+ throw attachStepTrace(error, trace);
1817
+ }
1678
1818
  return {
1679
1819
  output,
1680
1820
  promptText: null,
1681
- rawText,
1821
+ rawText: result.combinedOutput,
1682
1822
  sessionInfo: null,
1683
1823
  agentInfo: null,
1684
1824
  trace
1685
1825
  };
1686
1826
  }
1827
+ shellControl(runDir, attempt) {
1828
+ return {
1829
+ signal: attempt.signal,
1830
+ get terminationSignal() {
1831
+ return attempt.terminationSignal;
1832
+ },
1833
+ registerOwner: (owner) => {
1834
+ const releaseAttempt = attempt.registerCancellation((signal) => owner.cancel(signal));
1835
+ const releaseRun = this.registerShellOwner(runDir, owner);
1836
+ return () => {
1837
+ releaseAttempt();
1838
+ releaseRun();
1839
+ };
1840
+ }
1841
+ };
1842
+ }
1843
+ runCallbackShell(runDir, attempt, execution) {
1844
+ return attempt.own(() => runShellCommand({
1845
+ ...execution,
1846
+ cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd)
1847
+ }, this.shellControl(runDir, attempt)));
1848
+ }
1687
1849
  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
- });
1850
+ const attempt = context.attempt;
1851
+ const prepared = await this.prepareAcpPrompt(runDir, state, node, context);
1852
+ if (node.session?.isolated) return await this.executeIsolatedAcpPrompt(runDir, state, flow, node, context, prepared);
1853
+ const boundSession = await this.ensureSessionBinding(runDir, state, flow, node, prepared.agentInfo, attempt);
1854
+ return await this.executePersistentAcpPrompt(runDir, state, node, context, prepared, boundSession);
1699
1855
  }
1700
- async prepareAcpPrompt(runDir, state, node, context, nodeTimeoutMs) {
1856
+ async prepareAcpPrompt(runDir, state, node, context) {
1701
1857
  const resolvedAgent = this.resolveAgent(node.profile);
1702
1858
  const agentInfo = {
1703
1859
  ...resolvedAgent,
1704
- cwd: await resolveNodeCwd(resolvedAgent.cwd, node.cwd, context)
1860
+ agentArgv: resolvedAgent.agentArgv?.slice(),
1861
+ cwd: await resolveNodeCwd(resolvedAgent.cwd, node.cwd, context.nodeContext)
1705
1862
  };
1706
- const prompt = normalizePromptInput(await Promise.resolve(node.prompt(context)));
1863
+ context.attempt.assertActive();
1864
+ const prompt = normalizePromptInput(await Promise.resolve(node.prompt(context.nodeContext)));
1865
+ context.attempt.assertActive();
1707
1866
  const promptText = promptToDisplayText(prompt);
1867
+ const result = {
1868
+ output: void 0,
1869
+ promptText,
1870
+ rawText: null,
1871
+ sessionInfo: null,
1872
+ agentInfo,
1873
+ trace: null
1874
+ };
1875
+ context.acpResult = result;
1708
1876
  updateStatusDetail(state, summarizePrompt(promptText, node.statusDetail));
1709
- await this.writeAcpPromptHeartbeat(runDir, state);
1877
+ await context.attempt.own(() => this.writeAcpPromptHeartbeat(runDir, state, context.attempt));
1878
+ const promptArtifact = await context.attempt.own(() => this.store.writeArtifact(runDir, state, promptText, {
1879
+ mediaType: "text/plain",
1880
+ extension: "txt",
1881
+ nodeId: context.attempt.nodeId,
1882
+ attemptId: context.attempt.attemptId
1883
+ }));
1884
+ result.trace = { promptArtifact };
1710
1885
  return {
1711
1886
  agentInfo,
1712
1887
  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
1888
+ promptArtifact,
1889
+ attempt: context.attempt,
1890
+ result
1721
1891
  };
1722
1892
  }
1723
- async writeAcpPromptHeartbeat(runDir, state) {
1893
+ async writeAcpPromptHeartbeat(runDir, state, attempt) {
1724
1894
  await this.store.writeLive(runDir, state, {
1725
1895
  scope: "node",
1726
1896
  type: "node_heartbeat",
1727
- nodeId: state.currentNode,
1728
- attemptId: state.currentAttemptId,
1897
+ nodeId: attempt.nodeId,
1898
+ attemptId: attempt.attemptId,
1729
1899
  payload: { statusDetail: state.statusDetail }
1730
1900
  });
1731
1901
  }
1732
1902
  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();
1903
+ const binding = createIsolatedSessionBinding(flow.name, state.runId, prepared.attempt.attemptId, node.profile, prepared.agentInfo);
1904
+ prepared.result.sessionInfo = binding;
1905
+ await prepared.attempt.own(() => this.initializeIsolatedSessionBundle(runDir, state, binding, prepared.attempt));
1906
+ await prepared.attempt.own(() => this.appendAcpPromptPreparedTrace(runDir, state, binding, prepared.promptArtifact, prepared.attempt));
1907
+ const prompt = await prepared.attempt.own(() => this.runIsolatedPrompt(runDir, state, binding, prepared));
1908
+ return await this.finishAcpPrompt(runDir, state, node, context, prepared, prompt);
1909
+ }
1910
+ async initializeIsolatedSessionBundle(runDir, state, binding, attempt) {
1911
+ const timestamp = attempt.startedAt;
1741
1912
  const initialRecord = createSyntheticSessionRecord({
1742
1913
  binding,
1743
1914
  createdAt: timestamp,
@@ -1749,16 +1920,17 @@ var FlowRunner = class {
1749
1920
  await this.store.ensureSessionBundle(runDir, state, binding, initialRecord);
1750
1921
  }
1751
1922
  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);
1923
+ prepared.result.sessionInfo = binding;
1924
+ await prepared.attempt.own(() => this.appendAcpPromptPreparedTrace(runDir, state, binding, prepared.promptArtifact, prepared.attempt));
1925
+ const prompt = await prepared.attempt.own(() => this.runPersistentPrompt(runDir, state, binding, prepared));
1926
+ return await this.finishAcpPrompt(runDir, state, node, context, prepared, prompt);
1755
1927
  }
1756
- async appendAcpPromptPreparedTrace(runDir, state, binding, promptArtifact) {
1928
+ async appendAcpPromptPreparedTrace(runDir, state, binding, promptArtifact, attempt) {
1757
1929
  await this.store.appendTrace(runDir, state, {
1758
1930
  scope: "acp",
1759
1931
  type: "acp_prompt_prepared",
1760
- nodeId: state.currentNode,
1761
- attemptId: state.currentAttemptId,
1932
+ nodeId: attempt.nodeId,
1933
+ attemptId: attempt.attemptId,
1762
1934
  sessionId: binding.bundleId,
1763
1935
  payload: {
1764
1936
  sessionId: binding.bundleId,
@@ -1766,107 +1938,121 @@ var FlowRunner = class {
1766
1938
  }
1767
1939
  });
1768
1940
  }
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);
1941
+ async finishAcpPrompt(runDir, state, node, context, prepared, prompt) {
1942
+ await prepared.attempt.own(() => this.appendAcpResponseParsedTrace(runDir, state, prompt, prepared.attempt));
1943
+ const output = await this.parseAcpOutput(node, context, prompt.rawText);
1944
+ return {
1945
+ ...prepared.result,
1946
+ output
1947
+ };
1948
+ }
1949
+ async publishAcpCapture(runDir, state, prepared, sessionInfo, record, messageStart, rawText, events) {
1950
+ const result = prepared.result;
1951
+ result.sessionInfo = sessionInfo;
1772
1952
  const trace = {
1773
1953
  sessionId: sessionInfo.bundleId,
1774
- promptArtifact: prepared.promptArtifact,
1775
- rawResponseArtifact,
1776
- conversation: prompt.conversation
1954
+ promptArtifact: prepared.promptArtifact
1777
1955
  };
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
1956
+ result.trace = trace;
1957
+ await this.store.ensureSessionBundle(runDir, state, sessionInfo);
1958
+ await this.store.writeSessionRecord(runDir, state, sessionInfo, record);
1959
+ if (events) trace.conversation = {
1960
+ sessionId: sessionInfo.bundleId,
1961
+ messageStart,
1962
+ messageEnd: Math.max(messageStart, record.messages.length - 1),
1963
+ ...events
1785
1964
  };
1786
- }
1787
- async writeAcpRawResponseArtifact(runDir, state, prompt, sessionInfo) {
1788
- return await this.store.writeArtifact(runDir, state, prompt.rawText, {
1965
+ const rawResponseArtifact = await this.store.writeArtifact(runDir, state, rawText, {
1789
1966
  mediaType: "text/plain",
1790
1967
  extension: "txt",
1791
- nodeId: state.currentNode,
1792
- attemptId: state.currentAttemptId,
1968
+ nodeId: prepared.attempt.nodeId,
1969
+ attemptId: prepared.attempt.attemptId,
1793
1970
  sessionId: sessionInfo.bundleId
1794
1971
  });
1972
+ trace.rawResponseArtifact = rawResponseArtifact;
1973
+ return {
1974
+ rawText,
1975
+ sessionInfo,
1976
+ conversation: trace.conversation,
1977
+ rawResponseArtifact
1978
+ };
1795
1979
  }
1796
- async appendAcpResponseParsedTrace(runDir, state, prompt, sessionInfo, rawResponseArtifact) {
1980
+ async appendAcpResponseParsedTrace(runDir, state, prompt, attempt) {
1797
1981
  await this.store.appendTrace(runDir, state, {
1798
1982
  scope: "acp",
1799
1983
  type: "acp_response_parsed",
1800
- nodeId: state.currentNode,
1801
- attemptId: state.currentAttemptId,
1802
- sessionId: sessionInfo.bundleId,
1984
+ nodeId: attempt.nodeId,
1985
+ attemptId: attempt.attemptId,
1986
+ sessionId: prompt.sessionInfo.bundleId,
1803
1987
  payload: {
1804
- sessionId: sessionInfo.bundleId,
1988
+ sessionId: prompt.sessionInfo.bundleId,
1805
1989
  conversation: prompt.conversation,
1806
- rawResponseArtifact
1990
+ rawResponseArtifact: prompt.rawResponseArtifact
1807
1991
  }
1808
1992
  });
1809
1993
  }
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
- }
1994
+ async parseAcpOutput(node, context, rawText) {
1995
+ context.attempt.assertActive();
1996
+ const output = node.parse ? await node.parse(rawText, context.nodeContext) : rawText;
1997
+ context.attempt.assertActive();
1998
+ return output;
1816
1999
  }
1817
- async runWithHeartbeat(runDir, state, nodeId, node, timeoutMs, run, onTimeout) {
2000
+ startHeartbeat(runDir, state, node, attempt) {
1818
2001
  const heartbeatMs = Math.max(0, Math.round(node.heartbeatMs ?? DEFAULT_FLOW_HEARTBEAT_MS));
1819
- let timer;
1820
- let active = true;
1821
- let heartbeatPending = false;
2002
+ if (heartbeatMs === 0) return () => {};
2003
+ let pending = false;
1822
2004
  const heartbeat = async () => {
1823
- if (!active || heartbeatPending) return;
1824
- heartbeatPending = true;
2005
+ if (!attempt.active || pending) return;
2006
+ pending = true;
1825
2007
  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;
2008
+ await attempt.own(async () => {
2009
+ state.lastHeartbeatAt = isoNow$1();
2010
+ state.updatedAt = state.lastHeartbeatAt;
2011
+ await this.store.writeLive(runDir, state, {
2012
+ scope: "node",
2013
+ type: "node_heartbeat",
2014
+ nodeId: attempt.nodeId,
2015
+ attemptId: attempt.attemptId,
2016
+ payload: { statusDetail: state.statusDetail }
2017
+ });
2018
+ }, { bestEffort: true });
2019
+ } catch {} finally {
2020
+ pending = false;
1837
2021
  }
1838
2022
  };
1839
- if (heartbeatMs > 0) timer = setInterval(() => {
1840
- heartbeat().catch(() => {});
2023
+ const timer = setInterval(() => {
2024
+ heartbeat();
1841
2025
  }, 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
- }
2026
+ return () => clearInterval(timer);
1851
2027
  }
1852
- async ensureSessionBinding(runDir, state, flow, node, agent, timeoutMs) {
2028
+ async ensureSessionBinding(runDir, state, flow, node, agent, attempt) {
1853
2029
  const handle = node.session?.handle ?? "main";
1854
- const key = createSessionBindingKey(agent.agentCommand, agent.cwd, handle);
2030
+ const key = createSessionBindingKey(agent.agentCommand, agent.cwd, handle, agent.agentArgv);
1855
2031
  const existing = state.sessionBindings[key];
1856
2032
  if (existing) {
1857
- await this.store.ensureSessionBundle(runDir, state, existing);
2033
+ await attempt.own(() => this.store.ensureSessionBundle(runDir, state, existing));
1858
2034
  return existing;
1859
2035
  }
1860
2036
  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
2037
+ const created = await attempt.own(async () => {
2038
+ const acquired = await createSessionWithClient({
2039
+ agentCommand: agent.agentCommand,
2040
+ agentArgv: agent.agentArgv,
2041
+ cwd: agent.cwd,
2042
+ name,
2043
+ ...this.connectionOptions,
2044
+ signal: attempt.signal,
2045
+ handleProcessInterrupts: false,
2046
+ sessionOptions: this.sessionOptions
2047
+ });
2048
+ this.pendingClientReleases.set(acquired.client, attempt.registerCancellation(async () => {
2049
+ const clients = this.pendingPersistentSessionClients.get(runDir);
2050
+ if (clients?.get(key) === acquired.client) clients.delete(key);
2051
+ await acquired.client.close();
2052
+ }));
2053
+ return acquired;
1869
2054
  });
2055
+ attempt.assertActive();
1870
2056
  const binding = {
1871
2057
  key,
1872
2058
  handle,
@@ -1882,31 +2068,44 @@ var FlowRunner = class {
1882
2068
  agentSessionId: created.record.agentSessionId
1883
2069
  };
1884
2070
  state.sessionBindings[key] = binding;
1885
- this.pendingPersistentSessionClients.set(binding.key, created.client);
1886
- await this.store.ensureSessionBundle(runDir, state, binding, created.record);
2071
+ let clients = this.pendingPersistentSessionClients.get(runDir);
2072
+ if (!clients) {
2073
+ clients = /* @__PURE__ */ new Map();
2074
+ this.pendingPersistentSessionClients.set(runDir, clients);
2075
+ }
2076
+ clients.set(binding.key, created.client);
2077
+ await attempt.own(() => this.store.ensureSessionBundle(runDir, state, binding, created.record));
1887
2078
  return binding;
1888
2079
  }
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
2080
  createPromptEventCapture(runDir, binding) {
1898
- const pending = [];
2081
+ const pending = /* @__PURE__ */ new Set();
2082
+ let ordinal = 0;
2083
+ let failure;
2084
+ let eventStartSeq;
2085
+ let eventEndSeq = 0;
2086
+ const snapshot = (outcome) => ({
2087
+ outcome,
2088
+ events: !failure && eventStartSeq !== void 0 ? {
2089
+ eventStartSeq,
2090
+ eventEndSeq
2091
+ } : void 0,
2092
+ lastSeq: eventEndSeq
2093
+ });
1899
2094
  return {
1900
2095
  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
- })));
2096
+ const index = ordinal++;
2097
+ const write = this.store.appendSessionEvent(runDir, binding, direction, message).then((seq) => {
2098
+ eventStartSeq = Math.min(eventStartSeq ?? seq, seq);
2099
+ eventEndSeq = Math.max(eventEndSeq, seq);
2100
+ }, (reason) => {
2101
+ if (!failure || index < failure.ordinal) failure = {
2102
+ ordinal: index,
2103
+ reason
2104
+ };
2105
+ }).finally(() => pending.delete(write));
2106
+ pending.add(write);
1908
2107
  },
1909
- async run(operation) {
2108
+ async run(operation, finalize) {
1910
2109
  let result;
1911
2110
  try {
1912
2111
  result = {
@@ -1919,78 +2118,84 @@ var FlowRunner = class {
1919
2118
  reason
1920
2119
  };
1921
2120
  }
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);
2121
+ await Promise.all(pending);
2122
+ let finalized;
2123
+ try {
2124
+ finalized = {
2125
+ status: "fulfilled",
2126
+ value: await finalize(snapshot(result))
2127
+ };
2128
+ } catch (reason) {
2129
+ finalized = {
2130
+ status: "rejected",
2131
+ reason
2132
+ };
1930
2133
  }
2134
+ if (result.status === "rejected") throw result.reason;
2135
+ if (failure) throw failure.reason;
1931
2136
  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
- };
2137
+ if (finalized.status === "rejected") throw finalized.reason;
2138
+ return finalized.value;
1937
2139
  }
1938
2140
  };
1939
2141
  }
1940
- async runPersistentPrompt(runDir, state, binding, prompt, timeoutMs) {
2142
+ async runPersistentPrompt(runDir, state, binding, prepared) {
2143
+ const { attempt, prompt } = prepared;
1941
2144
  const capture = createQuietCaptureOutput();
1942
2145
  const beforeRecord = await resolveSessionRecord(binding.acpxRecordId);
2146
+ attempt.assertActive();
1943
2147
  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(() => {});
2148
+ const clients = this.pendingPersistentSessionClients.get(runDir);
2149
+ const initialClient = clients?.get(binding.key);
2150
+ if (initialClient) {
2151
+ clients?.delete(binding.key);
2152
+ this.pendingClientReleases.get(initialClient)?.();
2153
+ this.pendingClientReleases.delete(initialClient);
1978
2154
  }
2155
+ return await events.run(() => sendSessionDirect({
2156
+ sessionId: binding.acpxRecordId,
2157
+ prompt,
2158
+ resumePolicy: "same-session-only",
2159
+ ...this.connectionOptions,
2160
+ outputFormatter: capture.formatter,
2161
+ errorEmissionPolicy: { queueErrorAlreadyEmitted: false },
2162
+ onAcpMessage: events.onAcpMessage,
2163
+ suppressSdkConsoleErrors: this.suppressSdkConsoleErrors,
2164
+ client: initialClient
2165
+ }, {
2166
+ signal: attempt.signal,
2167
+ handleProcessInterrupts: false
2168
+ }), async (receipt) => {
2169
+ const rawText = capture.read();
2170
+ prepared.result.rawText = rawText;
2171
+ const afterRecord = await resolveSessionRecord(binding.acpxRecordId);
2172
+ const sessionInfo = {
2173
+ ...binding,
2174
+ acpSessionId: afterRecord.acpSessionId,
2175
+ agentSessionId: afterRecord.agentSessionId
2176
+ };
2177
+ state.sessionBindings[sessionInfo.key] = sessionInfo;
2178
+ return await this.publishAcpCapture(runDir, state, prepared, sessionInfo, afterRecord, findConversationDeltaStart(beforeRecord.messages, afterRecord.messages), rawText, receipt.events);
2179
+ });
1979
2180
  }
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) {
2181
+ async closePendingPersistentSessionClients(runDir) {
2182
+ const pendingClients = [...this.pendingPersistentSessionClients.get(runDir)?.values() ?? []];
2183
+ this.pendingPersistentSessionClients.delete(runDir);
2184
+ const failure = (await Promise.allSettled(pendingClients.map(async (client) => {
2185
+ this.pendingClientReleases.get(client)?.();
2186
+ this.pendingClientReleases.delete(client);
2187
+ await client.close();
2188
+ }))).find((result) => result.status === "rejected");
2189
+ if (failure?.status === "rejected") throw failure.reason;
2190
+ }
2191
+ async runIsolatedPrompt(runDir, state, binding, prepared) {
2192
+ const { agentInfo: agent, prompt, attempt } = prepared;
1988
2193
  const capture = createQuietCaptureOutput();
1989
- const conversation = createSessionConversation(state.currentNodeStartedAt ?? isoNow$1());
2194
+ const conversation = createSessionConversation(attempt.startedAt);
1990
2195
  let acpxState;
1991
- recordPromptSubmission(conversation, prompt, state.currentNodeStartedAt ?? isoNow$1());
2196
+ recordPromptSubmission(conversation, prompt, attempt.startedAt);
1992
2197
  const events = this.createPromptEventCapture(runDir, binding);
1993
- const { result, eventStartSeq, eventEndSeq } = await events.run(() => runOnce({
2198
+ return await events.run(() => runOnce({
1994
2199
  agentCommand: agent.agentCommand,
1995
2200
  agentArgv: agent.agentArgv,
1996
2201
  cwd: agent.cwd,
@@ -2006,35 +2211,29 @@ var FlowRunner = class {
2006
2211
  acpxState = recordClientOperation(conversation, acpxState, operation);
2007
2212
  },
2008
2213
  suppressSdkConsoleErrors: this.suppressSdkConsoleErrors,
2009
- timeoutMs,
2010
2214
  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
2215
+ }, {
2216
+ signal: attempt.signal,
2217
+ handleProcessInterrupts: false
2218
+ }), async (receipt) => {
2219
+ const rawText = capture.read();
2220
+ prepared.result.rawText = rawText;
2221
+ const sessionId = receipt.outcome.status === "fulfilled" ? receipt.outcome.value.sessionId : capture.sessionId();
2222
+ const sessionInfo = sessionId === void 0 ? binding : {
2223
+ ...binding,
2224
+ acpxRecordId: sessionId,
2225
+ acpSessionId: sessionId
2226
+ };
2227
+ const record = createSyntheticSessionRecord({
2228
+ binding: sessionInfo,
2229
+ createdAt: attempt.startedAt,
2230
+ updatedAt: conversation.updated_at,
2231
+ conversation,
2232
+ acpxState: cloneSessionAcpxState(acpxState),
2233
+ lastSeq: receipt.lastSeq
2234
+ });
2235
+ return await this.publishAcpCapture(runDir, state, prepared, sessionInfo, record, 0, rawText, receipt.events);
2025
2236
  });
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
2237
  }
2039
2238
  };
2040
2239
  //#endregion
@@ -2064,9 +2263,23 @@ function parseFencedJsonIfAllowed(text, mode) {
2064
2263
  const fencedText = extractFencedJsonText(text);
2065
2264
  return fencedText === null ? { ok: false } : tryParse(fencedText);
2066
2265
  }
2266
+ const COMPAT_JSON_WORK_MULTIPLIER = 8;
2267
+ function consumeRecoveryWork(budget, units) {
2268
+ if (units > budget.remaining) {
2269
+ budget.remaining = 0;
2270
+ return false;
2271
+ }
2272
+ budget.remaining -= units;
2273
+ return true;
2274
+ }
2067
2275
  function parseBalancedJsonCandidate(text) {
2068
- for (const candidate of extractBalancedJsonCandidates(text)) {
2069
- const parsed = tryParse(candidate);
2276
+ const budget = { remaining: text.length * COMPAT_JSON_WORK_MULTIPLIER };
2277
+ for (let index = 0; index < text.length && budget.remaining > 0; index += 1) {
2278
+ if (text[index] !== "{" && text[index] !== "[") continue;
2279
+ const end = scanBalanced(text, index, budget);
2280
+ if (end === null) continue;
2281
+ if (!consumeRecoveryWork(budget, end - index)) return { ok: false };
2282
+ const parsed = tryParse(text.slice(index, end));
2070
2283
  if (parsed.ok) return parsed;
2071
2284
  }
2072
2285
  return { ok: false };
@@ -2100,20 +2313,12 @@ function extractFencedJsonText(text) {
2100
2313
  function isFenceWhitespace(char) {
2101
2314
  return char === " " || char === "\n" || char === "\r" || char === " ";
2102
2315
  }
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) {
2316
+ function scanBalanced(text, startIndex, budget) {
2113
2317
  const stack = [];
2114
2318
  let inString = false;
2115
2319
  let escaped = false;
2116
2320
  for (let index = startIndex; index < text.length; index += 1) {
2321
+ if (!consumeRecoveryWork(budget, 1)) return null;
2117
2322
  const char = text[index];
2118
2323
  if (inString) {
2119
2324
  const next = scanStringChar(char, escaped);
@@ -2125,13 +2330,13 @@ function scanBalanced(text, startIndex) {
2125
2330
  inString = true;
2126
2331
  continue;
2127
2332
  }
2128
- const result = scanBalancedToken(text, startIndex, index, char, stack);
2333
+ const result = scanBalancedToken(index, char, stack);
2129
2334
  if (result !== SCAN_CONTINUE) return result;
2130
2335
  }
2131
2336
  return null;
2132
2337
  }
2133
2338
  const SCAN_CONTINUE = Symbol("scan-continue");
2134
- function scanBalancedToken(text, startIndex, index, char, stack) {
2339
+ function scanBalancedToken(index, char, stack) {
2135
2340
  if (char === "{" || char === "[") {
2136
2341
  stack.push(char);
2137
2342
  return SCAN_CONTINUE;
@@ -2139,7 +2344,7 @@ function scanBalancedToken(text, startIndex, index, char, stack) {
2139
2344
  if (char !== "}" && char !== "]") return SCAN_CONTINUE;
2140
2345
  if (!balancedClosingTokenMatches(stack.at(-1), char)) return null;
2141
2346
  stack.pop();
2142
- return stack.length === 0 ? text.slice(startIndex, index + 1) : SCAN_CONTINUE;
2347
+ return stack.length === 0 ? index + 1 : SCAN_CONTINUE;
2143
2348
  }
2144
2349
  function balancedClosingTokenMatches(open, close) {
2145
2350
  if (open === "{") return close === "}";
@@ -2229,4 +2434,4 @@ function formatDecisionPrompt(question, choices, field) {
2229
2434
  //#endregion
2230
2435
  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
2436
 
2232
- //# sourceMappingURL=flows-xGyhmoHO.js.map
2437
+ //# sourceMappingURL=flows--Ewxn_fQ.js.map