@orkestrel/workflow 0.0.8 → 0.0.9

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.
@@ -1,84 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_abort = require("@orkestrel/abort");
2
3
  let _orkestrel_contract = require("@orkestrel/contract");
3
4
  let _orkestrel_database = require("@orkestrel/database");
4
- let _orkestrel_abort = require("@orkestrel/abort");
5
5
  let _orkestrel_emitter = require("@orkestrel/emitter");
6
6
  let _orkestrel_timeout = require("@orkestrel/timeout");
7
7
  let _orkestrel_queue = require("@orkestrel/queue");
8
- //#region src/core/Scheduler.ts
9
- /**
10
- * The safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
11
- * built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
12
- * browser and Node.
13
- *
14
- * @remarks
15
- * - **Cross-environment.** Uses ONLY `setTimeout` / `clearTimeout` — universally
16
- * available. It deliberately avoids env-specific fast paths (`setImmediate`,
17
- * `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
18
- * `MessageChannel`); those belong to the environment backends, built with the
19
- * agent loop that consumes them.
20
- * - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
21
- * `setTimeout(0)`, NOT `queueMicrotask`. A microtask drains before the host
22
- * regains control, so it would not actually let pending I/O, timers, or
23
- * rendering run — it only defers within the current task. A zero-delay timer is
24
- * the correct cross-environment "give the host a turn".
25
- * - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` when
26
- * the signal aborts (the standard `AbortSignal` convention). An already-aborted
27
- * signal rejects immediately without arming a timer. Either settle path clears
28
- * the timer and removes the abort listener — no leaked timer, no leaked
29
- * listener, and no double-settle.
30
- * - **Priority is accepted but uniform.** `options.priority` is part of the
31
- * contract, but a `setTimeout`-based default cannot act on urgency, so it treats
32
- * every priority the same. Environment backends honour it.
33
- * - **Event-free.** A pure functional primitive — no Emitter, no events.
34
- *
35
- * @example
36
- * ```ts
37
- * const scheduler = new Scheduler()
38
- * while (!signal.aborted) {
39
- * doSomeWork()
40
- * await scheduler.yield({ signal }) // let the host run between work units
41
- * }
42
- * ```
43
- */
44
- var Scheduler = class {
45
- /**
46
- * Yield control back to the host so other tasks (I/O, timers, rendering) can
47
- * run, then resume — a macrotask turn via `setTimeout(0)` (NOT a microtask,
48
- * which would resume before the host regains control).
49
- */
50
- yield(options) {
51
- return this.#sleep(0, options?.signal);
52
- }
53
- /**
54
- * Resume after at least `ms` milliseconds; abort rejects with `signal.reason`.
55
- *
56
- * @remarks
57
- * `ms` should be a non-negative finite number. The primitive stays minimal and
58
- * does no validation: it passes `ms` straight to the host `setTimeout`, which
59
- * clamps a negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on
60
- * the next host turn rather than throwing.
61
- */
62
- delay(ms, options) {
63
- return this.#sleep(ms, options?.signal);
64
- }
65
- #sleep(ms, signal) {
66
- if (signal?.aborted === true) return Promise.reject(signal.reason);
67
- return new Promise((resolve, reject) => {
68
- const handle = setTimeout(() => {
69
- signal?.removeEventListener("abort", onAbort);
70
- resolve();
71
- }, ms);
72
- const onAbort = this.#abort.bind(this, handle, reject, signal);
73
- signal?.addEventListener("abort", onAbort, { once: true });
74
- });
75
- }
76
- #abort(handle, reject, signal) {
77
- clearTimeout(handle);
78
- reject(signal?.reason);
79
- }
80
- };
81
- //#endregion
82
8
  //#region src/core/constants.ts
83
9
  /** The default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
84
10
  var DEFAULT_BAIL = false;
@@ -184,50 +110,42 @@ var DEFAULT_PHASE_CONCURRENCY = 1024;
184
110
  */
185
111
  var MAX_TIMER_MS = 2147483647;
186
112
  //#endregion
187
- //#region src/core/errors.ts
113
+ //#region src/core/helpers.ts
188
114
  /**
189
- * An error raised by the workflow runtime.
115
+ * Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
190
116
  *
191
117
  * @remarks
192
- * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
193
- * offending node id / status. Raised for an illegal lifecycle transition
194
- * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
195
- * boundary (`RESTORE`), or a refused structural/activity edit (`MUTATION`).
196
- */
197
- var WorkflowError = class extends Error {
198
- code;
199
- context;
200
- constructor(code, message, context) {
201
- super(message);
202
- this.name = "WorkflowError";
203
- this.code = code;
204
- if (context !== void 0) this.context = context;
205
- }
206
- };
207
- /**
208
- * Narrow an unknown caught value to a {@link WorkflowError}.
118
+ * Direct property reads preserve inherited and non-enumerable option values while preventing
119
+ * accessor-backed caller bags from shifting policy, handlers, hooks, or nested options between
120
+ * construction stages. Nested bags and the functions registry retain their original identities so
121
+ * entity constructors can snapshot keyed child options and live additions can resolve against the
122
+ * same registry.
209
123
  *
210
- * @param value - The value to test (typically a `catch` binding)
211
- * @returns `true` when `value` is a {@link WorkflowError}
124
+ * @param options - The caller-owned workflow construction options
125
+ * @returns An owned top-level options bag containing the captured values
212
126
  *
213
127
  * @example
214
128
  * ```ts
215
- * try {
216
- * task.complete('done')
217
- * } catch (error) {
218
- * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
219
- * }
129
+ * const captured = captureWorkflowOptions(options)
130
+ * const workflow = createWorkflow(definition, captured)
220
131
  * ```
221
132
  */
222
- function isWorkflowError(value) {
223
- try {
224
- return value instanceof WorkflowError;
225
- } catch {
226
- return false;
227
- }
133
+ function captureWorkflowOptions(options) {
134
+ const on = options?.on;
135
+ const bail = options?.bail;
136
+ const error = options?.error;
137
+ const phases = options?.phases;
138
+ const functions = options?.functions;
139
+ const silence = options?.silence;
140
+ return Object.freeze({
141
+ ...on === void 0 ? {} : { on },
142
+ ...bail === void 0 ? {} : { bail },
143
+ ...error === void 0 ? {} : { error },
144
+ ...phases === void 0 ? {} : { phases },
145
+ ...functions === void 0 ? {} : { functions },
146
+ ...silence === void 0 ? {} : { silence }
147
+ });
228
148
  }
229
- //#endregion
230
- //#region src/core/helpers.ts
231
149
  /**
232
150
  * Test whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
233
151
  * transition further.
@@ -751,6 +669,61 @@ function createDeferred() {
751
669
  return Promise.withResolvers();
752
670
  }
753
671
  /**
672
+ * Schedule one cancellable host operation behind an owned settlement signal.
673
+ *
674
+ * @remarks
675
+ * The completion and failure paths each own an {@link AbortController}; their native composite is
676
+ * linked to the optional caller signal before `start` can arm host work. Scheduler backends attach
677
+ * only to that safe composite, so caller mutation of `addEventListener` or `removeEventListener`
678
+ * cannot strand the operation. The first completion resolves, the first host failure rejects with
679
+ * its exact value, and caller abort rejects with its exact linked reason. Caller abort and host
680
+ * failure cancel an armed handle; synchronous settlement also cancels the handle immediately after
681
+ * `start` returns it. Cancellation is secondary cleanup: if its closure throws, the already-winning
682
+ * completion, exact host failure, or exact caller reason still settles without escape or replacement.
683
+ *
684
+ * @param start - Arm host work and return its cancellation closure
685
+ * @param signal - Optional caller cancellation signal
686
+ * @returns A promise settled exactly once by completion, host failure, or caller abort
687
+ */
688
+ function scheduleHost(start, signal) {
689
+ const completion = new AbortController();
690
+ const failed = new AbortController();
691
+ let settled;
692
+ try {
693
+ settled = (0, _orkestrel_abort.linkSignal)(AbortSignal.any([completion.signal, failed.signal]), signal);
694
+ } catch (error) {
695
+ return Promise.reject(error);
696
+ }
697
+ if (settled.aborted) return Promise.reject(settled.reason);
698
+ return new Promise((resolve, reject) => {
699
+ let cancel;
700
+ let hostFailure;
701
+ settled.addEventListener("abort", () => {
702
+ if (completion.signal.aborted) {
703
+ resolve();
704
+ return;
705
+ }
706
+ const reason = failed.signal.aborted ? hostFailure : settled.reason;
707
+ try {
708
+ cancel?.();
709
+ } catch {}
710
+ reject(reason);
711
+ }, { once: true });
712
+ try {
713
+ cancel = start(() => completion.abort(), (error) => {
714
+ hostFailure = error;
715
+ failed.abort();
716
+ });
717
+ } catch (error) {
718
+ hostFailure = error;
719
+ failed.abort();
720
+ }
721
+ if (settled.aborted) try {
722
+ cancel?.();
723
+ } catch {}
724
+ });
725
+ }
726
+ /**
754
727
  * Park until `signal` aborts — a promise-parked wait (AGENTS §21), never a timer or
755
728
  * busy-loop, that NEVER rejects.
756
729
  *
@@ -778,6 +751,113 @@ function parkSignal(signal) {
778
751
  });
779
752
  }
780
753
  //#endregion
754
+ //#region src/core/Scheduler.ts
755
+ /**
756
+ * The safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
757
+ * built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
758
+ * browser and Node.
759
+ *
760
+ * @remarks
761
+ * - **Cross-environment.** Uses ONLY `setTimeout` / `clearTimeout` — universally
762
+ * available. It deliberately avoids env-specific fast paths (`setImmediate`,
763
+ * `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
764
+ * `MessageChannel`); those belong to the environment backends, built with the
765
+ * agent loop that consumes them.
766
+ * - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
767
+ * `setTimeout(0)`, NOT `queueMicrotask`. A microtask drains before the host
768
+ * regains control, so it would not actually let pending I/O, timers, or
769
+ * rendering run — it only defers within the current task. A zero-delay timer is
770
+ * the correct cross-environment "give the host a turn".
771
+ * - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` exactly.
772
+ * {@link scheduleHost} links an owned settlement composite to the caller before arming
773
+ * the timer, so pre-abort schedules nothing, caller signal method mutation is harmless,
774
+ * cancellation clears the handle, and native first-settlement wins exactly once.
775
+ * - **Priority is accepted but uniform.** `options.priority` is part of the
776
+ * contract, but a `setTimeout`-based default cannot act on urgency, so it treats
777
+ * every priority the same. Environment backends honour it.
778
+ * - **Event-free.** A pure functional primitive — no Emitter, no events.
779
+ *
780
+ * @example
781
+ * ```ts
782
+ * const scheduler = new Scheduler()
783
+ * while (!signal.aborted) {
784
+ * doSomeWork()
785
+ * await scheduler.yield({ signal }) // let the host run between work units
786
+ * }
787
+ * ```
788
+ */
789
+ var Scheduler = class {
790
+ /**
791
+ * Yield control back to the host so other tasks (I/O, timers, rendering) can
792
+ * run, then resume — a macrotask turn via `setTimeout(0)` (NOT a microtask,
793
+ * which would resume before the host regains control).
794
+ */
795
+ yield(options) {
796
+ return this.#sleep(0, options?.signal);
797
+ }
798
+ /**
799
+ * Resume after at least `ms` milliseconds; abort rejects with `signal.reason`.
800
+ *
801
+ * @remarks
802
+ * `ms` should be a non-negative finite number. The primitive stays minimal and
803
+ * does no validation: it passes `ms` straight to the host `setTimeout`, which
804
+ * clamps a negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on
805
+ * the next host turn rather than throwing.
806
+ */
807
+ delay(ms, options) {
808
+ return this.#sleep(ms, options?.signal);
809
+ }
810
+ #sleep(ms, signal) {
811
+ return scheduleHost((complete) => {
812
+ const handle = setTimeout(complete, ms);
813
+ return () => clearTimeout(handle);
814
+ }, signal);
815
+ }
816
+ };
817
+ //#endregion
818
+ //#region src/core/errors.ts
819
+ /**
820
+ * An error raised by the workflow runtime.
821
+ *
822
+ * @remarks
823
+ * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
824
+ * offending node id / status. Raised for an illegal lifecycle transition
825
+ * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
826
+ * boundary (`RESTORE`), or a refused structural/activity edit (`MUTATION`).
827
+ */
828
+ var WorkflowError = class extends Error {
829
+ code;
830
+ context;
831
+ constructor(code, message, context) {
832
+ super(message);
833
+ this.name = "WorkflowError";
834
+ this.code = code;
835
+ if (context !== void 0) this.context = context;
836
+ }
837
+ };
838
+ /**
839
+ * Narrow an unknown caught value to a {@link WorkflowError}.
840
+ *
841
+ * @param value - The value to test (typically a `catch` binding)
842
+ * @returns `true` when `value` is a {@link WorkflowError}
843
+ *
844
+ * @example
845
+ * ```ts
846
+ * try {
847
+ * task.complete('done')
848
+ * } catch (error) {
849
+ * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
850
+ * }
851
+ * ```
852
+ */
853
+ function isWorkflowError(value) {
854
+ try {
855
+ return value instanceof WorkflowError;
856
+ } catch {
857
+ return false;
858
+ }
859
+ }
860
+ //#endregion
781
861
  //#region src/core/validators.ts
782
862
  /** Test the workflow lifecycle vocabulary. */
783
863
  function isLifecycleStatus(value) {
@@ -869,9 +949,17 @@ function isWorkflowSnapshot(value) {
869
949
  const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
870
950
  return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
871
951
  }
872
- /** Test that every present behavior reference resolves before dispatch. */
873
- function hasWorkflowHandlers(snapshot, functions) {
874
- for (const phase of snapshot.phases) for (const task of phase.tasks) if (task.run !== void 0 && functions?.[task.run] === void 0) return false;
952
+ function hasWorkflowHandlers(workflow, functions) {
953
+ if ("destroyed" in workflow) {
954
+ for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
955
+ return true;
956
+ }
957
+ const runs = /* @__PURE__ */ new Set();
958
+ for (const phase of workflow.phases) for (const task of phase.tasks) {
959
+ if (task.run === void 0 || runs.has(task.run)) continue;
960
+ runs.add(task.run);
961
+ if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
962
+ }
875
963
  return true;
876
964
  }
877
965
  /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
@@ -975,18 +1063,25 @@ function isTaskActivity(value) {
975
1063
  * Validate and own a workflow snapshot before live construction.
976
1064
  *
977
1065
  * @param input - The hostile snapshot boundary
1066
+ * @param id - The optional storage key the owned snapshot must match
978
1067
  * @returns A deeply owned frozen snapshot
1068
+ * @throws {WorkflowError} With `RESTORE` when the snapshot is invalid or does not match `id`
979
1069
  */
980
- function cloneWorkflowSnapshot(input) {
1070
+ function cloneWorkflowSnapshot(input, id) {
1071
+ let cloned;
981
1072
  try {
982
- const cloned = (0, _orkestrel_contract.cloneJSONValue)(input);
983
- if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", workflowSnapshotContext(cloned));
984
- return cloned;
1073
+ cloned = (0, _orkestrel_contract.cloneJSONValue)(input);
985
1074
  } catch (error) {
986
1075
  if (isWorkflowError(error)) throw error;
987
1076
  if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
988
1077
  throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
989
1078
  }
1079
+ if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", workflowSnapshotContext(cloned));
1080
+ if (id !== void 0 && cloned.id !== id) throw new WorkflowError("RESTORE", `workflow snapshot '${cloned.id}' does not match storage key '${id}'`, {
1081
+ requested: id,
1082
+ payload: cloned.id
1083
+ });
1084
+ return cloned;
990
1085
  }
991
1086
  /**
992
1087
  * Validate and clone one complete task activity frame.
@@ -1211,7 +1306,8 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
1211
1306
  * the row `{ id: snapshot.id, snapshot }`.
1212
1307
  * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
1213
1308
  * a {@link WorkflowSnapshot} ({@link import('../helpers.js').isWorkflowSnapshot} — the AGENTS §14
1214
- * boundary narrow for an untrusted storage read), or `undefined` if none is stored.
1309
+ * boundary narrow for an untrusted storage read), or `undefined` if none is stored. A present
1310
+ * snapshot whose own id differs from the requested key rejects with normalized `RESTORE` evidence.
1215
1311
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
1216
1312
  *
1217
1313
  * UNLIKE the server package's `SessionStoreInterface` there is NO
@@ -1222,7 +1318,8 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
1222
1318
  *
1223
1319
  * @example
1224
1320
  * ```ts
1225
- * import { createDatabaseWorkflowStore, createMemoryDriver, createWorkflow, restoreWorkflow } from '@src/core'
1321
+ * import { createMemoryDriver } from '@orkestrel/database'
1322
+ * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
1226
1323
  *
1227
1324
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
1228
1325
  * const workflow = createWorkflow(definition)
@@ -1243,11 +1340,11 @@ var DatabaseWorkflowStore = class {
1243
1340
  constructor(table) {
1244
1341
  this.#table = table;
1245
1342
  }
1246
- /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `WorkflowSnapshot`. */
1343
+ /** Resolve and key-check the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
1247
1344
  async get(id) {
1248
1345
  const row = await this.#table.get(id);
1249
1346
  if (row === void 0) return void 0;
1250
- return cloneWorkflowSnapshot(row.snapshot);
1347
+ return cloneWorkflowSnapshot(row.snapshot, id);
1251
1348
  }
1252
1349
  /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
1253
1350
  async set(snapshot) {
@@ -1292,7 +1389,7 @@ var DatabaseWorkflowStore = class {
1292
1389
  *
1293
1390
  * @example
1294
1391
  * ```ts
1295
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@src/core'
1392
+ * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
1296
1393
  *
1297
1394
  * const store = createMemoryWorkflowStore()
1298
1395
  * const workflow = createWorkflow(definition)
@@ -1381,14 +1478,18 @@ var Task = class {
1381
1478
  this.#workflow = workflow;
1382
1479
  this.#recompute = recompute;
1383
1480
  try {
1384
- this.#metadata = (0, _orkestrel_contract.cloneJSONRecord)(options?.metadata ?? metadata);
1481
+ const metadataOption = options?.metadata;
1482
+ this.#metadata = (0, _orkestrel_contract.cloneJSONRecord)(metadataOption ?? metadata);
1385
1483
  } catch (error) {
1386
1484
  if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `task '${context.id}' metadata could not be read safely: ${error.message}`, { task: context.id });
1387
1485
  throw new WorkflowError("RESTORE", `task '${context.id}' metadata could not be read safely`, { task: context.id });
1388
1486
  }
1487
+ const on = options?.on;
1488
+ const listenerError = options?.error;
1489
+ const silenceOption = options?.silence;
1389
1490
  this.#emitter = new _orkestrel_emitter.Emitter({
1390
- ...options?.on === void 0 ? {} : { on: options.on },
1391
- ...options?.error === void 0 ? {} : { error: options.error }
1491
+ ...on === void 0 ? {} : { on },
1492
+ ...listenerError === void 0 ? {} : { error: listenerError }
1392
1493
  });
1393
1494
  this.#status = status;
1394
1495
  this.#result = result;
@@ -1403,7 +1504,7 @@ var Task = class {
1403
1504
  this.#attempts = attempts;
1404
1505
  this.#handler = handler;
1405
1506
  this.#abort = (0, _orkestrel_abort.createAbort)();
1406
- this.#silence = resolveTaskSilence(options?.silence, silence);
1507
+ this.#silence = resolveTaskSilence(silenceOption, silence);
1407
1508
  this.#onSilence = this.#expire.bind(this);
1408
1509
  this.#liveness = this.#silence === void 0 ? void 0 : (0, _orkestrel_timeout.createTimeout)({
1409
1510
  ms: this.#silence,
@@ -1761,9 +1862,9 @@ var TaskManager = class {
1761
1862
  *
1762
1863
  * @remarks
1763
1864
  * - **Derived status.** `status` is `#override` when one is in force, else
1764
- * {@link derivePhaseStatus} over the live tasks' statuses. {@link #recompute} (passed to
1865
+ * {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
1765
1866
  * each child {@link Task}) re-derives on every child transition; a CHANGE emits the matching
1766
- * event AND escalates to the workflow ({@link #escalate}, the upward step of the cascade).
1867
+ * event AND escalates to the workflow (`#escalate`, the upward step of the cascade).
1767
1868
  * - **Override (AGENTS §10).** `skip` / `stop` FORCE the phase's status (e.g. skipping a whole
1768
1869
  * phase), overriding the derived value; the override is PERSISTED in the snapshot's own
1769
1870
  * `override` field and restored DIRECTLY (no divergence guess), so a forced phase round-trips.
@@ -1794,10 +1895,12 @@ var TaskManager = class {
1794
1895
  * construction path {@link #append} uses at build time, so a live mint and a restored/built
1795
1896
  * task are wired IDENTICALLY. At construction, the workflow-level
1796
1897
  * {@link import('../types.js').WorkflowFunctions} registry (threaded from
1797
- * {@link import('../types.js').WorkflowOptions.functions}) resolves each task's `run` name into
1798
- * its runtime {@link import('../types.js').TaskInterface.handler} ONCE; a `run` that is omitted
1799
- * or unregistered resolves to no handler; only an omitted `run` is a no-op, while an
1800
- * unresolved present name makes the containing tree non-drivable.
1898
+ * {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `run`
1899
+ * name ONCE before any task is built; siblings sharing a name receive the exact same captured
1900
+ * runtime {@link import('../types.js').TaskInterface.handler}. A later live {@link add} reads
1901
+ * that name once from the retained registry at its own mint moment. An omitted or unregistered
1902
+ * `run` resolves to no handler; only omission is a no-op, while an unresolved present name makes
1903
+ * the containing tree non-drivable.
1801
1904
  * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
1802
1905
  * quartet, scoped to this phase — a driving
1803
1906
  * {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
@@ -1823,6 +1926,9 @@ var Phase = class {
1823
1926
  #paused;
1824
1927
  #gate;
1825
1928
  constructor(snapshot, workflow, escalate, options, bail, functions, silence) {
1929
+ const on = options?.on;
1930
+ const error = options?.error;
1931
+ const tasks = options?.tasks;
1826
1932
  this.#id = snapshot.id;
1827
1933
  this.#name = snapshot.name;
1828
1934
  if (snapshot.description !== void 0) Object.defineProperty(this, "description", {
@@ -1833,13 +1939,19 @@ var Phase = class {
1833
1939
  this.#escalateUp = escalate;
1834
1940
  this.#functions = functions;
1835
1941
  this.#silence = silence;
1942
+ const handlers = /* @__PURE__ */ new Map();
1943
+ for (const task of snapshot.tasks) if (task.run !== void 0 && !handlers.has(task.run)) handlers.set(task.run, functions?.[task.run]);
1836
1944
  this.#bail = bail ?? snapshot.bail;
1837
1945
  this.#concurrency = snapshot.concurrency;
1838
1946
  this.#emitter = new _orkestrel_emitter.Emitter({
1839
- ...options?.on === void 0 ? {} : { on: options.on },
1840
- ...options?.error === void 0 ? {} : { error: options.error }
1947
+ ...on === void 0 ? {} : { on },
1948
+ ...error === void 0 ? {} : { error }
1841
1949
  });
1842
- for (const task of snapshot.tasks) this.#append(task, options);
1950
+ for (const task of snapshot.tasks) {
1951
+ const taskOptions = tasks?.[task.id];
1952
+ const handler = task.run === void 0 ? void 0 : handlers.get(task.run);
1953
+ this.#append(task, taskOptions, handler);
1954
+ }
1843
1955
  this.#override = snapshot.override;
1844
1956
  this.#status = this.status;
1845
1957
  this.#paused = false;
@@ -2021,17 +2133,17 @@ var Phase = class {
2021
2133
  if (result.success) this.#emitter.emit("add", result.value, at);
2022
2134
  return result;
2023
2135
  }
2024
- #append(task, options) {
2025
- const created = this.#create(task, options?.tasks?.[task.id]);
2136
+ #append(task, options, handler) {
2137
+ const created = this.#create(task, options, handler);
2026
2138
  this.#tasks.append(created);
2027
2139
  }
2028
- #create(snapshot, options) {
2029
- const context = buildTaskContext(this.context, snapshot);
2030
- const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
2031
- return new Task(context, this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, snapshot.metadata, snapshot.attempts, snapshot.activity, handler, this.#silence);
2140
+ #create(snapshot, options, handler) {
2141
+ return new Task(buildTaskContext(this.context, snapshot), this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, snapshot.metadata, snapshot.attempts, snapshot.activity, handler, this.#silence);
2032
2142
  }
2033
2143
  #mint(definition) {
2034
- return this.#create(taskDefinitionToSnapshot(definition), void 0);
2144
+ const snapshot = taskDefinitionToSnapshot(definition);
2145
+ const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
2146
+ return this.#create(snapshot, void 0, handler);
2035
2147
  }
2036
2148
  #statuses() {
2037
2149
  return this.#tasks.tasks().map((task) => task.status);
@@ -2126,11 +2238,11 @@ var PhaseManager = class {
2126
2238
  * - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
2127
2239
  * {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
2128
2240
  * {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').restoreWorkflow}
2129
- * passes a persisted one). Each child {@link Phase} is wired to escalate to {@link #recompute}.
2241
+ * passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
2130
2242
  * - **Derived status.** `status` is `#override` when forced, else
2131
2243
  * {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
2132
2244
  * reachable ONLY under `bail: true` (a single failed task halts the workflow); under
2133
- * `bail: false` a failed phase folds into `completed`. {@link #recompute} diffs on each phase
2245
+ * `bail: false` a failed phase folds into `completed`. `#recompute` diffs on each phase
2134
2246
  * change; a CHANGE emits.
2135
2247
  * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; an executed task-free pending tree
2136
2248
  * may also be force-completed vacuously. The override is PERSISTED in the snapshot's own
@@ -2179,15 +2291,22 @@ var Workflow = class {
2179
2291
  #gate;
2180
2292
  #destroyed;
2181
2293
  constructor(snapshot, options) {
2294
+ const captured = captureWorkflowOptions(options);
2295
+ const on = captured.on;
2296
+ const bail = captured.bail;
2297
+ const error = captured.error;
2298
+ const phases = captured.phases;
2299
+ const functions = captured.functions;
2300
+ const silence = captured.silence;
2182
2301
  this.#context = buildWorkflowContext(snapshot);
2183
2302
  if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
2184
- this.#bail = options?.bail ?? snapshot.bail;
2185
- this.#bailOverride = options?.bail;
2186
- this.#functions = options?.functions;
2187
- this.#silence = options?.silence;
2303
+ this.#bail = bail ?? snapshot.bail;
2304
+ this.#bailOverride = bail;
2305
+ this.#functions = functions;
2306
+ this.#silence = silence;
2188
2307
  this.#emitter = new _orkestrel_emitter.Emitter({
2189
- ...options?.on === void 0 ? {} : { on: options.on },
2190
- ...options?.error === void 0 ? {} : { error: options.error }
2308
+ ...on === void 0 ? {} : { on },
2309
+ ...error === void 0 ? {} : { error }
2191
2310
  });
2192
2311
  this.#created = snapshot.created;
2193
2312
  this.#updated = snapshot.updated;
@@ -2195,7 +2314,10 @@ var Workflow = class {
2195
2314
  this.#paused = false;
2196
2315
  this.#gate = void 0;
2197
2316
  this.#destroyed = false;
2198
- for (const phase of snapshot.phases) this.#append(phase, options);
2317
+ for (const phase of snapshot.phases) {
2318
+ const phaseOptions = phases?.[phase.id];
2319
+ this.#append(phase, phaseOptions);
2320
+ }
2199
2321
  this.#override = snapshot.override;
2200
2322
  this.#status = this.status;
2201
2323
  }
@@ -2387,7 +2509,7 @@ var Workflow = class {
2387
2509
  return found;
2388
2510
  }
2389
2511
  #append(phase, options) {
2390
- const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride, this.#functions, this.#silence);
2512
+ const created = new Phase(phase, this, () => this.#recompute(), options, this.#bailOverride, this.#functions, this.#silence);
2391
2513
  this.#phases.append(created);
2392
2514
  }
2393
2515
  #mint(definition) {
@@ -2419,12 +2541,11 @@ var Workflow = class {
2419
2541
  * `functions` registry in) and stores it under `definition.id` — an already-present id
2420
2542
  * OVERWRITES (last write wins). `count` is the map size, `workflow(id)` looks one up,
2421
2543
  * `workflows()` lists them in insertion order.
2422
- * - **Durable open / save.** `open(id)` returns an already-registered workflow directly; on a
2423
- * registry MISS with a `store` set it rehydrates through {@link restoreWorkflow} (flowing the
2424
- * manager's `functions` registry in so the rehydrated tree is RUNNABLE), registers it, and
2425
- * returns it lenient (`undefined`) with no store or a store miss. `save(id)` persists a
2426
- * registered workflow's `snapshot()` to the `store` — lenient (`false`) with no store or an
2427
- * unknown id.
2544
+ * - **Durable open / save.** `open(id)` returns an already-registered workflow directly; same-id
2545
+ * misses share one hydration. A concurrent `add` wins, while `remove` / `clear` invalidate
2546
+ * earlier reads; wrong-key payloads reject with `RESTORE`. `save(id)` captures a registered
2547
+ * workflow's snapshot at invocation and serializes same-id writes without coupling other ids.
2548
+ * Both remain lenient without a store or registered id.
2428
2549
  * - **Removal.** `remove` drops one by id, or a batch (§9.2, array overload FIRST) — `true` when
2429
2550
  * any was removed. `clear` empties the registry.
2430
2551
  * - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
@@ -2442,6 +2563,12 @@ var Workflow = class {
2442
2563
  */
2443
2564
  var WorkflowManager = class {
2444
2565
  #workflows = /* @__PURE__ */ new Map();
2566
+ #opens = /* @__PURE__ */ new Map();
2567
+ #saves = /* @__PURE__ */ new Map();
2568
+ #mutations = /* @__PURE__ */ new Map();
2569
+ #additions = /* @__PURE__ */ new Map();
2570
+ #hydrations = /* @__PURE__ */ new Map();
2571
+ #generation = Symbol();
2445
2572
  #functions;
2446
2573
  #store;
2447
2574
  constructor(options) {
@@ -2459,36 +2586,140 @@ var WorkflowManager = class {
2459
2586
  }
2460
2587
  add(definition) {
2461
2588
  const workflow = createWorkflow(definition, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
2589
+ const mutation = this.#invalidate(workflow.id);
2590
+ if (mutation === void 0) this.#additions.delete(workflow.id);
2591
+ else this.#additions.set(workflow.id, mutation);
2462
2592
  this.#workflows.set(workflow.id, workflow);
2463
2593
  return workflow;
2464
2594
  }
2465
- async open(id) {
2595
+ open(id) {
2466
2596
  const existing = this.#workflows.get(id);
2467
- if (existing !== void 0) return existing;
2468
- if (this.#store === void 0) return void 0;
2469
- const snapshot = await this.#store.get(id);
2470
- if (snapshot === void 0) return void 0;
2471
- const workflow = restoreWorkflow(snapshot, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
2472
- this.#workflows.set(workflow.id, workflow);
2473
- return workflow;
2474
- }
2475
- async save(id) {
2597
+ if (existing !== void 0) return Promise.resolve(existing);
2598
+ if (this.#store === void 0) return Promise.resolve(void 0);
2599
+ const pending = this.#opens.get(id);
2600
+ if (pending !== void 0) return pending;
2601
+ const mutation = this.#mutations.get(id);
2602
+ const generation = this.#generation;
2603
+ const lease = this.#retain(id);
2604
+ const reservation = Promise.withResolvers();
2605
+ const opening = reservation.promise;
2606
+ this.#opens.set(id, opening);
2607
+ this.#hydrate(id, mutation, generation, lease, this.#store).then(reservation.resolve, reservation.reject);
2608
+ opening.then(() => this.#releaseOpen(id, opening), () => this.#releaseOpen(id, opening));
2609
+ return opening;
2610
+ }
2611
+ save(id) {
2476
2612
  const workflow = this.#workflows.get(id);
2477
- if (this.#store === void 0 || workflow === void 0) return false;
2478
- await this.#store.set(workflow.snapshot());
2479
- return true;
2613
+ if (this.#store === void 0 || workflow === void 0) return Promise.resolve(false);
2614
+ const snapshot = workflow.snapshot();
2615
+ const previous = this.#saves.get(id);
2616
+ const reservation = Promise.withResolvers();
2617
+ const saving = reservation.promise;
2618
+ this.#saves.set(id, saving);
2619
+ this.#persist(this.#store, previous, snapshot).then(reservation.resolve, reservation.reject);
2620
+ saving.then(() => this.#settle(id, saving), () => this.#settle(id, saving));
2621
+ return saving.then(() => true);
2480
2622
  }
2481
2623
  remove(ids) {
2482
2624
  if ((0, _orkestrel_contract.isArray)(ids)) {
2483
2625
  let removed = false;
2484
- for (const id of ids) if (this.#workflows.delete(id)) removed = true;
2626
+ for (const id of ids) {
2627
+ this.#invalidate(id);
2628
+ this.#additions.delete(id);
2629
+ if (this.#workflows.delete(id)) removed = true;
2630
+ }
2485
2631
  return removed;
2486
2632
  }
2633
+ this.#invalidate(ids);
2634
+ this.#additions.delete(ids);
2487
2635
  return this.#workflows.delete(ids);
2488
2636
  }
2489
2637
  clear() {
2638
+ this.#generation = Symbol();
2639
+ this.#mutations.clear();
2640
+ this.#additions.clear();
2641
+ this.#opens.clear();
2490
2642
  this.#workflows.clear();
2491
2643
  }
2644
+ async #hydrate(id, mutation, generation, lease, store) {
2645
+ try {
2646
+ const snapshot = await store.get(id);
2647
+ if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2648
+ if (snapshot === void 0) return void 0;
2649
+ let owned;
2650
+ try {
2651
+ owned = cloneWorkflowSnapshot(snapshot, id);
2652
+ } catch (error) {
2653
+ if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2654
+ throw error;
2655
+ }
2656
+ if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2657
+ let workflow;
2658
+ try {
2659
+ workflow = restoreWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
2660
+ } catch (error) {
2661
+ if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2662
+ throw error;
2663
+ }
2664
+ if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2665
+ return this.#register(id, workflow, mutation, generation);
2666
+ } finally {
2667
+ this.#releaseHydration(id, lease);
2668
+ }
2669
+ }
2670
+ #owns(id, mutation, generation) {
2671
+ return this.#generation === generation && this.#mutations.get(id) === mutation;
2672
+ }
2673
+ #resolve(id, generation) {
2674
+ if (this.#generation !== generation) return void 0;
2675
+ const mutation = this.#mutations.get(id);
2676
+ const workflow = this.#workflows.get(id);
2677
+ return workflow !== void 0 && this.#additions.get(id) === mutation ? workflow : void 0;
2678
+ }
2679
+ #register(id, workflow, mutation, generation) {
2680
+ if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
2681
+ this.#workflows.set(id, workflow);
2682
+ return workflow;
2683
+ }
2684
+ async #persist(store, previous, snapshot) {
2685
+ if (previous !== void 0) try {
2686
+ await previous;
2687
+ } catch {}
2688
+ await store.set(snapshot);
2689
+ }
2690
+ #invalidate(id) {
2691
+ this.#opens.delete(id);
2692
+ if (!this.#hydrations.has(id)) {
2693
+ this.#mutations.delete(id);
2694
+ this.#additions.delete(id);
2695
+ return;
2696
+ }
2697
+ const mutation = Symbol();
2698
+ this.#mutations.set(id, mutation);
2699
+ return mutation;
2700
+ }
2701
+ #retain(id) {
2702
+ const lease = Symbol();
2703
+ const hydrations = this.#hydrations.get(id);
2704
+ if (hydrations === void 0) this.#hydrations.set(id, /* @__PURE__ */ new Set([lease]));
2705
+ else hydrations.add(lease);
2706
+ return lease;
2707
+ }
2708
+ #releaseOpen(id, opening) {
2709
+ if (this.#opens.get(id) === opening) this.#opens.delete(id);
2710
+ }
2711
+ #releaseHydration(id, lease) {
2712
+ const hydrations = this.#hydrations.get(id);
2713
+ if (hydrations === void 0) return;
2714
+ hydrations.delete(lease);
2715
+ if (hydrations.size !== 0) return;
2716
+ this.#hydrations.delete(id);
2717
+ this.#mutations.delete(id);
2718
+ this.#additions.delete(id);
2719
+ }
2720
+ #settle(id, saving) {
2721
+ if (this.#saves.get(id) === saving) this.#saves.delete(id);
2722
+ }
2492
2723
  };
2493
2724
  //#endregion
2494
2725
  //#region src/core/Controller.ts
@@ -2607,6 +2838,7 @@ var Runner = class {
2607
2838
  #order = [];
2608
2839
  #values = /* @__PURE__ */ new Map();
2609
2840
  #dispatched = /* @__PURE__ */ new Set();
2841
+ #queued = /* @__PURE__ */ new Set();
2610
2842
  #count = 0;
2611
2843
  #drained;
2612
2844
  #started = false;
@@ -2614,18 +2846,28 @@ var Runner = class {
2614
2846
  #stopped = false;
2615
2847
  #stopping = false;
2616
2848
  #failure;
2849
+ #stopPromise;
2850
+ #abortPromise;
2851
+ #destroyPromise;
2617
2852
  constructor(options) {
2618
- this.#handler = options.handler;
2619
- this.#entries = options.entries;
2853
+ const handler = options.handler;
2854
+ const entries = options.entries;
2855
+ const on = options.on;
2856
+ const error = options.error;
2857
+ const concurrency = options.concurrency;
2858
+ const retries = options.retries;
2859
+ const timeout = options.timeout;
2860
+ this.#handler = handler;
2861
+ this.#entries = entries;
2620
2862
  this.#emitter = new _orkestrel_emitter.Emitter({
2621
- ...options.on === void 0 ? {} : { on: options.on },
2622
- ...options.error === void 0 ? {} : { error: options.error }
2863
+ ...on === void 0 ? {} : { on },
2864
+ ...error === void 0 ? {} : { error }
2623
2865
  });
2624
2866
  this.#queue = (0, _orkestrel_queue.createQueue)({
2625
2867
  handler: this.#dispatch.bind(this),
2626
- ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
2627
- ...options.retries === void 0 ? {} : { retries: options.retries },
2628
- ...options.timeout === void 0 ? {} : { timeout: options.timeout }
2868
+ ...concurrency === void 0 ? {} : { concurrency },
2869
+ ...retries === void 0 ? {} : { retries },
2870
+ ...timeout === void 0 ? {} : { timeout }
2629
2871
  });
2630
2872
  }
2631
2873
  get emitter() {
@@ -2667,7 +2909,7 @@ var Runner = class {
2667
2909
  * ```
2668
2910
  */
2669
2911
  spawn(input) {
2670
- if (this.#stopped || !this.#running) return void 0;
2912
+ if (!this.#accepts()) return void 0;
2671
2913
  return this.#launch(input, void 0, true);
2672
2914
  }
2673
2915
  async execute(inputs) {
@@ -2675,29 +2917,37 @@ var Runner = class {
2675
2917
  if (this.#stopped) throw new Error("runner is stopped");
2676
2918
  this.#started = true;
2677
2919
  this.#running = true;
2678
- this.#emitter.emit("start");
2679
- if (inputs.length === 0) {
2680
- this.#running = false;
2681
- this.#emitter.emit("finish", []);
2682
- return [];
2683
- }
2684
2920
  const drained = createDeferred();
2685
2921
  this.#drained = drained;
2686
- for (const input of inputs) this.#launch(input);
2922
+ for (const input of inputs) {
2923
+ if (!this.#accepts()) break;
2924
+ this.#launch(input);
2925
+ }
2926
+ this.#emitter.emit("start");
2927
+ if (this.#count === 0) drained.resolve();
2687
2928
  await drained.promise;
2688
2929
  this.#running = false;
2930
+ const cleanup = await this.#cleanup();
2689
2931
  if (this.#failure !== void 0) throw this.#failure.error;
2932
+ if (cleanup !== void 0) throw cleanup.error;
2690
2933
  const results = this.#collect();
2691
2934
  this.#emitter.emit("finish", results);
2935
+ const finishing = await this.#cleanup();
2936
+ if (finishing !== void 0) throw finishing.error;
2692
2937
  return results;
2693
2938
  }
2694
2939
  abort(reason) {
2695
- if (this.#stopped) return;
2940
+ if (this.#abortPromise !== void 0) return this.#abortPromise;
2941
+ const barrier = createDeferred();
2942
+ this.#abortPromise = barrier.promise;
2943
+ barrier.promise.catch(() => {});
2696
2944
  if (this.#running && this.#failure === void 0) this.#failure = { error: reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason };
2697
2945
  this.#cancel(reason);
2698
- this.#queue.abort(reason);
2699
2946
  this.#stopped = true;
2947
+ const cleanup = this.#queue.abort(reason);
2948
+ this.#settleLifecycle(barrier, cleanup);
2700
2949
  this.#emitter.emit("abort", reason);
2950
+ return barrier.promise;
2701
2951
  }
2702
2952
  /**
2703
2953
  * Suspend dispatch (AGENTS §10 — resumable): delegates to the backing queue's own
@@ -2735,18 +2985,28 @@ var Runner = class {
2735
2985
  * dispatched unit's rejection while stopping is still a real failure. Idempotent.
2736
2986
  */
2737
2987
  stop() {
2738
- if (this.#stopped) return;
2988
+ if (this.#destroyPromise !== void 0) return this.#destroyPromise;
2989
+ if (this.#abortPromise !== void 0) return this.#abortPromise;
2990
+ if (this.#stopPromise !== void 0) return this.#stopPromise;
2991
+ const barrier = createDeferred();
2992
+ this.#stopPromise = barrier.promise;
2993
+ barrier.promise.catch(() => {});
2739
2994
  this.#stopping = true;
2740
2995
  this.#stopped = true;
2741
- this.#queue.stop();
2996
+ const cleanup = this.#queue.stop();
2997
+ this.#settleLifecycle(barrier, cleanup);
2998
+ return barrier.promise;
2742
2999
  }
2743
3000
  destroy() {
2744
- if (this.#stopped) {
2745
- this.#queue.destroy();
2746
- return;
2747
- }
3001
+ if (this.#destroyPromise !== void 0) return this.#destroyPromise;
3002
+ const barrier = createDeferred();
3003
+ this.#destroyPromise = barrier.promise;
3004
+ barrier.promise.catch(() => {});
3005
+ this.#stopped = true;
2748
3006
  this.abort();
2749
- this.#queue.destroy();
3007
+ const cleanup = this.#queue.destroy();
3008
+ this.#settleDestroy(barrier, cleanup);
3009
+ return barrier.promise;
2750
3010
  }
2751
3011
  #launch(input, parent, announce = parent !== void 0) {
2752
3012
  const id = crypto.randomUUID();
@@ -2755,14 +3015,24 @@ var Runner = class {
2755
3015
  this.#order.push(id);
2756
3016
  this.#count += 1;
2757
3017
  if (announce) this.#emitter.emit("spawn", id, parent);
2758
- const promise = this.#queue.enqueue({
2759
- id,
2760
- input
2761
- }, {
2762
- id,
2763
- signal: abort.signal,
2764
- ...this.#entries?.(input)
2765
- });
3018
+ let promise;
3019
+ try {
3020
+ const entry = this.#entries?.(input);
3021
+ const retries = entry?.retries;
3022
+ const timeout = entry?.timeout;
3023
+ this.#queued.add(id);
3024
+ promise = this.#queue.enqueue({
3025
+ id,
3026
+ input
3027
+ }, {
3028
+ id,
3029
+ signal: abort.signal,
3030
+ ...retries === void 0 ? {} : { retries },
3031
+ ...timeout === void 0 ? {} : { timeout }
3032
+ });
3033
+ } catch (error) {
3034
+ promise = Promise.reject(error);
3035
+ }
2766
3036
  promise.then((value) => this.#settle(id, {
2767
3037
  ok: true,
2768
3038
  value
@@ -2781,14 +3051,14 @@ var Runner = class {
2781
3051
  return this.#handler(controller);
2782
3052
  }
2783
3053
  #spawn(input, parent) {
2784
- if (!this.#running) throw new Error("spawn is unavailable outside an active run");
3054
+ if (!this.#accepts()) throw new Error("spawn is unavailable outside an active run");
2785
3055
  return this.#launch(input, parent);
2786
3056
  }
2787
3057
  #settle(id, outcome) {
2788
3058
  if (outcome.ok) {
2789
3059
  this.#values.set(id, { value: outcome.value });
2790
3060
  this.#emitter.emit("settle", id);
2791
- } else if (this.#stopping && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
3061
+ } else if (this.#stopping && this.#queued.has(id) && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
2792
3062
  this.#failure = { error: outcome.error };
2793
3063
  this.#emitter.emit("fail", id, outcome.error);
2794
3064
  this.abort(outcome.error);
@@ -2804,6 +3074,46 @@ var Runner = class {
2804
3074
  }
2805
3075
  return results;
2806
3076
  }
3077
+ #accepts() {
3078
+ return this.#running && !this.#stopped;
3079
+ }
3080
+ async #cleanup() {
3081
+ const cleanup = this.#destroyPromise ?? this.#abortPromise ?? this.#stopPromise;
3082
+ if (cleanup === void 0) return void 0;
3083
+ try {
3084
+ await cleanup;
3085
+ return;
3086
+ } catch (error) {
3087
+ return { error };
3088
+ }
3089
+ }
3090
+ async #settleLifecycle(barrier, cleanup) {
3091
+ let failure;
3092
+ try {
3093
+ await cleanup;
3094
+ } catch (error) {
3095
+ failure = { error };
3096
+ }
3097
+ await this.#waitDrain();
3098
+ if (failure === void 0) barrier.resolve();
3099
+ else barrier.reject(failure.error);
3100
+ }
3101
+ async #settleDestroy(barrier, cleanup) {
3102
+ let failure;
3103
+ try {
3104
+ await cleanup;
3105
+ } catch (error) {
3106
+ failure = { error };
3107
+ }
3108
+ await this.#waitDrain();
3109
+ this.#emitter.destroy();
3110
+ if (failure === void 0) barrier.resolve();
3111
+ else barrier.reject(failure.error);
3112
+ }
3113
+ async #waitDrain() {
3114
+ if (this.#count === 0) return;
3115
+ await this.#drained?.promise;
3116
+ }
2807
3117
  #cancel(reason) {
2808
3118
  for (const abort of this.#aborts.values()) abort.abort(reason);
2809
3119
  }
@@ -3079,8 +3389,10 @@ var WorkflowPersistence = class {
3079
3389
  await this.#writing;
3080
3390
  return;
3081
3391
  }
3082
- const writing = this.#drain();
3392
+ const reservation = Promise.withResolvers();
3393
+ const writing = reservation.promise;
3083
3394
  this.#writing = writing;
3395
+ this.#drain().then(reservation.resolve, reservation.reject);
3084
3396
  try {
3085
3397
  await writing;
3086
3398
  } finally {
@@ -3116,8 +3428,9 @@ var WorkflowPersistence = class {
3116
3428
  * - **Composes, never re-implements.** Per-phase bounded concurrency is one
3117
3429
  * {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
3118
3430
  * `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
3119
- * timeout / budget / entity `signal` fold through {@link createAbort} / {@link createTimeout} +
3120
- * `AbortSignal.any` (exactly as the agent runtime folds its bounds); pacing is the shipped
3431
+ * timeout / budget / entity `signal` fold through the `@orkestrel/abort` signal contract,
3432
+ * {@link createTimeout}, and `AbortSignal.any` (exactly as the agent runtime folds its bounds);
3433
+ * pacing is the shipped
3121
3434
  * {@link SchedulerInterface}. The runner writes ZERO concurrency / retry / abort logic of
3122
3435
  * its own — it only sequences phases, dispatches a task's own handler, and drives the live
3123
3436
  * entity. The workflow layer owns per-task deadlines because timeout settlement must
@@ -3192,34 +3505,46 @@ var WorkflowRunner = class WorkflowRunner {
3192
3505
  }
3193
3506
  execute(target, options) {
3194
3507
  if (this.#isWorkflow(target)) {
3508
+ const signal = options?.signal;
3509
+ const timeout = options?.timeout;
3510
+ const budget = options?.budget;
3511
+ const store = options?.store;
3195
3512
  this.#acquire(target);
3196
- return this.#execute(target, options);
3513
+ return this.#execute(target, signal, timeout, budget, store);
3197
3514
  }
3198
- const workflow = new Workflow(definitionToSnapshot(target, options?.bail ?? target.bail ?? false), options);
3515
+ const captured = captureWorkflowOptions(options);
3516
+ const signal = options?.signal;
3517
+ const timeout = options?.timeout;
3518
+ const budget = options?.budget;
3519
+ const store = options?.store;
3520
+ const workflow = new Workflow(definitionToSnapshot(target, captured.bail ?? target.bail ?? false), captured);
3199
3521
  this.#acquire(workflow);
3200
- return this.#execute(workflow, options);
3522
+ return this.#execute(workflow, signal, timeout, budget, store);
3201
3523
  }
3202
3524
  #acquire(workflow) {
3203
3525
  const tasks = workflow.phases.phases().flatMap((phase) => phase.tasks.tasks());
3204
- if (!((workflow.status === "pending" || workflow.status === "running") && tasks.every((task) => task.status !== "running") && (tasks.length === 0 || tasks.some((task) => task.status === "pending")) && tasks.every((task) => task.run === void 0 || task.handler !== void 0)) || workflow.destroyed || WorkflowRunner.#executions.has(workflow)) throw new WorkflowError("TRANSITION", `workflow '${workflow.id}' is not drivable`, {
3526
+ if (!((workflow.status === "pending" || workflow.status === "running") && tasks.every((task) => task.status !== "running") && (tasks.length === 0 || tasks.some((task) => task.status === "pending")) && hasWorkflowHandlers(workflow)) || workflow.destroyed || WorkflowRunner.#executions.has(workflow)) throw new WorkflowError("TRANSITION", `workflow '${workflow.id}' is not drivable`, {
3205
3527
  id: workflow.id,
3206
3528
  status: workflow.status,
3207
3529
  destroyed: workflow.destroyed
3208
3530
  });
3209
3531
  WorkflowRunner.#executions.add(workflow);
3210
3532
  }
3211
- async #execute(workflow, options) {
3212
- const ms = options?.timeout;
3213
- const timeout = ms !== void 0 && Number.isFinite(ms) && ms > 0 && ms <= 2147483647 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
3214
- timeout?.start();
3215
- options?.budget?.start();
3216
- const runSignal = this.#fold(workflow, options, timeout);
3217
- const persistence = options?.store === void 0 ? void 0 : new WorkflowPersistence(workflow, options.store);
3533
+ async #execute(workflow, signal, ms, budget, store) {
3218
3534
  const holder = { runner: void 0 };
3219
- const onCancel = this.#abortActive.bind(this, holder, runSignal);
3220
- if (runSignal.aborted) onCancel();
3221
- else runSignal.addEventListener("abort", onCancel, { once: true });
3535
+ let timeout;
3536
+ let persistence;
3537
+ let runSignal;
3538
+ let onCancel;
3222
3539
  try {
3540
+ timeout = ms !== void 0 && Number.isFinite(ms) && ms > 0 && ms <= 2147483647 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
3541
+ timeout?.start();
3542
+ budget?.start();
3543
+ runSignal = this.#fold(workflow, signal, budget, timeout);
3544
+ persistence = store === void 0 ? void 0 : new WorkflowPersistence(workflow, store);
3545
+ onCancel = this.#abortActive.bind(this, holder, runSignal);
3546
+ if (runSignal.aborted) onCancel();
3547
+ else runSignal.addEventListener("abort", onCancel, { once: true });
3223
3548
  if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
3224
3549
  if (this.#stoppable(workflow)) workflow.stop();
3225
3550
  this.#skipFrom(workflow.phases.phases(), 0);
@@ -3253,11 +3578,7 @@ var WorkflowRunner = class WorkflowRunner {
3253
3578
  }
3254
3579
  index += 1;
3255
3580
  const remaining = workflow.phases.phases();
3256
- if (index < remaining.length && !this.#cancelled(runSignal)) try {
3257
- await this.#scheduler.yield({ signal: runSignal });
3258
- } catch (error) {
3259
- if (!runSignal.aborted) throw error;
3260
- }
3581
+ if (index < remaining.length && !this.#cancelled(runSignal)) await this.#pace(runSignal);
3261
3582
  }
3262
3583
  if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
3263
3584
  else if (this.#completable(workflow)) workflow.complete();
@@ -3277,7 +3598,14 @@ var WorkflowRunner = class WorkflowRunner {
3277
3598
  } finally {
3278
3599
  persistence?.detach();
3279
3600
  timeout?.clear();
3280
- runSignal.removeEventListener("abort", onCancel);
3601
+ if (runSignal !== void 0 && onCancel !== void 0) runSignal.removeEventListener("abort", onCancel);
3602
+ }
3603
+ }
3604
+ async #pace(signal) {
3605
+ try {
3606
+ await this.#scheduler.yield({ signal });
3607
+ } catch (error) {
3608
+ if (!signal.aborted) throw error;
3281
3609
  }
3282
3610
  }
3283
3611
  async #runPhase(workflow, phase, runSignal, holder, persistence) {
@@ -3305,8 +3633,11 @@ var WorkflowRunner = class WorkflowRunner {
3305
3633
  } catch {
3306
3634
  return !this.#cancelled(runSignal);
3307
3635
  } finally {
3308
- created.destroy();
3309
- holder.runner = void 0;
3636
+ try {
3637
+ await created.destroy();
3638
+ } finally {
3639
+ holder.runner = void 0;
3640
+ }
3310
3641
  }
3311
3642
  } finally {
3312
3643
  phase.emitter.off("add", onAdd);
@@ -3517,11 +3848,11 @@ var WorkflowRunner = class WorkflowRunner {
3517
3848
  if (timeout !== void 0) signals.push(timeout.signal);
3518
3849
  return AbortSignal.any(signals);
3519
3850
  }
3520
- #fold(workflow, options, timeout) {
3851
+ #fold(workflow, signal, budget, timeout) {
3521
3852
  const signals = [workflow.signal];
3522
- if (options?.signal !== void 0) signals.push(options.signal);
3853
+ if (signal !== void 0) signals.push(signal);
3523
3854
  if (timeout !== void 0) signals.push(timeout.signal);
3524
- if (options?.budget !== void 0) signals.push(options.budget.signal);
3855
+ if (budget !== void 0) signals.push(budget.signal);
3525
3856
  return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
3526
3857
  }
3527
3858
  #haltFrom(phases, index, workflow, runSignal) {
@@ -3584,7 +3915,7 @@ var WorkflowRunner = class WorkflowRunner {
3584
3915
  *
3585
3916
  * @example
3586
3917
  * ```ts
3587
- * import { createWorkflowContract } from '@src/core'
3918
+ * import { createWorkflowContract } from '@orkestrel/workflow'
3588
3919
  *
3589
3920
  * const contract = createWorkflowContract()
3590
3921
  * const definition = contract.generate() // a valid WorkflowDefinition
@@ -3622,7 +3953,7 @@ function createWorkflowContract() {
3622
3953
  *
3623
3954
  * @example
3624
3955
  * ```ts
3625
- * import { createWorkflow } from '@src/core'
3956
+ * import { createWorkflow } from '@orkestrel/workflow'
3626
3957
  *
3627
3958
  * const workflow = createWorkflow(definition, { on: { complete: () => done() } })
3628
3959
  * const phase = workflow.phase('phase-build')
@@ -3630,7 +3961,8 @@ function createWorkflowContract() {
3630
3961
  * ```
3631
3962
  */
3632
3963
  function createWorkflow(definition, options) {
3633
- return new Workflow(definitionToSnapshot(definition, options?.bail ?? definition.bail ?? false), options);
3964
+ const captured = captureWorkflowOptions(options);
3965
+ return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
3634
3966
  }
3635
3967
  /**
3636
3968
  * Rebuild an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
@@ -3658,27 +3990,35 @@ function createWorkflow(definition, options) {
3658
3990
  *
3659
3991
  * @example
3660
3992
  * ```ts
3661
- * import { restoreWorkflow } from '@src/core'
3993
+ * import { restoreWorkflow } from '@orkestrel/workflow'
3662
3994
  *
3663
3995
  * const restored = restoreWorkflow(workflow.snapshot()) // bail comes from the snapshot
3664
3996
  * restored.status === workflow.status // true
3665
3997
  * ```
3666
3998
  */
3667
3999
  function restoreWorkflow(snapshot, options) {
3668
- return new Workflow(cloneWorkflowSnapshot(snapshot), options);
4000
+ const captured = captureWorkflowOptions(options);
4001
+ return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
3669
4002
  }
3670
4003
  /**
3671
4004
  * Rebuild an interrupted workflow at its remaining retry budget.
3672
4005
  *
4006
+ * @remarks
4007
+ * Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
4008
+ * validates those live tasks' captured callable handlers without rereading the registry, while the
4009
+ * retained registry identity remains available to resolve future live additions at their mint time.
4010
+ *
3673
4011
  * @param snapshot - The hostile persisted snapshot
3674
4012
  * @param options - Runtime handlers and entity options
3675
4013
  * @returns A recoverable live workflow
3676
4014
  */
3677
4015
  function recoverWorkflow(snapshot, options) {
4016
+ const captured = captureWorkflowOptions(options);
3678
4017
  const owned = cloneWorkflowSnapshot(snapshot);
3679
4018
  if (owned.override !== void 0 || owned.phases.some((phase) => phase.override !== void 0)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has a terminal override`, { workflow: owned.id });
3680
- if (!hasWorkflowHandlers(owned, options?.functions)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved run`, { workflow: owned.id });
3681
- return new Workflow(cloneWorkflowSnapshot(recoverWorkflowSnapshot(owned)), options);
4019
+ const workflow = new Workflow(cloneWorkflowSnapshot(recoverWorkflowSnapshot(owned)), captured);
4020
+ if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved run`, { workflow: owned.id });
4021
+ return workflow;
3682
4022
  }
3683
4023
  /**
3684
4024
  * Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
@@ -3710,7 +4050,7 @@ function assertSnapshot(snapshot) {
3710
4050
  *
3711
4051
  * @remarks
3712
4052
  * The snapshot analogue of the server package's `createMemorySessionStore`
3713
- * (and the {@link createMemoryQueueStore} family), but LEANER — there is no idle-TTL, so no
4053
+ * (and the `createMemoryQueueStore` family), but LEANER — there is no idle-TTL, so no
3714
4054
  * options bag (AGENTS §21 minimal): a persisted run-state lives until an explicit `delete`. This is
3715
4055
  * the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
3716
4056
  * {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
@@ -3722,7 +4062,7 @@ function assertSnapshot(snapshot) {
3722
4062
  *
3723
4063
  * @example
3724
4064
  * ```ts
3725
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@src/core'
4065
+ * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
3726
4066
  *
3727
4067
  * const store = createMemoryWorkflowStore()
3728
4068
  * const workflow = createWorkflow(definition)
@@ -3742,7 +4082,7 @@ function createMemoryWorkflowStore() {
3742
4082
  * @remarks
3743
4083
  * Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver, the snapshot
3744
4084
  * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
3745
- * `rawShape` (a JSON blob), exactly as {@link createDatabaseQueueStore} stores its `input`. The
4085
+ * `rawShape` (a JSON blob), exactly as `createDatabaseQueueStore` stores its `input`. The
3746
4086
  * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
3747
4087
  * AND keeps the row type FLAT — a structured multi-column snapshot table would force the contract to
3748
4088
  * `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
@@ -3758,7 +4098,8 @@ function createMemoryWorkflowStore() {
3758
4098
  *
3759
4099
  * @example
3760
4100
  * ```ts
3761
- * import { createDatabaseWorkflowStore, createMemoryDriver, createWorkflow, restoreWorkflow } from '@src/core'
4101
+ * import { createMemoryDriver } from '@orkestrel/database'
4102
+ * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
3762
4103
  *
3763
4104
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
3764
4105
  * const workflow = createWorkflow(definition)
@@ -3809,7 +4150,7 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
3809
4150
  *
3810
4151
  * @example
3811
4152
  * ```ts
3812
- * import { createWorkflowRunner } from '@src/core'
4153
+ * import { createWorkflowRunner } from '@orkestrel/workflow'
3813
4154
  *
3814
4155
  * const runner = createWorkflowRunner()
3815
4156
  * const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
@@ -3845,7 +4186,7 @@ function createWorkflowRunner(options) {
3845
4186
  *
3846
4187
  * @example
3847
4188
  * ```ts
3848
- * import { createMemoryWorkflowStore, createWorkflowManager } from '@src/core'
4189
+ * import { createMemoryWorkflowStore, createWorkflowManager } from '@orkestrel/workflow'
3849
4190
  *
3850
4191
  * const manager = createWorkflowManager({
3851
4192
  * store: createMemoryWorkflowStore(),
@@ -3868,7 +4209,8 @@ function createWorkflowManager(options) {
3868
4209
  * `yield()` gives the host a turn via a zero-delay macrotask (so pending I/O,
3869
4210
  * timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
3870
4211
  * after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
3871
- * with the signal's `reason` on abort (with full timer/listener cleanup).
4212
+ * with the signal's exact `reason`; the shared owned-signal lifecycle clears the timer
4213
+ * without invoking caller-owned listener methods.
3872
4214
  * `options.priority` is accepted for contract compliance but treated uniformly by
3873
4215
  * this default — environment backends honour it.
3874
4216
  *
@@ -3876,7 +4218,8 @@ function createWorkflowManager(options) {
3876
4218
  *
3877
4219
  * @example
3878
4220
  * ```ts
3879
- * import { createAbort, createScheduler } from '@src/core'
4221
+ * import { createAbort } from '@orkestrel/abort'
4222
+ * import { createScheduler } from '@orkestrel/workflow'
3880
4223
  *
3881
4224
  * const abort = createAbort()
3882
4225
  * const scheduler = createScheduler()
@@ -3890,7 +4233,7 @@ function createWorkflowManager(options) {
3890
4233
  *
3891
4234
  * @example
3892
4235
  * ```ts
3893
- * import { createScheduler } from '@src/core'
4236
+ * import { createScheduler } from '@orkestrel/workflow'
3894
4237
  *
3895
4238
  * // A backoff: wait a growing interval between retries.
3896
4239
  * const scheduler = createScheduler()
@@ -3932,7 +4275,7 @@ function createScheduler() {
3932
4275
  *
3933
4276
  * @example
3934
4277
  * ```ts
3935
- * import { createRunner } from '@src/core'
4278
+ * import { createRunner } from '@orkestrel/workflow'
3936
4279
  *
3937
4280
  * // A handler that fans out one sibling per declared unit, then returns its own value.
3938
4281
  * const runner = createRunner<number, number>({
@@ -3979,6 +4322,7 @@ exports.buildPhaseContext = buildPhaseContext;
3979
4322
  exports.buildTaskContext = buildTaskContext;
3980
4323
  exports.buildWorkflowContext = buildWorkflowContext;
3981
4324
  exports.canTransitionTask = canTransitionTask;
4325
+ exports.captureWorkflowOptions = captureWorkflowOptions;
3982
4326
  exports.cloneTaskActivity = cloneTaskActivity;
3983
4327
  exports.cloneWorkflowSnapshot = cloneWorkflowSnapshot;
3984
4328
  exports.collectResults = collectResults;
@@ -4019,6 +4363,7 @@ exports.recoverWorkflow = recoverWorkflow;
4019
4363
  exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
4020
4364
  exports.resolveTaskSilence = resolveTaskSilence;
4021
4365
  exports.restoreWorkflow = restoreWorkflow;
4366
+ exports.scheduleHost = scheduleHost;
4022
4367
  exports.success = success;
4023
4368
  exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
4024
4369
  exports.taskShape = taskShape;