@relayflows/sdk 2.0.51 → 2.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/authored-budget.d.ts +1 -1
  2. package/dist/authored-budget.d.ts.map +1 -1
  3. package/dist/authored-budget.js +3 -3
  4. package/dist/authored-budget.js.map +1 -1
  5. package/dist/authored-flow-error.d.ts +3 -1
  6. package/dist/authored-flow-error.d.ts.map +1 -1
  7. package/dist/authored-flow-error.js.map +1 -1
  8. package/dist/authored-flow-executor.d.ts +2 -0
  9. package/dist/authored-flow-executor.d.ts.map +1 -1
  10. package/dist/authored-flow-executor.js +93 -12
  11. package/dist/authored-flow-executor.js.map +1 -1
  12. package/dist/authored-flow-lifecycle.d.ts +28 -3
  13. package/dist/authored-flow-lifecycle.d.ts.map +1 -1
  14. package/dist/authored-flow-lifecycle.js +206 -10
  15. package/dist/authored-flow-lifecycle.js.map +1 -1
  16. package/dist/authored-node-entry.js +16 -0
  17. package/dist/authored-node-entry.js.map +1 -1
  18. package/dist/authored-node-runner.d.ts.map +1 -1
  19. package/dist/authored-node-runner.js +26 -3
  20. package/dist/authored-node-runner.js.map +1 -1
  21. package/dist/authored-parallel-body.d.ts +10 -0
  22. package/dist/authored-parallel-body.d.ts.map +1 -0
  23. package/dist/authored-parallel-body.js +68 -0
  24. package/dist/authored-parallel-body.js.map +1 -0
  25. package/dist/authored-parallel-eligibility.d.ts +22 -0
  26. package/dist/authored-parallel-eligibility.d.ts.map +1 -0
  27. package/dist/authored-parallel-eligibility.js +335 -0
  28. package/dist/authored-parallel-eligibility.js.map +1 -0
  29. package/dist/authored-promise-graph.d.ts +16 -0
  30. package/dist/authored-promise-graph.d.ts.map +1 -1
  31. package/dist/authored-promise-graph.js +36 -0
  32. package/dist/authored-promise-graph.js.map +1 -1
  33. package/dist/authored-root-readback.d.ts +1 -2
  34. package/dist/authored-root-readback.d.ts.map +1 -1
  35. package/dist/authored-root-readback.js +1 -1
  36. package/dist/authored-root-readback.js.map +1 -1
  37. package/dist/authored-root.d.ts +6 -1
  38. package/dist/authored-root.d.ts.map +1 -1
  39. package/dist/authored-root.js +78 -8
  40. package/dist/authored-root.js.map +1 -1
  41. package/dist/authored-step-graph.d.ts +19 -4
  42. package/dist/authored-step-graph.d.ts.map +1 -1
  43. package/dist/authored-step-graph.js +49 -6
  44. package/dist/authored-step-graph.js.map +1 -1
  45. package/dist/authored-step-index.d.ts +2 -1
  46. package/dist/authored-step-index.d.ts.map +1 -1
  47. package/dist/authored-step-index.js +2 -1
  48. package/dist/authored-step-index.js.map +1 -1
  49. package/dist/authored-verdict.d.ts +4 -0
  50. package/dist/authored-verdict.d.ts.map +1 -1
  51. package/dist/authored-verdict.js +7 -1
  52. package/dist/authored-verdict.js.map +1 -1
  53. package/dist/authored-worker-step.d.ts +2 -1
  54. package/dist/authored-worker-step.d.ts.map +1 -1
  55. package/dist/authored-worker-step.js +12 -5
  56. package/dist/authored-worker-step.js.map +1 -1
  57. package/dist/cli/run.d.ts.map +1 -1
  58. package/dist/cli/run.js +4 -2
  59. package/dist/cli/run.js.map +1 -1
  60. package/dist/failure-kinds.d.ts +1 -1
  61. package/dist/failure-kinds.d.ts.map +1 -1
  62. package/dist/failure-kinds.js +2 -0
  63. package/dist/failure-kinds.js.map +1 -1
  64. package/dist/journal-client.d.ts +3 -1
  65. package/dist/journal-client.d.ts.map +1 -1
  66. package/dist/journal-client.js +18 -1
  67. package/dist/journal-client.js.map +1 -1
  68. package/package.json +3 -2
  69. package/src/authored-budget.ts +4 -3
  70. package/src/authored-flow-error.ts +2 -0
  71. package/src/authored-flow-executor.ts +85 -11
  72. package/src/authored-flow-lifecycle.ts +215 -9
  73. package/src/authored-node-entry.ts +17 -0
  74. package/src/authored-node-runner.ts +24 -3
  75. package/src/authored-parallel-body.ts +67 -0
  76. package/src/authored-parallel-eligibility.ts +296 -0
  77. package/src/authored-promise-graph.ts +35 -0
  78. package/src/authored-root-readback.ts +1 -2
  79. package/src/authored-root.ts +80 -9
  80. package/src/authored-step-graph.ts +50 -5
  81. package/src/authored-step-index.ts +2 -1
  82. package/src/authored-verdict.ts +7 -1
  83. package/src/authored-worker-step.ts +12 -3
  84. package/src/cli/run.ts +4 -2
  85. package/src/failure-kinds.ts +2 -0
  86. package/src/journal-client.ts +19 -0
@@ -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
- * This is observational metadata: it never gates execution, and it changes no
36
- * step id, admission key or resume identity.
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({ steps: reduced.steps, truncated: truncated || reduced.truncated });
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 so the index reads in the order the body ran.
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(
@@ -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
- return record(JSON.parse(step['instruction']))?.['kind'] === AUTHORED_ROOT_KIND;
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
- if (terminal) return journal.runStart(spec, undefined, admissionKey).then(consume);
390
- return budget.execute(journal, spec, consume, admissionKey);
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 && error.code === 'unsupported_promise_lifecycle') {
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', kind: 'invalid_spec', message: error.message }] } };
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'
@@ -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. */
@@ -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', {