@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
|
@@ -32,14 +32,16 @@ import { MAX_AFTER, displayLabel, type AuthoredStepEdges } from './authored-step
|
|
|
32
32
|
* body walks a few nodes per step. A walk that still exceeds `walkLimit` stops
|
|
33
33
|
* and says so (`afterTruncated`) rather than guessing silently.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* Most consumers use this as observational metadata. Parallel f.run follow-up
|
|
36
|
+
* IDs also use the causal frontier; the executor refuses a truncated frontier
|
|
37
|
+
* rather than choosing a replay key from incomplete predecessors.
|
|
37
38
|
*/
|
|
38
39
|
export const MAX_WALK = 10_000;
|
|
39
40
|
|
|
40
41
|
interface Frontier {
|
|
41
42
|
readonly steps: readonly string[];
|
|
42
43
|
readonly truncated: boolean;
|
|
44
|
+
readonly unregisteredAggregate: boolean;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
interface StepNode {
|
|
@@ -62,6 +64,7 @@ const EXPANDS_ON: Readonly<Record<Combinator, Outcome | 'always' | undefined>> =
|
|
|
62
64
|
|
|
63
65
|
interface Aggregate {
|
|
64
66
|
readonly promise: Promise<unknown>;
|
|
67
|
+
readonly combinator: Combinator;
|
|
65
68
|
/** Cached once settled: a settled promise never changes state. */
|
|
66
69
|
outcome?: Outcome;
|
|
67
70
|
readonly expandsOn: Outcome | 'always';
|
|
@@ -71,6 +74,10 @@ interface Aggregate {
|
|
|
71
74
|
export class AuthoredStepGraph {
|
|
72
75
|
private readonly stepPromises = new Map<number, string>();
|
|
73
76
|
private readonly aggregates = new Map<number, Aggregate>();
|
|
77
|
+
private readonly registeredAggregates = new Set<number>();
|
|
78
|
+
private readonly registeredCombinatorInternals = new Set<number>();
|
|
79
|
+
private readonly certifiedMembers = new Set<number>();
|
|
80
|
+
private readonly unsafeAggregates = new Set<number>();
|
|
74
81
|
private readonly contexts = new Map<number, Frontier>();
|
|
75
82
|
private readonly nodes = new Map<string, StepNode>();
|
|
76
83
|
private readonly edges = new Map<string, AuthoredStepEdges>();
|
|
@@ -78,6 +85,7 @@ export class AuthoredStepGraph {
|
|
|
78
85
|
constructor(
|
|
79
86
|
private readonly causesOf: (asyncId: number) => readonly number[],
|
|
80
87
|
private readonly walkLimit = MAX_WALK,
|
|
88
|
+
private readonly unobservedJoinShape: (asyncId: number) => boolean = () => false,
|
|
81
89
|
) {}
|
|
82
90
|
|
|
83
91
|
/** A combinator's aggregate and its members, for expansion once it settles. */
|
|
@@ -86,10 +94,19 @@ export class AuthoredStepGraph {
|
|
|
86
94
|
promise: Promise<unknown>,
|
|
87
95
|
combinator: Combinator,
|
|
88
96
|
members: readonly number[],
|
|
97
|
+
created: ReadonlySet<number> = new Set(),
|
|
89
98
|
): void {
|
|
99
|
+
this.registeredAggregates.add(aggregateId);
|
|
100
|
+
for (const id of created) this.registeredCombinatorInternals.add(id);
|
|
101
|
+
if (combinator === 'race' || combinator === 'any') this.unsafeAggregates.add(aggregateId);
|
|
90
102
|
const expandsOn = EXPANDS_ON[combinator];
|
|
91
103
|
if (expandsOn === undefined || members.length === 0) return;
|
|
92
|
-
this.aggregates.set(aggregateId, { promise, expandsOn, members });
|
|
104
|
+
this.aggregates.set(aggregateId, { promise, combinator, expandsOn, members });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Only direct steps and branch calls proven by the body grammar are safe members. */
|
|
108
|
+
certifyMember(asyncId: number): void {
|
|
109
|
+
this.certifiedMembers.add(asyncId);
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
/**
|
|
@@ -119,10 +136,19 @@ export class AuthoredStepGraph {
|
|
|
119
136
|
return this.edges.get(step);
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
/** Causal predecessors of a step being declared in the current async context. */
|
|
140
|
+
predecessorsAt(contextId: number): Frontier {
|
|
141
|
+
return this.frontierOf(contextId);
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
/** Drop the walk state; recorded edges stay readable for late record writes. */
|
|
123
145
|
release(): void {
|
|
124
146
|
this.stepPromises.clear();
|
|
125
147
|
this.aggregates.clear();
|
|
148
|
+
this.registeredAggregates.clear();
|
|
149
|
+
this.registeredCombinatorInternals.clear();
|
|
150
|
+
this.certifiedMembers.clear();
|
|
151
|
+
this.unsafeAggregates.clear();
|
|
126
152
|
this.contexts.clear();
|
|
127
153
|
this.nodes.clear();
|
|
128
154
|
}
|
|
@@ -132,6 +158,7 @@ export class AuthoredStepGraph {
|
|
|
132
158
|
if (memo !== undefined) return memo;
|
|
133
159
|
const candidates = new Set<string>();
|
|
134
160
|
let truncated = false;
|
|
161
|
+
let unregisteredAggregate = false;
|
|
135
162
|
const visited = new Set<number>([start]);
|
|
136
163
|
const queue = [start];
|
|
137
164
|
// Breadth-first, so that a walk cut short by the limit has already found
|
|
@@ -143,10 +170,24 @@ export class AuthoredStepGraph {
|
|
|
143
170
|
candidates.add(step);
|
|
144
171
|
continue;
|
|
145
172
|
}
|
|
173
|
+
// A join's resolution edge contains only its settling member. In
|
|
174
|
+
// parallel mode the full member list is safe only if our combinator
|
|
175
|
+
// observer registered it before the promise settled. The creation and
|
|
176
|
+
// resolution contexts also catch an unobserved join created after a
|
|
177
|
+
// previous step, where root-adoption alone cannot detect it.
|
|
178
|
+
const aggregate = this.aggregates.get(current);
|
|
179
|
+
const rejectedAll = aggregate?.combinator === 'all'
|
|
180
|
+
&& promiseOutcome(aggregate.promise) === 'rejected';
|
|
181
|
+
const possibleJoin = this.unsafeAggregates.has(current) || rejectedAll
|
|
182
|
+
|| (this.unobservedJoinShape(current)
|
|
183
|
+
&& !this.registeredAggregates.has(current) && !this.certifiedMembers.has(current)
|
|
184
|
+
&& !this.registeredCombinatorInternals.has(current));
|
|
185
|
+
unregisteredAggregate ||= possibleJoin;
|
|
146
186
|
const answered = current === start ? undefined : this.contexts.get(current);
|
|
147
187
|
if (answered !== undefined) {
|
|
148
188
|
for (const found of answered.steps) candidates.add(found);
|
|
149
189
|
truncated ||= answered.truncated;
|
|
190
|
+
unregisteredAggregate ||= answered.unregisteredAggregate;
|
|
150
191
|
continue;
|
|
151
192
|
}
|
|
152
193
|
for (const id of [...this.waitedFor(current), ...this.causesOf(current)]) {
|
|
@@ -162,7 +203,11 @@ export class AuthoredStepGraph {
|
|
|
162
203
|
}
|
|
163
204
|
}
|
|
164
205
|
const reduced = this.reduce(candidates);
|
|
165
|
-
const frontier = Object.freeze({
|
|
206
|
+
const frontier = Object.freeze({
|
|
207
|
+
steps: reduced.steps,
|
|
208
|
+
truncated: truncated || reduced.truncated,
|
|
209
|
+
unregisteredAggregate,
|
|
210
|
+
});
|
|
166
211
|
this.contexts.set(start, frontier);
|
|
167
212
|
return frontier;
|
|
168
213
|
}
|
|
@@ -184,7 +229,7 @@ export class AuthoredStepGraph {
|
|
|
184
229
|
* reduction reads recorded `after` lists; when one it reads is incomplete,
|
|
185
230
|
* a redundant ancestor can survive, and the result says so.
|
|
186
231
|
*/
|
|
187
|
-
private reduce(candidates: ReadonlySet<string>): Frontier {
|
|
232
|
+
private reduce(candidates: ReadonlySet<string>): Pick<Frontier, 'steps' | 'truncated'> {
|
|
188
233
|
const ordered = [...candidates].sort((a, b) => this.orderOf(a) - this.orderOf(b));
|
|
189
234
|
if (ordered.length <= 1) return { steps: ordered, truncated: false };
|
|
190
235
|
// An ancestor is always registered before its descendant, so nothing
|
|
@@ -150,7 +150,8 @@ export async function alsoRecord(
|
|
|
150
150
|
* Fold the root's stream back into one record per authored step.
|
|
151
151
|
*
|
|
152
152
|
* A `completed` record supersedes the `admitted` one for the same step; first
|
|
153
|
-
* admission order is preserved
|
|
153
|
+
* admission order is preserved for each step, while different children can
|
|
154
|
+
* first appear in completion order when they finish concurrently.
|
|
154
155
|
* Records this version does not recognise are skipped rather than trusted.
|
|
155
156
|
*/
|
|
156
157
|
export async function readAuthoredStepIndex(
|
package/src/authored-verdict.ts
CHANGED
|
@@ -21,6 +21,10 @@ import type { JournalEvent } from './journal-reader.js';
|
|
|
21
21
|
|
|
22
22
|
/** The `instruction` discriminator `authored-root.ts` writes into the root step. */
|
|
23
23
|
export const AUTHORED_ROOT_KIND = 'relayflows.authored-root.v1';
|
|
24
|
+
/** Parallel roots admitted before 2.0.53; the 2.0.52 CLI recognizes these. */
|
|
25
|
+
export const AUTHORED_PARALLEL_ROOT_KIND = 'relayflows.authored-root.parallel.v2';
|
|
26
|
+
/** Newly admitted parallel roots must be refused by the 2.0.52 CLI on rollback. */
|
|
27
|
+
export const AUTHORED_PARALLEL_ROOT_KIND_V3 = 'relayflows.authored-root.parallel.v3';
|
|
24
28
|
|
|
25
29
|
export interface AuthoredVerdict {
|
|
26
30
|
readonly reason: LoweredCompletionReason;
|
|
@@ -76,7 +80,9 @@ export function declaresAuthoredRoot(spawned: JournalEvent): boolean {
|
|
|
76
80
|
if (step === null || step['id'] !== 'authored-root') return false;
|
|
77
81
|
if (typeof step['instruction'] !== 'string') return false;
|
|
78
82
|
try {
|
|
79
|
-
|
|
83
|
+
const kind = record(JSON.parse(step['instruction']))?.['kind'];
|
|
84
|
+
return kind === AUTHORED_ROOT_KIND || kind === AUTHORED_PARALLEL_ROOT_KIND
|
|
85
|
+
|| kind === AUTHORED_PARALLEL_ROOT_KIND_V3;
|
|
80
86
|
} catch {
|
|
81
87
|
return false;
|
|
82
88
|
}
|
|
@@ -19,7 +19,7 @@ import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
|
19
19
|
import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
20
20
|
import type { StepFailedDetails } from './failure-kinds.js';
|
|
21
21
|
import { LOCAL_AGENT_WORKSPACE } from './local-agent-pins.js';
|
|
22
|
-
import { authoredWorkerSlots, type AuthoredWorkerSlots } from './worker-slots.js';
|
|
22
|
+
import { authoredWorkerSlots, WorkerSlots, type AuthoredWorkerSlots } from './worker-slots.js';
|
|
23
23
|
import { validateStepSecrets } from './step-secrets.js';
|
|
24
24
|
|
|
25
25
|
const AGENT_TIMEOUT = Symbol('declared agent timeout');
|
|
@@ -356,8 +356,14 @@ export function authoredWorkerRunner(
|
|
|
356
356
|
export function authoredDeterministicRunner(
|
|
357
357
|
name: string, journal: JournalClient, journalSteps: AuthoredFlowJournalStep[], budget: AuthoredBudget,
|
|
358
358
|
context: AuthoredStepContext = {},
|
|
359
|
+
parallel = false,
|
|
359
360
|
) {
|
|
360
361
|
const rootRunId = context.rootRunId;
|
|
362
|
+
// run.start drives a deterministic child to completion before replying. The
|
|
363
|
+
// daemon reads one connection in order, so sharing the body's connection
|
|
364
|
+
// would serialize Promise.all. Each admitted child gets its own connection;
|
|
365
|
+
// this limit bounds both daemon threads and shell commands per authored body.
|
|
366
|
+
const slots = new WorkerSlots(8);
|
|
361
367
|
// One lowering, two readers. `record` changes what the child run's verdict
|
|
362
368
|
// MEANS, so the reader changes with it; everything before the read — spec,
|
|
363
369
|
// admission key, budget, child index — is deliberately shared, because a
|
|
@@ -386,8 +392,10 @@ export function authoredDeterministicRunner(
|
|
|
386
392
|
});
|
|
387
393
|
return read(outcome);
|
|
388
394
|
};
|
|
389
|
-
|
|
390
|
-
|
|
395
|
+
const admit = async () => terminal
|
|
396
|
+
? consume(await journal.runStart(spec, undefined, admissionKey, false, parallel))
|
|
397
|
+
: budget.execute(journal, spec, consume, admissionKey, parallel);
|
|
398
|
+
return parallel ? slots.run(admit) : admit();
|
|
391
399
|
}
|
|
392
400
|
const lowerRun = (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate,
|
|
393
401
|
secrets?: string[]): Promise<string> =>
|
|
@@ -398,6 +406,7 @@ export function authoredDeterministicRunner(
|
|
|
398
406
|
secrets?: string[]): Promise<RecordedRunOutcome> =>
|
|
399
407
|
lower(id, command, false, leaseMs, verification, 'record', secrets,
|
|
400
408
|
outcome => readRecordedOutcome(journal, outcome, id, journalSteps, context));
|
|
409
|
+
lowerRun.stop = (reason: unknown): void => slots.close(reason);
|
|
401
410
|
return lowerRun;
|
|
402
411
|
}
|
|
403
412
|
|
package/src/cli/run.ts
CHANGED
|
@@ -415,9 +415,11 @@ export async function resumeFlow(
|
|
|
415
415
|
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
416
416
|
diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } };
|
|
417
417
|
}
|
|
418
|
-
if (error instanceof AuthoredFlowExecutionError
|
|
418
|
+
if (error instanceof AuthoredFlowExecutionError
|
|
419
|
+
&& (error.code === 'unsafe_parallel_runtime' || error.code === 'unsupported_promise_lifecycle')) {
|
|
419
420
|
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
420
|
-
diagnostics: [{ severity: 'refusal',
|
|
421
|
+
diagnostics: [{ severity: 'refusal',
|
|
422
|
+
kind: error.code === 'unsafe_parallel_runtime' ? error.code : 'invalid_spec', message: error.message }] } };
|
|
421
423
|
}
|
|
422
424
|
if (error instanceof AuthoredFlowExecutionError
|
|
423
425
|
&& (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required'
|
package/src/failure-kinds.ts
CHANGED
|
@@ -134,6 +134,8 @@ export const RUN_FAILURE_KINDS = [
|
|
|
134
134
|
'run_parked',
|
|
135
135
|
'run_declined',
|
|
136
136
|
'run_unavailable',
|
|
137
|
+
/** A pinned parallel root is still active, but this process cannot safely dispatch it. */
|
|
138
|
+
'unsafe_parallel_runtime',
|
|
137
139
|
/** A predicate `.gate(fn)` judged false; the verdict is journaled as `<step>.gate`. */
|
|
138
140
|
'gate_failed',
|
|
139
141
|
/** `flows answer` named a wait the run is not asking: unknown, or already answered. */
|
package/src/journal-client.ts
CHANGED
|
@@ -135,7 +135,12 @@ export class JournalClient extends JournalConnection {
|
|
|
135
135
|
reuseFromRunId?: string,
|
|
136
136
|
admissionKey?: string,
|
|
137
137
|
watch = false,
|
|
138
|
+
isolated = false,
|
|
138
139
|
): Promise<VerbContract['run.start']['result']> {
|
|
140
|
+
if (isolated) {
|
|
141
|
+
if (watch) throw new Error('an isolated run.start cannot keep a watch on its temporary session');
|
|
142
|
+
return this.runStartIsolated(spec, reuseFromRunId, admissionKey);
|
|
143
|
+
}
|
|
139
144
|
for (const step of spec.steps) {
|
|
140
145
|
for (const name of validateStepSecrets(step.secrets, process.env)) {
|
|
141
146
|
if (this.withheld.has(name)) {
|
|
@@ -155,6 +160,20 @@ export class JournalClient extends JournalConnection {
|
|
|
155
160
|
}, null);
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
/** Drive a blocking deterministic child on its own socket, then close it. */
|
|
164
|
+
private async runStartIsolated(
|
|
165
|
+
spec: KernelRunSpec, reuseFromRunId?: string, admissionKey?: string,
|
|
166
|
+
): Promise<VerbContract['run.start']['result']> {
|
|
167
|
+
const peer = this.createPeer();
|
|
168
|
+
try {
|
|
169
|
+
await peer.connect();
|
|
170
|
+
await peer.hello('flows-authored-deterministic');
|
|
171
|
+
return await peer.runStart(spec, reuseFromRunId, admissionKey);
|
|
172
|
+
} finally {
|
|
173
|
+
peer.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
158
177
|
/** §3 memoized resume. */
|
|
159
178
|
runResume(runId: string, allowHumanInfluenced = false): Promise<VerbContract['run.resume']['result']> {
|
|
160
179
|
return this.request('run.resume', {
|