@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.
- package/dist/authored-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +3 -3
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +3 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +2 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +93 -12
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-lifecycle.d.ts +28 -3
- package/dist/authored-flow-lifecycle.d.ts.map +1 -1
- package/dist/authored-flow-lifecycle.js +206 -10
- package/dist/authored-flow-lifecycle.js.map +1 -1
- package/dist/authored-node-entry.js +16 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +26 -3
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-parallel-body.d.ts +10 -0
- package/dist/authored-parallel-body.d.ts.map +1 -0
- package/dist/authored-parallel-body.js +68 -0
- package/dist/authored-parallel-body.js.map +1 -0
- package/dist/authored-parallel-eligibility.d.ts +22 -0
- package/dist/authored-parallel-eligibility.d.ts.map +1 -0
- package/dist/authored-parallel-eligibility.js +335 -0
- package/dist/authored-parallel-eligibility.js.map +1 -0
- package/dist/authored-promise-graph.d.ts +16 -0
- package/dist/authored-promise-graph.d.ts.map +1 -1
- package/dist/authored-promise-graph.js +36 -0
- package/dist/authored-promise-graph.js.map +1 -1
- package/dist/authored-root-readback.d.ts +1 -2
- package/dist/authored-root-readback.d.ts.map +1 -1
- package/dist/authored-root-readback.js +1 -1
- package/dist/authored-root-readback.js.map +1 -1
- package/dist/authored-root.d.ts +6 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +78 -8
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-graph.d.ts +19 -4
- package/dist/authored-step-graph.d.ts.map +1 -1
- package/dist/authored-step-graph.js +49 -6
- package/dist/authored-step-graph.js.map +1 -1
- package/dist/authored-step-index.d.ts +2 -1
- package/dist/authored-step-index.d.ts.map +1 -1
- package/dist/authored-step-index.js +2 -1
- package/dist/authored-step-index.js.map +1 -1
- package/dist/authored-verdict.d.ts +4 -0
- package/dist/authored-verdict.d.ts.map +1 -1
- package/dist/authored-verdict.js +7 -1
- package/dist/authored-verdict.js.map +1 -1
- package/dist/authored-worker-step.d.ts +2 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +12 -5
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +4 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +3 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +18 -1
- package/dist/journal-client.js.map +1 -1
- package/package.json +3 -2
- package/src/authored-budget.ts +4 -3
- package/src/authored-flow-error.ts +2 -0
- package/src/authored-flow-executor.ts +85 -11
- package/src/authored-flow-lifecycle.ts +215 -9
- package/src/authored-node-entry.ts +17 -0
- package/src/authored-node-runner.ts +24 -3
- package/src/authored-parallel-body.ts +67 -0
- package/src/authored-parallel-eligibility.ts +296 -0
- package/src/authored-promise-graph.ts +35 -0
- package/src/authored-root-readback.ts +1 -2
- package/src/authored-root.ts +80 -9
- package/src/authored-step-graph.ts +50 -5
- package/src/authored-step-index.ts +2 -1
- package/src/authored-verdict.ts +7 -1
- package/src/authored-worker-step.ts +12 -3
- package/src/cli/run.ts +4 -2
- package/src/failure-kinds.ts +2 -0
- package/src/journal-client.ts +19 -0
|
@@ -0,0 +1,296 @@
|
|
|
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 certifiedModuleBody(moduleSource, bodySource) !== undefined && parallelBodySyntaxEligible(body);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Match the body Node actually loaded against the same type-stripped source its
|
|
22
|
+
* TypeScript loader serves. First certify the original bytes: transforming
|
|
23
|
+
* away a declaration must never make an unsafe module appear closed.
|
|
24
|
+
*/
|
|
25
|
+
export async function parallelLoadedBodyEligible(
|
|
26
|
+
body: Function, moduleSource: string, sourceUrl: string,
|
|
27
|
+
mode: 'strip' | 'transform',
|
|
28
|
+
): Promise<boolean> {
|
|
29
|
+
if (!parallelPinnedSourceEligible(moduleSource)) return false;
|
|
30
|
+
const { stripTypeScriptTypes } = await import('node:module') as unknown as {
|
|
31
|
+
stripTypeScriptTypes?: (source: string, options: { mode: 'strip' | 'transform'; sourceUrl: string }) => string;
|
|
32
|
+
};
|
|
33
|
+
if (typeof stripTypeScriptTypes !== 'function') return false;
|
|
34
|
+
// The pinned loader suppresses this one Node warning on its own thread.
|
|
35
|
+
// Recomputing the text here happens on the main thread; suppress only this
|
|
36
|
+
// call's warning and restore the original writer before yielding again.
|
|
37
|
+
const emitWarning = process.emitWarning;
|
|
38
|
+
let transformed: string;
|
|
39
|
+
try {
|
|
40
|
+
process.emitWarning = ((warning: string | Error, ...rest: unknown[]) => {
|
|
41
|
+
if (rest[0] === 'ExperimentalWarning' && String(warning).startsWith('stripTypeScriptTypes')) return;
|
|
42
|
+
(emitWarning as (...args: unknown[]) => void).call(process, warning, ...rest);
|
|
43
|
+
}) as typeof process.emitWarning;
|
|
44
|
+
transformed = stripTypeScriptTypes(moduleSource, { mode, sourceUrl });
|
|
45
|
+
} catch {
|
|
46
|
+
// A loader mode unavailable in this Node version cannot certify a body.
|
|
47
|
+
return false;
|
|
48
|
+
} finally {
|
|
49
|
+
process.emitWarning = emitWarning;
|
|
50
|
+
}
|
|
51
|
+
return parallelRunEligible(body, transformed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The compiled Bun CLI owns the root lease, but the pinned source is executed
|
|
56
|
+
* by its embedded Node child. Bun reformats function toString output, so its
|
|
57
|
+
* in-memory body cannot be compared byte-for-byte with the source. Certify
|
|
58
|
+
* the closed source module instead; the Node child loads those hash-pinned
|
|
59
|
+
* bytes and checks exact body/source identity and grammar before any child starts.
|
|
60
|
+
*/
|
|
61
|
+
export function parallelPinnedSourceEligible(moduleSource: string): boolean {
|
|
62
|
+
const bodySource = certifiedModuleBody(moduleSource);
|
|
63
|
+
return bodySource !== undefined && parallelBodySourceSyntaxEligible(bodySource);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Independently re-check the closed body grammar at execution time. */
|
|
67
|
+
export function parallelBodySyntaxEligible(body: Function): boolean {
|
|
68
|
+
return parallelBodySourceSyntaxEligible(authoredFunctionSource(body));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parallelBodySourceSyntaxEligible(bodySource: string): boolean {
|
|
72
|
+
const file = ts.createSourceFile('authored-body.js', `const body = ${bodySource};`,
|
|
73
|
+
ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS);
|
|
74
|
+
if (file.statements.length !== 1) return false;
|
|
75
|
+
const declaration = file.statements[0];
|
|
76
|
+
if (!declaration || !ts.isVariableStatement(declaration)) return false;
|
|
77
|
+
const expression = declaration.declarationList.declarations[0]?.initializer;
|
|
78
|
+
// Function expressions can obtain the context through `arguments[0]`.
|
|
79
|
+
if (expression === undefined || !ts.isArrowFunction(expression) || !ts.isBlock(expression.body)
|
|
80
|
+
|| expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) !== true) return false;
|
|
81
|
+
const parameter = expression.parameters[0]?.name;
|
|
82
|
+
if (parameter === undefined || !ts.isIdentifier(parameter) || expression.parameters.length !== 1) return false;
|
|
83
|
+
const context = parameter.text;
|
|
84
|
+
if (['Promise', 'globalThis', 'global'].includes(context)) return false;
|
|
85
|
+
// The instrumented body is re-evaluated with a private callback parameter.
|
|
86
|
+
// A lexical binding or reference with that spelling could capture it.
|
|
87
|
+
let namesCertifier = false;
|
|
88
|
+
const checkCertifierName = (node: ts.Node): void => {
|
|
89
|
+
if (ts.isIdentifier(node) && node.text === reservedCertifier) namesCertifier = true;
|
|
90
|
+
ts.forEachChild(node, checkCertifierName);
|
|
91
|
+
};
|
|
92
|
+
checkCertifierName(expression);
|
|
93
|
+
if (namesCertifier) return false;
|
|
94
|
+
const branches = new Map<string, ts.ArrowFunction>();
|
|
95
|
+
const rootValues = new Set<string>();
|
|
96
|
+
let groups = 0;
|
|
97
|
+
let runs = 0;
|
|
98
|
+
|
|
99
|
+
const unwrap = unwrapParallelExpression;
|
|
100
|
+
const method = (node: ts.Expression, owner: string, name: string): node is ts.CallExpression => {
|
|
101
|
+
const value = unwrap(node);
|
|
102
|
+
return ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)
|
|
103
|
+
&& ts.isIdentifier(value.expression.expression) && value.expression.expression.text === owner
|
|
104
|
+
&& value.expression.name.text === name;
|
|
105
|
+
};
|
|
106
|
+
const pure = (node: ts.Expression, values: ReadonlySet<string>): boolean => {
|
|
107
|
+
const value = unwrap(node);
|
|
108
|
+
if (ts.isStringLiteral(value) || ts.isNumericLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
|
|
109
|
+
|| value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword
|
|
110
|
+
|| value.kind === ts.SyntaxKind.NullKeyword) return true;
|
|
111
|
+
if (ts.isIdentifier(value)) return values.has(value.text);
|
|
112
|
+
if (ts.isTemplateExpression(value)) return value.templateSpans.every(span => pure(span.expression, values));
|
|
113
|
+
if (ts.isPrefixUnaryExpression(value)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken,
|
|
114
|
+
ts.SyntaxKind.ExclamationToken].includes(value.operator) && pure(value.operand, values);
|
|
115
|
+
if (ts.isBinaryExpression(value)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken,
|
|
116
|
+
ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.SlashToken, ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
117
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.LessThanToken,
|
|
118
|
+
ts.SyntaxKind.GreaterThanToken].includes(value.operatorToken.kind)
|
|
119
|
+
&& pure(value.left, values) && pure(value.right, values);
|
|
120
|
+
if (ts.isConditionalExpression(value)) return pure(value.condition, values)
|
|
121
|
+
&& pure(value.whenTrue, values) && pure(value.whenFalse, values);
|
|
122
|
+
if (ts.isElementAccessExpression(value)) return value.argumentExpression !== undefined
|
|
123
|
+
&& pure(value.expression, values) && pure(value.argumentExpression, values);
|
|
124
|
+
if (ts.isPropertyAccessExpression(value)) return pure(value.expression, values);
|
|
125
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.every(element =>
|
|
126
|
+
ts.isExpression(element) && pure(element, values));
|
|
127
|
+
if (ts.isObjectLiteralExpression(value)) return value.properties.every(property =>
|
|
128
|
+
ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
|
|
129
|
+
&& pure(property.initializer, values));
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
const run = (node: ts.Expression, values: ReadonlySet<string>): boolean => {
|
|
133
|
+
const value = unwrap(node);
|
|
134
|
+
if (!method(value, context, 'run') || value.arguments.length < 1 || value.arguments.length > 2
|
|
135
|
+
|| !value.arguments.every(argument => pure(argument, values))) return false;
|
|
136
|
+
runs++;
|
|
137
|
+
return true;
|
|
138
|
+
};
|
|
139
|
+
const group = (node: ts.Expression, values: ReadonlySet<string>, depth: number): boolean => {
|
|
140
|
+
const value = unwrap(node);
|
|
141
|
+
if (!method(value, 'Promise', 'all') || value.arguments.length !== 1 || depth > 4) return false;
|
|
142
|
+
groups++;
|
|
143
|
+
const members = value.arguments[0]!;
|
|
144
|
+
if (ts.isArrayLiteralExpression(members)) {
|
|
145
|
+
if (members.elements.length < 2) return false;
|
|
146
|
+
return members.elements.every(member => {
|
|
147
|
+
if (!ts.isExpression(member)) return false;
|
|
148
|
+
if (run(member, values)) return true;
|
|
149
|
+
const call = unwrap(member);
|
|
150
|
+
if (depth === 0 && ts.isCallExpression(call) && ts.isIdentifier(call.expression)
|
|
151
|
+
&& call.arguments.length === 0 && branches.has(call.expression.text)) {
|
|
152
|
+
return branch(branches.get(call.expression.text)!, depth + 1);
|
|
153
|
+
}
|
|
154
|
+
if (depth === 0 && ts.isCallExpression(call) && call.arguments.length === 0
|
|
155
|
+
&& ts.isParenthesizedExpression(call.expression) && ts.isArrowFunction(call.expression.expression)) {
|
|
156
|
+
return branch(call.expression.expression, depth + 1);
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
const awaitStep = (node: ts.Expression, values: ReadonlySet<string>, depth: number): boolean => {
|
|
164
|
+
const value = unwrap(node);
|
|
165
|
+
if (!ts.isAwaitExpression(value)) return false;
|
|
166
|
+
return run(value.expression, values) || group(value.expression, values, depth);
|
|
167
|
+
};
|
|
168
|
+
const statements = (block: ts.Block, values: Set<string>, depth: number): boolean => {
|
|
169
|
+
for (const statement of block.statements) {
|
|
170
|
+
if (ts.isExpressionStatement(statement)) {
|
|
171
|
+
if (awaitStep(statement.expression, values, depth)) continue;
|
|
172
|
+
const value = unwrap(statement.expression);
|
|
173
|
+
if (depth === 0 && method(value, context, 'done') && value.arguments.length >= 1
|
|
174
|
+
&& value.arguments.every(argument => pure(argument, values))
|
|
175
|
+
&& statement === block.statements[block.statements.length - 1]) continue;
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (ts.isReturnStatement(statement) && depth > 0 && statement.expression !== undefined
|
|
179
|
+
&& awaitStep(statement.expression, values, depth)) continue;
|
|
180
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) return false;
|
|
181
|
+
for (const item of statement.declarationList.declarations) {
|
|
182
|
+
if (!ts.isIdentifier(item.name) || item.initializer === undefined) return false;
|
|
183
|
+
if (['Promise', 'globalThis', 'global'].includes(item.name.text)) return false;
|
|
184
|
+
if (depth === 0 && ts.isArrowFunction(item.initializer)) {
|
|
185
|
+
if (branches.has(item.name.text) || item.initializer.parameters.length !== 0
|
|
186
|
+
|| item.initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) !== true
|
|
187
|
+
) return false;
|
|
188
|
+
branches.set(item.name.text, item.initializer);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!awaitStep(item.initializer, values, depth)) return false;
|
|
192
|
+
values.add(item.name.text);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
};
|
|
197
|
+
const branch = (arrow: ts.ArrowFunction, depth: number): boolean => {
|
|
198
|
+
if (arrow.parameters.length !== 0) return false;
|
|
199
|
+
const before = runs;
|
|
200
|
+
const valid = ts.isBlock(arrow.body) ? statements(arrow.body, new Set(rootValues), depth)
|
|
201
|
+
: awaitStep(arrow.body, rootValues, depth);
|
|
202
|
+
// An empty async branch creates a join with no causal step predecessor.
|
|
203
|
+
return valid && runs > before;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (!statements(expression.body, rootValues, 0)) return false;
|
|
207
|
+
return groups > 0 && runs > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Reject any source that can change what a free `Promise` identifier means. */
|
|
211
|
+
function certifiedModuleBody(source: string, bodySource?: string): string | undefined {
|
|
212
|
+
const file = ts.createSourceFile('authored-module.ts', source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS);
|
|
213
|
+
if (((file as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }).parseDiagnostics?.length ?? 0) > 0) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
// Top-level code can alter the constructor or its methods without binding
|
|
217
|
+
// the name. Leave these modules on the established serial scheduler too.
|
|
218
|
+
const touchesPromiseOutsideDefinition = (node: ts.Node): boolean => {
|
|
219
|
+
if (ts.isIdentifier(node) && ['Promise', 'globalThis', 'global'].includes(node.text)) return true;
|
|
220
|
+
return ts.forEachChild(node, touchesPromiseOutsideDefinition) === true;
|
|
221
|
+
};
|
|
222
|
+
let definesDefaultBody = false;
|
|
223
|
+
let importsSurfaceFlow = false;
|
|
224
|
+
let certifiedBody: string | undefined;
|
|
225
|
+
for (const statement of file.statements) {
|
|
226
|
+
if (ts.isImportDeclaration(statement)) {
|
|
227
|
+
// Imports can execute code before the body starts. Only the direct,
|
|
228
|
+
// immutable Surface binding can certify the exported body's identity.
|
|
229
|
+
if (statement.moduleSpecifier.getText(file) !== "'@relayflows/surface'"
|
|
230
|
+
&& statement.moduleSpecifier.getText(file) !== '"@relayflows/surface"') return undefined;
|
|
231
|
+
const bindings = statement.importClause?.namedBindings;
|
|
232
|
+
if (statement.importClause?.name !== undefined || bindings === undefined
|
|
233
|
+
|| !ts.isNamedImports(bindings) || bindings.elements.length !== 1
|
|
234
|
+
|| bindings.elements[0]?.name.text !== 'flow'
|
|
235
|
+
|| bindings.elements[0]?.propertyName !== undefined || importsSurfaceFlow) return undefined;
|
|
236
|
+
importsSurfaceFlow = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (ts.isExportAssignment(statement)) {
|
|
240
|
+
// Only the final body argument is allowed to mention Promise.all. Do
|
|
241
|
+
// not let an expression around flow() mutate Promise while we skip it.
|
|
242
|
+
const call = statement.expression;
|
|
243
|
+
if (statement.isExportEquals || !ts.isCallExpression(call)
|
|
244
|
+
|| !ts.isIdentifier(call.expression) || call.expression.text !== 'flow') return undefined;
|
|
245
|
+
const args = call.arguments;
|
|
246
|
+
if (args.length !== 2 || !ts.isStringLiteral(args[0]!)) return undefined;
|
|
247
|
+
if (!ts.isArrowFunction(args[args.length - 1]!)) return undefined;
|
|
248
|
+
certifiedBody = args[args.length - 1]!.getText(file);
|
|
249
|
+
if (bodySource !== undefined && certifiedBody !== bodySource) return undefined;
|
|
250
|
+
if (args.slice(0, -1).some(touchesPromiseOutsideDefinition)) return undefined;
|
|
251
|
+
if (definesDefaultBody) return undefined;
|
|
252
|
+
definesDefaultBody = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// The certified body cannot use module captures. Other top-level code is
|
|
256
|
+
// unnecessary for it and may schedule a later mutation through an alias.
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
const forbidden = new Set(['Promise', 'globalThis']);
|
|
260
|
+
const bindsForbidden = (name: ts.BindingName): boolean => ts.isIdentifier(name)
|
|
261
|
+
? forbidden.has(name.text)
|
|
262
|
+
: name.elements.some(element => ts.isBindingElement(element) && bindsForbidden(element.name));
|
|
263
|
+
const isGlobalPromise = (node: ts.Node): boolean => {
|
|
264
|
+
if (ts.isIdentifier(node)) return forbidden.has(node.text);
|
|
265
|
+
if (ts.isPropertyAccessExpression(node)) return ts.isIdentifier(node.expression)
|
|
266
|
+
&& node.expression.text === 'globalThis' && node.name.text === 'Promise';
|
|
267
|
+
return ts.isElementAccessExpression(node) && ts.isIdentifier(node.expression)
|
|
268
|
+
&& node.expression.text === 'globalThis' && ts.isStringLiteral(node.argumentExpression)
|
|
269
|
+
&& node.argumentExpression.text === 'Promise';
|
|
270
|
+
};
|
|
271
|
+
let unsafe = false;
|
|
272
|
+
const visit = (node: ts.Node): void => {
|
|
273
|
+
if (unsafe) return;
|
|
274
|
+
if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node))
|
|
275
|
+
&& bindsForbidden(node.name)) unsafe = true;
|
|
276
|
+
if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
277
|
+
|| ts.isClassDeclaration(node) || ts.isClassExpression(node)
|
|
278
|
+
|| ts.isEnumDeclaration(node) || ts.isImportEqualsDeclaration(node)
|
|
279
|
+
|| ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node))
|
|
280
|
+
&& node.name !== undefined && forbidden.has(node.name.text)) unsafe = true;
|
|
281
|
+
if (ts.isImportClause(node) && node.name !== undefined && forbidden.has(node.name.text)) unsafe = true;
|
|
282
|
+
if (ts.isNamespaceImport(node) && forbidden.has(node.name.text)) unsafe = true;
|
|
283
|
+
if (ts.isImportSpecifier(node) && forbidden.has(node.name.text)) unsafe = true;
|
|
284
|
+
if (ts.isBinaryExpression(node)
|
|
285
|
+
&& node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
|
|
286
|
+
&& node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
287
|
+
&& isGlobalPromise(node.left)) unsafe = true;
|
|
288
|
+
if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node))
|
|
289
|
+
&& isGlobalPromise(node.operand)) unsafe = true;
|
|
290
|
+
ts.forEachChild(node, visit);
|
|
291
|
+
};
|
|
292
|
+
visit(file);
|
|
293
|
+
// The body may come from another module, where a free Promise name can be
|
|
294
|
+
// shadowed without appearing in this source. In that case use serial IDs.
|
|
295
|
+
return importsSurfaceFlow && definesDefaultBody && !unsafe ? certifiedBody : undefined;
|
|
296
|
+
}
|
|
@@ -30,12 +30,15 @@ export class AuthoredPromiseGraph {
|
|
|
30
30
|
private readonly triggers = new Map<number, number>();
|
|
31
31
|
private readonly creationContexts = new Map<number, number>();
|
|
32
32
|
private readonly resolutionCauses = new Map<number, number>();
|
|
33
|
+
/** Promises attributed only when another promise's reaction resolved them. */
|
|
34
|
+
private readonly adoptedFromResolvingContext = new Set<number>();
|
|
33
35
|
private readonly promiseIds = new WeakMap<object, number>();
|
|
34
36
|
private readonly handles = new Map<number, Promise<unknown>>();
|
|
35
37
|
private readonly unattributedChildren = new Map<number, number[]>();
|
|
36
38
|
private readonly attributedRoots = new Map<number, number>();
|
|
37
39
|
private readonly roots = new Set<number>();
|
|
38
40
|
private readonly pending = new Set<number>();
|
|
41
|
+
private readonly combinatorCaptures: Set<number>[] = [];
|
|
39
42
|
private dependencyCache: { readonly start: number; readonly found: ReadonlySet<number>; readonly version: number } | undefined;
|
|
40
43
|
/** Bumped by every graph change, so derived answers can be cached between changes. */
|
|
41
44
|
private changes = 0;
|
|
@@ -52,6 +55,7 @@ export class AuthoredPromiseGraph {
|
|
|
52
55
|
this.triggers.set(asyncId, triggerAsyncId);
|
|
53
56
|
this.creationContexts.set(asyncId, executionAsyncId());
|
|
54
57
|
this.promiseIds.set(resource, asyncId);
|
|
58
|
+
for (const capture of this.combinatorCaptures) capture.add(asyncId);
|
|
55
59
|
this.handles.set(asyncId, resource as Promise<unknown>);
|
|
56
60
|
this.pending.add(asyncId);
|
|
57
61
|
const root = this.roots.has(triggerAsyncId)
|
|
@@ -77,6 +81,7 @@ export class AuthoredPromiseGraph {
|
|
|
77
81
|
// success. Measured; see ops/probes/pr134-repair-0903/combinators.mjs.
|
|
78
82
|
const adopted = this.attributedRoots.get(cause);
|
|
79
83
|
if (adopted !== undefined) {
|
|
84
|
+
this.adoptedFromResolvingContext.add(asyncId);
|
|
80
85
|
this.attribute(asyncId, adopted);
|
|
81
86
|
return;
|
|
82
87
|
}
|
|
@@ -100,6 +105,18 @@ export class AuthoredPromiseGraph {
|
|
|
100
105
|
return this.promiseIds.get(value);
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
isPending(asyncId: number): boolean {
|
|
109
|
+
return this.pending.has(asyncId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The intrinsic creates per-member reaction promises as well as its aggregate. */
|
|
113
|
+
captureCombinator<T>(call: () => T): { readonly value: T; readonly created: ReadonlySet<number> } {
|
|
114
|
+
const created = new Set<number>();
|
|
115
|
+
this.combinatorCaptures.push(created);
|
|
116
|
+
try { return { value: call(), created }; }
|
|
117
|
+
finally { this.combinatorCaptures.pop(); }
|
|
118
|
+
}
|
|
119
|
+
|
|
103
120
|
/** Mark a promise whose descendants belong to an authored operation. */
|
|
104
121
|
registerRoot(asyncId: number): void {
|
|
105
122
|
if (asyncId <= 0 || this.roots.has(asyncId)) return;
|
|
@@ -145,6 +162,22 @@ export class AuthoredPromiseGraph {
|
|
|
145
162
|
return causes;
|
|
146
163
|
}
|
|
147
164
|
|
|
165
|
+
/**
|
|
166
|
+
* A promise created directly in one async context but settled in another
|
|
167
|
+
* can be a join. Its resolution edge names only the settling member, so a
|
|
168
|
+
* frontier may use it only when a combinator registered all members.
|
|
169
|
+
*/
|
|
170
|
+
hasUnobservedJoinShape(asyncId: number): boolean {
|
|
171
|
+
const trigger = this.triggers.get(asyncId);
|
|
172
|
+
const resolution = this.resolutionCauses.get(asyncId);
|
|
173
|
+
return trigger !== undefined && trigger === this.creationContexts.get(asyncId)
|
|
174
|
+
&& resolution !== undefined && resolution !== trigger;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
wasAdoptedFromResolvingContext(asyncId: number): boolean {
|
|
178
|
+
return this.adoptedFromResolvingContext.has(asyncId);
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
dependsOn(descendant: number, ancestor: number): boolean {
|
|
149
182
|
return this.dependenciesOf(descendant).has(ancestor);
|
|
150
183
|
}
|
|
@@ -167,11 +200,13 @@ export class AuthoredPromiseGraph {
|
|
|
167
200
|
this.triggers.clear();
|
|
168
201
|
this.creationContexts.clear();
|
|
169
202
|
this.resolutionCauses.clear();
|
|
203
|
+
this.adoptedFromResolvingContext.clear();
|
|
170
204
|
this.handles.clear();
|
|
171
205
|
this.unattributedChildren.clear();
|
|
172
206
|
this.attributedRoots.clear();
|
|
173
207
|
this.roots.clear();
|
|
174
208
|
this.pending.clear();
|
|
209
|
+
this.combinatorCaptures.length = 0;
|
|
175
210
|
this.dependencyCache = undefined;
|
|
176
211
|
}
|
|
177
212
|
|
|
@@ -3,7 +3,6 @@ import type { AuthoredFlowExecutionResult } from './authored-flow-executor.js';
|
|
|
3
3
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
4
4
|
import { isSurfaceCompletionReason } from './authored-step-output.js';
|
|
5
5
|
import { JournalFrameError, JournalProtocolError, type JournalClient } from './journal-client.js';
|
|
6
|
-
import type { RunOutcome } from './protocol.js';
|
|
7
6
|
|
|
8
7
|
/*
|
|
9
8
|
* Reading an authored root back from its journal, and what a failed read
|
|
@@ -44,7 +43,7 @@ export async function rootStepIfPresent(journal: JournalClient, runId: string):
|
|
|
44
43
|
* it back is interrupted, the outcome is still known: say so, rather than
|
|
45
44
|
* recasting a finished run as resumable.
|
|
46
45
|
*/
|
|
47
|
-
export function completedResult(journal: JournalClient,
|
|
46
|
+
export function completedResult(journal: JournalClient, rootRunId: string): Promise<AuthoredFlowExecutionResult & { readonly rootRunId: string }> {
|
|
48
47
|
return completedRootResult(journal, rootRunId).catch(error => {
|
|
49
48
|
// A bad frame or a journal with no stored result is a definite fact; any
|
|
50
49
|
// other failure — interrupted, canceled, or a daemon-side read error such
|
package/src/authored-root.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { loadPinnedAuthoredSource } from './authored-source-authority.js';
|
|
|
3
3
|
import { assertAuthoredRuntimeAvailable, runAuthoredInNode } from './authored-node-runner.js';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
6
7
|
import { canonicalize } from './canonical.js';
|
|
7
8
|
import { compileSpec, toKernelSpec } from './compile.js';
|
|
8
9
|
import { executeAuthoredFlow, type AuthoredFlowExecutionResult, type AuthoredFlowSuspendedResult } from './authored-flow-executor.js';
|
|
9
|
-
import { AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
10
|
+
import { AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_PARALLEL_ROOT_KIND_V3, AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
10
11
|
import {
|
|
11
12
|
loadAuthoredFlow,
|
|
12
13
|
type LoadedAuthoredFlow,
|
|
@@ -23,6 +24,7 @@ import { helperReceiptWaitId, releaseHelperReceiptWaits } from './helper-park.js
|
|
|
23
24
|
import { readSubscriptionPark } from './authored-subscription-park.js';
|
|
24
25
|
import { localAgentCredentialEnvironment } from './local-agent-environment.js';
|
|
25
26
|
import { completedResult, rootKernelStep, rootStepIfPresent } from './authored-root-readback.js';
|
|
27
|
+
import { parallelPromiseIntrinsicsIntact } from './authored-flow-lifecycle.js';
|
|
26
28
|
|
|
27
29
|
export type DurableAuthoredFlowResult =
|
|
28
30
|
| (AuthoredFlowExecutionResult & { readonly rootRunId: string })
|
|
@@ -42,7 +44,7 @@ export interface AuthoredRootExtension {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface AuthoredRootMetadata {
|
|
45
|
-
readonly kind: typeof ROOT_KIND;
|
|
47
|
+
readonly kind: typeof ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND_V3;
|
|
46
48
|
readonly flowName: string;
|
|
47
49
|
readonly flowPath: string;
|
|
48
50
|
readonly sourceSha256: string;
|
|
@@ -50,6 +52,10 @@ export interface AuthoredRootMetadata {
|
|
|
50
52
|
readonly sources: readonly AuthoredRootSourceAuthority[];
|
|
51
53
|
/** Plugin-level provenance in lock order; empty when the project declares none. */
|
|
52
54
|
readonly extensions: readonly AuthoredRootExtension[];
|
|
55
|
+
/** Absent on roots admitted before causal identities existed. */
|
|
56
|
+
readonly identityVersion?: 2;
|
|
57
|
+
/** Immutable admission choice; serial uses the 2.0.50 child identity contract. */
|
|
58
|
+
readonly executionMode?: 'serial' | 'parallel';
|
|
53
59
|
readonly localAgentStream?: string;
|
|
54
60
|
readonly inputPresent: boolean;
|
|
55
61
|
readonly input?: unknown;
|
|
@@ -89,8 +95,35 @@ export async function executeDurableAuthoredFlow(
|
|
|
89
95
|
surface: node.surfaceAuthority,
|
|
90
96
|
})));
|
|
91
97
|
const definition = loaded.getDefinition(loaded.handle);
|
|
98
|
+
let parallelEligible = false;
|
|
99
|
+
let parallelIneligibleReason: string | undefined;
|
|
100
|
+
if (loaded.graph.length !== 1) parallelIneligibleReason = 'source_graph';
|
|
101
|
+
else if ((loaded.extensions?.length ?? 0) !== 0) parallelIneligibleReason = 'extensions';
|
|
102
|
+
else if (definition.header.budget !== undefined) parallelIneligibleReason = 'shared_budget';
|
|
103
|
+
else if (!parallelPromiseIntrinsicsIntact()) parallelIneligibleReason = 'runtime_intrinsics';
|
|
104
|
+
else {
|
|
105
|
+
// The parser is needed only for an authored run, never for an ordinary
|
|
106
|
+
// CLI check or watch reload. Recheck intrinsics after the async import;
|
|
107
|
+
// author code could have replaced them while it was loading.
|
|
108
|
+
const { parallelLoadedBodyEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js');
|
|
109
|
+
parallelEligible = parallelPromiseIntrinsicsIntact()
|
|
110
|
+
&& (process.versions['bun'] === undefined
|
|
111
|
+
// Native imports strip by default, or transform with Node's opt-in
|
|
112
|
+
// flag. Require exact identity with one of those loader outputs.
|
|
113
|
+
? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip')
|
|
114
|
+
|| await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform')
|
|
115
|
+
: parallelPinnedSourceEligible(source.toString('utf8')));
|
|
116
|
+
if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact()
|
|
117
|
+
? 'source_certificate' : 'runtime_intrinsics';
|
|
118
|
+
}
|
|
119
|
+
// Opt-in operator diagnostic only: changing root metadata would change its
|
|
120
|
+
// admission spec hash and could disrupt a 2.0.52 in-flight serial root.
|
|
121
|
+
if (parallelIneligibleReason !== undefined && process.env['FLOWS_PARALLEL_DEBUG'] === '1') {
|
|
122
|
+
process.stderr.write(`[flows] parallel f.run admission: serial (${parallelIneligibleReason})\n`);
|
|
123
|
+
}
|
|
124
|
+
const executionMode = parallelEligible ? 'parallel' : 'serial';
|
|
92
125
|
const metadata: AuthoredRootMetadata = Object.freeze({
|
|
93
|
-
kind: ROOT_KIND,
|
|
126
|
+
kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND_V3 : ROOT_KIND,
|
|
94
127
|
flowName: definition.name,
|
|
95
128
|
flowPath: loaded.sourcePath,
|
|
96
129
|
sourceSha256: sha256(source),
|
|
@@ -99,6 +132,8 @@ export async function executeDurableAuthoredFlow(
|
|
|
99
132
|
extensions: Object.freeze((loaded.extensions ?? []).map(extension => Object.freeze({
|
|
100
133
|
name: extension.name, digest: extension.digest, ref: extension.ref,
|
|
101
134
|
}))),
|
|
135
|
+
identityVersion: 2,
|
|
136
|
+
executionMode,
|
|
102
137
|
...(options.localAgentStream === undefined ? {} : { localAgentStream: options.localAgentStream }),
|
|
103
138
|
inputPresent: input !== undefined,
|
|
104
139
|
...(input === undefined ? {} : { input: jsonSnapshot(input, 'authored root input') }),
|
|
@@ -121,7 +156,7 @@ export async function executeDurableAuthoredFlow(
|
|
|
121
156
|
options.lifecycle?.onRunReceipt?.({ runId: outcome.run_id, flow: definition.name });
|
|
122
157
|
if (outcome.status === 'completed') {
|
|
123
158
|
dispatchWait.cancel();
|
|
124
|
-
return await completedResult(journal, outcome
|
|
159
|
+
return await completedResult(journal, outcome.run_id);
|
|
125
160
|
}
|
|
126
161
|
assertRootCanDispatch(outcome);
|
|
127
162
|
await releaseHelperReceiptWaits(journal, outcome.run_id);
|
|
@@ -152,11 +187,23 @@ export async function resumeDurableAuthoredFlow(
|
|
|
152
187
|
): Promise<DurableAuthoredFlowResult | undefined> {
|
|
153
188
|
const metadata = await readAuthoredRootMetadata(journal, rootRunId);
|
|
154
189
|
if (metadata === undefined) return undefined;
|
|
155
|
-
assertAuthoredRuntimeAvailable();
|
|
156
|
-
const loaded = await loadPinnedAuthoredSource(metadata);
|
|
157
190
|
if (metadata.localAgentStream !== options.localAgentStream) {
|
|
158
191
|
throw new Error('authored root local agent surface mismatch');
|
|
159
192
|
}
|
|
193
|
+
const completed = await completedParallelRootOrAssertRuntime(metadata, rootRunId, journal);
|
|
194
|
+
if (completed !== undefined) {
|
|
195
|
+
options.lifecycle?.onRunReceipt?.({ runId: rootRunId, flow: metadata.flowName });
|
|
196
|
+
return completed;
|
|
197
|
+
}
|
|
198
|
+
assertAuthoredRuntimeAvailable();
|
|
199
|
+
const loaded = await loadPinnedAuthoredSource(metadata);
|
|
200
|
+
// Source loading itself can alter a process-wide intrinsic. Refuse before
|
|
201
|
+
// run.resume dispatches a pinned parallel root in that environment.
|
|
202
|
+
const completedAfterLoad = await completedParallelRootOrAssertRuntime(metadata, rootRunId, journal);
|
|
203
|
+
if (completedAfterLoad !== undefined) {
|
|
204
|
+
options.lifecycle?.onRunReceipt?.({ runId: rootRunId, flow: metadata.flowName });
|
|
205
|
+
return completedAfterLoad;
|
|
206
|
+
}
|
|
160
207
|
const stream = rootStreamFromMetadata(await rootKernelStep(journal, rootRunId));
|
|
161
208
|
const peer = journal.createPeer();
|
|
162
209
|
await peer.connect();
|
|
@@ -173,7 +220,7 @@ export async function resumeDurableAuthoredFlow(
|
|
|
173
220
|
options.lifecycle?.onRunReceipt?.({ runId: rootRunId, flow: metadata.flowName });
|
|
174
221
|
if (outcome.status === 'completed') {
|
|
175
222
|
dispatchWait.cancel();
|
|
176
|
-
return await completedResult(journal,
|
|
223
|
+
return await completedResult(journal, rootRunId);
|
|
177
224
|
}
|
|
178
225
|
assertRootCanDispatch(outcome);
|
|
179
226
|
await assertNoOpenHumanWait(journal, outcome);
|
|
@@ -189,6 +236,22 @@ export async function resumeDurableAuthoredFlow(
|
|
|
189
236
|
}
|
|
190
237
|
}
|
|
191
238
|
|
|
239
|
+
async function completedParallelRootOrAssertRuntime(
|
|
240
|
+
metadata: AuthoredRootMetadata, rootRunId: string, journal: JournalClient,
|
|
241
|
+
): Promise<DurableAuthoredFlowResult | undefined> {
|
|
242
|
+
if (metadata.identityVersion !== 2 || metadata.executionMode !== 'parallel'
|
|
243
|
+
|| parallelPromiseIntrinsicsIntact()) return undefined;
|
|
244
|
+
// A completed root has nothing left to dispatch. A read-only snapshot avoids
|
|
245
|
+
// refusing it under a preload while preserving the pre-dispatch guard for
|
|
246
|
+
// an active root. Readback still uses the journaled authored verdict.
|
|
247
|
+
const snapshot = await journal.runGet(rootRunId);
|
|
248
|
+
if (snapshot.status === 'completed') return completedResult(journal, rootRunId);
|
|
249
|
+
const error = new AuthoredFlowExecutionError('unsafe_parallel_runtime',
|
|
250
|
+
`pinned parallel root ${rootRunId} cannot resume while a Promise or Array intrinsic is replaced or a Node preload is active. The run remains resumable in a clean process.`);
|
|
251
|
+
error.rootRunId = rootRunId;
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
|
|
192
255
|
/**
|
|
193
256
|
* A root parked on an unanswered `f.human` will not be dispatched: the
|
|
194
257
|
* kernel holds it in `needs_human` until `event.emit` closes the wait. Report
|
|
@@ -353,6 +416,8 @@ async function driveRoot(
|
|
|
353
416
|
...(options.agentEnvironment === undefined ? {} : { agentEnvironment: options.agentEnvironment }),
|
|
354
417
|
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
355
418
|
rootRunId: dispatch.run_id,
|
|
419
|
+
executionMode: metadata.identityVersion === 2 && metadata.executionMode === 'parallel'
|
|
420
|
+
? 'parallel' : 'serial',
|
|
356
421
|
extensions: loaded.extensions,
|
|
357
422
|
flowGraph: loaded.graph,
|
|
358
423
|
...options.lifecycle,
|
|
@@ -600,7 +665,11 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata {
|
|
|
600
665
|
const root = value as Partial<AuthoredRootMetadata>;
|
|
601
666
|
const surface = root.surface as Partial<SurfaceModuleAuthority> | undefined;
|
|
602
667
|
const sources = root.sources;
|
|
603
|
-
return root.kind === ROOT_KIND
|
|
668
|
+
return (root.kind === ROOT_KIND || root.kind === AUTHORED_PARALLEL_ROOT_KIND
|
|
669
|
+
|| root.kind === AUTHORED_PARALLEL_ROOT_KIND_V3)
|
|
670
|
+
&& (root.kind === ROOT_KIND
|
|
671
|
+
|| (root.identityVersion === 2 && root.executionMode === 'parallel'))
|
|
672
|
+
&& typeof root.flowName === 'string'
|
|
604
673
|
&& typeof root.flowPath === 'string' && /^[a-f0-9]{64}$/.test(root.sourceSha256 ?? '')
|
|
605
674
|
&& typeof root.inputPresent === 'boolean' && surface?.packageName === '@relayflows/surface'
|
|
606
675
|
&& typeof surface.version === 'string' && /^[a-f0-9]{64}$/.test(surface.packageSha256 ?? '')
|
|
@@ -608,7 +677,9 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata {
|
|
|
608
677
|
&& Array.isArray(sources) && sources.length > 0 && sources.every(isRootSourceAuthority)
|
|
609
678
|
&& (root.extensions === undefined || (Array.isArray(root.extensions) && root.extensions.every(isRootExtension)))
|
|
610
679
|
&& (root.localAgentStream === undefined
|
|
611
|
-
|| /^local-agent-[a-f0-9-]+$/.test(root.localAgentStream))
|
|
680
|
+
|| /^local-agent-[a-f0-9-]+$/.test(root.localAgentStream))
|
|
681
|
+
&& ((root.identityVersion === undefined && root.executionMode === undefined)
|
|
682
|
+
|| (root.identityVersion === 2 && (root.executionMode === 'serial' || root.executionMode === 'parallel')));
|
|
612
683
|
}
|
|
613
684
|
|
|
614
685
|
function isRootExtension(value: unknown): value is AuthoredRootExtension {
|