@relayflows/sdk 2.0.55 → 2.0.57

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 (214) hide show
  1. package/dist/adapters/base.d.ts +2 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/devin.d.ts +4 -0
  4. package/dist/adapters/devin.d.ts.map +1 -0
  5. package/dist/adapters/devin.js +20 -0
  6. package/dist/adapters/devin.js.map +1 -0
  7. package/dist/adapters/index.d.ts +3 -2
  8. package/dist/adapters/index.d.ts.map +1 -1
  9. package/dist/adapters/index.js +5 -1
  10. package/dist/adapters/index.js.map +1 -1
  11. package/dist/authored-broker.d.ts +13 -0
  12. package/dist/authored-broker.d.ts.map +1 -0
  13. package/dist/authored-broker.js +75 -0
  14. package/dist/authored-broker.js.map +1 -0
  15. package/dist/authored-budget.d.ts +1 -1
  16. package/dist/authored-budget.d.ts.map +1 -1
  17. package/dist/authored-budget.js +8 -2
  18. package/dist/authored-budget.js.map +1 -1
  19. package/dist/authored-flow-error.d.ts +6 -0
  20. package/dist/authored-flow-error.d.ts.map +1 -1
  21. package/dist/authored-flow-error.js.map +1 -1
  22. package/dist/authored-flow-executor.d.ts +11 -0
  23. package/dist/authored-flow-executor.d.ts.map +1 -1
  24. package/dist/authored-flow-executor.js +104 -20
  25. package/dist/authored-flow-executor.js.map +1 -1
  26. package/dist/authored-flow-loader.d.ts.map +1 -1
  27. package/dist/authored-flow-loader.js +42 -3
  28. package/dist/authored-flow-loader.js.map +1 -1
  29. package/dist/authored-helper-effect.d.ts.map +1 -1
  30. package/dist/authored-helper-effect.js +1 -1
  31. package/dist/authored-helper-effect.js.map +1 -1
  32. package/dist/authored-hooks.d.ts +2 -1
  33. package/dist/authored-hooks.d.ts.map +1 -1
  34. package/dist/authored-hooks.js +2 -2
  35. package/dist/authored-hooks.js.map +1 -1
  36. package/dist/authored-human.d.ts +18 -0
  37. package/dist/authored-human.d.ts.map +1 -1
  38. package/dist/authored-human.js +32 -4
  39. package/dist/authored-human.js.map +1 -1
  40. package/dist/authored-mcp.d.ts.map +1 -1
  41. package/dist/authored-mcp.js +3 -2
  42. package/dist/authored-mcp.js.map +1 -1
  43. package/dist/authored-node-entry.js +4 -0
  44. package/dist/authored-node-entry.js.map +1 -1
  45. package/dist/authored-node-runner.d.ts.map +1 -1
  46. package/dist/authored-node-runner.js +1 -0
  47. package/dist/authored-node-runner.js.map +1 -1
  48. package/dist/authored-plugin-effect.d.ts.map +1 -1
  49. package/dist/authored-plugin-effect.js +3 -2
  50. package/dist/authored-plugin-effect.js.map +1 -1
  51. package/dist/authored-preflight.d.ts.map +1 -1
  52. package/dist/authored-preflight.js +2 -1
  53. package/dist/authored-preflight.js.map +1 -1
  54. package/dist/authored-root.d.ts.map +1 -1
  55. package/dist/authored-root.js +13 -3
  56. package/dist/authored-root.js.map +1 -1
  57. package/dist/authored-serial-ordinals.d.ts +41 -0
  58. package/dist/authored-serial-ordinals.d.ts.map +1 -0
  59. package/dist/authored-serial-ordinals.js +45 -0
  60. package/dist/authored-serial-ordinals.js.map +1 -0
  61. package/dist/authored-worker-step.d.ts +5 -1
  62. package/dist/authored-worker-step.d.ts.map +1 -1
  63. package/dist/authored-worker-step.js +19 -2
  64. package/dist/authored-worker-step.js.map +1 -1
  65. package/dist/budget-preflight.d.ts.map +1 -1
  66. package/dist/budget-preflight.js +18 -3
  67. package/dist/budget-preflight.js.map +1 -1
  68. package/dist/bundle-transport.d.ts +2 -0
  69. package/dist/bundle-transport.d.ts.map +1 -1
  70. package/dist/bundle-transport.js +4 -0
  71. package/dist/bundle-transport.js.map +1 -1
  72. package/dist/cached-flow-runtime.d.ts +3 -0
  73. package/dist/cached-flow-runtime.d.ts.map +1 -0
  74. package/dist/cached-flow-runtime.js +37 -0
  75. package/dist/cached-flow-runtime.js.map +1 -0
  76. package/dist/cli/check.d.ts.map +1 -1
  77. package/dist/cli/check.js +13 -1
  78. package/dist/cli/check.js.map +1 -1
  79. package/dist/cli/cli-probe.d.ts.map +1 -1
  80. package/dist/cli/cli-probe.js +72 -11
  81. package/dist/cli/cli-probe.js.map +1 -1
  82. package/dist/cli/cloud-deploy.js +6 -5
  83. package/dist/cli/cloud-deploy.js.map +1 -1
  84. package/dist/cli/cloud-mirror-session.d.ts +8 -2
  85. package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
  86. package/dist/cli/cloud-mirror-session.js +20 -27
  87. package/dist/cli/cloud-mirror-session.js.map +1 -1
  88. package/dist/cli/direct-run.d.ts.map +1 -1
  89. package/dist/cli/direct-run.js +2 -1
  90. package/dist/cli/direct-run.js.map +1 -1
  91. package/dist/cli/run.d.ts.map +1 -1
  92. package/dist/cli/run.js +19 -4
  93. package/dist/cli/run.js.map +1 -1
  94. package/dist/cli.d.ts.map +1 -1
  95. package/dist/cli.js +39 -18
  96. package/dist/cli.js.map +1 -1
  97. package/dist/cloud-deploy.d.ts +3 -3
  98. package/dist/cloud-deploy.d.ts.map +1 -1
  99. package/dist/cloud-deploy.js +15 -9
  100. package/dist/cloud-deploy.js.map +1 -1
  101. package/dist/cloud-flow-source.d.ts +13 -0
  102. package/dist/cloud-flow-source.d.ts.map +1 -0
  103. package/dist/cloud-flow-source.js +55 -0
  104. package/dist/cloud-flow-source.js.map +1 -0
  105. package/dist/cloud-mirror-transport.d.ts +2 -0
  106. package/dist/cloud-mirror-transport.d.ts.map +1 -1
  107. package/dist/cloud-mirror-transport.js +2 -0
  108. package/dist/cloud-mirror-transport.js.map +1 -1
  109. package/dist/cloud-mirror.d.ts.map +1 -1
  110. package/dist/cloud-mirror.js +10 -1
  111. package/dist/cloud-mirror.js.map +1 -1
  112. package/dist/communication/broker-preflight.d.ts +3 -0
  113. package/dist/communication/broker-preflight.d.ts.map +1 -0
  114. package/dist/communication/broker-preflight.js +22 -0
  115. package/dist/communication/broker-preflight.js.map +1 -0
  116. package/dist/communication/broker-spec.d.ts +20 -0
  117. package/dist/communication/broker-spec.d.ts.map +1 -0
  118. package/dist/communication/broker-spec.js +46 -0
  119. package/dist/communication/broker-spec.js.map +1 -0
  120. package/dist/communication/broker-worker.d.ts +17 -0
  121. package/dist/communication/broker-worker.d.ts.map +1 -0
  122. package/dist/communication/broker-worker.js +214 -0
  123. package/dist/communication/broker-worker.js.map +1 -0
  124. package/dist/communication/environment.d.ts.map +1 -1
  125. package/dist/communication/environment.js +1 -0
  126. package/dist/communication/environment.js.map +1 -1
  127. package/dist/communication/pty.d.ts +29 -0
  128. package/dist/communication/pty.d.ts.map +1 -0
  129. package/dist/communication/pty.js +44 -0
  130. package/dist/communication/pty.js.map +1 -0
  131. package/dist/communication/relay.d.ts +18 -0
  132. package/dist/communication/relay.d.ts.map +1 -1
  133. package/dist/communication/relay.js +4 -0
  134. package/dist/communication/relay.js.map +1 -1
  135. package/dist/communication/skills.d.ts +3 -0
  136. package/dist/communication/skills.d.ts.map +1 -0
  137. package/dist/communication/skills.js +18 -0
  138. package/dist/communication/skills.js.map +1 -0
  139. package/dist/communication/worker.d.ts.map +1 -1
  140. package/dist/communication/worker.js +9 -26
  141. package/dist/communication/worker.js.map +1 -1
  142. package/dist/devin-runtime.d.ts +10 -0
  143. package/dist/devin-runtime.d.ts.map +1 -0
  144. package/dist/devin-runtime.js +37 -0
  145. package/dist/devin-runtime.js.map +1 -0
  146. package/dist/failure-kinds.d.ts +1 -1
  147. package/dist/failure-kinds.d.ts.map +1 -1
  148. package/dist/failure-kinds.js +2 -0
  149. package/dist/failure-kinds.js.map +1 -1
  150. package/dist/plugin-manifest.js +1 -1
  151. package/dist/plugin-manifest.js.map +1 -1
  152. package/dist/preflight.d.ts +1 -1
  153. package/dist/preflight.d.ts.map +1 -1
  154. package/dist/preflight.js +14 -4
  155. package/dist/preflight.js.map +1 -1
  156. package/dist/step-secrets.d.ts.map +1 -1
  157. package/dist/step-secrets.js +1 -0
  158. package/dist/step-secrets.js.map +1 -1
  159. package/dist/worker-cli.d.ts.map +1 -1
  160. package/dist/worker-cli.js +15 -3
  161. package/dist/worker-cli.js.map +1 -1
  162. package/dist/worker.d.ts +1 -0
  163. package/dist/worker.d.ts.map +1 -1
  164. package/dist/worker.js +12 -0
  165. package/dist/worker.js.map +1 -1
  166. package/package.json +2 -2
  167. package/src/adapters/base.ts +2 -0
  168. package/src/adapters/devin.ts +22 -0
  169. package/src/adapters/index.ts +5 -2
  170. package/src/authored-broker.ts +73 -0
  171. package/src/authored-budget.ts +6 -2
  172. package/src/authored-flow-error.ts +6 -0
  173. package/src/authored-flow-executor.ts +115 -20
  174. package/src/authored-flow-loader.ts +43 -3
  175. package/src/authored-helper-effect.ts +2 -1
  176. package/src/authored-hooks.ts +4 -3
  177. package/src/authored-human.ts +35 -4
  178. package/src/authored-mcp.ts +7 -2
  179. package/src/authored-node-entry.ts +4 -1
  180. package/src/authored-node-runner.ts +1 -0
  181. package/src/authored-plugin-effect.ts +7 -2
  182. package/src/authored-preflight.ts +2 -1
  183. package/src/authored-root.ts +13 -3
  184. package/src/authored-serial-ordinals.ts +76 -0
  185. package/src/authored-worker-step.ts +17 -2
  186. package/src/budget-preflight.ts +16 -2
  187. package/src/bundle-transport.ts +4 -0
  188. package/src/cached-flow-runtime.ts +32 -0
  189. package/src/cli/check.ts +11 -1
  190. package/src/cli/cli-probe.ts +65 -11
  191. package/src/cli/cloud-deploy.ts +5 -5
  192. package/src/cli/cloud-mirror-session.ts +23 -29
  193. package/src/cli/direct-run.ts +2 -1
  194. package/src/cli/run.ts +18 -3
  195. package/src/cli.ts +31 -13
  196. package/src/cloud-deploy.ts +19 -14
  197. package/src/cloud-flow-source.ts +58 -0
  198. package/src/cloud-mirror-transport.ts +4 -0
  199. package/src/cloud-mirror.ts +9 -1
  200. package/src/communication/broker-preflight.ts +22 -0
  201. package/src/communication/broker-spec.ts +48 -0
  202. package/src/communication/broker-worker.ts +198 -0
  203. package/src/communication/environment.ts +1 -0
  204. package/src/communication/pty.ts +51 -0
  205. package/src/communication/relay.ts +12 -1
  206. package/src/communication/skills.ts +17 -0
  207. package/src/communication/worker.ts +9 -26
  208. package/src/devin-runtime.ts +33 -0
  209. package/src/failure-kinds.ts +2 -0
  210. package/src/plugin-manifest.ts +1 -1
  211. package/src/preflight.ts +15 -4
  212. package/src/step-secrets.ts +1 -0
  213. package/src/worker-cli.ts +13 -5
  214. package/src/worker.ts +11 -0
@@ -48,7 +48,7 @@ import {
48
48
  AuthoredHumanParked,
49
49
  type AuthoredFlowExecutionErrorCode,
50
50
  } from './authored-flow-error.js';
51
- import { readHumanAnswer } from './authored-human.js';
51
+ import { readHumanWait } from './authored-human.js';
52
52
  import { parseHumanTo } from './human-to.js';
53
53
  import {
54
54
  AuthoredFlowOperation,
@@ -58,6 +58,7 @@ import {
58
58
  import { AuthoredFlowLifecycle, parallelPromiseIntrinsicsIntact } from './authored-flow-lifecycle.js';
59
59
  import { AuthoredActivities } from './authored-activity.js';
60
60
  import { displayLabel, type AuthoredStepEdges } from './authored-step-index.js';
61
+ import { assertSerialOrdinal, serialOrdinals, serialReplayOrderMismatch, type SerialOrdinals } from './authored-serial-ordinals.js';
61
62
  import { JournalClient } from './journal-client.js';
62
63
  import { PluginError } from './plugin-manifest.js';
63
64
  import { createHookEvaluator } from './authored-hooks.js';
@@ -209,6 +210,15 @@ export interface ExecuteAuthoredFlowOptions {
209
210
  readonly flowGraph?: readonly LoadedAuthoredFlowNode[];
210
211
  /** Internal namespace for descendant step/admission identities. */
211
212
  readonly stepPrefix?: string;
213
+ /**
214
+ * The step ids the root's index held when this attempt began, read by the
215
+ * root driver on a re-dispatch and shared with dispatched children, so a
216
+ * serial body is checked against the journal it is replaying
217
+ * (authored-serial-ordinals.ts). Absent on a first attempt: nothing to replay.
218
+ */
219
+ readonly journaledSteps?: readonly string[];
220
+ /** Root-owned replay refusal, shared with dispatched children so one refusal stops the whole tree. */
221
+ readonly replayLatch?: { violation?: AuthoredFlowExecutionError };
212
222
  /** Current child depth. Root is zero; dispatch is capped at three. */
213
223
  readonly dispatchDepth?: number;
214
224
  /** Parent predecessors inherited by the first operation in a child body. */
@@ -327,10 +337,42 @@ export async function executeAuthoredFlow<Input = undefined>(
327
337
  const assertParallelRunOnly = (kind: string): void => {
328
338
  if (options.executionMode === 'parallel') rejectParallel(`parallel f.run body attempted ${kind} before admission`);
329
339
  };
340
+ // Serial bodies only: parallel ids come from the causal frontier, not one
341
+ // shared counter. A refusal is latched, so an author `catch` around the
342
+ // call cannot let the body go on to admit anything else.
343
+ const journaledSteps = options.executionMode === 'parallel' || options.rootRunId === undefined
344
+ ? undefined : options.journaledSteps;
345
+ const ordinals: SerialOrdinals | undefined = journaledSteps === undefined
346
+ ? undefined : serialOrdinals(journaledSteps, options.stepPrefix ?? '');
347
+ const replayLatch = options.replayLatch ?? {};
348
+ const refuseReplay = (error: AuthoredFlowExecutionError): never => {
349
+ replayLatch.violation ??= error;
350
+ throw replayLatch.violation;
351
+ };
352
+ const assertNoReplayViolation = (): void => {
353
+ if (replayLatch.violation !== undefined) throw replayLatch.violation;
354
+ };
355
+ // Every operation re-checks at start: a step declared before the refusal
356
+ // and awaited after it is refused too, not admitted.
357
+ const assertStartAllowed = (verb: string): void => {
358
+ assertNoReplayViolation();
359
+ assertOperationAllowed(verb, definition.name, requestedCompletion);
360
+ };
361
+ const claimedOrdinals = new Set<number>();
362
+ const serialId = (kind: string, sequence: number): string => {
363
+ assertNoReplayViolation();
364
+ claimedOrdinals.add(sequence);
365
+ if (ordinals !== undefined) {
366
+ try { assertSerialOrdinal(ordinals, kind, sequence); } catch (error) {
367
+ refuseReplay(error as AuthoredFlowExecutionError);
368
+ }
369
+ }
370
+ return qualify(`${kind}-${sequence}`);
371
+ };
330
372
  const nextId = (kind: string): string => {
331
373
  if (parallelViolation !== undefined) throw parallelViolation;
374
+ if (options.executionMode !== 'parallel') return serialId(kind, nextStep++);
332
375
  const sequence = nextStep++;
333
- if (options.executionMode !== 'parallel') return qualify(`${kind}-${sequence}`);
334
376
  if (kind !== 'run') rejectParallel(`parallel f.run body attempted ${kind} before admission`);
335
377
  if (!lifecycle.combinatorsObserved()) rejectParallel(
336
378
  'cannot assign a replay-stable f.run identity: a Promise intrinsic was replaced');
@@ -486,7 +528,7 @@ export async function executeAuthoredFlow<Input = undefined>(
486
528
  let llmOp!: AuthoredFlowOperation<unknown>;
487
529
  llmOp = new AuthoredFlowOperation(
488
530
  id, 'llm',
489
- () => assertOperationAllowed('llm', definition.name, requestedCompletion),
531
+ () => assertStartAllowed('llm'),
490
532
  () => observeStep(id, 'llm', () => {
491
533
  if (typeof prompt === 'string') {
492
534
  if (values.length !== 1 || values[0] === undefined) {
@@ -532,7 +574,7 @@ export async function executeAuthoredFlow<Input = undefined>(
532
574
  let recordOp!: AuthoredFlowOperation<RunResult>;
533
575
  recordOp = new AuthoredFlowOperation<RunResult>(
534
576
  id, 'run',
535
- () => assertOperationAllowed('run', definition.name, requestedCompletion),
577
+ () => assertStartAllowed('run'),
536
578
  () => observeStep(id, 'deterministic',
537
579
  () => lowerDeterministic.recording(id, command, leaseMs, recordOp.namedGate, secrets), options.onProgress),
538
580
  lifecycle,
@@ -545,7 +587,7 @@ export async function executeAuthoredFlow<Input = undefined>(
545
587
  let runOp!: AuthoredFlowOperation<string>;
546
588
  runOp = new AuthoredFlowOperation<string>(
547
589
  id, 'run',
548
- () => assertOperationAllowed('run', definition.name, requestedCompletion),
590
+ () => assertStartAllowed('run'),
549
591
  () => observeStep(id, 'deterministic', () => lowerDeterministic(id, command, false, leaseMs, runOp.namedGate, secrets), options.onProgress),
550
592
  lifecycle,
551
593
  // No label: a command is not a display name. It carries literal
@@ -561,10 +603,10 @@ export async function executeAuthoredFlow<Input = undefined>(
561
603
  assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion);
562
604
  assertHelperAllowed(call.provider);
563
605
  const snapshot = snapshotJsonValue(call, 'f.slack call') as unknown as SlackCall;
564
- const id = qualify(`slack-${slackRun}-${nextStep++}`);
606
+ const id = serialId(`slack-${slackRun}`, nextStep++);
565
607
  return trackStep(authoredSteps, new AuthoredFlowOperation<T>(
566
608
  id, `slack.${call.verb}`,
567
- () => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion),
609
+ () => assertStartAllowed(`slack.${call.verb}`),
568
610
  async () => {
569
611
  const receipt = await runHelperEffect(journal, definition.name, id, snapshot,
570
612
  options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId);
@@ -581,7 +623,22 @@ export async function executeAuthoredFlow<Input = undefined>(
581
623
  declared: definition.header?.hooks ?? [],
582
624
  extensions: options.extensions ?? [],
583
625
  peekStep: () => nextStep,
584
- restoreStep: (step) => { nextStep = step; },
626
+ restoreStep: (step, hook) => {
627
+ // A replayed verdict jumps the counter to the watermark it recorded,
628
+ // which also counts ordinals other branches took while the hook ran.
629
+ // Skipping one of those would hand that branch's completed step a fresh
630
+ // ordinal and run it again, so a jump over a journaled ordinal this
631
+ // attempt has not claimed is refused. (It also refuses skipping the
632
+ // hook's own steps; resuming past those already fails at the kernel.)
633
+ for (let ordinal = nextStep; ordinals !== undefined && ordinal < step; ordinal++) {
634
+ const journaled = ordinals.get(ordinal)?.[0];
635
+ if (journaled !== undefined && !claimedOrdinals.has(ordinal)) {
636
+ refuseReplay(serialReplayOrderMismatch(
637
+ `hook step "${hook}" would skip ordinal ${ordinal}, journaled as step "${journaled.step}"`));
638
+ }
639
+ }
640
+ nextStep = step;
641
+ },
585
642
  ...(options.signal === undefined ? {} : { signal: options.signal }),
586
643
  });
587
644
 
@@ -592,9 +649,9 @@ export async function executeAuthoredFlow<Input = undefined>(
592
649
  assertOperationAllowed(verb, definition.name, requestedCompletion);
593
650
  assertHelperAllowed(call.provider);
594
651
  const snapshot = snapshotJsonValue(call, `f.${verb} call`) as unknown as HelperCall;
595
- const id = qualify(`${call.provider}-${slackRun}-${nextStep++}`);
652
+ const id = serialId(`${call.provider}-${slackRun}`, nextStep++);
596
653
  return trackStep(authoredSteps, new AuthoredFlowOperation<T>(id, verb,
597
- () => assertOperationAllowed(verb, definition.name, requestedCompletion),
654
+ () => assertStartAllowed(verb),
598
655
  async () => await runHelperEffect(journal, definition.name, id, snapshot,
599
656
  options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId) as T,
600
657
  lifecycle));
@@ -610,7 +667,7 @@ export async function executeAuthoredFlow<Input = undefined>(
610
667
  const id = nextId('mcp');
611
668
  return trackStep(authoredSteps, new AuthoredFlowOperation(
612
669
  id, 'mcp',
613
- () => assertOperationAllowed('mcp', definition.name, requestedCompletion),
670
+ () => assertStartAllowed('mcp'),
614
671
  () => runMcpEffect(journal, definition.name, id, server, tool, args,
615
672
  known, checkedMcp.servers[server]!, journalSteps, options.rootRunId),
616
673
  lifecycle,
@@ -618,7 +675,7 @@ export async function executeAuthoredFlow<Input = undefined>(
618
675
  }),
619
676
  memory: authoredMemory(
620
677
  scriptMemoryScope(flowPath, definition.name),
621
- () => { assertParallelRunOnly('memory'); assertOperationAllowed('memory', definition.name, requestedCompletion); },
678
+ () => { assertParallelRunOnly('memory'); assertNoReplayViolation(); assertOperationAllowed('memory', definition.name, requestedCompletion); },
622
679
  definition.header.memory?.script !== false,
623
680
  memoryWrites,
624
681
  options.rootRunId === undefined ? undefined : journalLearnLedger(journal, options.rootRunId),
@@ -634,15 +691,41 @@ export async function executeAuthoredFlow<Input = undefined>(
634
691
  agentOp = new AuthoredFlowOperation<AgentResult>(
635
692
  id,
636
693
  'agent',
637
- () => assertOperationAllowed('agent', definition.name, requestedCompletion),
638
- () => observeStep(id, 'agent', () => worker.agent(id, options, agentOp.namedGate), onProgress),
694
+ () => assertStartAllowed('agent'),
695
+ () => observeStep(id, 'agent', () => {
696
+ if (options.transport === 'broker' && options.keepAlive
697
+ && (agentOp.namedGate !== undefined || agentOp.predicateGate !== undefined)) {
698
+ throw new AuthoredFlowExecutionError('agent_cli_unresolved',
699
+ 'Retained broker agents do not support gates; validate their result in a later step');
700
+ }
701
+ return worker.agent(id, options, agentOp.namedGate, name);
702
+ }, onProgress),
639
703
  lifecycle,
640
704
  displayLabel(typeof name === 'string' ? name : undefined),
641
705
  );
642
706
  return trackStep(authoredSteps, agentOp);
643
707
  },
708
+ message(agentName, text, messageOptions) {
709
+ assertOperationAllowed('message', definition.name, requestedCompletion);
710
+ const id = nextId('agent');
711
+ let messageOp!: AuthoredFlowOperation<AgentResult>;
712
+ messageOp = new AuthoredFlowOperation<AgentResult>(id, 'message',
713
+ () => assertStartAllowed('message'),
714
+ () => observeStep(id, 'agent',
715
+ () => worker.message(id, agentName, text, messageOptions, messageOp.namedGate), onProgress),
716
+ lifecycle);
717
+ return trackStep(authoredSteps, messageOp);
718
+ },
719
+ release(agentName) {
720
+ assertOperationAllowed('release', definition.name, requestedCompletion);
721
+ const id = nextId('agent');
722
+ return trackStep(authoredSteps, new AuthoredFlowOperation<void>(id, 'release',
723
+ () => assertStartAllowed('release'),
724
+ () => observeStep(id, 'agent', () => worker.release(id, agentName), onProgress), lifecycle));
725
+ },
644
726
  on(source, activityOptions) {
645
727
  assertParallelRunOnly('on');
728
+ assertNoReplayViolation();
646
729
  assertOperationAllowed('on', definition.name, requestedCompletion);
647
730
  // Activity ids are root-scoped (`activity-N`); a child's own counter would collide.
648
731
  if ((options.dispatchDepth ?? 0) > 0) {
@@ -697,7 +780,7 @@ export async function executeAuthoredFlow<Input = undefined>(
697
780
  let humanOp!: AuthoredFlowOperation<boolean>;
698
781
  humanOp = new AuthoredFlowOperation<boolean>(
699
782
  id, 'human',
700
- () => assertOperationAllowed('human', definition.name, requestedCompletion),
783
+ () => assertStartAllowed('human'),
701
784
  () => observeStep(id, 'deterministic', async () => {
702
785
  const rootRunId = options.rootRunId;
703
786
  if (rootRunId === undefined) {
@@ -706,7 +789,15 @@ export async function executeAuthoredFlow<Input = undefined>(
706
789
  `f.human needs a durable root to park in; "${id}" has no run to wait on. Run the flow with flows run.`,
707
790
  );
708
791
  }
709
- const recorded = await readHumanAnswer(journal, rootRunId, id);
792
+ const { answer: recorded, asked } = await readHumanWait(journal, rootRunId, id);
793
+ // The answer is keyed by the wait id alone, and a reordered resume
794
+ // can hand that id to a different f.human: never apply an answer
795
+ // to a question nobody was asked.
796
+ if (asked.some(wait => wait.question !== question || wait.to !== to)) {
797
+ refuseReplay(serialReplayOrderMismatch(
798
+ `ordinal ${id.slice(id.lastIndexOf('-') + 1)} is journaled as human step "${id}" asking a `
799
+ + 'different question or recipient, but this resume reached it with another f.human'));
800
+ }
710
801
  if (recorded === undefined) throw new AuthoredHumanParked({ waitId: id, question, to }, rootRunId);
711
802
  const record = { human: id, to, answer: recorded.answer,
712
803
  ...(recorded.note === undefined ? {} : { note: recorded.note }),
@@ -735,7 +826,7 @@ export async function executeAuthoredFlow<Input = undefined>(
735
826
  dispatchOp = new AuthoredFlowOperation<DispatchResult>(
736
827
  id,
737
828
  'dispatch',
738
- () => assertOperationAllowed('dispatch', definition.name, requestedCompletion),
829
+ () => assertStartAllowed('dispatch'),
739
830
  async () => {
740
831
  const inheritedAfter = stepEdges(id)?.after ?? [];
741
832
  const currentHelperAuthority = helperAuthority(definition);
@@ -743,6 +834,8 @@ export async function executeAuthoredFlow<Input = undefined>(
743
834
  ? currentHelperAuthority
744
835
  : new Set([...currentHelperAuthority].filter(provider => options.allowedHelperProviders!.has(provider)));
745
836
  const childResult = await executeAuthoredFlow(child.handle, journal, inputSnapshot, {
837
+ ...(journaledSteps === undefined ? {} : { journaledSteps }),
838
+ replayLatch,
746
839
  getDefinition: child.getDefinition,
747
840
  dataDir: options.dataDir,
748
841
  flowPath: child.path,
@@ -799,7 +892,7 @@ export async function executeAuthoredFlow<Input = undefined>(
799
892
  const snapshot = snapshotJsonValue(input, 'f.hook input');
800
893
  return trackStep(authoredSteps, new AuthoredFlowOperation<boolean>(
801
894
  id, 'hook',
802
- () => assertOperationAllowed('hook', definition.name, requestedCompletion),
895
+ () => assertStartAllowed('hook'),
803
896
  async () => {
804
897
  const verdict = await evaluateHook(id, name, snapshot, context);
805
898
  const record = { hook: name, step: id, verdict: verdict ? 'pass' : 'fail' };
@@ -862,7 +955,7 @@ export async function executeAuthoredFlow<Input = undefined>(
862
955
  assertOperationAllowed(label, definition.name, requestedCompletion);
863
956
  const id = nextId('plugin');
864
957
  return trackStep(authoredSteps, new AuthoredFlowOperation(
865
- id, label, () => assertOperationAllowed(label, definition.name, requestedCompletion),
958
+ id, label, () => assertStartAllowed(label),
866
959
  () => runPluginEffect(journal, definition.name, id, plugin, verb, args,
867
960
  journalSteps, budget, options.rootRunId),
868
961
  lifecycle,
@@ -890,6 +983,7 @@ export async function executeAuthoredFlow<Input = undefined>(
890
983
  // A source certificate is a performance admission decision, never a way
891
984
  // for authored try/catch to turn a refused effect into successful output.
892
985
  if (parallelViolation !== undefined) { bodyFailed = true; bodyFailure = parallelViolation; }
986
+ if (replayLatch.violation !== undefined) { bodyFailed = true; bodyFailure = replayLatch.violation; }
893
987
  if (bodyFailed) {
894
988
  // Refuse queued work immediately. A slow memory commit must not leave an
895
989
  // admission slot open after the body has already failed.
@@ -982,7 +1076,8 @@ export async function executeAuthoredFlow<Input = undefined>(
982
1076
  // explanation the root had already journaled to `run_admission_conflict`.
983
1077
  // With no detail there is no environment in the command and nothing to
984
1078
  // commit, so that path is untouched.
985
- const terminalId = qualify(`complete-${nextStep}`);
1079
+ const terminalId = options.executionMode === 'parallel'
1080
+ ? qualify(`complete-${nextStep}`) : serialId('complete', nextStep);
986
1081
  const verdict = await commitAuthoredVerdict(journal, options.rootRunId, terminalId, {
987
1082
  reason: requestedCompletion,
988
1083
  ...(requestedDetail === undefined ? {} : { detail: requestedDetail }),
@@ -311,14 +311,54 @@ export async function packageTreeSha256(root: string): Promise<string> {
311
311
  }
312
312
  await visit(root);
313
313
  const hash = createHash('sha256');
314
- for (const path of files) {
315
- const bytes = await readFile(path);
314
+ await readInOrder(files, SURFACE_READ_CONCURRENCY, (path, bytes) => {
316
315
  const name = relative(root, path).split(sep).join('/');
317
316
  hash.update(name).update('\0').update(String(bytes.length)).update('\0').update(bytes);
318
- }
317
+ });
319
318
  return hash.digest('hex');
320
319
  }
321
320
 
321
+ /**
322
+ * Surface is about 600 small files, and both the CLI and the authored Node
323
+ * child hash it on the startup path. Awaiting one read at a time paid a
324
+ * filesystem round trip per file; a sandbox disk made that several hundred ms.
325
+ */
326
+ const SURFACE_READ_CONCURRENCY = 32;
327
+
328
+ /**
329
+ * Read `paths` through a window of at most `limit` files, started in order
330
+ * and handed to `consume` strictly in `paths` order, so the digest is the
331
+ * serial walk's. The window bounds buffered bytes as well as reads in flight:
332
+ * a slow early file stops new reads instead of letting later files pile up.
333
+ * After any read fails no new read starts. The lowest-index failure, the
334
+ * error a serial walk would meet first, is thrown once every started read
335
+ * has settled.
336
+ */
337
+ async function readInOrder(
338
+ paths: readonly string[],
339
+ limit: number,
340
+ consume: (path: string, bytes: Buffer) => void,
341
+ ): Promise<void> {
342
+ const reads: Array<Promise<Buffer>> = [];
343
+ let failed = false, next = 0;
344
+ const start = (): void => {
345
+ const read = readFile(paths[next++]!);
346
+ read.catch(() => { failed = true; });
347
+ reads.push(read);
348
+ };
349
+ while (next < paths.length && reads.length < limit) start();
350
+ for (let index = 0; reads.length !== 0; index += 1) {
351
+ let bytes: Buffer;
352
+ try { bytes = await reads[0]!; } catch (error) {
353
+ await Promise.allSettled(reads);
354
+ throw error;
355
+ }
356
+ reads.shift();
357
+ consume(paths[index]!, bytes);
358
+ if (!failed && next < paths.length) start();
359
+ }
360
+ }
361
+
322
362
  // Exported for callers (internal SDK tests, and any co-located flow that is
323
363
  // provably the same `@relayflows/surface` instance as this package) that
324
364
  // have no need to resolve a separate copy.
@@ -51,7 +51,8 @@ export async function runHelperEffect(
51
51
  const outcome = await journal.runStart(spec, undefined, admissionKey);
52
52
  await atomicJson(join(dataDir, 'helper-runs', `${outcome.run_id}.json`), { provider: call.provider });
53
53
  await driveHelperEffect(journal, outcome.run_id, spec.steps[0] as KernelAgentStep, call, dataDir);
54
- const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps);
54
+ const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps,
55
+ rootRunId === undefined ? {} : { rootRunId });
55
56
  return (output as { receipt: unknown }).receipt;
56
57
  }
57
58
 
@@ -32,7 +32,8 @@ export function createHookEvaluator(options: {
32
32
  readonly declared: readonly string[];
33
33
  readonly extensions: readonly LoadedFlowExtension[];
34
34
  readonly peekStep?: () => number;
35
- readonly restoreStep?: (step: number) => void;
35
+ /** Restore the parent watermark a recorded verdict carries; `hook` is the replayed hook step. */
36
+ readonly restoreStep?: (step: number, hook: string) => void;
36
37
  readonly signal?: AbortSignal;
37
38
  }): (id: string, name: string, input: unknown, context: Ctx) => Promise<boolean> {
38
39
  let recorded: Promise<Map<string, HookRecord>> | undefined;
@@ -83,7 +84,7 @@ export function createHookEvaluator(options: {
83
84
  const existing = await lookup(`${id}:noop`);
84
85
  const record = existing ?? { hook: name, step: id, plugin: null, verdict: 'noop' as const };
85
86
  if (existing === undefined) await append(record);
86
- else if (existing.afterStep !== undefined) options.restoreStep?.(existing.afterStep);
87
+ else if (existing.afterStep !== undefined) options.restoreStep?.(existing.afterStep, id);
87
88
  return true;
88
89
  }
89
90
  for (const extension of impls) {
@@ -111,7 +112,7 @@ export function createHookEvaluator(options: {
111
112
  };
112
113
  await append(record);
113
114
  } else if (record.afterStep !== undefined) {
114
- options.restoreStep?.(record.afterStep);
115
+ options.restoreStep?.(record.afterStep, id);
115
116
  }
116
117
  if (record.verdict === 'fail') return false;
117
118
  }
@@ -105,13 +105,44 @@ export async function readHumanAnswer(
105
105
  runId: string,
106
106
  waitId: string,
107
107
  ): Promise<HumanAnswer | undefined> {
108
+ return (await readHumanWait(journal, runId, waitId)).answer;
109
+ }
110
+
111
+ /**
112
+ * `readHumanAnswer`, plus every question the run has parked on under
113
+ * `waitId`. The answer is keyed by the wait id alone, so the executor checks
114
+ * that the question it is about to answer is the one that was asked.
115
+ */
116
+ export async function readHumanWait(
117
+ journal: JournalClient,
118
+ runId: string,
119
+ waitId: string,
120
+ ): Promise<{ answer?: HumanAnswer; asked: Array<{ question: unknown; to: unknown }> }> {
121
+ const asked: Array<{ question: unknown; to: unknown }> = [];
108
122
  for (const entry of await readWaitEntries(journal, runId)) {
109
- if (entry.entry_type === 'wait.completed' && entry.payload?.wait_id === waitId
110
- && entry.payload.completionReason === 'human_responded') {
111
- return parseHumanAnswer(entry.payload.result, waitId);
123
+ if (entry.payload?.wait_id !== waitId) continue;
124
+ if (entry.entry_type === 'wait.human') {
125
+ asked.push({ question: entry.payload.prompt, to: entry.payload.requested_of });
126
+ } else if (entry.payload.completionReason === 'human_responded') {
127
+ return { answer: parseHumanAnswer(entry.payload.result, waitId), asked };
112
128
  }
113
129
  }
114
- return undefined;
130
+ return { asked };
131
+ }
132
+
133
+ /**
134
+ * Every `f.human` wait id the run has parked on, answered or not. A parked
135
+ * question is not in the step index until its answer is lowered, but it
136
+ * already holds its serial ordinal.
137
+ */
138
+ export async function readHumanWaitIds(journal: JournalClient, runId: string): Promise<string[]> {
139
+ const ids = new Set<string>();
140
+ for (const entry of await readWaitEntries(journal, runId)) {
141
+ const waitId = entry.payload?.wait_id;
142
+ if (entry.entry_type === 'wait.human' && typeof waitId === 'string'
143
+ && /(?:^|--)human-[1-9][0-9]*$/u.test(waitId)) ids.add(waitId);
144
+ }
145
+ return [...ids];
115
146
  }
116
147
 
117
148
  /** Every `wait.human` on the run that no `wait.completed` has closed. */
@@ -142,13 +142,18 @@ export async function runMcpEffect(
142
142
  childRunId = outcome.run_id;
143
143
  if (outcome.status !== 'completed') await completed;
144
144
  if (diagnostic !== undefined) {
145
- try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status); }
145
+ try {
146
+ await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
147
+ rootRunId === undefined ? {} : { rootRunId }, outcome.status);
148
+ }
146
149
  catch (error) {
147
150
  if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
148
151
  throw new McpStepError(diagnostic, outcome.run_id);
149
152
  }
150
153
  }
151
- const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status) as { output: unknown };
154
+ // Indexed on the root like every other child, so a resume can see its ordinal.
155
+ const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
156
+ rootRunId === undefined ? {} : { rootRunId }, outcome.status) as { output: unknown };
152
157
  return receipt.output;
153
158
  } finally {
154
159
  clearTimeout(timer);
@@ -53,7 +53,7 @@ try {
53
53
  executableSha256: hash(process.execPath), payloadSha256: hash(process.argv[1]!) } });
54
54
  const request = await new Promise<{ channelKey: string; metadata: AuthoredRootMetadata; socketPath: string;
55
55
  rootRunId: string; dataDir: string; localAgentStream?: string; workerCapacity?: number;
56
- agentEnvironment?: unknown }>((resolve, reject) => {
56
+ journaledSteps?: unknown; agentEnvironment?: unknown }>((resolve, reject) => {
57
57
  let buffer = '';
58
58
  process.stdin.setEncoding('utf8');
59
59
  const onData = (chunk: string): void => {
@@ -90,6 +90,8 @@ try {
90
90
  }
91
91
  if (request.localAgentStream !== request.metadata.localAgentStream) throw new Error('authored root local agent surface mismatch');
92
92
  if (request.workerCapacity !== undefined && !isAgentCapacity(request.workerCapacity)) throw new Error('invalid authored worker capacity');
93
+ if (request.journaledSteps !== undefined && (!Array.isArray(request.journaledSteps)
94
+ || !request.journaledSteps.every(step => typeof step === 'string'))) throw new Error('invalid authored journaled steps');
93
95
  client = new JournalClient(request.socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS, budgetHandshake: true });
94
96
  await client.connect(); await client.hello('flows-authored-node');
95
97
  const result = await executeAuthoredFlow(loaded.handle, client,
@@ -102,6 +104,7 @@ try {
102
104
  flowGraph: loaded.graph,
103
105
  localAgentStream: request.localAgentStream, signal: controller.signal,
104
106
  ...(request.workerCapacity === undefined ? {} : { workerCapacity: request.workerCapacity }),
107
+ ...(request.journaledSteps === undefined ? {} : { journaledSteps: request.journaledSteps as string[] }),
105
108
  ...(agentEnvironment === undefined ? {} : { agentEnvironment: { ...process.env, ...agentEnvironment } }),
106
109
  onProgress: event => send({ type: 'progress', event }),
107
110
  onWait: event => send({ type: 'wait', event }),
@@ -191,6 +191,7 @@ export async function runAuthoredInNode(
191
191
  child.stdin!.write(JSON.stringify({ channelKey, metadata, socketPath, rootRunId,
192
192
  dataDir: options.dataDir, localAgentStream: options.localAgentStream,
193
193
  workerCapacity: options.workerCapacity,
194
+ ...(options.journaledSteps === undefined ? {} : { journaledSteps: options.journaledSteps }),
194
195
  agentEnvironment: options.agentEnvironment }) + '\n');
195
196
  } else if (!ready || result) throw new Error('unexpected authored runtime message');
196
197
  else if (message.type === 'progress') options.onProgress?.(message.event);
@@ -127,13 +127,18 @@ export async function runPluginEffect(
127
127
  if (dispatchExpired) await cancelChildRun();
128
128
  if (outcome.status !== 'completed') await completed;
129
129
  if (diagnostic !== undefined) {
130
- try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status); }
130
+ try {
131
+ await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
132
+ rootRunId === undefined ? {} : { rootRunId }, outcome.status);
133
+ }
131
134
  catch (error) {
132
135
  if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
133
136
  throw new PluginStepError(diagnostic, outcome.run_id);
134
137
  }
135
138
  }
136
- const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status) as { output: unknown };
139
+ // Indexed on the root like every other child, so a resume can see its ordinal.
140
+ const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
141
+ rootRunId === undefined ? {} : { rootRunId }, outcome.status) as { output: unknown };
137
142
  return receipt.output;
138
143
  }, admissionKey), deadline]);
139
144
  } finally {
@@ -1,3 +1,4 @@
1
+ import { brokerInstruction } from './communication/broker-spec.js';
1
2
  import { dirname, resolve } from 'node:path';
2
3
  import { checkAuthoredFlow, readProjectConfig, type ProjectConfig } from './cli/check.js';
3
4
  import { probeCliAsync } from './cli/cli-probe.js';
@@ -21,7 +22,7 @@ export function authoredPreflight(path: string, environment?: NodeJS.ProcessEnv)
21
22
  if (!resolved.ok) return checkAuthoredFlow(flow, path, config, { environment });
22
23
  await Promise.all(resolved.resolutions.map(async resolution => {
23
24
  const step = resolved.compiled!.steps.find(step => step.id === resolution.stepId)!;
24
- const managed = step.type === 'agent' && communicationInstruction(step.instruction) !== undefined;
25
+ const managed = step.type === 'agent' && (communicationInstruction(step.instruction) !== undefined || brokerInstruction(step.instruction) !== undefined);
25
26
  const key = cliProbeKey(resolution, managed, step.secrets);
26
27
  if (probeCache.has(key)) return;
27
28
  let probing = pending.get(key);
@@ -19,12 +19,13 @@ import { SPEC_SCHEMA_VERSION } from './spec.js';
19
19
  import type { RunLifecycleOptions } from './cli/run.js';
20
20
  import { isLeaseLost, withWorkerLease } from './worker-lease.js';
21
21
  import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow-error.js';
22
- import { readOpenHumanWaits, resumeCommand } from './authored-human.js';
22
+ import { readHumanWaitIds, readOpenHumanWaits, resumeCommand } from './authored-human.js';
23
23
  import { helperReceiptWaitId, releaseHelperReceiptWaits } from './helper-park.js';
24
24
  import { readSubscriptionPark } from './authored-subscription-park.js';
25
25
  import { localAgentCredentialEnvironment } from './local-agent-environment.js';
26
26
  import { completedResult, rootKernelStep, rootStepIfPresent } from './authored-root-readback.js';
27
27
  import { parallelPromiseIntrinsicsIntact } from './authored-flow-lifecycle.js';
28
+ import { readAuthoredStepIndex } from './authored-step-index.js';
28
29
 
29
30
  export type DurableAuthoredFlowResult =
30
31
  | (AuthoredFlowExecutionResult & { readonly rootRunId: string })
@@ -391,6 +392,14 @@ async function driveRoot(
391
392
  // A lost root lease must also stop body reads still inside their budget.
392
393
  const releaseReads = journal.scopeReads?.(rootSignal) ?? (() => {});
393
394
  try {
395
+ // A re-dispatched serial body replays an earlier attempt; give it that
396
+ // attempt's step index so a reordered resume is refused, not re-run.
397
+ // A first attempt has nothing to replay and reads nothing.
398
+ const parallel = metadata.identityVersion === 2 && metadata.executionMode === 'parallel';
399
+ const journaledSteps = parallel || dispatch.attempt <= 1 ? undefined : [
400
+ ...(await readAuthoredStepIndex(journal, dispatch.run_id)).map(record => record.step),
401
+ ...await readHumanWaitIds(journal, dispatch.run_id),
402
+ ];
394
403
  const callerSignal = options.lifecycle?.signal;
395
404
  const signal = callerSignal === undefined ? rootSignal : AbortSignal.any([callerSignal, rootSignal]);
396
405
  if (process.versions['bun'] !== undefined) {
@@ -401,6 +410,7 @@ async function driveRoot(
401
410
  agentEnvironment: localAgentCredentialEnvironment(options.agentEnvironment),
402
411
  }),
403
412
  ...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
413
+ ...(journaledSteps === undefined ? {} : { journaledSteps }),
404
414
  ...options.lifecycle, signal,
405
415
  });
406
416
  }
@@ -416,8 +426,8 @@ async function driveRoot(
416
426
  ...(options.agentEnvironment === undefined ? {} : { agentEnvironment: options.agentEnvironment }),
417
427
  ...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
418
428
  rootRunId: dispatch.run_id,
419
- executionMode: metadata.identityVersion === 2 && metadata.executionMode === 'parallel'
420
- ? 'parallel' : 'serial',
429
+ executionMode: parallel ? 'parallel' : 'serial',
430
+ ...(journaledSteps === undefined ? {} : { journaledSteps }),
421
431
  extensions: loaded.extensions,
422
432
  flowGraph: loaded.graph,
423
433
  ...options.lifecycle,