@relayflows/sdk 2.0.50 → 2.0.52

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 (167) 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 +97 -14
  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 +2 -0
  17. package/dist/authored-node-entry.js.map +1 -1
  18. package/dist/authored-parallel-body.d.ts +10 -0
  19. package/dist/authored-parallel-body.d.ts.map +1 -0
  20. package/dist/authored-parallel-body.js +68 -0
  21. package/dist/authored-parallel-body.js.map +1 -0
  22. package/dist/authored-parallel-eligibility.d.ts +8 -0
  23. package/dist/authored-parallel-eligibility.d.ts.map +1 -0
  24. package/dist/authored-parallel-eligibility.js +287 -0
  25. package/dist/authored-parallel-eligibility.js.map +1 -0
  26. package/dist/authored-preflight.js +4 -4
  27. package/dist/authored-preflight.js.map +1 -1
  28. package/dist/authored-promise-graph.d.ts +16 -0
  29. package/dist/authored-promise-graph.d.ts.map +1 -1
  30. package/dist/authored-promise-graph.js +36 -0
  31. package/dist/authored-promise-graph.js.map +1 -1
  32. package/dist/authored-root-readback.d.ts +1 -2
  33. package/dist/authored-root-readback.d.ts.map +1 -1
  34. package/dist/authored-root-readback.js +1 -1
  35. package/dist/authored-root-readback.js.map +1 -1
  36. package/dist/authored-root.d.ts +6 -1
  37. package/dist/authored-root.d.ts.map +1 -1
  38. package/dist/authored-root.js +55 -8
  39. package/dist/authored-root.js.map +1 -1
  40. package/dist/authored-step-graph.d.ts +19 -4
  41. package/dist/authored-step-graph.d.ts.map +1 -1
  42. package/dist/authored-step-graph.js +49 -6
  43. package/dist/authored-step-graph.js.map +1 -1
  44. package/dist/authored-step-index.d.ts +2 -1
  45. package/dist/authored-step-index.d.ts.map +1 -1
  46. package/dist/authored-step-index.js +2 -1
  47. package/dist/authored-step-index.js.map +1 -1
  48. package/dist/authored-verdict.d.ts +2 -0
  49. package/dist/authored-verdict.d.ts.map +1 -1
  50. package/dist/authored-verdict.js +4 -1
  51. package/dist/authored-verdict.js.map +1 -1
  52. package/dist/authored-worker-step.d.ts +4 -3
  53. package/dist/authored-worker-step.d.ts.map +1 -1
  54. package/dist/authored-worker-step.js +23 -10
  55. package/dist/authored-worker-step.js.map +1 -1
  56. package/dist/cli/check.js +2 -2
  57. package/dist/cli/check.js.map +1 -1
  58. package/dist/cli/cli-probe.d.ts +1 -1
  59. package/dist/cli/cli-probe.d.ts.map +1 -1
  60. package/dist/cli/cli-probe.js +8 -7
  61. package/dist/cli/cli-probe.js.map +1 -1
  62. package/dist/cli/run.d.ts.map +1 -1
  63. package/dist/cli/run.js +4 -2
  64. package/dist/cli/run.js.map +1 -1
  65. package/dist/cloud-http.d.ts +1 -0
  66. package/dist/cloud-http.d.ts.map +1 -1
  67. package/dist/cloud-http.js +1 -1
  68. package/dist/cloud-http.js.map +1 -1
  69. package/dist/cloud-run.d.ts +1 -1
  70. package/dist/cloud-run.d.ts.map +1 -1
  71. package/dist/cloud-run.js +45 -6
  72. package/dist/cloud-run.js.map +1 -1
  73. package/dist/cloud-signal.d.ts +12 -0
  74. package/dist/cloud-signal.d.ts.map +1 -0
  75. package/dist/cloud-signal.js +117 -0
  76. package/dist/cloud-signal.js.map +1 -0
  77. package/dist/cloud-sync.d.ts +8 -1
  78. package/dist/cloud-sync.d.ts.map +1 -1
  79. package/dist/cloud-sync.js +36 -4
  80. package/dist/cloud-sync.js.map +1 -1
  81. package/dist/communication/environment.d.ts +1 -1
  82. package/dist/communication/environment.d.ts.map +1 -1
  83. package/dist/communication/environment.js +26 -3
  84. package/dist/communication/environment.js.map +1 -1
  85. package/dist/communication/worker.js +1 -1
  86. package/dist/communication/worker.js.map +1 -1
  87. package/dist/compile.d.ts.map +1 -1
  88. package/dist/compile.js +5 -2
  89. package/dist/compile.js.map +1 -1
  90. package/dist/failure-kinds.d.ts +1 -1
  91. package/dist/failure-kinds.d.ts.map +1 -1
  92. package/dist/failure-kinds.js +2 -0
  93. package/dist/failure-kinds.js.map +1 -1
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +1 -0
  97. package/dist/index.js.map +1 -1
  98. package/dist/journal-client.d.ts +5 -1
  99. package/dist/journal-client.d.ts.map +1 -1
  100. package/dist/journal-client.js +44 -4
  101. package/dist/journal-client.js.map +1 -1
  102. package/dist/llm-worker.d.ts.map +1 -1
  103. package/dist/llm-worker.js +1 -1
  104. package/dist/llm-worker.js.map +1 -1
  105. package/dist/preflight.d.ts +2 -2
  106. package/dist/preflight.d.ts.map +1 -1
  107. package/dist/preflight.js +12 -7
  108. package/dist/preflight.js.map +1 -1
  109. package/dist/spec.d.ts +4 -0
  110. package/dist/spec.d.ts.map +1 -1
  111. package/dist/spec.js.map +1 -1
  112. package/dist/step-fields.d.ts +1 -1
  113. package/dist/step-fields.d.ts.map +1 -1
  114. package/dist/step-fields.js +1 -0
  115. package/dist/step-fields.js.map +1 -1
  116. package/dist/step-secrets.d.ts +20 -0
  117. package/dist/step-secrets.d.ts.map +1 -0
  118. package/dist/step-secrets.js +146 -0
  119. package/dist/step-secrets.js.map +1 -0
  120. package/dist/validate.d.ts.map +1 -1
  121. package/dist/validate.js +10 -0
  122. package/dist/validate.js.map +1 -1
  123. package/dist/worker-cli.d.ts +5 -1
  124. package/dist/worker-cli.d.ts.map +1 -1
  125. package/dist/worker-cli.js +43 -14
  126. package/dist/worker-cli.js.map +1 -1
  127. package/dist/worker.d.ts +1 -1
  128. package/dist/worker.d.ts.map +1 -1
  129. package/dist/worker.js +1 -1
  130. package/dist/worker.js.map +1 -1
  131. package/package.json +3 -2
  132. package/src/authored-budget.ts +4 -3
  133. package/src/authored-flow-error.ts +2 -0
  134. package/src/authored-flow-executor.ts +89 -13
  135. package/src/authored-flow-lifecycle.ts +215 -9
  136. package/src/authored-node-entry.ts +2 -0
  137. package/src/authored-parallel-body.ts +67 -0
  138. package/src/authored-parallel-eligibility.ts +245 -0
  139. package/src/authored-preflight.ts +4 -4
  140. package/src/authored-promise-graph.ts +35 -0
  141. package/src/authored-root-readback.ts +1 -2
  142. package/src/authored-root.ts +62 -9
  143. package/src/authored-step-graph.ts +50 -5
  144. package/src/authored-step-index.ts +2 -1
  145. package/src/authored-verdict.ts +4 -1
  146. package/src/authored-worker-step.ts +29 -9
  147. package/src/cli/check.ts +3 -3
  148. package/src/cli/cli-probe.ts +9 -6
  149. package/src/cli/run.ts +4 -2
  150. package/src/cloud-http.ts +1 -1
  151. package/src/cloud-run.ts +45 -8
  152. package/src/cloud-signal.ts +110 -0
  153. package/src/cloud-sync.ts +39 -4
  154. package/src/communication/environment.ts +28 -3
  155. package/src/communication/worker.ts +2 -1
  156. package/src/compile.ts +5 -2
  157. package/src/failure-kinds.ts +2 -0
  158. package/src/index.ts +1 -0
  159. package/src/journal-client.ts +48 -2
  160. package/src/llm-worker.ts +2 -1
  161. package/src/preflight.ts +13 -7
  162. package/src/spec.ts +4 -0
  163. package/src/step-fields.ts +1 -0
  164. package/src/step-secrets.ts +145 -0
  165. package/src/validate.ts +11 -0
  166. package/src/worker-cli.ts +41 -13
  167. package/src/worker.ts +3 -2
@@ -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;
@@ -81,6 +81,8 @@ try {
81
81
  request.metadata.inputPresent ? request.metadata.input : undefined, {
82
82
  getDefinition: loaded.getDefinition, dataDir: request.dataDir,
83
83
  flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
84
+ executionMode: request.metadata.identityVersion === 2 && request.metadata.executionMode === 'parallel'
85
+ ? 'parallel' : 'serial',
84
86
  extensions: loaded.extensions,
85
87
  flowGraph: loaded.graph,
86
88
  localAgentStream: request.localAgentStream, signal: controller.signal,
@@ -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
+ }
@@ -0,0 +1,245 @@
1
+ import ts from 'typescript';
2
+ import { authoredFunctionSource } from './authored-flow-lifecycle.js';
3
+
4
+ const reservedCertifier = '__certifyParallelBranch';
5
+
6
+ /** Keep admission and call-site instrumentation in agreement on parentheses. */
7
+ export function unwrapParallelExpression(node: ts.Expression): ts.Expression {
8
+ return ts.isParenthesizedExpression(node) ? unwrapParallelExpression(node.expression) : node;
9
+ }
10
+
11
+ /** A closed grammar for deterministic Promise.all branches. Unknown bodies stay serial. */
12
+ export function parallelRunEligible(body: Function, moduleSource: string): boolean {
13
+ // Promise.all is intercepted only when `Promise` resolves to the intrinsic.
14
+ // Function.toString cannot see a module-level lexical binding, so inspect
15
+ // the complete source before using the body's narrower grammar.
16
+ const bodySource = authoredFunctionSource(body);
17
+ return moduleUsesIntrinsicPromise(moduleSource, bodySource) && parallelBodySyntaxEligible(body);
18
+ }
19
+
20
+ /** Independently re-check the closed body grammar at execution time. */
21
+ export function parallelBodySyntaxEligible(body: Function): boolean {
22
+ const bodySource = authoredFunctionSource(body);
23
+ const file = ts.createSourceFile('authored-body.js', `const body = ${bodySource};`,
24
+ ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS);
25
+ if (file.statements.length !== 1) return false;
26
+ const declaration = file.statements[0];
27
+ if (!declaration || !ts.isVariableStatement(declaration)) return false;
28
+ const expression = declaration.declarationList.declarations[0]?.initializer;
29
+ // Function expressions can obtain the context through `arguments[0]`.
30
+ if (expression === undefined || !ts.isArrowFunction(expression) || !ts.isBlock(expression.body)
31
+ || expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) !== true) return false;
32
+ const parameter = expression.parameters[0]?.name;
33
+ if (parameter === undefined || !ts.isIdentifier(parameter) || expression.parameters.length !== 1) return false;
34
+ const context = parameter.text;
35
+ if (['Promise', 'globalThis', 'global'].includes(context)) return false;
36
+ // The instrumented body is re-evaluated with a private callback parameter.
37
+ // A lexical binding or reference with that spelling could capture it.
38
+ let namesCertifier = false;
39
+ const checkCertifierName = (node: ts.Node): void => {
40
+ if (ts.isIdentifier(node) && node.text === reservedCertifier) namesCertifier = true;
41
+ ts.forEachChild(node, checkCertifierName);
42
+ };
43
+ checkCertifierName(expression);
44
+ if (namesCertifier) return false;
45
+ const branches = new Map<string, ts.ArrowFunction>();
46
+ const rootValues = new Set<string>();
47
+ let groups = 0;
48
+ let runs = 0;
49
+
50
+ const unwrap = unwrapParallelExpression;
51
+ const method = (node: ts.Expression, owner: string, name: string): node is ts.CallExpression => {
52
+ const value = unwrap(node);
53
+ return ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)
54
+ && ts.isIdentifier(value.expression.expression) && value.expression.expression.text === owner
55
+ && value.expression.name.text === name;
56
+ };
57
+ const pure = (node: ts.Expression, values: ReadonlySet<string>): boolean => {
58
+ const value = unwrap(node);
59
+ if (ts.isStringLiteral(value) || ts.isNumericLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
60
+ || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword
61
+ || value.kind === ts.SyntaxKind.NullKeyword) return true;
62
+ if (ts.isIdentifier(value)) return values.has(value.text);
63
+ if (ts.isTemplateExpression(value)) return value.templateSpans.every(span => pure(span.expression, values));
64
+ if (ts.isPrefixUnaryExpression(value)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken,
65
+ ts.SyntaxKind.ExclamationToken].includes(value.operator) && pure(value.operand, values);
66
+ if (ts.isBinaryExpression(value)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken,
67
+ ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.SlashToken, ts.SyntaxKind.EqualsEqualsEqualsToken,
68
+ ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.LessThanToken,
69
+ ts.SyntaxKind.GreaterThanToken].includes(value.operatorToken.kind)
70
+ && pure(value.left, values) && pure(value.right, values);
71
+ if (ts.isConditionalExpression(value)) return pure(value.condition, values)
72
+ && pure(value.whenTrue, values) && pure(value.whenFalse, values);
73
+ if (ts.isElementAccessExpression(value)) return value.argumentExpression !== undefined
74
+ && pure(value.expression, values) && pure(value.argumentExpression, values);
75
+ if (ts.isPropertyAccessExpression(value)) return pure(value.expression, values);
76
+ if (ts.isArrayLiteralExpression(value)) return value.elements.every(element =>
77
+ ts.isExpression(element) && pure(element, values));
78
+ if (ts.isObjectLiteralExpression(value)) return value.properties.every(property =>
79
+ ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
80
+ && pure(property.initializer, values));
81
+ return false;
82
+ };
83
+ const run = (node: ts.Expression, values: ReadonlySet<string>): boolean => {
84
+ const value = unwrap(node);
85
+ if (!method(value, context, 'run') || value.arguments.length < 1 || value.arguments.length > 2
86
+ || !value.arguments.every(argument => pure(argument, values))) return false;
87
+ runs++;
88
+ return true;
89
+ };
90
+ const group = (node: ts.Expression, values: ReadonlySet<string>, depth: number): boolean => {
91
+ const value = unwrap(node);
92
+ if (!method(value, 'Promise', 'all') || value.arguments.length !== 1 || depth > 4) return false;
93
+ groups++;
94
+ const members = value.arguments[0]!;
95
+ if (ts.isArrayLiteralExpression(members)) {
96
+ if (members.elements.length < 2) return false;
97
+ return members.elements.every(member => {
98
+ if (!ts.isExpression(member)) return false;
99
+ if (run(member, values)) return true;
100
+ const call = unwrap(member);
101
+ if (depth === 0 && ts.isCallExpression(call) && ts.isIdentifier(call.expression)
102
+ && call.arguments.length === 0 && branches.has(call.expression.text)) {
103
+ return branch(branches.get(call.expression.text)!, depth + 1);
104
+ }
105
+ if (depth === 0 && ts.isCallExpression(call) && call.arguments.length === 0
106
+ && ts.isParenthesizedExpression(call.expression) && ts.isArrowFunction(call.expression.expression)) {
107
+ return branch(call.expression.expression, depth + 1);
108
+ }
109
+ return false;
110
+ });
111
+ }
112
+ return false;
113
+ };
114
+ const awaitStep = (node: ts.Expression, values: ReadonlySet<string>, depth: number): boolean => {
115
+ const value = unwrap(node);
116
+ if (!ts.isAwaitExpression(value)) return false;
117
+ return run(value.expression, values) || group(value.expression, values, depth);
118
+ };
119
+ const statements = (block: ts.Block, values: Set<string>, depth: number): boolean => {
120
+ for (const statement of block.statements) {
121
+ if (ts.isExpressionStatement(statement)) {
122
+ if (awaitStep(statement.expression, values, depth)) continue;
123
+ const value = unwrap(statement.expression);
124
+ if (depth === 0 && method(value, context, 'done') && value.arguments.length >= 1
125
+ && value.arguments.every(argument => pure(argument, values))
126
+ && statement === block.statements[block.statements.length - 1]) continue;
127
+ return false;
128
+ }
129
+ if (ts.isReturnStatement(statement) && depth > 0 && statement.expression !== undefined
130
+ && awaitStep(statement.expression, values, depth)) continue;
131
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) return false;
132
+ for (const item of statement.declarationList.declarations) {
133
+ if (!ts.isIdentifier(item.name) || item.initializer === undefined) return false;
134
+ if (['Promise', 'globalThis', 'global'].includes(item.name.text)) return false;
135
+ if (depth === 0 && ts.isArrowFunction(item.initializer)) {
136
+ if (branches.has(item.name.text) || item.initializer.parameters.length !== 0
137
+ || item.initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) !== true
138
+ ) return false;
139
+ branches.set(item.name.text, item.initializer);
140
+ continue;
141
+ }
142
+ if (!awaitStep(item.initializer, values, depth)) return false;
143
+ values.add(item.name.text);
144
+ }
145
+ }
146
+ return true;
147
+ };
148
+ const branch = (arrow: ts.ArrowFunction, depth: number): boolean => {
149
+ if (arrow.parameters.length !== 0) return false;
150
+ const before = runs;
151
+ const valid = ts.isBlock(arrow.body) ? statements(arrow.body, new Set(rootValues), depth)
152
+ : awaitStep(arrow.body, rootValues, depth);
153
+ // An empty async branch creates a join with no causal step predecessor.
154
+ return valid && runs > before;
155
+ };
156
+
157
+ if (!statements(expression.body, rootValues, 0)) return false;
158
+ return groups > 0 && runs > 0;
159
+ }
160
+
161
+ /** Reject any source that can change what a free `Promise` identifier means. */
162
+ function moduleUsesIntrinsicPromise(source: string, bodySource: string): boolean {
163
+ const file = ts.createSourceFile('authored-module.ts', source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS);
164
+ if (((file as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }).parseDiagnostics?.length ?? 0) > 0) {
165
+ return false;
166
+ }
167
+ // Top-level code can alter the constructor or its methods without binding
168
+ // the name. Leave these modules on the established serial scheduler too.
169
+ const touchesPromiseOutsideDefinition = (node: ts.Node): boolean => {
170
+ if (ts.isIdentifier(node) && ['Promise', 'globalThis', 'global'].includes(node.text)) return true;
171
+ return ts.forEachChild(node, touchesPromiseOutsideDefinition) === true;
172
+ };
173
+ let definesDefaultBody = false;
174
+ let importsSurfaceFlow = false;
175
+ for (const statement of file.statements) {
176
+ if (ts.isImportDeclaration(statement)) {
177
+ // Imports can execute code before the body starts. Only the direct,
178
+ // immutable Surface binding can certify the exported body's identity.
179
+ if (statement.moduleSpecifier.getText(file) !== "'@relayflows/surface'"
180
+ && statement.moduleSpecifier.getText(file) !== '"@relayflows/surface"') return false;
181
+ const bindings = statement.importClause?.namedBindings;
182
+ if (statement.importClause?.name !== undefined || bindings === undefined
183
+ || !ts.isNamedImports(bindings) || bindings.elements.length !== 1
184
+ || bindings.elements[0]?.name.text !== 'flow'
185
+ || bindings.elements[0]?.propertyName !== undefined || importsSurfaceFlow) return false;
186
+ importsSurfaceFlow = true;
187
+ continue;
188
+ }
189
+ if (ts.isExportAssignment(statement)) {
190
+ // Only the final body argument is allowed to mention Promise.all. Do
191
+ // not let an expression around flow() mutate Promise while we skip it.
192
+ const call = statement.expression;
193
+ if (statement.isExportEquals || !ts.isCallExpression(call)
194
+ || !ts.isIdentifier(call.expression) || call.expression.text !== 'flow') return false;
195
+ const args = call.arguments;
196
+ if (args.length !== 2 || !ts.isStringLiteral(args[0]!)) return false;
197
+ if (!ts.isArrowFunction(args[args.length - 1]!)) return false;
198
+ if (args[args.length - 1]!.getText(file) !== bodySource) return false;
199
+ if (args.slice(0, -1).some(touchesPromiseOutsideDefinition)) return false;
200
+ if (definesDefaultBody) return false;
201
+ definesDefaultBody = true;
202
+ continue;
203
+ }
204
+ // The certified body cannot use module captures. Other top-level code is
205
+ // unnecessary for it and may schedule a later mutation through an alias.
206
+ return false;
207
+ }
208
+ const forbidden = new Set(['Promise', 'globalThis']);
209
+ const bindsForbidden = (name: ts.BindingName): boolean => ts.isIdentifier(name)
210
+ ? forbidden.has(name.text)
211
+ : name.elements.some(element => ts.isBindingElement(element) && bindsForbidden(element.name));
212
+ const isGlobalPromise = (node: ts.Node): boolean => {
213
+ if (ts.isIdentifier(node)) return forbidden.has(node.text);
214
+ if (ts.isPropertyAccessExpression(node)) return ts.isIdentifier(node.expression)
215
+ && node.expression.text === 'globalThis' && node.name.text === 'Promise';
216
+ return ts.isElementAccessExpression(node) && ts.isIdentifier(node.expression)
217
+ && node.expression.text === 'globalThis' && ts.isStringLiteral(node.argumentExpression)
218
+ && node.argumentExpression.text === 'Promise';
219
+ };
220
+ let unsafe = false;
221
+ const visit = (node: ts.Node): void => {
222
+ if (unsafe) return;
223
+ if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node))
224
+ && bindsForbidden(node.name)) unsafe = true;
225
+ if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
226
+ || ts.isClassDeclaration(node) || ts.isClassExpression(node)
227
+ || ts.isEnumDeclaration(node) || ts.isImportEqualsDeclaration(node)
228
+ || ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node))
229
+ && node.name !== undefined && forbidden.has(node.name.text)) unsafe = true;
230
+ if (ts.isImportClause(node) && node.name !== undefined && forbidden.has(node.name.text)) unsafe = true;
231
+ if (ts.isNamespaceImport(node) && forbidden.has(node.name.text)) unsafe = true;
232
+ if (ts.isImportSpecifier(node) && forbidden.has(node.name.text)) unsafe = true;
233
+ if (ts.isBinaryExpression(node)
234
+ && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
235
+ && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
236
+ && isGlobalPromise(node.left)) unsafe = true;
237
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node))
238
+ && isGlobalPromise(node.operand)) unsafe = true;
239
+ ts.forEachChild(node, visit);
240
+ };
241
+ visit(file);
242
+ // The body may come from another module, where a free Promise name can be
243
+ // shadowed without appearing in this source. In that case use serial IDs.
244
+ return importsSurfaceFlow && definesDefaultBody && !unsafe;
245
+ }