@relayflows/sdk 2.0.51 → 2.0.53

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 (86) hide show
  1. package/dist/authored-budget.d.ts +1 -1
  2. package/dist/authored-budget.d.ts.map +1 -1
  3. package/dist/authored-budget.js +3 -3
  4. package/dist/authored-budget.js.map +1 -1
  5. package/dist/authored-flow-error.d.ts +3 -1
  6. package/dist/authored-flow-error.d.ts.map +1 -1
  7. package/dist/authored-flow-error.js.map +1 -1
  8. package/dist/authored-flow-executor.d.ts +2 -0
  9. package/dist/authored-flow-executor.d.ts.map +1 -1
  10. package/dist/authored-flow-executor.js +93 -12
  11. package/dist/authored-flow-executor.js.map +1 -1
  12. package/dist/authored-flow-lifecycle.d.ts +28 -3
  13. package/dist/authored-flow-lifecycle.d.ts.map +1 -1
  14. package/dist/authored-flow-lifecycle.js +206 -10
  15. package/dist/authored-flow-lifecycle.js.map +1 -1
  16. package/dist/authored-node-entry.js +16 -0
  17. package/dist/authored-node-entry.js.map +1 -1
  18. package/dist/authored-node-runner.d.ts.map +1 -1
  19. package/dist/authored-node-runner.js +26 -3
  20. package/dist/authored-node-runner.js.map +1 -1
  21. package/dist/authored-parallel-body.d.ts +10 -0
  22. package/dist/authored-parallel-body.d.ts.map +1 -0
  23. package/dist/authored-parallel-body.js +68 -0
  24. package/dist/authored-parallel-body.js.map +1 -0
  25. package/dist/authored-parallel-eligibility.d.ts +22 -0
  26. package/dist/authored-parallel-eligibility.d.ts.map +1 -0
  27. package/dist/authored-parallel-eligibility.js +335 -0
  28. package/dist/authored-parallel-eligibility.js.map +1 -0
  29. package/dist/authored-promise-graph.d.ts +16 -0
  30. package/dist/authored-promise-graph.d.ts.map +1 -1
  31. package/dist/authored-promise-graph.js +36 -0
  32. package/dist/authored-promise-graph.js.map +1 -1
  33. package/dist/authored-root-readback.d.ts +1 -2
  34. package/dist/authored-root-readback.d.ts.map +1 -1
  35. package/dist/authored-root-readback.js +1 -1
  36. package/dist/authored-root-readback.js.map +1 -1
  37. package/dist/authored-root.d.ts +6 -1
  38. package/dist/authored-root.d.ts.map +1 -1
  39. package/dist/authored-root.js +78 -8
  40. package/dist/authored-root.js.map +1 -1
  41. package/dist/authored-step-graph.d.ts +19 -4
  42. package/dist/authored-step-graph.d.ts.map +1 -1
  43. package/dist/authored-step-graph.js +49 -6
  44. package/dist/authored-step-graph.js.map +1 -1
  45. package/dist/authored-step-index.d.ts +2 -1
  46. package/dist/authored-step-index.d.ts.map +1 -1
  47. package/dist/authored-step-index.js +2 -1
  48. package/dist/authored-step-index.js.map +1 -1
  49. package/dist/authored-verdict.d.ts +4 -0
  50. package/dist/authored-verdict.d.ts.map +1 -1
  51. package/dist/authored-verdict.js +7 -1
  52. package/dist/authored-verdict.js.map +1 -1
  53. package/dist/authored-worker-step.d.ts +2 -1
  54. package/dist/authored-worker-step.d.ts.map +1 -1
  55. package/dist/authored-worker-step.js +12 -5
  56. package/dist/authored-worker-step.js.map +1 -1
  57. package/dist/cli/run.d.ts.map +1 -1
  58. package/dist/cli/run.js +4 -2
  59. package/dist/cli/run.js.map +1 -1
  60. package/dist/failure-kinds.d.ts +1 -1
  61. package/dist/failure-kinds.d.ts.map +1 -1
  62. package/dist/failure-kinds.js +2 -0
  63. package/dist/failure-kinds.js.map +1 -1
  64. package/dist/journal-client.d.ts +3 -1
  65. package/dist/journal-client.d.ts.map +1 -1
  66. package/dist/journal-client.js +18 -1
  67. package/dist/journal-client.js.map +1 -1
  68. package/package.json +3 -2
  69. package/src/authored-budget.ts +4 -3
  70. package/src/authored-flow-error.ts +2 -0
  71. package/src/authored-flow-executor.ts +85 -11
  72. package/src/authored-flow-lifecycle.ts +215 -9
  73. package/src/authored-node-entry.ts +17 -0
  74. package/src/authored-node-runner.ts +24 -3
  75. package/src/authored-parallel-body.ts +67 -0
  76. package/src/authored-parallel-eligibility.ts +296 -0
  77. package/src/authored-promise-graph.ts +35 -0
  78. package/src/authored-root-readback.ts +1 -2
  79. package/src/authored-root.ts +80 -9
  80. package/src/authored-step-graph.ts +50 -5
  81. package/src/authored-step-index.ts +2 -1
  82. package/src/authored-verdict.ts +7 -1
  83. package/src/authored-worker-step.ts +12 -3
  84. package/src/cli/run.ts +4 -2
  85. package/src/failure-kinds.ts +2 -0
  86. package/src/journal-client.ts +19 -0
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage, executionAsyncId } from 'node:async_hooks';
2
2
  import { AuthoredFlowExecutionError } from './authored-flow-error.js';
3
3
  import { AuthoredPromiseGraph } from './authored-promise-graph.js';
4
- import { AuthoredStepGraph } from './authored-step-graph.js';
4
+ import { AuthoredStepGraph, promiseOutcome } from './authored-step-graph.js';
5
5
  import type { AuthoredStepEdges } from './authored-step-index.js';
6
6
 
7
7
  type OperationToken = object;
@@ -10,12 +10,38 @@ interface PromiseAllGroup {
10
10
  aggregate: number;
11
11
  members: ReadonlySet<number>;
12
12
  }
13
+ interface AllSettlementGuard {
14
+ readonly aggregate: Promise<unknown>;
15
+ readonly aggregateId: number;
16
+ readonly combinator: 'all' | 'allSettled';
17
+ readonly members: readonly number[];
18
+ }
13
19
  export interface AuthoredOperationInvocation {
14
20
  readonly asyncId: number;
15
21
  bound: boolean;
16
22
  }
17
23
 
24
+ const nativePromiseConstructor = Promise;
25
+ const nativePromisePrototype = Promise.prototype;
18
26
  const nativePromiseThen = Promise.prototype.then;
27
+ const nativePromiseCatch = Promise.prototype.catch;
28
+ const nativePromiseFinally = Promise.prototype.finally;
29
+ const nativePromiseResolve = Promise.resolve;
30
+ const nativePromiseReject = Promise.reject;
31
+ const nativePromisePrototypeConstructorDescriptor = Object.getOwnPropertyDescriptor(Promise.prototype, 'constructor');
32
+ const nativePromisePrototypeConstructor = Promise.prototype.constructor;
33
+ const nativePromiseSpeciesDescriptor = Object.getOwnPropertyDescriptor(Promise, Symbol.species);
34
+ const nativePromiseSpeciesGetter = nativePromiseSpeciesDescriptor?.get;
35
+ const nativeFunctionToString = Function.prototype.toString;
36
+ /** Use the startup snapshot even when a source parser is loaded on demand. */
37
+ export function authoredFunctionSource(value: Function): string {
38
+ return nativeFunctionToString.call(value);
39
+ }
40
+ const nativeArrayIteratorDescriptor = Object.getOwnPropertyDescriptor(Array.prototype, Symbol.iterator);
41
+ const nativeArrayIterator = Array.prototype[Symbol.iterator];
42
+ const nativeArrayIteratorPrototype = Object.getPrototypeOf(nativeArrayIterator.call([])) as Iterator<unknown>;
43
+ const nativeArrayIteratorNextDescriptor = Object.getOwnPropertyDescriptor(nativeArrayIteratorPrototype, 'next');
44
+ const nativeArrayIteratorNext = nativeArrayIteratorPrototype.next;
19
45
  const activeLifecycle = new AsyncLocalStorage<AuthoredFlowLifecycle>();
20
46
  const stepOwners = new WeakMap<object, {
21
47
  lifecycle: AuthoredFlowLifecycle;
@@ -42,7 +68,8 @@ const stepOwners = new WeakMap<object, {
42
68
  * claim was wrong: it covered whichever member happened to resolve the
43
69
  * aggregate. What is true is narrower and is what the code now implements —
44
70
  * these four are exact, and `adoptFromResolvingContext` in the promise graph is
45
- * a best-effort fallback for aggregates built by hand.
71
+ * a fallback for aggregates built by hand. Parallel f.run admission additionally
72
+ * rejects unregistered join shapes on its causal path and checks observer identity.
46
73
  *
47
74
  * The interception is disclosed to authors in `docs/SURFACE.md`. It is spec
48
75
  * transparent: `Symbol.iterator` is read exactly once (as the intrinsic does),
@@ -58,6 +85,51 @@ const nativeCombinators = Object.freeze(
58
85
  Object.fromEntries(COMBINATORS.map((name) => [name, Promise[name] as unknown as Combinator])),
59
86
  ) as Readonly<Record<CombinatorName, Combinator>>;
60
87
 
88
+ const originallyNative = (value: unknown): boolean => typeof value === 'function'
89
+ && /\{\s*\[native code\]\s*\}/u.test(nativeFunctionToString.call(value));
90
+ const nativeAtModuleLoad = [
91
+ nativePromiseConstructor, nativePromiseThen, nativePromiseCatch, nativePromiseFinally,
92
+ nativePromiseResolve, nativePromiseReject, nativePromisePrototypeConstructor,
93
+ nativePromiseSpeciesGetter, nativeFunctionToString, nativeArrayIterator,
94
+ nativeArrayIteratorNext, ...Object.values(nativeCombinators),
95
+ ].every(originallyNative);
96
+
97
+ function sameDescriptor(actual: PropertyDescriptor | undefined, original: PropertyDescriptor | undefined): boolean {
98
+ return actual !== undefined && original !== undefined
99
+ && actual.configurable === original.configurable && actual.enumerable === original.enumerable
100
+ && actual.writable === original.writable && actual.value === original.value
101
+ && actual.get === original.get && actual.set === original.set;
102
+ }
103
+
104
+ /** Preloaded code can replace an intrinsic before this module takes its snapshot. */
105
+ function authoredProcessHasPreload(): boolean {
106
+ const preload = (arg: string): boolean => arg === '-r' || arg.startsWith('-r')
107
+ || ['--import', '--require', '--loader', '--experimental-loader']
108
+ .some(flag => arg === flag || arg.startsWith(`${flag}=`));
109
+ return process.execArgv.some(preload)
110
+ || nodeOptionTokens(process.env['NODE_OPTIONS'] ?? '').some(preload);
111
+ }
112
+
113
+ /** NODE_OPTIONS is a command-line string, while execArgv is already tokenized. */
114
+ function nodeOptionTokens(options: string): string[] {
115
+ const tokens: string[] = [];
116
+ let token = '';
117
+ let quote: '"' | "'" | undefined;
118
+ let escaped = false;
119
+ for (const char of options) {
120
+ if (escaped) { token += char; escaped = false; continue; }
121
+ if (char === '\\' && quote !== "'") { escaped = true; continue; }
122
+ if (char === quote) { quote = undefined; continue; }
123
+ if ((char === '"' || char === "'") && quote === undefined) { quote = char; continue; }
124
+ if (/\s/u.test(char) && quote === undefined) {
125
+ if (token.length > 0) tokens.push(token);
126
+ token = '';
127
+ } else token += char;
128
+ }
129
+ if (token.length > 0) tokens.push(token);
130
+ return tokens;
131
+ }
132
+
61
133
  const observedCombinators: Record<CombinatorName, Combinator> = Object.fromEntries(
62
134
  COMBINATORS.map((name) => {
63
135
  const native = nativeCombinators[name];
@@ -65,11 +137,16 @@ const observedCombinators: Record<CombinatorName, Combinator> = Object.fromEntri
65
137
  this: PromiseConstructor,
66
138
  values: Iterable<unknown>,
67
139
  ): Promise<unknown> {
140
+ const lifecycle = activeLifecycle.getStore();
141
+ const declared = lifecycle?.captureParallelArrayMembers(values);
68
142
  const members = collectMembers(values);
69
- if (members === undefined) return native.call(this, values);
70
- const aggregate = native.call(this, members);
71
- activeLifecycle.getStore()?.registerCombinator(members, aggregate, name);
72
- return aggregate;
143
+ if (members === undefined) {
144
+ if (declared !== undefined) lifecycle?.refuseUnsafeCombinator(
145
+ `parallel Promise.${name} could not inspect its array members`);
146
+ return native.call(this, values);
147
+ }
148
+ if (lifecycle === undefined) return native.call(this, members);
149
+ return lifecycle.observeCombinator(native, this, members, name, declared);
73
150
  };
74
151
  Object.defineProperty(observed, 'name', { value: name, configurable: true });
75
152
  Object.defineProperty(observed, 'length', { value: 1, configurable: true });
@@ -79,6 +156,30 @@ const observedCombinators: Record<CombinatorName, Combinator> = Object.fromEntri
79
156
 
80
157
  let combinatorObservers = 0;
81
158
 
159
+ /** An imported module must not substitute any primitive used to construct joins. */
160
+ export function parallelPromiseIntrinsicsIntact(): boolean {
161
+ if (!nativeAtModuleLoad || authoredProcessHasPreload()
162
+ || Function.prototype.toString !== nativeFunctionToString
163
+ || !sameDescriptor(Object.getOwnPropertyDescriptor(Array.prototype, Symbol.iterator),
164
+ nativeArrayIteratorDescriptor)
165
+ || Array.prototype[Symbol.iterator] !== nativeArrayIterator
166
+ || !sameDescriptor(Object.getOwnPropertyDescriptor(nativeArrayIteratorPrototype, 'next'),
167
+ nativeArrayIteratorNextDescriptor)
168
+ || nativeArrayIteratorPrototype.next !== nativeArrayIteratorNext
169
+ || Promise !== nativePromiseConstructor || Promise.prototype !== nativePromisePrototype
170
+ || !sameDescriptor(Object.getOwnPropertyDescriptor(Promise.prototype, 'constructor'),
171
+ nativePromisePrototypeConstructorDescriptor)
172
+ || Promise.prototype.constructor !== nativePromisePrototypeConstructor
173
+ || !sameDescriptor(Object.getOwnPropertyDescriptor(Promise, Symbol.species), nativePromiseSpeciesDescriptor)
174
+ || Object.getOwnPropertyDescriptor(Promise, Symbol.species)?.get !== nativePromiseSpeciesGetter
175
+ || Promise.prototype.then !== nativePromiseThen
176
+ || Promise.prototype.catch !== nativePromiseCatch
177
+ || Promise.prototype.finally !== nativePromiseFinally
178
+ || Promise.resolve !== nativePromiseResolve || Promise.reject !== nativePromiseReject) return false;
179
+ return COMBINATORS.every(name => Promise[name] === (combinatorObservers > 0
180
+ ? observedCombinators[name] : nativeCombinators[name]) as unknown);
181
+ }
182
+
82
183
  /**
83
184
  * Drain an iterable into an array, reading `Symbol.iterator` exactly once.
84
185
  *
@@ -125,6 +226,8 @@ export class AuthoredFlowLifecycle {
125
226
  private readonly invocations = new Map<OperationToken, AuthoredOperationInvocation[]>();
126
227
  private readonly promiseAllAggregates = new Map<OperationToken, Set<number>>();
127
228
  private readonly promiseAllGroups: PromiseAllGroup[] = [];
229
+ private readonly allSettlementGuards: AllSettlementGuard[] = [];
230
+ private invalidAllSettlement = false;
128
231
  private readonly callbackFailures = new Map<OperationToken, unknown>();
129
232
  private completionAsyncId: number | undefined;
130
233
  private closed = false;
@@ -132,14 +235,19 @@ export class AuthoredFlowLifecycle {
132
235
  private registrations = 0;
133
236
  private groupsByInvocation: { readonly key: string; readonly groups: Map<number, ReadonlySet<number>> } | undefined;
134
237
 
135
- constructor() {
238
+ constructor(private readonly refuseUnknownThenable?: (message: string) => void) {
136
239
  this.graph = new AuthoredPromiseGraph(
137
240
  () => activeLifecycle.getStore() === this,
138
241
  (asyncId) => {
139
242
  for (const probe of this.activeResolverProbes) probe.add(asyncId);
140
243
  },
141
244
  );
142
- this.stepGraph = new AuthoredStepGraph((asyncId) => this.graph.causesOf(asyncId));
245
+ this.stepGraph = new AuthoredStepGraph(
246
+ (asyncId) => this.graph.causesOf(asyncId),
247
+ undefined,
248
+ (asyncId) => this.graph.wasAdoptedFromResolvingContext(asyncId)
249
+ || this.graph.hasUnobservedJoinShape(asyncId),
250
+ );
143
251
  installPromiseAllObserver();
144
252
  try {
145
253
  this.graph.enable();
@@ -153,6 +261,19 @@ export class AuthoredFlowLifecycle {
153
261
  return activeLifecycle.run(this, body);
154
262
  }
155
263
 
264
+ /** Compare the literal array's indexed members with its iterable view. */
265
+ captureParallelArrayMembers(values: Iterable<unknown>): readonly unknown[] | undefined {
266
+ if (this.refuseUnknownThenable === undefined || !Array.isArray(values)) return undefined;
267
+ const members = new Array<unknown>(values.length);
268
+ for (let i = 0; i < values.length; i++) members[i] = values[i];
269
+ return members;
270
+ }
271
+
272
+ refuseUnsafeCombinator(message: string): never {
273
+ this.refuseUnknownThenable?.(message);
274
+ throw new AuthoredFlowExecutionError('step_failed', message);
275
+ }
276
+
156
277
  registerStep(step: object, operation: OperationToken): void {
157
278
  stepOwners.set(step, { lifecycle: this, operation });
158
279
  }
@@ -161,6 +282,7 @@ export class AuthoredFlowLifecycle {
161
282
  values: readonly unknown[],
162
283
  aggregate: Promise<unknown>,
163
284
  combinator: CombinatorName,
285
+ created: ReadonlySet<number>,
164
286
  ): void {
165
287
  const aggregateId = this.graph.idOf(aggregate);
166
288
  if (aggregateId === undefined) return;
@@ -177,6 +299,7 @@ export class AuthoredFlowLifecycle {
177
299
  const awaited = owner?.lifecycle === this ? this.operationPromises.get(owner.operation) : memberId;
178
300
  if (awaited !== undefined) awaitedMembers.push(awaited);
179
301
  if (owner?.lifecycle !== this) continue;
302
+ if (awaited !== undefined) this.stepGraph.certifyMember(awaited);
180
303
  let operationAggregates = this.promiseAllAggregates.get(owner.operation);
181
304
  if (operationAggregates === undefined) {
182
305
  operationAggregates = new Set();
@@ -185,8 +308,53 @@ export class AuthoredFlowLifecycle {
185
308
  operationAggregates.add(aggregateId);
186
309
  }
187
310
  this.promiseAllGroups.push({ aggregate: aggregateId, members: memberIds });
311
+ if (this.refuseUnknownThenable !== undefined
312
+ && (combinator === 'all' || combinator === 'allSettled')) {
313
+ this.allSettlementGuards.push({ aggregate, aggregateId, combinator, members: awaitedMembers });
314
+ }
188
315
  this.registrations++;
189
- this.stepGraph.registerAggregate(aggregateId, aggregate, combinator, awaitedMembers);
316
+ this.stepGraph.registerAggregate(aggregateId, aggregate, combinator, awaitedMembers, created);
317
+ }
318
+
319
+ observeCombinator(
320
+ native: Combinator,
321
+ constructor: PromiseConstructor,
322
+ values: readonly unknown[],
323
+ name: CombinatorName,
324
+ declared?: readonly unknown[],
325
+ ): Promise<unknown> {
326
+ if (declared !== undefined) {
327
+ if (declared.length !== values.length) this.refuseUnsafeCombinator(
328
+ `parallel Promise.${name} iterated a different set of array members`);
329
+ for (let i = 0; i < declared.length; i++) {
330
+ if (declared[i] !== values[i]) this.refuseUnsafeCombinator(
331
+ `parallel Promise.${name} iterated a different set of array members`);
332
+ }
333
+ }
334
+ // Check before the native combinator reads any unknown `then` getter. A
335
+ // member with no tracked promise id or owned step has no causal identity
336
+ // to expand on a later follow-up. This also covers proxy thenables whose
337
+ // `then` is synthesized by a get trap and has no property descriptor.
338
+ if (this.refuseUnknownThenable !== undefined) {
339
+ for (const value of values) {
340
+ if ((typeof value !== 'object' && typeof value !== 'function') || value === null) continue;
341
+ if (stepOwners.get(value)?.lifecycle === this || this.graph.idOf(value) !== undefined) continue;
342
+ const message = `parallel Promise.${name} contains a member with no tracked provenance`;
343
+ this.refuseUnsafeCombinator(message);
344
+ }
345
+ }
346
+ const { value, created } = this.graph.captureCombinator(() => native.call(constructor, values));
347
+ this.registerCombinator(values, value, name, created);
348
+ return value;
349
+ }
350
+
351
+ /** Mark the exact promise returned by a grammar-certified async branch call. */
352
+ certifyBranchPromise(promise: Promise<unknown>): void {
353
+ const id = this.graph.idOf(promise);
354
+ if (id === undefined) {
355
+ throw new AuthoredFlowExecutionError('step_failed', 'certified branch promise was not tracked');
356
+ }
357
+ this.stepGraph.certifyMember(id);
190
358
  }
191
359
 
192
360
  /**
@@ -215,6 +383,43 @@ export class AuthoredFlowLifecycle {
215
383
  return this.stepGraph.edgesOf(step);
216
384
  }
217
385
 
386
+ /** Predecessors for a new invocation, before it has an operation promise. */
387
+ invocationPredecessors(): {
388
+ readonly steps: readonly string[];
389
+ readonly truncated: boolean;
390
+ readonly unregisteredAggregate: boolean;
391
+ } {
392
+ const frontier = this.stepGraph.predecessorsAt(executionAsyncId());
393
+ return this.hasInvalidAllSettlement()
394
+ ? { ...frontier, unregisteredAggregate: true }
395
+ : frontier;
396
+ }
397
+
398
+ /** A fulfilled all (or settled allSettled) must have waited for every member. */
399
+ private hasInvalidAllSettlement(): boolean {
400
+ if (this.invalidAllSettlement) return true;
401
+ let retained = 0;
402
+ for (const guard of this.allSettlementGuards) {
403
+ if (this.graph.isPending(guard.aggregateId)) {
404
+ this.allSettlementGuards[retained++] = guard;
405
+ continue;
406
+ }
407
+ if ((guard.combinator === 'allSettled' || promiseOutcome(guard.aggregate) === 'fulfilled')
408
+ && guard.members.some(id => this.graph.isPending(id))) {
409
+ this.invalidAllSettlement = true;
410
+ return true;
411
+ }
412
+ // Once an honest aggregate has settled, its invariant cannot change.
413
+ }
414
+ this.allSettlementGuards.length = retained;
415
+ return false;
416
+ }
417
+
418
+ /** The observation contract must still be installed when a child is admitted. */
419
+ combinatorsObserved(): boolean {
420
+ return combinatorObservers > 0 && parallelPromiseIntrinsicsIntact();
421
+ }
422
+
218
423
  registerInvocation(
219
424
  operation: OperationToken,
220
425
  asyncId: number,
@@ -350,6 +555,7 @@ export class AuthoredFlowLifecycle {
350
555
  this.invocations.clear();
351
556
  this.promiseAllAggregates.clear();
352
557
  this.promiseAllGroups.length = 0;
558
+ this.allSettlementGuards.length = 0;
353
559
  this.groupsByInvocation = undefined;
354
560
  this.callbackFailures.clear();
355
561
  this.activeResolverProbes.length = 0;
@@ -2,6 +2,7 @@ import { FLOW_READ_BUDGET_MS, isReadInterruptionError } from './journal-read-pol
2
2
  import { Worker } from 'node:worker_threads';
3
3
  import { createHash, createHmac } from 'node:crypto';
4
4
  import { readFileSync, writeSync } from 'node:fs';
5
+ import { pathToFileURL } from 'node:url';
5
6
  import { JournalClient } from './journal-client.js';
6
7
  import { executeAuthoredFlow } from './authored-flow-executor.js';
7
8
  import { loadPinnedAuthoredSource } from './authored-source-authority.js';
@@ -73,6 +74,20 @@ try {
73
74
  // NODE_OPTIONS remain the child's inherited values.
74
75
  const agentEnvironment = validatedLocalAgentEnvironment(request.agentEnvironment);
75
76
  const loaded = await loadPinnedAuthoredSource(request.metadata, true);
77
+ if (request.metadata.identityVersion === 2 && request.metadata.executionMode === 'parallel') {
78
+ // Bun admitted the root from the source AST because its Function.toString
79
+ // is reformatted. Reconfirm that exact source and the Node-loaded body
80
+ // before executing any child. A changed file or a changed body fails closed.
81
+ const bytes = readFileSync(request.metadata.flowPath);
82
+ if (createHash('sha256').update(bytes).digest('hex') !== request.metadata.sourceSha256) {
83
+ throw new Error('authored root source authority mismatch');
84
+ }
85
+ const { parallelLoadedBodyEligible } = await import('./authored-parallel-eligibility.js');
86
+ if (!await parallelLoadedBodyEligible(loaded.getDefinition(loaded.handle).body,
87
+ bytes.toString('utf8'), pathToFileURL(request.metadata.flowPath).href, 'transform')) {
88
+ throw new AuthoredFlowExecutionError('step_failed', 'parallel f.run body lost its source certificate');
89
+ }
90
+ }
76
91
  if (request.localAgentStream !== request.metadata.localAgentStream) throw new Error('authored root local agent surface mismatch');
77
92
  if (request.workerCapacity !== undefined && !isAgentCapacity(request.workerCapacity)) throw new Error('invalid authored worker capacity');
78
93
  client = new JournalClient(request.socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS, budgetHandshake: true });
@@ -81,6 +96,8 @@ try {
81
96
  request.metadata.inputPresent ? request.metadata.input : undefined, {
82
97
  getDefinition: loaded.getDefinition, dataDir: request.dataDir,
83
98
  flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
99
+ executionMode: request.metadata.identityVersion === 2 && request.metadata.executionMode === 'parallel'
100
+ ? 'parallel' : 'serial',
84
101
  extensions: loaded.extensions,
85
102
  flowGraph: loaded.graph,
86
103
  localAgentStream: request.localAgentStream, signal: controller.signal,
@@ -20,6 +20,7 @@ import type {
20
20
  import { HUMAN_WAIT_ID } from './authored-human.js';
21
21
  import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
22
22
  import { settlingCompletion } from './step-settling.js';
23
+ import { readAuthoredStepIndex } from './authored-step-index.js';
23
24
  import { isTransportRetries } from './validate.js';
24
25
 
25
26
  let embeddedSource: string | undefined;
@@ -347,10 +348,16 @@ export async function verifyAuthoredNodeResult(
347
348
  const authored = result.journalSteps.filter(step => typeof step?.id === 'string' && !isGate(step.id));
348
349
  const gates = result.journalSteps.filter(step => typeof step?.id === 'string' && isGate(step.id));
349
350
  if (!Number.isSafeInteger(count) || authored.length !== count || isGate(terminal.id)) invalid(`count ${authored.length}!=${count}`);
351
+ const parallel = metadata.identityVersion === 2 && metadata.executionMode === 'parallel';
350
352
  const ordinal = (id: string): number => Number(/-([1-9][0-9]*)$/.exec(id)?.[1]);
351
- // Parallel awaits may finish in either order; validate a copy in declaration order.
352
- const ordered = [...authored].sort((a,b)=>ordinal(a.id)-ordinal(b.id));
353
+ // Serial ids have one global ordinal. Parallel ids have one ordinal per
354
+ // causal predecessor, so several valid siblings end in `-1`. Validate the
355
+ // complete parallel claim against the root's durable child index instead.
356
+ const ordered = parallel ? [...authored] : [...authored].sort((a,b)=>ordinal(a.id)-ordinal(b.id));
353
357
  const authoredIds = new Set(ordered.map(step => step.id));
358
+ if (parallel && (gates.length !== 0 || authoredIds.size !== count
359
+ || ordered.some(step => step === terminal ? !/^complete-[1-9][0-9]*$/u.test(step.id)
360
+ : !/^run-(?:[1-9][0-9]*|after-[a-f0-9]{64}-[1-9][0-9]*)$/u.test(step.id)))) invalid('parallel ids');
354
361
  for (const gate of gates) {
355
362
  if (!authoredIds.has(gate.id.slice(0, -'.gate'.length))) invalid(`orphan ${gate.id}`);
356
363
  }
@@ -361,9 +368,23 @@ export async function verifyAuthoredNodeResult(
361
368
  await journal.connect();
362
369
  try {
363
370
  await journal.hello('flows-authored-result-verifier');
371
+ if (parallel) {
372
+ const indexed = await readAuthoredStepIndex(journal, rootRunId);
373
+ const claimed = new Map(ordered.map(step => [step.id, step]));
374
+ if (indexed.length !== count || indexed.some(step => {
375
+ const matching = claimed.get(step.step);
376
+ return matching === undefined || step.state !== 'completed'
377
+ || step.runId !== matching.runId || step.completionReason !== matching.completionReason
378
+ || step.label !== matching.label || step.afterTruncated !== matching.afterTruncated
379
+ || (step.after === undefined ? matching.after !== undefined
380
+ : !Array.isArray(step.after) || !Array.isArray(matching.after) || step.after.length !== matching.after.length
381
+ || step.after.some((id, index) => id !== matching.after?.[index]));
382
+ })) invalid('parallel index');
383
+ }
364
384
  for (const [index, claimed] of [...ordered, ...gates].entries()) {
365
385
  if (!claimed || typeof claimed.id !== 'string' || typeof claimed.runId !== 'string'
366
- || (index < ordered.length && ordinal(claimed.id) !== index+1) || !['success', 'timeout'].includes(claimed.completionReason)
386
+ || (!parallel && index < ordered.length && ordinal(claimed.id) !== index+1)
387
+ || !['success', 'timeout'].includes(claimed.completionReason)
367
388
  || runs.has(claimed.runId)) invalid(`claim ${claimed?.id}`);
368
389
  runs.add(claimed.runId);
369
390
  const state = await journal.runGet(claimed.runId);
@@ -0,0 +1,67 @@
1
+ import ts from 'typescript';
2
+ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
3
+ import { authoredFunctionSource } from './authored-flow-lifecycle.js';
4
+ import { parallelBodySyntaxEligible, unwrapParallelExpression } from './authored-parallel-eligibility.js';
5
+
6
+ /**
7
+ * Execute a certified body with its branch-call promises marked at the call
8
+ * site. A combinator member is not evidence that it is a safe branch: it may
9
+ * be a hand-built join that resolves from whichever child finishes first.
10
+ *
11
+ * The closed grammar has no module captures, so re-evaluating this function
12
+ * preserves its meaning. Unknown source is refused before the first effect.
13
+ */
14
+ export function instrumentParallelBody<T extends Function>(
15
+ body: T,
16
+ certifyBranch: (promise: Promise<unknown>) => void,
17
+ ): T {
18
+ const source = authoredFunctionSource(body);
19
+ if (!parallelBodySyntaxEligible(body)) {
20
+ throw new AuthoredFlowExecutionError('step_failed', 'parallel f.run body lost its source certificate');
21
+ }
22
+ const prefix = 'const body = ';
23
+ const file = ts.createSourceFile('parallel-body.js', `${prefix}${source};`,
24
+ ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS);
25
+ const declaration = file.statements[0];
26
+ if (!declaration || !ts.isVariableStatement(declaration)) throw new Error('certified body parse failed');
27
+ const arrow = declaration.declarationList.declarations[0]?.initializer;
28
+ if (!arrow || !ts.isArrowFunction(arrow) || !ts.isBlock(arrow.body)) throw new Error('certified body changed');
29
+ const branches = new Set<string>();
30
+ for (const statement of arrow.body.statements) {
31
+ if (!ts.isVariableStatement(statement)) continue;
32
+ for (const item of statement.declarationList.declarations) {
33
+ if (ts.isIdentifier(item.name) && item.initializer && ts.isArrowFunction(item.initializer)) {
34
+ branches.add(item.name.text);
35
+ }
36
+ }
37
+ }
38
+ const calls: Array<{ start: number; end: number }> = [];
39
+ const visit = (node: ts.Node): void => {
40
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
41
+ && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'Promise'
42
+ && node.expression.name.text === 'all' && node.arguments.length === 1) {
43
+ const members = node.arguments[0];
44
+ if (members && ts.isArrayLiteralExpression(members)) {
45
+ for (const outer of members.elements) {
46
+ if (!ts.isExpression(outer)) continue;
47
+ const member = unwrapParallelExpression(outer);
48
+ if (!ts.isCallExpression(member) || member.arguments.length !== 0) continue;
49
+ const named = ts.isIdentifier(member.expression) && branches.has(member.expression.text);
50
+ const inline = ts.isParenthesizedExpression(member.expression)
51
+ && ts.isArrowFunction(member.expression.expression);
52
+ if (named || inline) calls.push({ start: outer.getStart(file) - prefix.length,
53
+ end: outer.getEnd() - prefix.length });
54
+ }
55
+ }
56
+ }
57
+ ts.forEachChild(node, visit);
58
+ };
59
+ visit(arrow);
60
+ let instrumented = source;
61
+ for (const { start, end } of calls.sort((left, right) => right.start - left.start)) {
62
+ instrumented = `${instrumented.slice(0, start)}__certifyParallelBranch(${instrumented.slice(start, end)})${instrumented.slice(end)}`;
63
+ }
64
+ const factory = new Function('__certifyParallelBranch', `return (${instrumented});`) as
65
+ (record: (promise: Promise<unknown>) => Promise<unknown>) => T;
66
+ return factory((promise) => { certifyBranch(promise); return promise; });
67
+ }