@orkestrel/workflow 0.0.7 → 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;
@@ -179,52 +105,47 @@ var TASK_TRANSITIONS = Object.freeze({
179
105
  * phase may still DECLARE a larger explicit `concurrency` and pays that allocation knowingly.
180
106
  */
181
107
  var DEFAULT_PHASE_CONCURRENCY = 1024;
108
+ /**
109
+ * The largest delay representable by the host timer APIs without overflow or clamping.
110
+ */
111
+ var MAX_TIMER_MS = 2147483647;
182
112
  //#endregion
183
- //#region src/core/errors.ts
113
+ //#region src/core/helpers.ts
184
114
  /**
185
- * An error thrown by the workflow entity + W-c2 recursion layer.
115
+ * Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
186
116
  *
187
117
  * @remarks
188
- * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
189
- * offending node id / status. Thrown for an illegal lifecycle transition
190
- * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
191
- * passed to {@link import('./factories.js').restoreWorkflow} (`RESTORE`), an over-deep /
192
- * cyclic nested-workflow dispatch (`DEPTH`), and a malformed workflow-authoring-tool args
193
- * blob (`TOOL`). `DEPTH` and `TOOL` are public type surface constructed by the
194
- * `@orkestrel/tool` package's workflow-tool / agent-function adapters; on that seam the
195
- * throw is ISOLATED by its `ToolManager` into the tool result's top-level `error`
196
- * (AGENTS §14 — the universal tool-handler contract).
197
- */
198
- var WorkflowError = class extends Error {
199
- code;
200
- context;
201
- constructor(code, message, context) {
202
- super(message);
203
- this.name = "WorkflowError";
204
- this.code = code;
205
- if (context !== void 0) this.context = context;
206
- }
207
- };
208
- /**
209
- * 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.
210
123
  *
211
- * @param value - The value to test (typically a `catch` binding)
212
- * @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
213
126
  *
214
127
  * @example
215
128
  * ```ts
216
- * try {
217
- * task.complete('done')
218
- * } catch (error) {
219
- * if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
220
- * }
129
+ * const captured = captureWorkflowOptions(options)
130
+ * const workflow = createWorkflow(definition, captured)
221
131
  * ```
222
132
  */
223
- function isWorkflowError(value) {
224
- return value instanceof WorkflowError;
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
+ });
225
148
  }
226
- //#endregion
227
- //#region src/core/helpers.ts
228
149
  /**
229
150
  * Test whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
230
151
  * transition further.
@@ -358,6 +279,17 @@ function canTransitionTask(from, to) {
358
279
  return TASK_TRANSITIONS[from].includes(to);
359
280
  }
360
281
  /**
282
+ * Resolve a task's runtime silence window against its workflow default.
283
+ *
284
+ * @param value - The task-level override; any present non-positive or non-finite value disables
285
+ * @param fallback - The workflow-level default
286
+ * @returns A host-safe effective window (`1..MAX_TIMER_MS`), or `undefined`
287
+ */
288
+ function resolveTaskSilence(value, fallback) {
289
+ if (value !== void 0) return Number.isFinite(value) && value > 0 && value <= 2147483647 ? value : void 0;
290
+ return fallback !== void 0 && Number.isFinite(fallback) && fallback > 0 && fallback <= 2147483647 ? fallback : void 0;
291
+ }
292
+ /**
361
293
  * Box a value as a {@link Success} — the graceful outcome half of a {@link Result}.
362
294
  *
363
295
  * @typeParam T - The boxed value's type
@@ -394,6 +326,20 @@ function failure(error) {
394
326
  };
395
327
  }
396
328
  /**
329
+ * Normalize an unknown thrown value to a non-empty persistence-safe message.
330
+ *
331
+ * @param error - The caught value
332
+ * @returns A non-empty message without stack or cause data
333
+ */
334
+ function errorToMessage(error) {
335
+ try {
336
+ const message = error instanceof Error ? error.message : String(error);
337
+ return typeof message === "string" && message.length > 0 ? message : "unknown failure";
338
+ } catch {
339
+ return "unknown failure";
340
+ }
341
+ }
342
+ /**
397
343
  * Find the first {@link TaskResult} in a positional list whose boxed outcome is a
398
344
  * `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
399
345
  * `fail`-event lookup.
@@ -430,11 +376,11 @@ function findFailure(results) {
430
376
  * @returns The {@link WorkflowContext}
431
377
  */
432
378
  function buildWorkflowContext(node) {
433
- return {
379
+ return Object.freeze({
434
380
  id: node.id,
435
381
  name: node.name,
436
382
  ...node.description === void 0 ? {} : { description: node.description }
437
- };
383
+ });
438
384
  }
439
385
  /**
440
386
  * Build a {@link PhaseContext} — a phase's own identity plus a back-reference to its
@@ -445,10 +391,10 @@ function buildWorkflowContext(node) {
445
391
  * @returns The {@link PhaseContext}
446
392
  */
447
393
  function buildPhaseContext(workflow, node) {
448
- return {
394
+ return Object.freeze({
449
395
  ...buildWorkflowContext(node),
450
- workflow
451
- };
396
+ workflow: buildWorkflowContext(workflow)
397
+ });
452
398
  }
453
399
  /**
454
400
  * Build a {@link TaskContext} — a task's own identity plus a back-reference to its phase
@@ -460,31 +406,10 @@ function buildPhaseContext(workflow, node) {
460
406
  * @returns The {@link TaskContext}
461
407
  */
462
408
  function buildTaskContext(phase, node) {
463
- return {
409
+ return Object.freeze({
464
410
  ...buildWorkflowContext(node),
465
- phase
466
- };
467
- }
468
- /**
469
- * Narrow an `unknown` to a {@link WorkflowSnapshot} — the AGENTS §14 boundary guard for an
470
- * UNTRUSTED snapshot read (a storage row a {@link import('./stores/DatabaseWorkflowStore.js').DatabaseWorkflowStore}
471
- * reads back from its opaque JSON column, a snapshot loaded from disk).
472
- *
473
- * @remarks
474
- * A total guard (it NEVER throws — adversarial input returns `false`, AGENTS §14). It checks the
475
- * snapshot's SHAPE — `id` / `name` / `status` strings, a `boolean` `bail`, an array of `phases`,
476
- * `created` / `updated` numbers — enough to safely impose the {@link WorkflowSnapshot} type at a
477
- * storage boundary WITHOUT a cast. It is complementary to
478
- * {@link import('./factories.js').assertSnapshot}, which validates the DEEPER invariant (every
479
- * node's status / override drawn from the lifecycle vocabulary) and THROWS a `RESTORE`
480
- * {@link import('./errors.js').WorkflowError} — the deep gate a {@link import('./factories.js').restoreWorkflow}
481
- * applies. A boundary read narrows shape with this guard; a restore validates vocabulary with `assertSnapshot`.
482
- *
483
- * @param value - The value to test (an opaque storage read)
484
- * @returns `true` when `value` has the structural shape of a {@link WorkflowSnapshot}
485
- */
486
- function isWorkflowSnapshot(value) {
487
- return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.id) && (0, _orkestrel_contract.isString)(value.name) && (0, _orkestrel_contract.isString)(value.status) && (0, _orkestrel_contract.isBoolean)(value.bail) && (0, _orkestrel_contract.isArray)(value.phases) && (0, _orkestrel_contract.isNumber)(value.created) && (0, _orkestrel_contract.isNumber)(value.updated);
411
+ phase: buildPhaseContext(phase.workflow, phase)
412
+ });
488
413
  }
489
414
  /**
490
415
  * Convert a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
@@ -574,12 +499,91 @@ function taskDefinitionToSnapshot(task) {
574
499
  ...task.description === void 0 ? {} : { description: task.description },
575
500
  status: "pending",
576
501
  metadata: {},
502
+ attempts: 0,
577
503
  ...task.run === void 0 ? {} : { run: task.run },
578
504
  ...task.retries === void 0 ? {} : { retries: task.retries },
579
505
  ...task.timeout === void 0 ? {} : { timeout: task.timeout }
580
506
  };
581
507
  }
582
508
  /**
509
+ * Convert interrupted running work into a recoverable pending suffix or an
510
+ * exhausted recovery failure without replenishing attempts.
511
+ *
512
+ * @param snapshot - A fully validated owned snapshot with no terminal overrides
513
+ * @returns The recovery projection
514
+ */
515
+ function recoverWorkflowSnapshot(snapshot) {
516
+ const phases = [];
517
+ let halted = false;
518
+ const now = Math.max(Date.now(), snapshot.updated);
519
+ const workflow = buildWorkflowContext(snapshot);
520
+ for (const phase of snapshot.phases) {
521
+ const exhausted = /* @__PURE__ */ new Set();
522
+ for (const task of phase.tasks) {
523
+ const budget = (task.retries ?? 0) + 1;
524
+ if (task.status === "running" && task.attempts >= budget) exhausted.add(task.id);
525
+ }
526
+ const strict = phase.bail && (exhausted.size > 0 || phase.tasks.some((task) => task.status === "failed"));
527
+ const tasks = [];
528
+ for (const task of phase.tasks) {
529
+ const eligible = task.status === "pending" || task.status === "running";
530
+ if ((halted || strict) && eligible && !exhausted.has(task.id)) {
531
+ tasks.push({
532
+ ...task,
533
+ status: "skipped"
534
+ });
535
+ continue;
536
+ }
537
+ if (!exhausted.has(task.id)) {
538
+ if (task.status === "running") {
539
+ const { activity: _activity, ...pending } = task;
540
+ tasks.push({
541
+ ...pending,
542
+ status: "pending"
543
+ });
544
+ } else tasks.push(task);
545
+ continue;
546
+ }
547
+ const phaseContext = buildPhaseContext(workflow, phase);
548
+ const result = {
549
+ task: buildTaskContext(phaseContext, task),
550
+ phase: phaseContext,
551
+ workflow,
552
+ status: "failed",
553
+ result: {
554
+ success: false,
555
+ error: {
556
+ origin: "recovery",
557
+ message: `task '${task.id}' exhausted its retry budget during recovery`
558
+ }
559
+ },
560
+ timestamp: now
561
+ };
562
+ tasks.push({
563
+ ...task,
564
+ status: "failed",
565
+ result
566
+ });
567
+ }
568
+ const status = derivePhaseStatus(tasks.map((task) => task.status));
569
+ phases.push({
570
+ ...phase,
571
+ status,
572
+ tasks
573
+ });
574
+ if (strict) halted = true;
575
+ }
576
+ return {
577
+ ...snapshot,
578
+ status: deriveWorkflowStatus(phases.map((phase) => ({
579
+ status: phase.status,
580
+ bail: phase.bail
581
+ }))),
582
+ phases,
583
+ updated: now
584
+ };
585
+ }
586
+ /**
583
587
  * Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
584
588
  * — the workflow tier of the result tree, built from each phase's `results()`.
585
589
  *
@@ -665,6 +669,61 @@ function createDeferred() {
665
669
  return Promise.withResolvers();
666
670
  }
667
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
+ /**
668
727
  * Park until `signal` aborts — a promise-parked wait (AGENTS §21), never a timer or
669
728
  * busy-loop, that NEVER rejects.
670
729
  *
@@ -692,6 +751,424 @@ function parkSignal(signal) {
692
751
  });
693
752
  }
694
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
861
+ //#region src/core/validators.ts
862
+ /** Test the workflow lifecycle vocabulary. */
863
+ function isLifecycleStatus(value) {
864
+ return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
865
+ }
866
+ /** Test a normalized persisted task failure. */
867
+ function isTaskFailure(value) {
868
+ try {
869
+ return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
870
+ } catch {
871
+ return false;
872
+ }
873
+ }
874
+ /** Compare two optional description values. */
875
+ function matchesDescription(left, right) {
876
+ return left === right && (left === void 0 || typeof left === "string");
877
+ }
878
+ /** Test a result's lineage against its containing snapshot nodes. */
879
+ function isTaskResult(value, workflow, phase, task) {
880
+ try {
881
+ if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.isRecord)(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.isRecord)(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
882
+ if (value.task.id !== task.id || value.task.name !== task.name || !matchesDescription(value.task.description, task.description) || value.phase.id !== phase.id || value.phase.name !== phase.name || !matchesDescription(value.phase.description, phase.description) || value.workflow.id !== workflow.id || value.workflow.name !== workflow.name || !matchesDescription(value.workflow.description, workflow.description)) return false;
883
+ if (!(0, _orkestrel_contract.isRecord)(value.task.phase) || !(0, _orkestrel_contract.isRecord)(value.task.phase.workflow) || !(0, _orkestrel_contract.isRecord)(value.phase.workflow) || !Object.keys(value.task.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task.phase.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase.workflow).every((key) => key === "id" || key === "name" || key === "description")) return false;
884
+ if (value.task.phase.id !== phase.id || value.task.phase.name !== phase.name || !matchesDescription(value.task.phase.description, phase.description) || value.phase.workflow.id !== workflow.id || value.phase.workflow.name !== workflow.name || !matchesDescription(value.phase.workflow.description, workflow.description) || value.task.phase.workflow.id !== workflow.id || value.task.phase.workflow.name !== workflow.name || !matchesDescription(value.task.phase.workflow.description, workflow.description)) return false;
885
+ if (value.status === "completed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && (0, _orkestrel_contract.isJSONValue)(value.result.value);
886
+ if (value.status === "failed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
887
+ return false;
888
+ } catch {
889
+ return false;
890
+ }
891
+ }
892
+ /**
893
+ * Validate a safe owned JSON graph as a coherent workflow snapshot.
894
+ *
895
+ * @remarks
896
+ * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
897
+ * graph first so this semantic pass never observes accessors or prototypes.
898
+ */
899
+ function isOwnedWorkflowSnapshot(value) {
900
+ try {
901
+ if (!(0, _orkestrel_contract.isRecord)(value) || !Object.keys(value).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "phases" || key === "created" || key === "updated") || !(0, _orkestrel_contract.isNonEmptyString)(value.id) || !(0, _orkestrel_contract.isNonEmptyString)(value.name) || value.description !== void 0 && typeof value.description !== "string" || !isLifecycleStatus(value.status) || value.override !== void 0 && value.override !== "completed" && value.override !== "skipped" && value.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(value.bail) || !(0, _orkestrel_contract.isArray)(value.phases) || !(0, _orkestrel_contract.isFiniteNumber)(value.created) || value.created < 0 || !(0, _orkestrel_contract.isFiniteNumber)(value.updated) || value.updated < value.created) return false;
902
+ const phaseIds = /* @__PURE__ */ new Set();
903
+ const derivations = [];
904
+ let frontier = false;
905
+ let running = false;
906
+ let vacuous = true;
907
+ for (const phase of value.phases) {
908
+ if (!(0, _orkestrel_contract.isRecord)(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !(0, _orkestrel_contract.isNonEmptyString)(phase.id) || phaseIds.has(phase.id) || !(0, _orkestrel_contract.isNonEmptyString)(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return false;
909
+ const forced = phase.override === "skipped" || phase.override === "stopped";
910
+ const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
911
+ if (!forced && frontier && started || phase.status === "running" && running) return false;
912
+ if (phase.status === "running") running = true;
913
+ if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
914
+ phaseIds.add(phase.id);
915
+ const taskIds = /* @__PURE__ */ new Set();
916
+ const statuses = [];
917
+ if (phase.tasks.length > 0) vacuous = false;
918
+ for (const task of phase.tasks) {
919
+ if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "run" || key === "retries" || key === "timeout" || key === "activity") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
920
+ const budget = (task.retries ?? 0) + 1;
921
+ if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
922
+ if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
923
+ if (task.status === "running" || task.status === "completed" || task.status === "failed") {
924
+ if (task.attempts < 1 || task.activity === void 0) return false;
925
+ }
926
+ if (task.status === "pending" && task.activity !== void 0) return false;
927
+ if (task.status === "completed" || task.status === "failed") {
928
+ if (!isTaskResult(task.result, value, phase, task)) return false;
929
+ } else if (task.result !== void 0) return false;
930
+ taskIds.add(task.id);
931
+ statuses.push(task.status);
932
+ }
933
+ const derived = derivePhaseStatus(statuses);
934
+ if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
935
+ derivations.push({
936
+ status: phase.status,
937
+ bail: phase.bail
938
+ });
939
+ }
940
+ const derived = deriveWorkflowStatus(derivations);
941
+ if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
942
+ return value.status === (value.override ?? derived);
943
+ } catch {
944
+ return false;
945
+ }
946
+ }
947
+ /** Total hostile-boundary workflow snapshot guard. */
948
+ function isWorkflowSnapshot(value) {
949
+ const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
950
+ return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
951
+ }
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
+ }
963
+ return true;
964
+ }
965
+ /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
966
+ function workflowSnapshotContext(value) {
967
+ if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
968
+ for (const phase of value.phases) {
969
+ if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
970
+ const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
971
+ if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
972
+ for (const task of phase.tasks) {
973
+ if (!(0, _orkestrel_contract.isRecord)(task)) continue;
974
+ if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
975
+ ...phaseContext ?? {},
976
+ ...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
977
+ };
978
+ }
979
+ }
980
+ }
981
+ /**
982
+ * Test whether an unknown value is a valid whole-frame activity report.
983
+ */
984
+ function isTaskActivityInput(value) {
985
+ try {
986
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
987
+ const prototype = Object.getPrototypeOf(value);
988
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
989
+ const note = value.note;
990
+ const progress = value.progress;
991
+ const operations = value.operations;
992
+ const constraints = value.constraints;
993
+ if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
994
+ if (progress !== void 0) {
995
+ if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
996
+ const progressPrototype = Object.getPrototypeOf(progress);
997
+ if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
998
+ const current = progress.current;
999
+ const total = progress.total;
1000
+ const unit = progress.unit;
1001
+ if (!(0, _orkestrel_contract.isFiniteNumber)(current) || current < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < current) || unit !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(unit)) return false;
1002
+ }
1003
+ if (operations !== void 0) {
1004
+ if (!(0, _orkestrel_contract.isArray)(operations)) return false;
1005
+ const ids = /* @__PURE__ */ new Set();
1006
+ for (const operation of operations) {
1007
+ if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
1008
+ const operationPrototype = Object.getPrototypeOf(operation);
1009
+ if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
1010
+ const id = operation.id;
1011
+ const name = operation.name;
1012
+ const started = operation.started;
1013
+ if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
1014
+ ids.add(id);
1015
+ }
1016
+ }
1017
+ if (constraints !== void 0) {
1018
+ if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
1019
+ const ids = /* @__PURE__ */ new Set();
1020
+ for (const constraint of constraints) {
1021
+ if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
1022
+ const constraintPrototype = Object.getPrototypeOf(constraint);
1023
+ if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
1024
+ const id = constraint.id;
1025
+ const name = constraint.name;
1026
+ const started = constraint.started;
1027
+ if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
1028
+ ids.add(id);
1029
+ }
1030
+ }
1031
+ return true;
1032
+ } catch {
1033
+ return false;
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Test whether an unknown value is valid persisted task activity.
1038
+ */
1039
+ function isTaskActivity(value) {
1040
+ try {
1041
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
1042
+ const prototype = Object.getPrototypeOf(value);
1043
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
1044
+ const note = value.note;
1045
+ const progress = value.progress;
1046
+ const operations = value.operations;
1047
+ const constraints = value.constraints;
1048
+ const updated = value.updated;
1049
+ if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
1050
+ return isTaskActivityInput({
1051
+ ...note === void 0 ? {} : { note },
1052
+ ...progress === void 0 ? {} : { progress },
1053
+ operations,
1054
+ constraints
1055
+ });
1056
+ } catch {
1057
+ return false;
1058
+ }
1059
+ }
1060
+ //#endregion
1061
+ //#region src/core/cloners.ts
1062
+ /**
1063
+ * Validate and own a workflow snapshot before live construction.
1064
+ *
1065
+ * @param input - The hostile snapshot boundary
1066
+ * @param id - The optional storage key the owned snapshot must match
1067
+ * @returns A deeply owned frozen snapshot
1068
+ * @throws {WorkflowError} With `RESTORE` when the snapshot is invalid or does not match `id`
1069
+ */
1070
+ function cloneWorkflowSnapshot(input, id) {
1071
+ let cloned;
1072
+ try {
1073
+ cloned = (0, _orkestrel_contract.cloneJSONValue)(input);
1074
+ } catch (error) {
1075
+ if (isWorkflowError(error)) throw error;
1076
+ if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
1077
+ throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
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;
1085
+ }
1086
+ /**
1087
+ * Validate and clone one complete task activity frame.
1088
+ *
1089
+ * @remarks
1090
+ * This is the hostile boundary behind task reports and snapshot hydration. Supplying
1091
+ * `updated` stamps an input frame without reading an `updated` property from it; omitting
1092
+ * `updated` restores a stored frame and reads its persisted timestamp exactly once. Every
1093
+ * untrusted property is captured once inside one protected boundary. The returned frame,
1094
+ * collections, progress, operations, and constraints are copied and frozen.
1095
+ *
1096
+ * @param input - The untrusted complete activity frame
1097
+ * @param updated - An optional accepted timestamp used instead of a persisted `updated`
1098
+ * @returns An immutable cloned {@link TaskActivity}
1099
+ * @throws {WorkflowError} With `MUTATION` when the frame cannot be read or validated
1100
+ */
1101
+ function cloneTaskActivity(input, updated) {
1102
+ try {
1103
+ if (!(0, _orkestrel_contract.isRecord)(input)) throw new WorkflowError("MUTATION", "task activity must be a record");
1104
+ const inputPrototype = Object.getPrototypeOf(input);
1105
+ if (inputPrototype !== Object.prototype && inputPrototype !== null || !Object.keys(input).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || updated === void 0 && key === "updated")) throw new WorkflowError("MUTATION", "task activity must be a record");
1106
+ const note = input.note;
1107
+ const progressInput = input.progress;
1108
+ const operationsInput = input.operations;
1109
+ const constraintsInput = input.constraints;
1110
+ const accepted = updated === void 0 ? input.updated : updated;
1111
+ const operationInputs = operationsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(operationsInput) ? [...operationsInput] : void 0;
1112
+ if (operationInputs === void 0) throw new WorkflowError("MUTATION", "task activity operations must be an array");
1113
+ const operations = [];
1114
+ for (const operation of operationInputs) {
1115
+ if (!(0, _orkestrel_contract.isRecord)(operation)) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
1116
+ const operationPrototype = Object.getPrototypeOf(operation);
1117
+ if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
1118
+ const id = operation.id;
1119
+ const name = operation.name;
1120
+ const started = operation.started;
1121
+ operations.push(Object.freeze({
1122
+ id,
1123
+ name,
1124
+ started
1125
+ }));
1126
+ }
1127
+ let progress;
1128
+ if (progressInput !== void 0) {
1129
+ if (!(0, _orkestrel_contract.isRecord)(progressInput)) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
1130
+ const progressPrototype = Object.getPrototypeOf(progressInput);
1131
+ if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progressInput).every((key) => key === "current" || key === "total" || key === "unit")) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
1132
+ const current = progressInput.current;
1133
+ const total = progressInput.total;
1134
+ const unit = progressInput.unit;
1135
+ progress = Object.freeze({
1136
+ current,
1137
+ ...total === void 0 ? {} : { total },
1138
+ ...unit === void 0 ? {} : { unit }
1139
+ });
1140
+ }
1141
+ const constraintInputs = constraintsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(constraintsInput) ? [...constraintsInput] : void 0;
1142
+ if (constraintInputs === void 0) throw new WorkflowError("MUTATION", "task activity constraints must be an array");
1143
+ const constraints = [];
1144
+ for (const constraint of constraintInputs) {
1145
+ if (!(0, _orkestrel_contract.isRecord)(constraint)) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
1146
+ const constraintPrototype = Object.getPrototypeOf(constraint);
1147
+ if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
1148
+ const id = constraint.id;
1149
+ const name = constraint.name;
1150
+ const started = constraint.started;
1151
+ constraints.push(Object.freeze({
1152
+ id,
1153
+ name,
1154
+ started
1155
+ }));
1156
+ }
1157
+ const activity = Object.freeze({
1158
+ ...note === void 0 ? {} : { note },
1159
+ ...progress === void 0 ? {} : { progress },
1160
+ operations: Object.freeze(operations),
1161
+ constraints: Object.freeze(constraints),
1162
+ updated: accepted
1163
+ });
1164
+ if (!isTaskActivity(activity)) throw new WorkflowError("MUTATION", "task activity is invalid");
1165
+ return activity;
1166
+ } catch (error) {
1167
+ if (isWorkflowError(error)) throw error;
1168
+ throw new WorkflowError("MUTATION", "task activity could not be read safely");
1169
+ }
1170
+ }
1171
+ //#endregion
695
1172
  //#region src/core/shapers.ts
696
1173
  /**
697
1174
  * The shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
@@ -718,6 +1195,7 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
718
1195
  })),
719
1196
  timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
720
1197
  min: 0,
1198
+ max: MAX_TIMER_MS,
721
1199
  description: "Per-attempt deadline in milliseconds; overrides the phase default. Omitted means no deadline."
722
1200
  }))
723
1201
  });
@@ -828,7 +1306,8 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
828
1306
  * the row `{ id: snapshot.id, snapshot }`.
829
1307
  * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
830
1308
  * a {@link WorkflowSnapshot} ({@link import('../helpers.js').isWorkflowSnapshot} — the AGENTS §14
831
- * 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.
832
1311
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
833
1312
  *
834
1313
  * UNLIKE the server package's `SessionStoreInterface` there is NO
@@ -839,7 +1318,8 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
839
1318
  *
840
1319
  * @example
841
1320
  * ```ts
842
- * import { createDatabaseWorkflowStore, createMemoryDriver, createWorkflow, restoreWorkflow } from '@src/core'
1321
+ * import { createMemoryDriver } from '@orkestrel/database'
1322
+ * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
843
1323
  *
844
1324
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
845
1325
  * const workflow = createWorkflow(definition)
@@ -860,17 +1340,18 @@ var DatabaseWorkflowStore = class {
860
1340
  constructor(table) {
861
1341
  this.#table = table;
862
1342
  }
863
- /** 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`. */
864
1344
  async get(id) {
865
1345
  const row = await this.#table.get(id);
866
1346
  if (row === void 0) return void 0;
867
- return isWorkflowSnapshot(row.snapshot) ? row.snapshot : void 0;
1347
+ return cloneWorkflowSnapshot(row.snapshot, id);
868
1348
  }
869
1349
  /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
870
1350
  async set(snapshot) {
1351
+ const owned = cloneWorkflowSnapshot(snapshot);
871
1352
  await this.#table.set({
872
- id: snapshot.id,
873
- snapshot
1353
+ id: owned.id,
1354
+ snapshot: owned
874
1355
  });
875
1356
  }
876
1357
  /** Drop a snapshot by id; an absent id is a no-op (no throw). */
@@ -908,7 +1389,7 @@ var DatabaseWorkflowStore = class {
908
1389
  *
909
1390
  * @example
910
1391
  * ```ts
911
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@src/core'
1392
+ * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
912
1393
  *
913
1394
  * const store = createMemoryWorkflowStore()
914
1395
  * const workflow = createWorkflow(definition)
@@ -921,10 +1402,12 @@ var DatabaseWorkflowStore = class {
921
1402
  var MemoryWorkflowStore = class {
922
1403
  #snapshots = /* @__PURE__ */ new Map();
923
1404
  get(id) {
924
- return Promise.resolve(this.#snapshots.get(id));
1405
+ const snapshot = this.#snapshots.get(id);
1406
+ return Promise.resolve(snapshot === void 0 ? void 0 : cloneWorkflowSnapshot(snapshot));
925
1407
  }
926
1408
  set(snapshot) {
927
- this.#snapshots.set(snapshot.id, snapshot);
1409
+ const owned = cloneWorkflowSnapshot(snapshot);
1410
+ this.#snapshots.set(owned.id, owned);
928
1411
  return Promise.resolve();
929
1412
  }
930
1413
  delete(id) {
@@ -947,9 +1430,8 @@ var MemoryWorkflowStore = class {
947
1430
  * `TRANSITION` {@link WorkflowError} on an illegal move (e.g. completing a non-`running`
948
1431
  * task) — the legal graph is the single source of truth, so the leaf can never reach an
949
1432
  * impossible state.
950
- * - **Override (snapshot fidelity).** `skip` / `stop` set `#override` to the forced terminal
951
- * status, so a RESTORE can tell a forced leaf (`skipped` / `stopped`) from a run-produced
952
- * one and reinstate it AS an override — preserving the round-trip.
1433
+ * - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
1434
+ * statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
953
1435
  * - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
954
1436
  * OWN event, THEN calls the parent phase's `#recompute` (injected at construction) so the
955
1437
  * transition propagates UP (Task → Phase → Workflow re-derive). The own-event-before-cascade
@@ -964,7 +1446,8 @@ var MemoryWorkflowStore = class {
964
1446
  * matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
965
1447
  * is the RUNTIME-ONLY counterpart — `run` resolved ONCE at construction against the
966
1448
  * workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
967
- * NEVER persisted; `undefined` when `run` is omitted or unregistered (the no-handler rule).
1449
+ * NEVER persisted; `undefined` when `run` is omitted or unregistered. Only omission is a
1450
+ * deliberate no-op; unresolved named work is rejected before dispatch.
968
1451
  */
969
1452
  var Task = class {
970
1453
  #context;
@@ -979,16 +1462,34 @@ var Task = class {
979
1462
  #run;
980
1463
  #retries;
981
1464
  #timeout;
1465
+ #attempts;
982
1466
  #handler;
983
- constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, handler) {
984
- this.#context = context;
1467
+ #abort;
1468
+ #silence;
1469
+ #onSilence;
1470
+ #liveness;
1471
+ #activity;
1472
+ #paused;
1473
+ #gate;
1474
+ #timerSignal;
1475
+ constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
1476
+ this.#context = buildTaskContext(context.phase, context);
985
1477
  this.#phase = phase;
986
1478
  this.#workflow = workflow;
987
1479
  this.#recompute = recompute;
988
- this.#metadata = options?.metadata ?? {};
1480
+ try {
1481
+ const metadataOption = options?.metadata;
1482
+ this.#metadata = (0, _orkestrel_contract.cloneJSONRecord)(metadataOption ?? metadata);
1483
+ } catch (error) {
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 });
1485
+ throw new WorkflowError("RESTORE", `task '${context.id}' metadata could not be read safely`, { task: context.id });
1486
+ }
1487
+ const on = options?.on;
1488
+ const listenerError = options?.error;
1489
+ const silenceOption = options?.silence;
989
1490
  this.#emitter = new _orkestrel_emitter.Emitter({
990
- ...options?.on === void 0 ? {} : { on: options.on },
991
- ...options?.error === void 0 ? {} : { error: options.error }
1491
+ ...on === void 0 ? {} : { on },
1492
+ ...listenerError === void 0 ? {} : { error: listenerError }
992
1493
  });
993
1494
  this.#status = status;
994
1495
  this.#result = result;
@@ -1000,7 +1501,19 @@ var Task = class {
1000
1501
  this.#run = run;
1001
1502
  this.#retries = retries;
1002
1503
  this.#timeout = timeout;
1504
+ this.#attempts = attempts;
1003
1505
  this.#handler = handler;
1506
+ this.#abort = (0, _orkestrel_abort.createAbort)();
1507
+ this.#silence = resolveTaskSilence(silenceOption, silence);
1508
+ this.#onSilence = this.#expire.bind(this);
1509
+ this.#liveness = this.#silence === void 0 ? void 0 : (0, _orkestrel_timeout.createTimeout)({
1510
+ ms: this.#silence,
1511
+ signal: this.#abort.signal
1512
+ });
1513
+ this.#activity = activity === void 0 ? void 0 : cloneTaskActivity(activity);
1514
+ this.#paused = false;
1515
+ this.#gate = void 0;
1516
+ this.#timerSignal = void 0;
1004
1517
  }
1005
1518
  get emitter() {
1006
1519
  return this.#emitter;
@@ -1026,6 +1539,9 @@ var Task = class {
1026
1539
  get result() {
1027
1540
  return this.#result;
1028
1541
  }
1542
+ get attempts() {
1543
+ return this.#attempts;
1544
+ }
1029
1545
  get run() {
1030
1546
  return this.#run;
1031
1547
  }
@@ -1038,40 +1554,120 @@ var Task = class {
1038
1554
  get timeout() {
1039
1555
  return this.#timeout;
1040
1556
  }
1557
+ get activity() {
1558
+ return this.#activity;
1559
+ }
1560
+ get silence() {
1561
+ return this.#silence;
1562
+ }
1563
+ get silent() {
1564
+ return this.#status === "running" && this.#liveness?.expired === true;
1565
+ }
1566
+ get paused() {
1567
+ return this.#paused;
1568
+ }
1569
+ get signal() {
1570
+ return this.#abort.signal;
1571
+ }
1041
1572
  start() {
1042
- this.#transition("running");
1573
+ const budget = Math.max(0, this.#retries ?? 0) + 1;
1574
+ if (this.#status !== "pending" && this.#status !== "running" || this.#attempts >= budget) throw new WorkflowError("TRANSITION", `task '${this.id}' cannot start another attempt`, {
1575
+ task: this.id,
1576
+ status: this.#status,
1577
+ attempts: this.#attempts,
1578
+ budget
1579
+ });
1580
+ if (this.#status === "pending") this.#transition("running");
1581
+ this.#attempts += 1;
1582
+ this.#activity = cloneTaskActivity({}, this.#stamp());
1583
+ this.#arm();
1043
1584
  this.#emitter.emit("start", this.id);
1044
1585
  this.#escalate();
1045
1586
  }
1046
1587
  complete(value) {
1588
+ let owned;
1589
+ try {
1590
+ owned = (0, _orkestrel_contract.cloneJSONValue)(value);
1591
+ } catch (error) {
1592
+ if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `task '${this.id}' result could not be read safely: ${error.message}`, { task: this.id });
1593
+ throw new WorkflowError("RESTORE", `task '${this.id}' result could not be read safely`, { task: this.id });
1594
+ }
1047
1595
  this.#transition("completed");
1048
- const result = this.#record("completed", {
1596
+ this.#finish();
1597
+ const result = this.#record("completed", Object.freeze({
1049
1598
  success: true,
1050
- value
1051
- });
1599
+ value: owned
1600
+ }));
1052
1601
  this.#emitter.emit("complete", result);
1053
1602
  this.#escalate();
1054
1603
  }
1055
1604
  fail(error) {
1605
+ const origin = error.origin === "handler" || error.origin === "timeout" || error.origin === "recovery" ? error.origin : "handler";
1606
+ const message = typeof error.message === "string" && error.message.length > 0 ? error.message : "unknown failure";
1056
1607
  this.#transition("failed");
1057
- const reason = error instanceof Error ? error : new Error(String(error), { cause: error });
1058
- const result = this.#record("failed", {
1608
+ this.#finish();
1609
+ const result = this.#record("failed", Object.freeze({
1059
1610
  success: false,
1060
- error: reason
1061
- });
1611
+ error: Object.freeze({
1612
+ origin,
1613
+ message
1614
+ })
1615
+ }));
1062
1616
  this.#emitter.emit("fail", result);
1063
1617
  this.#escalate();
1064
1618
  }
1065
1619
  skip() {
1066
1620
  this.#transition("skipped");
1621
+ this.#finish();
1622
+ this.#abort.abort();
1067
1623
  this.#emitter.emit("skip");
1068
1624
  this.#escalate();
1069
1625
  }
1070
1626
  stop() {
1071
1627
  this.#transition("stopped");
1628
+ this.#finish();
1629
+ this.#abort.abort();
1072
1630
  this.#emitter.emit("stop");
1073
1631
  this.#escalate();
1074
1632
  }
1633
+ report(input) {
1634
+ if (this.#status !== "running") return failure(new WorkflowError("TRANSITION", `task '${this.id}' cannot report while '${this.#status}'`, {
1635
+ task: this.id,
1636
+ status: this.#status
1637
+ }));
1638
+ try {
1639
+ const activity = cloneTaskActivity(input, this.#stamp());
1640
+ this.#activity = activity;
1641
+ this.#arm();
1642
+ this.#emitter.emit("report", activity);
1643
+ return success(activity);
1644
+ } catch (error) {
1645
+ return failure(error instanceof WorkflowError ? error : new WorkflowError("MUTATION", "task activity report was refused", { task: this.id }));
1646
+ }
1647
+ }
1648
+ pulse() {
1649
+ if (this.#status !== "running" || this.#activity === void 0) return false;
1650
+ this.#touch();
1651
+ this.#arm();
1652
+ const activity = this.#activity;
1653
+ this.#emitter.emit("pulse", activity);
1654
+ return true;
1655
+ }
1656
+ pause() {
1657
+ if (this.#paused || this.#status !== "pending" && this.#status !== "running") return;
1658
+ this.#paused = true;
1659
+ this.#gate = createDeferred();
1660
+ this.#emitter.emit("pause");
1661
+ }
1662
+ resume() {
1663
+ if (!this.#paused) return;
1664
+ this.#paused = false;
1665
+ this.#release();
1666
+ this.#emitter.emit("resume");
1667
+ }
1668
+ wait() {
1669
+ return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
1670
+ }
1075
1671
  /**
1076
1672
  * Apply a validated declarative patch to SELF (`name` / `description`).
1077
1673
  *
@@ -1106,9 +1702,11 @@ var Task = class {
1106
1702
  status: this.#status,
1107
1703
  ...this.#result === void 0 ? {} : { result: this.#result },
1108
1704
  metadata: this.#metadata,
1705
+ attempts: this.#attempts,
1109
1706
  ...this.#run === void 0 ? {} : { run: this.#run },
1110
1707
  ...this.#retries === void 0 ? {} : { retries: this.#retries },
1111
- ...this.#timeout === void 0 ? {} : { timeout: this.#timeout }
1708
+ ...this.#timeout === void 0 ? {} : { timeout: this.#timeout },
1709
+ ...this.#activity === void 0 ? {} : { activity: this.#activity }
1112
1710
  };
1113
1711
  }
1114
1712
  #transition(to) {
@@ -1128,12 +1726,53 @@ var Task = class {
1128
1726
  ...result === void 0 ? {} : { result },
1129
1727
  timestamp: Date.now()
1130
1728
  };
1131
- this.#result = record;
1132
- return record;
1729
+ const frozen = Object.freeze(record);
1730
+ this.#result = frozen;
1731
+ return frozen;
1133
1732
  }
1134
1733
  #escalate() {
1135
1734
  this.#recompute();
1136
1735
  }
1736
+ #touch() {
1737
+ if (this.#activity === void 0) return;
1738
+ this.#activity = Object.freeze({
1739
+ ...this.#activity,
1740
+ updated: this.#stamp()
1741
+ });
1742
+ }
1743
+ #stamp() {
1744
+ return Math.max(Date.now(), this.#activity?.updated ?? 0);
1745
+ }
1746
+ #finish() {
1747
+ this.#clear();
1748
+ this.#paused = false;
1749
+ this.#release();
1750
+ }
1751
+ #arm() {
1752
+ this.#clear();
1753
+ const liveness = this.#liveness;
1754
+ if (liveness === void 0 || this.#status !== "running") return;
1755
+ liveness.start();
1756
+ const signal = liveness.signal;
1757
+ this.#timerSignal = signal;
1758
+ signal.addEventListener("abort", this.#onSilence, { once: true });
1759
+ }
1760
+ #clear() {
1761
+ const signal = this.#timerSignal;
1762
+ if (signal !== void 0) signal.removeEventListener("abort", this.#onSilence);
1763
+ this.#timerSignal = void 0;
1764
+ this.#liveness?.clear();
1765
+ }
1766
+ #expire() {
1767
+ this.#timerSignal = void 0;
1768
+ if (this.#status !== "running" || this.#liveness?.expired !== true) return;
1769
+ this.#emitter.emit("silence");
1770
+ }
1771
+ #release() {
1772
+ if (this.#gate === void 0) return;
1773
+ this.#gate.resolve();
1774
+ this.#gate = void 0;
1775
+ }
1137
1776
  };
1138
1777
  //#endregion
1139
1778
  //#region src/core/tasks/TaskManager.ts
@@ -1223,9 +1862,9 @@ var TaskManager = class {
1223
1862
  *
1224
1863
  * @remarks
1225
1864
  * - **Derived status.** `status` is `#override` when one is in force, else
1226
- * {@link derivePhaseStatus} over the live tasks' statuses. {@link #recompute} (passed to
1865
+ * {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
1227
1866
  * each child {@link Task}) re-derives on every child transition; a CHANGE emits the matching
1228
- * 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).
1229
1868
  * - **Override (AGENTS §10).** `skip` / `stop` FORCE the phase's status (e.g. skipping a whole
1230
1869
  * phase), overriding the derived value; the override is PERSISTED in the snapshot's own
1231
1870
  * `override` field and restored DIRECTLY (no divergence guess), so a forced phase round-trips.
@@ -1234,9 +1873,11 @@ var TaskManager = class {
1234
1873
  * `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
1235
1874
  * tree); `workflow` navigates UP to the live parent.
1236
1875
  * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link PhaseEventMap}) fires
1237
- * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE, strictly AFTER the
1238
- * recompute + escalate; the emitter isolates a listener throw and routes it to its `error`
1239
- * handler (the `error` option); `fail` carries the failing task's {@link TaskResult}.
1876
+ * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
1877
+ * corresponding status or runtime-gate change. Status events fire after the phase recomputes
1878
+ * and before it escalates to the workflow, preserving child/phase cause before parent effect.
1879
+ * The emitter isolates a listener throw and routes it to its `error` handler (the `error`
1880
+ * option); `fail` carries the failing task's {@link TaskResult}.
1240
1881
  * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1241
1882
  * delegating to {@link tasks} (the manager gates the target's own existence/status/id/
1242
1883
  * bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
@@ -1254,9 +1895,12 @@ var TaskManager = class {
1254
1895
  * construction path {@link #append} uses at build time, so a live mint and a restored/built
1255
1896
  * task are wired IDENTICALLY. At construction, the workflow-level
1256
1897
  * {@link import('../types.js').WorkflowFunctions} registry (threaded from
1257
- * {@link import('../types.js').WorkflowOptions.functions}) resolves each task's `run` name into
1258
- * its runtime {@link import('../types.js').TaskInterface.handler} ONCE; a `run` that is omitted
1259
- * or unregistered resolves to no handler (the no-handler rule).
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.
1260
1904
  * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
1261
1905
  * quartet, scoped to this phase — a driving
1262
1906
  * {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
@@ -1273,6 +1917,7 @@ var Phase = class {
1273
1917
  #escalateUp;
1274
1918
  #tasks = new TaskManager();
1275
1919
  #functions;
1920
+ #silence;
1276
1921
  #bail;
1277
1922
  #concurrency;
1278
1923
  #emitter;
@@ -1280,7 +1925,10 @@ var Phase = class {
1280
1925
  #override;
1281
1926
  #paused;
1282
1927
  #gate;
1283
- constructor(snapshot, workflow, escalate, options, bail, functions) {
1928
+ constructor(snapshot, workflow, escalate, options, bail, functions, silence) {
1929
+ const on = options?.on;
1930
+ const error = options?.error;
1931
+ const tasks = options?.tasks;
1284
1932
  this.#id = snapshot.id;
1285
1933
  this.#name = snapshot.name;
1286
1934
  if (snapshot.description !== void 0) Object.defineProperty(this, "description", {
@@ -1290,13 +1938,20 @@ var Phase = class {
1290
1938
  this.#workflow = workflow;
1291
1939
  this.#escalateUp = escalate;
1292
1940
  this.#functions = functions;
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]);
1293
1944
  this.#bail = bail ?? snapshot.bail;
1294
1945
  this.#concurrency = snapshot.concurrency;
1295
1946
  this.#emitter = new _orkestrel_emitter.Emitter({
1296
- ...options?.on === void 0 ? {} : { on: options.on },
1297
- ...options?.error === void 0 ? {} : { error: options.error }
1947
+ ...on === void 0 ? {} : { on },
1948
+ ...error === void 0 ? {} : { error }
1298
1949
  });
1299
- 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
+ }
1300
1955
  this.#override = snapshot.override;
1301
1956
  this.#status = this.status;
1302
1957
  this.#paused = false;
@@ -1358,11 +2013,13 @@ var Phase = class {
1358
2013
  if (this.#paused || isTerminalStatus(this.status)) return;
1359
2014
  this.#paused = true;
1360
2015
  this.#gate = createDeferred();
2016
+ this.#emitter.emit("pause");
1361
2017
  }
1362
2018
  resume() {
1363
2019
  if (!this.#paused) return;
1364
2020
  this.#paused = false;
1365
2021
  this.#release();
2022
+ this.#emitter.emit("resume");
1366
2023
  }
1367
2024
  wait() {
1368
2025
  return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
@@ -1443,6 +2100,10 @@ var Phase = class {
1443
2100
  return;
1444
2101
  }
1445
2102
  this.#status = next;
2103
+ if (isTerminalStatus(next)) {
2104
+ this.#paused = false;
2105
+ this.#release();
2106
+ }
1446
2107
  this.#emitFor(next);
1447
2108
  this.#escalateUp();
1448
2109
  }
@@ -1454,6 +2115,7 @@ var Phase = class {
1454
2115
  if (status === "running") this.#emitter.emit("start", this.id);
1455
2116
  else if (status === "completed") this.#emitter.emit("complete");
1456
2117
  else if (status === "failed") this.#emitter.emit("fail", this.#failure());
2118
+ else if (status === "skipped") this.#emitter.emit("skip");
1457
2119
  else if (status === "stopped") this.#emitter.emit("stop");
1458
2120
  }
1459
2121
  #failure() {
@@ -1471,17 +2133,17 @@ var Phase = class {
1471
2133
  if (result.success) this.#emitter.emit("add", result.value, at);
1472
2134
  return result;
1473
2135
  }
1474
- #append(task, options) {
1475
- const created = this.#create(task, options?.tasks?.[task.id]);
2136
+ #append(task, options, handler) {
2137
+ const created = this.#create(task, options, handler);
1476
2138
  this.#tasks.append(created);
1477
2139
  }
1478
- #create(snapshot, options) {
1479
- const context = buildTaskContext(this.context, snapshot);
1480
- const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
1481
- return new Task(context, this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, handler);
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);
1482
2142
  }
1483
2143
  #mint(definition) {
1484
- 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);
1485
2147
  }
1486
2148
  #statuses() {
1487
2149
  return this.#tasks.tasks().map((task) => task.status);
@@ -1576,24 +2238,26 @@ var PhaseManager = class {
1576
2238
  * - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
1577
2239
  * {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
1578
2240
  * {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').restoreWorkflow}
1579
- * 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`.
1580
2242
  * - **Derived status.** `status` is `#override` when forced, else
1581
2243
  * {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
1582
2244
  * reachable ONLY under `bail: true` (a single failed task halts the workflow); under
1583
- * `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
1584
2246
  * change; a CHANGE emits.
1585
- * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; the override is PERSISTED in the
1586
- * snapshot's own `override` field and restored DIRECTLY (no divergence guess). The snapshot also
1587
- * persists `bail`, so a restore re-derives status identically without a silent policy default.
2247
+ * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; an executed task-free pending tree
2248
+ * may also be force-completed vacuously. The override is PERSISTED in the snapshot's own
2249
+ * `override` field and restored DIRECTLY (no divergence guess). The snapshot also persists
2250
+ * `bail`, so a restore re-derives status identically without a silent policy default.
1588
2251
  * - **Result tree.** `results()` flattens every phase's `results()` ({@link collectResults}) — the
1589
2252
  * workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
1590
2253
  * navigate UP.
1591
2254
  * - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
1592
2255
  * JSON); {@link import('./factories.js').restoreWorkflow} rebuilds an equivalent live tree.
1593
2256
  * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
1594
- * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE; the emitter isolates a
1595
- * listener throw and routes it to its `error` handler (the `error` option); `fail` carries
1596
- * the failing task's {@link TaskResult}.
2257
+ * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
2258
+ * corresponding status or runtime-gate change; the emitter isolates a listener throw and
2259
+ * routes it to its `error` handler (the `error` option); `fail` carries the failing task's
2260
+ * {@link TaskResult}.
1597
2261
  * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1598
2262
  * delegating to {@link phases} (the manager gates the target's own existence/status/id/
1599
2263
  * bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
@@ -1605,16 +2269,17 @@ var PhaseManager = class {
1605
2269
  * naturally accepted.
1606
2270
  * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` gate execution at the runner's
1607
2271
  * phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
1608
- * persisted. `destroy` is a terminal teardown: it aborts {@link signal}, `stop`s every
1609
- * non-terminal live phase (so an engine parked on a phase's own gate unparks and the tree
1610
- * lands coherent), forces the `stop` override on THIS workflow when not already terminal,
1611
- * releases any parked {@link wait} waiter, and marks {@link destroyed} — all four idempotent.
2272
+ * persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
2273
+ * phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
2274
+ * workflow `stop` override when needed, releases its parked waiter, and marks
2275
+ * {@link destroyed} — all idempotent.
1612
2276
  */
1613
2277
  var Workflow = class {
1614
2278
  #context;
1615
2279
  #bail;
1616
2280
  #bailOverride;
1617
2281
  #functions;
2282
+ #silence;
1618
2283
  #phases = new PhaseManager();
1619
2284
  #emitter;
1620
2285
  #created;
@@ -1626,14 +2291,22 @@ var Workflow = class {
1626
2291
  #gate;
1627
2292
  #destroyed;
1628
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;
1629
2301
  this.#context = buildWorkflowContext(snapshot);
1630
2302
  if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
1631
- this.#bail = options?.bail ?? snapshot.bail;
1632
- this.#bailOverride = options?.bail;
1633
- this.#functions = options?.functions;
2303
+ this.#bail = bail ?? snapshot.bail;
2304
+ this.#bailOverride = bail;
2305
+ this.#functions = functions;
2306
+ this.#silence = silence;
1634
2307
  this.#emitter = new _orkestrel_emitter.Emitter({
1635
- ...options?.on === void 0 ? {} : { on: options.on },
1636
- ...options?.error === void 0 ? {} : { error: options.error }
2308
+ ...on === void 0 ? {} : { on },
2309
+ ...error === void 0 ? {} : { error }
1637
2310
  });
1638
2311
  this.#created = snapshot.created;
1639
2312
  this.#updated = snapshot.updated;
@@ -1641,7 +2314,10 @@ var Workflow = class {
1641
2314
  this.#paused = false;
1642
2315
  this.#gate = void 0;
1643
2316
  this.#destroyed = false;
1644
- 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
+ }
1645
2321
  this.#override = snapshot.override;
1646
2322
  this.#status = this.status;
1647
2323
  }
@@ -1692,26 +2368,35 @@ var Workflow = class {
1692
2368
  this.#release();
1693
2369
  }
1694
2370
  complete() {
1695
- if (this.status === "pending") this.#force("completed");
2371
+ if (this.status === "pending" && this.#phases.phases().every((phase) => phase.tasks.count === 0)) this.#force("completed");
1696
2372
  }
1697
2373
  pause() {
1698
2374
  if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
1699
2375
  this.#paused = true;
1700
2376
  this.#gate = createDeferred();
2377
+ this.#emitter.emit("pause");
1701
2378
  }
1702
2379
  resume() {
1703
2380
  if (!this.#paused) return;
1704
2381
  this.#paused = false;
1705
2382
  this.#release();
2383
+ this.#emitter.emit("resume");
1706
2384
  }
1707
2385
  destroy() {
1708
2386
  if (this.#destroyed) return;
1709
2387
  this.#destroyed = true;
1710
- this.#abort.abort();
1711
- for (const phase of this.#phases.phases()) if (!isTerminalStatus(phase.status)) phase.stop();
2388
+ const phases = this.#phases.phases();
1712
2389
  if (!isTerminalStatus(this.status)) this.stop();
2390
+ for (const phase of phases) phase.stop();
2391
+ for (const phase of phases) for (const task of phase.tasks.tasks()) if (!isTerminalStatus(task.status)) task.stop();
1713
2392
  this.#paused = false;
1714
2393
  this.#release();
2394
+ this.#abort.abort();
2395
+ for (const phase of phases) {
2396
+ for (const task of phase.tasks.tasks()) task.emitter.destroy();
2397
+ phase.emitter.destroy();
2398
+ }
2399
+ this.#emitter.destroy();
1715
2400
  }
1716
2401
  wait() {
1717
2402
  return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
@@ -1773,7 +2458,7 @@ var Workflow = class {
1773
2458
  return result;
1774
2459
  }
1775
2460
  snapshot() {
1776
- return {
2461
+ return cloneWorkflowSnapshot({
1777
2462
  id: this.id,
1778
2463
  name: this.name,
1779
2464
  ...this.description === void 0 ? {} : { description: this.description },
@@ -1783,13 +2468,17 @@ var Workflow = class {
1783
2468
  phases: this.#phases.phases().map((phase) => phase.snapshot()),
1784
2469
  created: this.#created,
1785
2470
  updated: this.#updated
1786
- };
2471
+ });
1787
2472
  }
1788
2473
  #recompute() {
1789
2474
  const next = this.status;
1790
2475
  if (next === this.#status) return;
1791
2476
  this.#status = next;
1792
- this.#updated = Date.now();
2477
+ if (isTerminalStatus(next)) {
2478
+ this.#paused = false;
2479
+ this.#release();
2480
+ }
2481
+ this.#updated = Math.max(Date.now(), this.#updated);
1793
2482
  this.#emitFor(next);
1794
2483
  }
1795
2484
  #force(status) {
@@ -1800,6 +2489,7 @@ var Workflow = class {
1800
2489
  if (status === "running") this.#emitter.emit("start", this.id);
1801
2490
  else if (status === "completed") this.#emitter.emit("complete");
1802
2491
  else if (status === "failed") this.#emitter.emit("fail", this.#failure());
2492
+ else if (status === "skipped") this.#emitter.emit("skip");
1803
2493
  else if (status === "stopped") this.#emitter.emit("stop");
1804
2494
  }
1805
2495
  #addTo(phase, index, at) {
@@ -1819,11 +2509,11 @@ var Workflow = class {
1819
2509
  return found;
1820
2510
  }
1821
2511
  #append(phase, options) {
1822
- const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride, this.#functions);
2512
+ const created = new Phase(phase, this, () => this.#recompute(), options, this.#bailOverride, this.#functions, this.#silence);
1823
2513
  this.#phases.append(created);
1824
2514
  }
1825
2515
  #mint(definition) {
1826
- return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions);
2516
+ return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions, this.#silence);
1827
2517
  }
1828
2518
  #release() {
1829
2519
  if (this.#gate === void 0) return;
@@ -1851,12 +2541,11 @@ var Workflow = class {
1851
2541
  * `functions` registry in) and stores it under `definition.id` — an already-present id
1852
2542
  * OVERWRITES (last write wins). `count` is the map size, `workflow(id)` looks one up,
1853
2543
  * `workflows()` lists them in insertion order.
1854
- * - **Durable open / save.** `open(id)` returns an already-registered workflow directly; on a
1855
- * registry MISS with a `store` set it rehydrates through {@link restoreWorkflow} (flowing the
1856
- * manager's `functions` registry in so the rehydrated tree is RUNNABLE), registers it, and
1857
- * returns it lenient (`undefined`) with no store or a store miss. `save(id)` persists a
1858
- * registered workflow's `snapshot()` to the `store` — lenient (`false`) with no store or an
1859
- * 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.
1860
2549
  * - **Removal.** `remove` drops one by id, or a batch (§9.2, array overload FIRST) — `true` when
1861
2550
  * any was removed. `clear` empties the registry.
1862
2551
  * - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
@@ -1874,6 +2563,12 @@ var Workflow = class {
1874
2563
  */
1875
2564
  var WorkflowManager = class {
1876
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();
1877
2572
  #functions;
1878
2573
  #store;
1879
2574
  constructor(options) {
@@ -1891,36 +2586,140 @@ var WorkflowManager = class {
1891
2586
  }
1892
2587
  add(definition) {
1893
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);
1894
2592
  this.#workflows.set(workflow.id, workflow);
1895
2593
  return workflow;
1896
2594
  }
1897
- async open(id) {
2595
+ open(id) {
1898
2596
  const existing = this.#workflows.get(id);
1899
- if (existing !== void 0) return existing;
1900
- if (this.#store === void 0) return void 0;
1901
- const snapshot = await this.#store.get(id);
1902
- if (snapshot === void 0) return void 0;
1903
- const workflow = restoreWorkflow(snapshot, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
1904
- this.#workflows.set(workflow.id, workflow);
1905
- return workflow;
1906
- }
1907
- 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) {
1908
2612
  const workflow = this.#workflows.get(id);
1909
- if (this.#store === void 0 || workflow === void 0) return false;
1910
- await this.#store.set(workflow.snapshot());
1911
- 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);
1912
2622
  }
1913
2623
  remove(ids) {
1914
2624
  if ((0, _orkestrel_contract.isArray)(ids)) {
1915
2625
  let removed = false;
1916
- 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
+ }
1917
2631
  return removed;
1918
2632
  }
2633
+ this.#invalidate(ids);
2634
+ this.#additions.delete(ids);
1919
2635
  return this.#workflows.delete(ids);
1920
2636
  }
1921
2637
  clear() {
2638
+ this.#generation = Symbol();
2639
+ this.#mutations.clear();
2640
+ this.#additions.clear();
2641
+ this.#opens.clear();
1922
2642
  this.#workflows.clear();
1923
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
+ }
1924
2723
  };
1925
2724
  //#endregion
1926
2725
  //#region src/core/Controller.ts
@@ -2039,6 +2838,7 @@ var Runner = class {
2039
2838
  #order = [];
2040
2839
  #values = /* @__PURE__ */ new Map();
2041
2840
  #dispatched = /* @__PURE__ */ new Set();
2841
+ #queued = /* @__PURE__ */ new Set();
2042
2842
  #count = 0;
2043
2843
  #drained;
2044
2844
  #started = false;
@@ -2046,18 +2846,28 @@ var Runner = class {
2046
2846
  #stopped = false;
2047
2847
  #stopping = false;
2048
2848
  #failure;
2849
+ #stopPromise;
2850
+ #abortPromise;
2851
+ #destroyPromise;
2049
2852
  constructor(options) {
2050
- this.#handler = options.handler;
2051
- 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;
2052
2862
  this.#emitter = new _orkestrel_emitter.Emitter({
2053
- ...options.on === void 0 ? {} : { on: options.on },
2054
- ...options.error === void 0 ? {} : { error: options.error }
2863
+ ...on === void 0 ? {} : { on },
2864
+ ...error === void 0 ? {} : { error }
2055
2865
  });
2056
2866
  this.#queue = (0, _orkestrel_queue.createQueue)({
2057
2867
  handler: this.#dispatch.bind(this),
2058
- ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
2059
- ...options.retries === void 0 ? {} : { retries: options.retries },
2060
- ...options.timeout === void 0 ? {} : { timeout: options.timeout }
2868
+ ...concurrency === void 0 ? {} : { concurrency },
2869
+ ...retries === void 0 ? {} : { retries },
2870
+ ...timeout === void 0 ? {} : { timeout }
2061
2871
  });
2062
2872
  }
2063
2873
  get emitter() {
@@ -2099,7 +2909,7 @@ var Runner = class {
2099
2909
  * ```
2100
2910
  */
2101
2911
  spawn(input) {
2102
- if (this.#stopped || !this.#running) return void 0;
2912
+ if (!this.#accepts()) return void 0;
2103
2913
  return this.#launch(input, void 0, true);
2104
2914
  }
2105
2915
  async execute(inputs) {
@@ -2107,29 +2917,37 @@ var Runner = class {
2107
2917
  if (this.#stopped) throw new Error("runner is stopped");
2108
2918
  this.#started = true;
2109
2919
  this.#running = true;
2110
- this.#emitter.emit("start");
2111
- if (inputs.length === 0) {
2112
- this.#running = false;
2113
- this.#emitter.emit("finish", []);
2114
- return [];
2115
- }
2116
2920
  const drained = createDeferred();
2117
2921
  this.#drained = drained;
2118
- 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();
2119
2928
  await drained.promise;
2120
2929
  this.#running = false;
2930
+ const cleanup = await this.#cleanup();
2121
2931
  if (this.#failure !== void 0) throw this.#failure.error;
2932
+ if (cleanup !== void 0) throw cleanup.error;
2122
2933
  const results = this.#collect();
2123
2934
  this.#emitter.emit("finish", results);
2935
+ const finishing = await this.#cleanup();
2936
+ if (finishing !== void 0) throw finishing.error;
2124
2937
  return results;
2125
2938
  }
2126
2939
  abort(reason) {
2127
- 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(() => {});
2128
2944
  if (this.#running && this.#failure === void 0) this.#failure = { error: reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason };
2129
2945
  this.#cancel(reason);
2130
- this.#queue.abort(reason);
2131
2946
  this.#stopped = true;
2947
+ const cleanup = this.#queue.abort(reason);
2948
+ this.#settleLifecycle(barrier, cleanup);
2132
2949
  this.#emitter.emit("abort", reason);
2950
+ return barrier.promise;
2133
2951
  }
2134
2952
  /**
2135
2953
  * Suspend dispatch (AGENTS §10 — resumable): delegates to the backing queue's own
@@ -2167,18 +2985,28 @@ var Runner = class {
2167
2985
  * dispatched unit's rejection while stopping is still a real failure. Idempotent.
2168
2986
  */
2169
2987
  stop() {
2170
- 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(() => {});
2171
2994
  this.#stopping = true;
2172
2995
  this.#stopped = true;
2173
- this.#queue.stop();
2996
+ const cleanup = this.#queue.stop();
2997
+ this.#settleLifecycle(barrier, cleanup);
2998
+ return barrier.promise;
2174
2999
  }
2175
3000
  destroy() {
2176
- if (this.#stopped) {
2177
- this.#queue.destroy();
2178
- return;
2179
- }
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;
2180
3006
  this.abort();
2181
- this.#queue.destroy();
3007
+ const cleanup = this.#queue.destroy();
3008
+ this.#settleDestroy(barrier, cleanup);
3009
+ return barrier.promise;
2182
3010
  }
2183
3011
  #launch(input, parent, announce = parent !== void 0) {
2184
3012
  const id = crypto.randomUUID();
@@ -2187,14 +3015,24 @@ var Runner = class {
2187
3015
  this.#order.push(id);
2188
3016
  this.#count += 1;
2189
3017
  if (announce) this.#emitter.emit("spawn", id, parent);
2190
- const promise = this.#queue.enqueue({
2191
- id,
2192
- input
2193
- }, {
2194
- id,
2195
- signal: abort.signal,
2196
- ...this.#entries?.(input)
2197
- });
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
+ }
2198
3036
  promise.then((value) => this.#settle(id, {
2199
3037
  ok: true,
2200
3038
  value
@@ -2213,14 +3051,14 @@ var Runner = class {
2213
3051
  return this.#handler(controller);
2214
3052
  }
2215
3053
  #spawn(input, parent) {
2216
- 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");
2217
3055
  return this.#launch(input, parent);
2218
3056
  }
2219
3057
  #settle(id, outcome) {
2220
3058
  if (outcome.ok) {
2221
3059
  this.#values.set(id, { value: outcome.value });
2222
3060
  this.#emitter.emit("settle", id);
2223
- } 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) {
2224
3062
  this.#failure = { error: outcome.error };
2225
3063
  this.#emitter.emit("fail", id, outcome.error);
2226
3064
  this.abort(outcome.error);
@@ -2236,6 +3074,46 @@ var Runner = class {
2236
3074
  }
2237
3075
  return results;
2238
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
+ }
2239
3117
  #cancel(reason) {
2240
3118
  for (const abort of this.#aborts.values()) abort.abort(reason);
2241
3119
  }
@@ -2243,18 +3121,17 @@ var Runner = class {
2243
3121
  //#endregion
2244
3122
  //#region src/core/tasks/TaskController.ts
2245
3123
  /**
2246
- * The lean per-task handle a {@link import('./types.js').WorkflowFunction} receives — the
2247
- * running task's folded cancellation, its input, its lineage, and read-UP access to the
2248
- * result tree.
3124
+ * The attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
2249
3125
  *
2250
3126
  * @remarks
2251
3127
  * - **A leaf handle, NOT the runner `Controller`.** A workflow task is a leaf of the
2252
- * declarative W-b tree, not a fan-out unit, so this carries none of the runner
2253
- * `Controller`'s `spawn` / `wait` only what a leaf needs.
2254
- * - **Folded signal.** `signal` is the cancellation the runner folds for THIS run: it fires
2255
- * on a workflow-level abort / timeout / budget ceiling, or under `bail: true` — when a
2256
- * sibling task fails (the runner aborts the in-flight siblings via the substrate's
2257
- * fail-fast). A handler races its work against it; `aborted` reads it.
3128
+ * declarative W-b tree, not a fan-out unit, so it has no `spawn`; its `wait` instead
3129
+ * checkpoints the workflow, phase, and task cooperative gates.
3130
+ * - **Folded signal.** `signal` is the cancellation folded for THIS attempt: its per-attempt
3131
+ * deadline, task stop/skip, workflow abort/timeout/budget/destroy, or a sibling fail-fast.
3132
+ * A handler races its work against it; `aborted` reads it.
3133
+ * - **Attempt ownership.** `report` / `pulse` are closures supplied by the runner and refuse
3134
+ * after this signal aborts or a retry token supersedes this handle.
2258
3135
  * - **Input + lineage.** `input` is the task's open `metadata` bag (its
2259
3136
  * {@link import('./types.js').TaskInput} payload, `{}` when none); `task` is the full
2260
3137
  * {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate UP the lineage.
@@ -2270,19 +3147,275 @@ var TaskController = class {
2270
3147
  signal;
2271
3148
  input;
2272
3149
  task;
3150
+ attempt;
3151
+ #entity;
3152
+ #report;
3153
+ #pulse;
2273
3154
  #results;
2274
- constructor(signal, input, task, results) {
3155
+ constructor(signal, input, task, attempt, results, report, pulse) {
2275
3156
  this.signal = signal;
2276
3157
  this.input = input;
2277
- this.task = task;
3158
+ this.task = task.context;
3159
+ this.attempt = attempt;
3160
+ this.#entity = task;
2278
3161
  this.#results = results;
3162
+ this.#report = report;
3163
+ this.#pulse = pulse;
2279
3164
  }
2280
3165
  get aborted() {
2281
3166
  return this.signal.aborted;
2282
3167
  }
3168
+ get paused() {
3169
+ if (this.#ancestorTerminal()) return false;
3170
+ return this.#entity.workflow.paused || this.#entity.phase.paused || !isTerminalStatus(this.#entity.status) && this.#entity.paused;
3171
+ }
3172
+ report(input) {
3173
+ return this.#report(input);
3174
+ }
3175
+ pulse() {
3176
+ return this.#pulse();
3177
+ }
3178
+ async wait() {
3179
+ while (this.paused && !this.signal.aborted) await this.#race(this.#gates());
3180
+ }
2283
3181
  results() {
2284
3182
  return this.#results();
2285
3183
  }
3184
+ #gates() {
3185
+ if (this.#ancestorTerminal()) return [];
3186
+ const gates = [];
3187
+ if (this.#entity.workflow.paused) gates.push(this.#entity.workflow.wait());
3188
+ if (this.#entity.phase.paused) gates.push(this.#entity.phase.wait());
3189
+ if (!isTerminalStatus(this.#entity.status) && this.#entity.paused) gates.push(this.#entity.wait());
3190
+ return gates;
3191
+ }
3192
+ async #race(gates) {
3193
+ if (this.signal.aborted || gates.length === 0) return;
3194
+ const deferred = Promise.withResolvers();
3195
+ const onAbort = this.#resolve.bind(this, deferred);
3196
+ const onTerminal = this.#resolve.bind(this, deferred);
3197
+ this.signal.addEventListener("abort", onAbort, { once: true });
3198
+ this.#entity.workflow.emitter.on("skip", onTerminal);
3199
+ this.#entity.workflow.emitter.on("stop", onTerminal);
3200
+ this.#entity.phase.emitter.on("skip", onTerminal);
3201
+ this.#entity.phase.emitter.on("stop", onTerminal);
3202
+ try {
3203
+ if (this.#ancestorTerminal()) deferred.resolve();
3204
+ await Promise.race([Promise.all(gates), deferred.promise]);
3205
+ } finally {
3206
+ this.signal.removeEventListener("abort", onAbort);
3207
+ this.#entity.workflow.emitter.off("skip", onTerminal);
3208
+ this.#entity.workflow.emitter.off("stop", onTerminal);
3209
+ this.#entity.phase.emitter.off("skip", onTerminal);
3210
+ this.#entity.phase.emitter.off("stop", onTerminal);
3211
+ }
3212
+ }
3213
+ #ancestorTerminal() {
3214
+ return isTerminalStatus(this.#entity.workflow.status) || isTerminalStatus(this.#entity.phase.status);
3215
+ }
3216
+ #resolve(deferred) {
3217
+ deferred.resolve();
3218
+ }
3219
+ };
3220
+ //#endregion
3221
+ //#region src/core/WorkflowPersistence.ts
3222
+ /**
3223
+ * Advanced run-local snapshot persistence with one writer and one coalesced latest obligation.
3224
+ *
3225
+ * @remarks
3226
+ * Normally composed by `WorkflowRunner.execute({ store })`; exported for hosts that need to
3227
+ * coordinate the same required boundaries around their own runner integration.
3228
+ */
3229
+ var WorkflowPersistence = class {
3230
+ #workflow;
3231
+ #store;
3232
+ #phases = /* @__PURE__ */ new Set();
3233
+ #tasks = /* @__PURE__ */ new Set();
3234
+ #onWorkflowChange;
3235
+ #onWorkflowAdd;
3236
+ #onWorkflowRemove;
3237
+ #onPhaseChange;
3238
+ #onPhaseAdd;
3239
+ #onPhaseRemove;
3240
+ #onTaskChange;
3241
+ #writing;
3242
+ #error;
3243
+ #fault;
3244
+ #attached = true;
3245
+ #revision = 0;
3246
+ #stored = 0;
3247
+ constructor(workflow, store) {
3248
+ this.#workflow = workflow;
3249
+ this.#store = store;
3250
+ this.#onWorkflowChange = this.#change.bind(this);
3251
+ this.#onWorkflowAdd = this.#addPhase.bind(this);
3252
+ this.#onWorkflowRemove = this.#removePhase.bind(this);
3253
+ this.#onPhaseChange = this.#change.bind(this);
3254
+ this.#onPhaseAdd = this.#addTask.bind(this);
3255
+ this.#onPhaseRemove = this.#removeTask.bind(this);
3256
+ this.#onTaskChange = this.#change.bind(this);
3257
+ this.#attachWorkflow();
3258
+ }
3259
+ get fault() {
3260
+ return this.#fault;
3261
+ }
3262
+ /**
3263
+ * Persist every change through this required boundary.
3264
+ *
3265
+ * @param checkpoint - The boundary being made durable
3266
+ * @param task - The task owning an attempt or settlement
3267
+ * @param attempt - The persisted attempt number
3268
+ * @returns Whether the latest state reached the store
3269
+ */
3270
+ async checkpoint(checkpoint, task, attempt) {
3271
+ const revision = this.#mark();
3272
+ while (this.#stored < revision) await this.#flush();
3273
+ if (this.#error === void 0) return true;
3274
+ if (this.#fault === void 0) this.#fault = Object.freeze({
3275
+ origin: "persistence",
3276
+ checkpoint,
3277
+ message: this.#error,
3278
+ ...task === void 0 ? {} : { task: task.id },
3279
+ ...attempt === void 0 ? {} : { attempt }
3280
+ });
3281
+ return false;
3282
+ }
3283
+ /**
3284
+ * Stop observing the live tree and persist its final state.
3285
+ *
3286
+ * @returns Whether the final snapshot reached the store
3287
+ */
3288
+ async finalize() {
3289
+ this.detach();
3290
+ return this.checkpoint("final");
3291
+ }
3292
+ /** Stop observing the live tree. */
3293
+ detach() {
3294
+ if (!this.#attached) return;
3295
+ this.#attached = false;
3296
+ this.#workflow.emitter.off("start", this.#onWorkflowChange);
3297
+ this.#workflow.emitter.off("complete", this.#onWorkflowChange);
3298
+ this.#workflow.emitter.off("fail", this.#onWorkflowChange);
3299
+ this.#workflow.emitter.off("skip", this.#onWorkflowChange);
3300
+ this.#workflow.emitter.off("stop", this.#onWorkflowChange);
3301
+ this.#workflow.emitter.off("move", this.#onWorkflowChange);
3302
+ this.#workflow.emitter.off("update", this.#onWorkflowChange);
3303
+ this.#workflow.emitter.off("add", this.#onWorkflowAdd);
3304
+ this.#workflow.emitter.off("remove", this.#onWorkflowRemove);
3305
+ for (const phase of this.#phases) this.#detachPhase(phase);
3306
+ }
3307
+ #attachWorkflow() {
3308
+ this.#workflow.emitter.on("start", this.#onWorkflowChange);
3309
+ this.#workflow.emitter.on("complete", this.#onWorkflowChange);
3310
+ this.#workflow.emitter.on("fail", this.#onWorkflowChange);
3311
+ this.#workflow.emitter.on("skip", this.#onWorkflowChange);
3312
+ this.#workflow.emitter.on("stop", this.#onWorkflowChange);
3313
+ this.#workflow.emitter.on("move", this.#onWorkflowChange);
3314
+ this.#workflow.emitter.on("update", this.#onWorkflowChange);
3315
+ this.#workflow.emitter.on("add", this.#onWorkflowAdd);
3316
+ this.#workflow.emitter.on("remove", this.#onWorkflowRemove);
3317
+ for (const phase of this.#workflow.phases.phases()) this.#attachPhase(phase);
3318
+ }
3319
+ #attachPhase(phase) {
3320
+ if (this.#phases.has(phase)) return;
3321
+ this.#phases.add(phase);
3322
+ phase.emitter.on("start", this.#onPhaseChange);
3323
+ phase.emitter.on("complete", this.#onPhaseChange);
3324
+ phase.emitter.on("fail", this.#onPhaseChange);
3325
+ phase.emitter.on("skip", this.#onPhaseChange);
3326
+ phase.emitter.on("stop", this.#onPhaseChange);
3327
+ phase.emitter.on("move", this.#onPhaseChange);
3328
+ phase.emitter.on("update", this.#onPhaseChange);
3329
+ phase.emitter.on("add", this.#onPhaseAdd);
3330
+ phase.emitter.on("remove", this.#onPhaseRemove);
3331
+ for (const task of phase.tasks.tasks()) this.#attachTask(task);
3332
+ }
3333
+ #detachPhase(phase) {
3334
+ if (!this.#phases.delete(phase)) return;
3335
+ phase.emitter.off("start", this.#onPhaseChange);
3336
+ phase.emitter.off("complete", this.#onPhaseChange);
3337
+ phase.emitter.off("fail", this.#onPhaseChange);
3338
+ phase.emitter.off("skip", this.#onPhaseChange);
3339
+ phase.emitter.off("stop", this.#onPhaseChange);
3340
+ phase.emitter.off("move", this.#onPhaseChange);
3341
+ phase.emitter.off("update", this.#onPhaseChange);
3342
+ phase.emitter.off("add", this.#onPhaseAdd);
3343
+ phase.emitter.off("remove", this.#onPhaseRemove);
3344
+ for (const task of phase.tasks.tasks()) this.#detachTask(task);
3345
+ }
3346
+ #attachTask(task) {
3347
+ if (this.#tasks.has(task)) return;
3348
+ this.#tasks.add(task);
3349
+ task.emitter.on("start", this.#onTaskChange);
3350
+ task.emitter.on("complete", this.#onTaskChange);
3351
+ task.emitter.on("fail", this.#onTaskChange);
3352
+ task.emitter.on("skip", this.#onTaskChange);
3353
+ task.emitter.on("stop", this.#onTaskChange);
3354
+ task.emitter.on("report", this.#onTaskChange);
3355
+ task.emitter.on("pulse", this.#onTaskChange);
3356
+ }
3357
+ #detachTask(task) {
3358
+ if (!this.#tasks.delete(task)) return;
3359
+ task.emitter.off("start", this.#onTaskChange);
3360
+ task.emitter.off("complete", this.#onTaskChange);
3361
+ task.emitter.off("fail", this.#onTaskChange);
3362
+ task.emitter.off("skip", this.#onTaskChange);
3363
+ task.emitter.off("stop", this.#onTaskChange);
3364
+ task.emitter.off("report", this.#onTaskChange);
3365
+ task.emitter.off("pulse", this.#onTaskChange);
3366
+ }
3367
+ #addPhase(phase) {
3368
+ this.#attachPhase(phase);
3369
+ this.#change();
3370
+ }
3371
+ #removePhase(phase) {
3372
+ this.#detachPhase(phase);
3373
+ this.#change();
3374
+ }
3375
+ #addTask(task) {
3376
+ this.#attachTask(task);
3377
+ this.#change();
3378
+ }
3379
+ #removeTask(task) {
3380
+ this.#detachTask(task);
3381
+ this.#change();
3382
+ }
3383
+ #change() {
3384
+ this.#mark();
3385
+ this.#flush();
3386
+ }
3387
+ async #flush() {
3388
+ if (this.#writing !== void 0) {
3389
+ await this.#writing;
3390
+ return;
3391
+ }
3392
+ const reservation = Promise.withResolvers();
3393
+ const writing = reservation.promise;
3394
+ this.#writing = writing;
3395
+ this.#drain().then(reservation.resolve, reservation.reject);
3396
+ try {
3397
+ await writing;
3398
+ } finally {
3399
+ if (this.#writing === writing) this.#writing = void 0;
3400
+ if (this.#stored < this.#revision) this.#flush();
3401
+ }
3402
+ }
3403
+ async #drain() {
3404
+ while (this.#stored < this.#revision) {
3405
+ const revision = this.#revision;
3406
+ try {
3407
+ await this.#store.set(this.#workflow.snapshot());
3408
+ this.#error = void 0;
3409
+ } catch (error) {
3410
+ this.#error = errorToMessage(error);
3411
+ }
3412
+ this.#stored = revision;
3413
+ }
3414
+ }
3415
+ #mark() {
3416
+ this.#revision += 1;
3417
+ return this.#revision;
3418
+ }
2286
3419
  };
2287
3420
  //#endregion
2288
3421
  //#region src/core/WorkflowRunner.ts
@@ -2295,21 +3428,21 @@ var TaskController = class {
2295
3428
  * - **Composes, never re-implements.** Per-phase bounded concurrency is one
2296
3429
  * {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
2297
3430
  * `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
2298
- * timeout / budget / entity `signal` fold through {@link createAbort} / {@link createTimeout} +
2299
- * `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
2300
3434
  * {@link SchedulerInterface}. The runner writes ZERO concurrency / retry / abort logic of
2301
3435
  * its own — it only sequences phases, dispatches a task's own handler, and drives the live
2302
- * entity.
2303
- * - **Pure engine no registries, no tool/agent knowledge.** The runner carries no
2304
- * `functions` / `tools` / `agents` registry: each live {@link TaskInterface} already
3436
+ * entity. The workflow layer owns per-task deadlines because timeout settlement must
3437
+ * update the live leaf under the phase's `bail` policy before the substrate unit settles.
3438
+ * - **Pure engine no integration registry.** The runner carries no behavior or provider
3439
+ * registry: each live {@link TaskInterface} already
2305
3440
  * resolved its own {@link import('./types.js').WorkflowFunction} into
2306
3441
  * {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
2307
3442
  * or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
2308
- * dispatch is simply "invoke the task's own handler". Static tool / agent calling is an
2309
- * OPT-IN concern of the `@orkestrel/tool` package's adapter factories — plain
2310
- * {@link import('./types.js').WorkflowFunction}s a caller wires into
2311
- * {@link WorkflowOptions.functions} like any other behavior. This module never imports
2312
- * any tool/agent package.
3443
+ * dispatch is simply "invoke the task's own handler". Provider, protocol, and tool
3444
+ * integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
3445
+ * composed into {@link WorkflowOptions.functions}. This module imports none of them.
2313
3446
  * - **Two `execute` forms, one engine.** `execute(definition, options)` BUILDS the live tree
2314
3447
  * from a {@link WorkflowDefinition} (single source of truth for the `run` / `concurrency`
2315
3448
  * metadata); `execute(workflow, options)` DRIVES a caller-owned, ALREADY-BUILT
@@ -2329,10 +3462,9 @@ var TaskController = class {
2329
3462
  * for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
2330
3463
  * phase always reaches a coherent terminal state.
2331
3464
  * - **Dispatch by handler.** `#runTask` invokes the live task's own
2332
- * {@link import('./types.js').TaskInterface.handler} directly: `undefined` (an omitted `run`,
2333
- * or a `run` name absent from the {@link WorkflowOptions.functions} registry it was resolved
2334
- * against) AUTO-COMPLETES the ROADMAP no-handler rule; otherwise the handler runs with the
2335
- * task's {@link import('./types.js').TaskControllerInterface} handle.
3465
+ * {@link import('./types.js').TaskInterface.handler} directly. An omitted `run` deliberately
3466
+ * auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
3467
+ * execution claim and never false-completes.
2336
3468
  * - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
2337
3469
  * THEN re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
2338
3470
  * (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
@@ -2340,11 +3472,12 @@ var TaskController = class {
2340
3472
  * Under `bail: false` (graceful) a failure `fail`s the leaf and RESOLVES (never throws), so
2341
3473
  * the Runner settles every unit (allSettled) and the run finishes (the workflow derives
2342
3474
  * `completed`, the failure recorded in the result tree).
2343
- * - **Pause / stop / destroy gates.** `workflow.pause()` is honoured at exactly two points
2344
- * the next phase boundary (workflow-only) and each task's own pre-dispatch (before
2345
- * `task.start()`, workflow gate FIRST then this task's own `phase.pause()`) by parking on
2346
- * {@link WorkflowInterface.wait} / {@link PhaseInterface.wait}; an in-flight task body is
2347
- * NEVER suspended mid-flight. A GRACEFUL `workflow.stop()` (no signal involved) is caught at
3475
+ * - **Pause / stop / destroy gates.** Workflow, phase, and task gates are checked before
3476
+ * dispatch, and a running handler can checkpoint their folded state through
3477
+ * {@link import('./types.js').TaskControllerInterface.wait}. Because the substrate acquires
3478
+ * concurrency before this handler gate, a paused task occupies one phase slot until resume;
3479
+ * already-running siblings continue and its per-attempt timeout keeps counting. A GRACEFUL
3480
+ * `workflow.stop()` (no signal involved) is caught at
2348
3481
  * those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
2349
3482
  * HARD `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
2350
3483
  * into the run's composed signal — so it cancels the active phase Runner (and every
@@ -2361,37 +3494,61 @@ var TaskController = class {
2361
3494
  * {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
2362
3495
  * `runSignal`, so a handler observes either cause directly.
2363
3496
  * - **Re-entrant-safe.** No shared per-run mutable field: the active-Runner holder is LOCAL to
2364
- * each `#execute`, so a nested `execute` (a bound workflow-tool handler re-entering this
2365
- * instance while the outer run is suspended awaiting it) cannot clobber the outer run's state.
3497
+ * each `#execute`, so a nested application-level `execute` cannot clobber the outer run's
3498
+ * state.
2366
3499
  */
2367
- var WorkflowRunner = class {
3500
+ var WorkflowRunner = class WorkflowRunner {
3501
+ static #executions = /* @__PURE__ */ new WeakSet();
2368
3502
  #scheduler;
2369
3503
  constructor(scheduler) {
2370
3504
  this.#scheduler = scheduler;
2371
3505
  }
2372
3506
  execute(target, options) {
2373
3507
  if (this.#isWorkflow(target)) {
2374
- if (target.status !== "pending" || target.destroyed) throw new WorkflowError("TRANSITION", `workflow '${target.id}' is not drivable`, {
2375
- id: target.id,
2376
- status: target.status,
2377
- destroyed: target.destroyed
2378
- });
2379
- return this.#execute(target, options);
3508
+ const signal = options?.signal;
3509
+ const timeout = options?.timeout;
3510
+ const budget = options?.budget;
3511
+ const store = options?.store;
3512
+ this.#acquire(target);
3513
+ return this.#execute(target, signal, timeout, budget, store);
2380
3514
  }
2381
- const workflow = new Workflow(definitionToSnapshot(target, options?.bail ?? target.bail ?? false), options);
2382
- return this.#execute(workflow, options);
2383
- }
2384
- async #execute(workflow, options) {
2385
- const ms = options?.timeout;
2386
- const timeout = ms !== void 0 && ms > 0 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
2387
- timeout?.start();
2388
- options?.budget?.start();
2389
- const runSignal = this.#fold(workflow, options, timeout);
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);
3521
+ this.#acquire(workflow);
3522
+ return this.#execute(workflow, signal, timeout, budget, store);
3523
+ }
3524
+ #acquire(workflow) {
3525
+ const tasks = workflow.phases.phases().flatMap((phase) => phase.tasks.tasks());
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`, {
3527
+ id: workflow.id,
3528
+ status: workflow.status,
3529
+ destroyed: workflow.destroyed
3530
+ });
3531
+ WorkflowRunner.#executions.add(workflow);
3532
+ }
3533
+ async #execute(workflow, signal, ms, budget, store) {
2390
3534
  const holder = { runner: void 0 };
2391
- const onCancel = this.#abortActive.bind(this, holder, runSignal);
2392
- if (runSignal.aborted) onCancel();
2393
- else runSignal.addEventListener("abort", onCancel, { once: true });
3535
+ let timeout;
3536
+ let persistence;
3537
+ let runSignal;
3538
+ let onCancel;
2394
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 });
3548
+ if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
3549
+ if (this.#stoppable(workflow)) workflow.stop();
3550
+ this.#skipFrom(workflow.phases.phases(), 0);
3551
+ }
2395
3552
  let index = 0;
2396
3553
  for (;;) {
2397
3554
  const phases = workflow.phases.phases();
@@ -2405,36 +3562,53 @@ var WorkflowRunner = class {
2405
3562
  this.#haltFrom(phases, index, workflow, runSignal);
2406
3563
  break;
2407
3564
  }
2408
- if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
3565
+ if (workflow.paused) await this.#raceWait(workflow.wait(), runSignal, void 0, workflow);
2409
3566
  if (this.#cancelled(runSignal) || this.#halted(workflow)) {
2410
3567
  this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
2411
3568
  break;
2412
3569
  }
2413
- if (await this.#runPhase(workflow, phase, runSignal, holder)) {
3570
+ if (phase.status === "skipped" || phase.status === "stopped") {
3571
+ this.#skipFrom([phase], 0);
3572
+ index += 1;
3573
+ continue;
3574
+ }
3575
+ if (await this.#runPhase(workflow, phase, runSignal, holder, persistence)) {
2414
3576
  this.#skipFrom(workflow.phases.phases(), index + 1);
2415
3577
  break;
2416
3578
  }
2417
3579
  index += 1;
2418
3580
  const remaining = workflow.phases.phases();
2419
- if (index < remaining.length && !this.#cancelled(runSignal)) try {
2420
- await this.#scheduler.yield({ signal: runSignal });
2421
- } catch (error) {
2422
- if (!runSignal.aborted) throw error;
2423
- }
3581
+ if (index < remaining.length && !this.#cancelled(runSignal)) await this.#pace(runSignal);
2424
3582
  }
2425
3583
  if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
2426
3584
  else if (this.#completable(workflow)) workflow.complete();
3585
+ const durable = await persistence?.finalize();
2427
3586
  return {
2428
3587
  workflow,
2429
3588
  status: workflow.status,
2430
- results: workflow.results()
3589
+ results: workflow.results(),
3590
+ ...durable === void 0 ? {} : { durable },
3591
+ ...persistence?.fault === void 0 ? {} : { fault: persistence.fault }
2431
3592
  };
3593
+ } catch (error) {
3594
+ if (this.#stoppable(workflow)) workflow.stop();
3595
+ this.#skipFrom(workflow.phases.phases(), 0);
3596
+ await persistence?.finalize();
3597
+ throw error;
2432
3598
  } finally {
3599
+ persistence?.detach();
2433
3600
  timeout?.clear();
2434
- 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;
2435
3609
  }
2436
3610
  }
2437
- async #runPhase(workflow, phase, runSignal, holder) {
3611
+ async #runPhase(workflow, phase, runSignal, holder, persistence) {
2438
3612
  const launched = /* @__PURE__ */ new Set();
2439
3613
  const onAdd = this.#spawnAdded.bind(this, launched, holder);
2440
3614
  phase.emitter.on("add", onAdd);
@@ -2445,10 +3619,12 @@ var WorkflowRunner = class {
2445
3619
  const bail = phase.bail;
2446
3620
  const concurrency = phase.concurrency !== void 0 && phase.concurrency > 0 ? phase.concurrency : DEFAULT_PHASE_CONCURRENCY;
2447
3621
  const attempts = /* @__PURE__ */ new Map();
3622
+ for (const task of tasks) attempts.set(task.id, task.attempts);
3623
+ const owners = /* @__PURE__ */ new Map();
2448
3624
  const created = new Runner({
2449
3625
  concurrency,
2450
3626
  entries: this.#entry.bind(this),
2451
- handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts)
3627
+ handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts, owners, persistence)
2452
3628
  });
2453
3629
  holder.runner = created;
2454
3630
  try {
@@ -2457,8 +3633,11 @@ var WorkflowRunner = class {
2457
3633
  } catch {
2458
3634
  return !this.#cancelled(runSignal);
2459
3635
  } finally {
2460
- created.destroy();
2461
- holder.runner = void 0;
3636
+ try {
3637
+ await created.destroy();
3638
+ } finally {
3639
+ holder.runner = void 0;
3640
+ }
2462
3641
  }
2463
3642
  } finally {
2464
3643
  phase.emitter.off("add", onAdd);
@@ -2475,81 +3654,205 @@ var WorkflowRunner = class {
2475
3654
  holder.runner?.spawn(task);
2476
3655
  }
2477
3656
  #entry(task) {
2478
- return {
2479
- ...task.retries === void 0 ? {} : { retries: task.retries },
2480
- ...task.timeout === void 0 ? {} : { timeout: task.timeout }
2481
- };
3657
+ const retries = Math.max(0, (task.retries ?? 0) - task.attempts);
3658
+ return retries === 0 ? {} : { retries };
2482
3659
  }
2483
- #runUnit(workflow, runSignal, bail, attempts, controller) {
2484
- return this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts);
3660
+ #runUnit(workflow, runSignal, bail, attempts, owners, persistence, controller) {
3661
+ return this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts, owners, persistence);
2485
3662
  }
2486
- async #runTask(workflow, task, controller, runSignal, bail, attempts) {
2487
- const signal = this.#taskSignal(controller.signal, runSignal);
3663
+ async #runTask(workflow, task, controller, runSignal, bail, attempts, owners, persistence) {
2488
3664
  const attempt = (attempts.get(task.id) ?? 0) + 1;
2489
3665
  attempts.set(task.id, attempt);
2490
3666
  const last = attempt > Math.max(0, task.retries ?? 0);
2491
- if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
2492
- if (task.phase.paused) await this.#raceWait(() => task.phase.wait(), runSignal);
2493
- if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2494
- this.#skipCancelled(task, workflow, runSignal);
2495
- return;
2496
- }
2497
- if (task.status === "pending") task.start();
2498
- if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2499
- this.#skipCancelled(task, workflow, runSignal);
3667
+ if (task.status !== "pending" && task.status !== "running") return;
3668
+ if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
3669
+ this.#settleCancelled(task, workflow, runSignal);
2500
3670
  return;
2501
3671
  }
2502
- const handle = new TaskController(signal, task.snapshot().metadata, task.context, () => workflow.results());
3672
+ const ms = task.timeout;
3673
+ const deadline = ms !== void 0 && Number.isFinite(ms) && ms > 0 && ms <= 2147483647 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
3674
+ const signal = this.#taskSignal(task, controller.signal, runSignal, deadline);
2503
3675
  try {
2504
- const value = task.handler === void 0 ? void 0 : await task.handler(handle);
2505
- if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2506
- this.#skipCancelled(task, workflow, runSignal);
3676
+ task.start();
3677
+ if (task.attempts !== attempt) return;
3678
+ owners.set(task.id, attempt);
3679
+ deadline?.start();
3680
+ const durable = persistence === void 0 ? true : await persistence.checkpoint("attempt", task, attempt);
3681
+ if (!this.#owns(owners, task, attempt)) return;
3682
+ if (!durable) {
3683
+ if (this.#stoppable(workflow)) workflow.stop();
2507
3684
  return;
2508
3685
  }
2509
- if (signal.aborted) {
2510
- this.#timedOut(task, last);
3686
+ if (task.run !== void 0 && task.handler === void 0) {
3687
+ const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved run '${task.run}'`, {
3688
+ task: task.id,
3689
+ run: task.run
3690
+ });
3691
+ task.fail({
3692
+ origin: "handler",
3693
+ message: error.message
3694
+ });
3695
+ if (bail) throw error;
2511
3696
  return;
2512
3697
  }
2513
- task.complete(value);
2514
- } catch (error) {
2515
- if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2516
- this.#skipCancelled(task, workflow, runSignal);
3698
+ if (await this.#gate(workflow.paused ? workflow.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3699
+ if (await this.#gate(task.phase.paused ? task.phase.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3700
+ if (await this.#gate(task.paused ? task.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3701
+ if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
3702
+ this.#settleCancelled(task, workflow, runSignal);
3703
+ return;
3704
+ }
3705
+ if (task.status !== "running") return;
3706
+ const handle = new TaskController(signal, task.snapshot().metadata, task, attempt, () => workflow.results(), (input) => this.#owns(owners, task, attempt) && !signal.aborted ? task.report(input) : failure(new WorkflowError("TRANSITION", `task '${task.id}' attempt '${attempt}' no longer owns activity`, {
3707
+ task: task.id,
3708
+ attempt
3709
+ })), () => this.#owns(owners, task, attempt) && !signal.aborted && task.pulse());
3710
+ let outcome;
3711
+ try {
3712
+ outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, this.#skipping.bind(this, task, controller, runSignal));
3713
+ } catch (error) {
3714
+ if (!this.#owns(owners, task, attempt)) return;
3715
+ if (task.status !== "running" || this.#skipping(task, controller, runSignal)) {
3716
+ this.#settleCancelled(task, workflow, runSignal);
3717
+ return;
3718
+ }
3719
+ if (signal.aborted) {
3720
+ this.#timedOut(owners, task, attempt, last, bail);
3721
+ return;
3722
+ }
3723
+ this.#failed(owners, task, attempt, error, last, bail);
3724
+ return;
3725
+ }
3726
+ if (!this.#owns(owners, task, attempt)) return;
3727
+ if (!outcome[0]) {
3728
+ this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, outcome[2]);
3729
+ return;
3730
+ }
3731
+ if (task.status !== "running") return;
3732
+ if (this.#skipping(task, controller, runSignal)) {
3733
+ this.#settleCancelled(task, workflow, runSignal);
2517
3734
  return;
2518
3735
  }
2519
3736
  if (signal.aborted) {
2520
- this.#timedOut(task, last);
3737
+ this.#timedOut(owners, task, attempt, last, bail);
2521
3738
  return;
2522
3739
  }
2523
- if (!last) throw error;
2524
- task.fail(error);
2525
- if (bail) throw error;
3740
+ if (!this.#owns(owners, task, attempt)) return;
3741
+ try {
3742
+ task.complete(outcome[1]);
3743
+ } catch (error) {
3744
+ if (!this.#owns(owners, task, attempt)) return;
3745
+ if (task.status !== "running") throw error;
3746
+ this.#failed(owners, task, attempt, error, last, bail);
3747
+ }
3748
+ } finally {
3749
+ deadline?.clear();
3750
+ if (persistence !== void 0 && this.#owns(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && this.#stoppable(workflow)) workflow.stop();
3751
+ this.#revoke(owners, task.id, attempt);
2526
3752
  }
2527
3753
  }
2528
- #timedOut(task, last) {
2529
- if (!last) return;
2530
- task.fail(/* @__PURE__ */ new Error(`task '${task.id}' timed out`));
3754
+ async #gate(wait, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail) {
3755
+ const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, this.#skipping.bind(this, task, controller, runSignal), workflow, task.phase);
3756
+ return this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine);
3757
+ }
3758
+ #settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine) {
3759
+ if (attempts.get(task.id) !== attempt || !this.#owns(owners, task, attempt)) return true;
3760
+ if (signal.aborted) {
3761
+ if (genuine ?? this.#skipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
3762
+ else this.#timedOut(owners, task, attempt, last, bail);
3763
+ return true;
3764
+ }
3765
+ if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
3766
+ this.#settleCancelled(task, workflow, runSignal);
3767
+ return true;
3768
+ }
3769
+ return task.status !== "running";
3770
+ }
3771
+ async #raceHandler(handler, signal, cancelled) {
3772
+ if (signal.aborted) return [
3773
+ false,
3774
+ void 0,
3775
+ cancelled()
3776
+ ];
3777
+ const deferred = Promise.withResolvers();
3778
+ const onAbort = this.#resolveHandlerAbort.bind(this, deferred, cancelled);
3779
+ signal.addEventListener("abort", onAbort, { once: true });
3780
+ try {
3781
+ return await Promise.race([handler.then((value) => [true, value]), deferred.promise]);
3782
+ } finally {
3783
+ signal.removeEventListener("abort", onAbort);
3784
+ }
2531
3785
  }
2532
- async #raceWait(wait, runSignal) {
2533
- if (runSignal.aborted) return;
2534
- let onAbort;
2535
- const cancelled = new Promise((resolve) => {
2536
- onAbort = resolve.bind(void 0, void 0);
2537
- runSignal.addEventListener("abort", onAbort, { once: true });
3786
+ #resolveHandlerAbort(deferred, cancelled) {
3787
+ deferred.resolve([
3788
+ false,
3789
+ void 0,
3790
+ cancelled()
3791
+ ]);
3792
+ }
3793
+ #timedOut(owners, task, attempt, last, bail) {
3794
+ if (!this.#owns(owners, task, attempt)) return;
3795
+ const error = /* @__PURE__ */ new Error(`task '${task.id}' timed out`);
3796
+ if (last) task.fail({
3797
+ origin: "timeout",
3798
+ message: error.message
2538
3799
  });
3800
+ if (!last || bail) throw error;
3801
+ }
3802
+ #failed(owners, task, attempt, error, last, bail) {
3803
+ if (!this.#owns(owners, task, attempt)) return;
3804
+ if (!last) throw error;
3805
+ task.fail({
3806
+ origin: "handler",
3807
+ message: errorToMessage(error)
3808
+ });
3809
+ if (bail) throw error;
3810
+ }
3811
+ #owns(owners, task, attempt) {
3812
+ return owners.get(task.id) === attempt && task.attempts === attempt;
3813
+ }
3814
+ #revoke(owners, id, attempt) {
3815
+ if (owners.get(id) === attempt) owners.delete(id);
3816
+ }
3817
+ async #raceWait(wait, signal, cancelled, workflow, phase) {
3818
+ if (signal.aborted) return cancelled?.();
3819
+ const deferred = Promise.withResolvers();
3820
+ const onAbort = this.#resolveWaitAbort.bind(this, deferred, cancelled);
3821
+ const onTerminal = this.#resolveWaitAbort.bind(this, deferred, void 0);
3822
+ signal.addEventListener("abort", onAbort, { once: true });
3823
+ workflow?.emitter.on("skip", onTerminal);
3824
+ workflow?.emitter.on("stop", onTerminal);
3825
+ phase?.emitter.on("skip", onTerminal);
3826
+ phase?.emitter.on("stop", onTerminal);
2539
3827
  try {
2540
- await Promise.race([wait(), cancelled]);
3828
+ if (workflow !== void 0 && this.#halted(workflow, phase)) deferred.resolve(void 0);
3829
+ const outcome = await Promise.race([wait, deferred.promise]);
3830
+ return typeof outcome === "boolean" ? outcome : void 0;
2541
3831
  } finally {
2542
- if (onAbort !== void 0) runSignal.removeEventListener("abort", onAbort);
3832
+ signal.removeEventListener("abort", onAbort);
3833
+ workflow?.emitter.off("skip", onTerminal);
3834
+ workflow?.emitter.off("stop", onTerminal);
3835
+ phase?.emitter.off("skip", onTerminal);
3836
+ phase?.emitter.off("stop", onTerminal);
2543
3837
  }
2544
3838
  }
2545
- #taskSignal(unitSignal, runSignal) {
2546
- return AbortSignal.any([unitSignal, runSignal]);
3839
+ #resolveWaitAbort(deferred, cancelled) {
3840
+ deferred.resolve(cancelled?.());
3841
+ }
3842
+ #taskSignal(task, unitSignal, runSignal, timeout) {
3843
+ const signals = [
3844
+ task.signal,
3845
+ unitSignal,
3846
+ runSignal
3847
+ ];
3848
+ if (timeout !== void 0) signals.push(timeout.signal);
3849
+ return AbortSignal.any(signals);
2547
3850
  }
2548
- #fold(workflow, options, timeout) {
3851
+ #fold(workflow, signal, budget, timeout) {
2549
3852
  const signals = [workflow.signal];
2550
- if (options?.signal !== void 0) signals.push(options.signal);
3853
+ if (signal !== void 0) signals.push(signal);
2551
3854
  if (timeout !== void 0) signals.push(timeout.signal);
2552
- if (options?.budget !== void 0) signals.push(options.budget.signal);
3855
+ if (budget !== void 0) signals.push(budget.signal);
2553
3856
  return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
2554
3857
  }
2555
3858
  #haltFrom(phases, index, workflow, runSignal) {
@@ -2563,22 +3866,22 @@ var WorkflowRunner = class {
2563
3866
  for (const task of phase.tasks.tasks()) this.#skip(task);
2564
3867
  }
2565
3868
  }
2566
- #skipCancelled(task, workflow, runSignal) {
3869
+ #settleCancelled(task, workflow, runSignal) {
2567
3870
  if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2568
3871
  this.#skip(task);
2569
3872
  }
2570
3873
  #skip(task) {
2571
3874
  if (task.status === "pending" || task.status === "running") task.skip();
2572
3875
  }
2573
- #skipping(controller, runSignal) {
2574
- return controller.aborted || runSignal.aborted;
3876
+ #skipping(task, controller, runSignal) {
3877
+ return task.signal.aborted || controller.aborted || runSignal.aborted;
2575
3878
  }
2576
3879
  #cancelled(runSignal) {
2577
3880
  return runSignal.aborted;
2578
3881
  }
2579
- #halted(workflow) {
3882
+ #halted(workflow, phase) {
2580
3883
  const status = workflow.status;
2581
- return status === "failed" || status === "skipped" || status === "stopped";
3884
+ return status === "failed" || status === "skipped" || status === "stopped" || phase?.status === "skipped" || phase?.status === "stopped";
2582
3885
  }
2583
3886
  #stoppable(workflow) {
2584
3887
  const status = workflow.status;
@@ -2612,7 +3915,7 @@ var WorkflowRunner = class {
2612
3915
  *
2613
3916
  * @example
2614
3917
  * ```ts
2615
- * import { createWorkflowContract } from '@src/core'
3918
+ * import { createWorkflowContract } from '@orkestrel/workflow'
2616
3919
  *
2617
3920
  * const contract = createWorkflowContract()
2618
3921
  * const definition = contract.generate() // a valid WorkflowDefinition
@@ -2641,8 +3944,8 @@ function createWorkflowContract() {
2641
3944
  *
2642
3945
  * `options.functions` is the {@link import('./types.js').WorkflowFunctions} registry each live
2643
3946
  * task's `run` name resolves against ONCE at construction into its runtime
2644
- * {@link import('./types.js').TaskInterface.handler} a name omitted or absent from the
2645
- * registry resolves to no handler (the no-handler rule).
3947
+ * {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
3948
+ * an unresolved present name remains inspectable but is rejected if execution is attempted.
2646
3949
  *
2647
3950
  * @param definition - The workflow definition to bring to life
2648
3951
  * @param options - Runtime options (initial listeners, `bail` override, per-node options)
@@ -2650,7 +3953,7 @@ function createWorkflowContract() {
2650
3953
  *
2651
3954
  * @example
2652
3955
  * ```ts
2653
- * import { createWorkflow } from '@src/core'
3956
+ * import { createWorkflow } from '@orkestrel/workflow'
2654
3957
  *
2655
3958
  * const workflow = createWorkflow(definition, { on: { complete: () => done() } })
2656
3959
  * const phase = workflow.phase('phase-build')
@@ -2658,7 +3961,8 @@ function createWorkflowContract() {
2658
3961
  * ```
2659
3962
  */
2660
3963
  function createWorkflow(definition, options) {
2661
- 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);
2662
3966
  }
2663
3967
  /**
2664
3968
  * Rebuild an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
@@ -2676,6 +3980,9 @@ function createWorkflow(definition, options) {
2676
3980
  * still wins when supplied (to deliberately re-run under a different policy). A structurally
2677
3981
  * invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
2678
3982
  * non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
3983
+ * Runtime handlers are optional: without a matching `functions` entry, a persisted `run`
3984
+ * remains visible with an undefined `handler` so the exact state is inspectable. The runner
3985
+ * rejects that unresolved tree if execution is attempted.
2679
3986
  *
2680
3987
  * @param snapshot - The snapshot to restore (carries its own `bail` + `override`)
2681
3988
  * @param options - Runtime options (initial listeners, an optional `bail` override, per-node options)
@@ -2683,15 +3990,35 @@ function createWorkflow(definition, options) {
2683
3990
  *
2684
3991
  * @example
2685
3992
  * ```ts
2686
- * import { restoreWorkflow } from '@src/core'
3993
+ * import { restoreWorkflow } from '@orkestrel/workflow'
2687
3994
  *
2688
3995
  * const restored = restoreWorkflow(workflow.snapshot()) // bail comes from the snapshot
2689
3996
  * restored.status === workflow.status // true
2690
3997
  * ```
2691
3998
  */
2692
3999
  function restoreWorkflow(snapshot, options) {
2693
- assertSnapshot(snapshot);
2694
- return new Workflow(snapshot, options);
4000
+ const captured = captureWorkflowOptions(options);
4001
+ return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
4002
+ }
4003
+ /**
4004
+ * Rebuild an interrupted workflow at its remaining retry budget.
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
+ *
4011
+ * @param snapshot - The hostile persisted snapshot
4012
+ * @param options - Runtime handlers and entity options
4013
+ * @returns A recoverable live workflow
4014
+ */
4015
+ function recoverWorkflow(snapshot, options) {
4016
+ const captured = captureWorkflowOptions(options);
4017
+ const owned = cloneWorkflowSnapshot(snapshot);
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 });
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;
2695
4022
  }
2696
4023
  /**
2697
4024
  * Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
@@ -2714,54 +4041,7 @@ function restoreWorkflow(snapshot, options) {
2714
4041
  * @param snapshot - The snapshot to validate
2715
4042
  */
2716
4043
  function assertSnapshot(snapshot) {
2717
- if (typeof snapshot.bail !== "boolean") throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has a non-boolean bail`, {
2718
- workflow: snapshot.id,
2719
- bail: snapshot.bail
2720
- });
2721
- if (!WORKFLOW_STATUSES.includes(snapshot.status)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid status`, {
2722
- workflow: snapshot.id,
2723
- status: snapshot.status
2724
- });
2725
- if (snapshot.override !== void 0 && !WORKFLOW_STATUSES.includes(snapshot.override)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid override`, {
2726
- workflow: snapshot.id,
2727
- override: snapshot.override
2728
- });
2729
- for (const phase of snapshot.phases) {
2730
- if (typeof phase.bail !== "boolean") throw new WorkflowError("RESTORE", `phase '${phase.id}' has a non-boolean bail`, {
2731
- phase: phase.id,
2732
- bail: phase.bail
2733
- });
2734
- if (!PHASE_STATUSES.includes(phase.status)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid status`, {
2735
- phase: phase.id,
2736
- status: phase.status
2737
- });
2738
- if (phase.override !== void 0 && !PHASE_STATUSES.includes(phase.override)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid override`, {
2739
- phase: phase.id,
2740
- override: phase.override
2741
- });
2742
- if (phase.concurrency !== void 0 && (!Number.isInteger(phase.concurrency) || phase.concurrency < 1)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid concurrency`, {
2743
- phase: phase.id,
2744
- concurrency: phase.concurrency
2745
- });
2746
- for (const task of phase.tasks) {
2747
- if (!TASK_STATUSES.includes(task.status)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid status`, {
2748
- task: task.id,
2749
- status: task.status
2750
- });
2751
- if (task.run !== void 0 && task.run.length < 1) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid run`, {
2752
- task: task.id,
2753
- run: task.run
2754
- });
2755
- if (task.retries !== void 0 && (!Number.isInteger(task.retries) || task.retries < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid retries`, {
2756
- task: task.id,
2757
- retries: task.retries
2758
- });
2759
- if (task.timeout !== void 0 && (!Number.isInteger(task.timeout) || task.timeout < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid timeout`, {
2760
- task: task.id,
2761
- timeout: task.timeout
2762
- });
2763
- }
2764
- }
4044
+ cloneWorkflowSnapshot(snapshot);
2765
4045
  }
2766
4046
  /**
2767
4047
  * Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
@@ -2770,7 +4050,7 @@ function assertSnapshot(snapshot) {
2770
4050
  *
2771
4051
  * @remarks
2772
4052
  * The snapshot analogue of the server package's `createMemorySessionStore`
2773
- * (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
2774
4054
  * options bag (AGENTS §21 minimal): a persisted run-state lives until an explicit `delete`. This is
2775
4055
  * the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
2776
4056
  * {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
@@ -2782,7 +4062,7 @@ function assertSnapshot(snapshot) {
2782
4062
  *
2783
4063
  * @example
2784
4064
  * ```ts
2785
- * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@src/core'
4065
+ * import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
2786
4066
  *
2787
4067
  * const store = createMemoryWorkflowStore()
2788
4068
  * const workflow = createWorkflow(definition)
@@ -2802,7 +4082,7 @@ function createMemoryWorkflowStore() {
2802
4082
  * @remarks
2803
4083
  * Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver, the snapshot
2804
4084
  * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
2805
- * `rawShape` (a JSON blob), exactly as {@link createDatabaseQueueStore} stores its `input`. The
4085
+ * `rawShape` (a JSON blob), exactly as `createDatabaseQueueStore` stores its `input`. The
2806
4086
  * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
2807
4087
  * AND keeps the row type FLAT — a structured multi-column snapshot table would force the contract to
2808
4088
  * `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
@@ -2818,7 +4098,8 @@ function createMemoryWorkflowStore() {
2818
4098
  *
2819
4099
  * @example
2820
4100
  * ```ts
2821
- * import { createDatabaseWorkflowStore, createMemoryDriver, createWorkflow, restoreWorkflow } from '@src/core'
4101
+ * import { createMemoryDriver } from '@orkestrel/database'
4102
+ * import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
2822
4103
  *
2823
4104
  * const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
2824
4105
  * const workflow = createWorkflow(definition)
@@ -2828,12 +4109,13 @@ function createMemoryWorkflowStore() {
2828
4109
  * ```
2829
4110
  */
2830
4111
  function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
4112
+ const columns = {
4113
+ id: (0, _orkestrel_contract.stringShape)(),
4114
+ snapshot: (0, _orkestrel_contract.rawShape)({})
4115
+ };
2831
4116
  return new DatabaseWorkflowStore((0, _orkestrel_database.createDatabase)({
2832
4117
  driver,
2833
- tables: { snapshots: {
2834
- id: (0, _orkestrel_contract.stringShape)(),
2835
- snapshot: (0, _orkestrel_contract.rawShape)({})
2836
- } }
4118
+ tables: { snapshots: columns }
2837
4119
  }).table("snapshots"));
2838
4120
  }
2839
4121
  /**
@@ -2843,7 +4125,7 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
2843
4125
  *
2844
4126
  * @remarks
2845
4127
  * The runner is a PURE engine — it re-implements no concurrency / retry / abort logic, AND it
2846
- * carries no `functions` / `tools` / `agents` registry of its own: each live task already
4128
+ * carries no behavior or provider registry of its own: each live task already
2847
4129
  * resolved its own {@link import('./types.js').WorkflowFunction} into
2848
4130
  * {@link import('./types.js').TaskInterface.handler} ONCE at construction, from the
2849
4131
  * {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
@@ -2857,11 +4139,10 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
2857
4139
  * the live entity (`start` → `complete` / `fail`), and resolves a
2858
4140
  * {@link import('./types.js').WorkflowResult}.
2859
4141
  *
2860
- * Static tool / agent calling is OPT-IN: a caller wires a plain
2861
- * {@link import('./types.js').WorkflowFunction} into its OWN {@link WorkflowOptions.functions}
2862
- * registry, same as any other behavior the `@orkestrel/tool` package ships the
2863
- * tool/agent adapter factories for that. A task with no resolved handler AUTO-COMPLETES
2864
- * (the ROADMAP no-handler rule).
4142
+ * External integrations remain application-owned: a caller wires an ordinary
4143
+ * {@link import('./types.js').WorkflowFunction} into its own {@link WorkflowOptions.functions}
4144
+ * registry. Only a task that omits `run` auto-completes; unresolved named work is rejected
4145
+ * before dispatch.
2865
4146
  *
2866
4147
  * @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
2867
4148
  * See {@link WorkflowRunnerOptions}.
@@ -2869,7 +4150,7 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
2869
4150
  *
2870
4151
  * @example
2871
4152
  * ```ts
2872
- * import { createWorkflowRunner } from '@src/core'
4153
+ * import { createWorkflowRunner } from '@orkestrel/workflow'
2873
4154
  *
2874
4155
  * const runner = createWorkflowRunner()
2875
4156
  * const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
@@ -2905,7 +4186,7 @@ function createWorkflowRunner(options) {
2905
4186
  *
2906
4187
  * @example
2907
4188
  * ```ts
2908
- * import { createMemoryWorkflowStore, createWorkflowManager } from '@src/core'
4189
+ * import { createMemoryWorkflowStore, createWorkflowManager } from '@orkestrel/workflow'
2909
4190
  *
2910
4191
  * const manager = createWorkflowManager({
2911
4192
  * store: createMemoryWorkflowStore(),
@@ -2928,7 +4209,8 @@ function createWorkflowManager(options) {
2928
4209
  * `yield()` gives the host a turn via a zero-delay macrotask (so pending I/O,
2929
4210
  * timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
2930
4211
  * after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
2931
- * 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.
2932
4214
  * `options.priority` is accepted for contract compliance but treated uniformly by
2933
4215
  * this default — environment backends honour it.
2934
4216
  *
@@ -2936,7 +4218,8 @@ function createWorkflowManager(options) {
2936
4218
  *
2937
4219
  * @example
2938
4220
  * ```ts
2939
- * import { createAbort, createScheduler } from '@src/core'
4221
+ * import { createAbort } from '@orkestrel/abort'
4222
+ * import { createScheduler } from '@orkestrel/workflow'
2940
4223
  *
2941
4224
  * const abort = createAbort()
2942
4225
  * const scheduler = createScheduler()
@@ -2950,7 +4233,7 @@ function createWorkflowManager(options) {
2950
4233
  *
2951
4234
  * @example
2952
4235
  * ```ts
2953
- * import { createScheduler } from '@src/core'
4236
+ * import { createScheduler } from '@orkestrel/workflow'
2954
4237
  *
2955
4238
  * // A backoff: wait a growing interval between retries.
2956
4239
  * const scheduler = createScheduler()
@@ -2992,7 +4275,7 @@ function createScheduler() {
2992
4275
  *
2993
4276
  * @example
2994
4277
  * ```ts
2995
- * import { createRunner } from '@src/core'
4278
+ * import { createRunner } from '@orkestrel/workflow'
2996
4279
  *
2997
4280
  * // A handler that fans out one sibling per declared unit, then returns its own value.
2998
4281
  * const runner = createRunner<number, number>({
@@ -3015,6 +4298,7 @@ exports.Controller = Controller;
3015
4298
  exports.DEFAULT_BAIL = DEFAULT_BAIL;
3016
4299
  exports.DEFAULT_PHASE_CONCURRENCY = DEFAULT_PHASE_CONCURRENCY;
3017
4300
  exports.DatabaseWorkflowStore = DatabaseWorkflowStore;
4301
+ exports.MAX_TIMER_MS = MAX_TIMER_MS;
3018
4302
  exports.MemoryWorkflowStore = MemoryWorkflowStore;
3019
4303
  exports.PHASE_STATUSES = PHASE_STATUSES;
3020
4304
  exports.Phase = Phase;
@@ -3031,12 +4315,16 @@ exports.WORKFLOW_STATUSES = WORKFLOW_STATUSES;
3031
4315
  exports.Workflow = Workflow;
3032
4316
  exports.WorkflowError = WorkflowError;
3033
4317
  exports.WorkflowManager = WorkflowManager;
4318
+ exports.WorkflowPersistence = WorkflowPersistence;
3034
4319
  exports.WorkflowRunner = WorkflowRunner;
3035
4320
  exports.assertSnapshot = assertSnapshot;
3036
4321
  exports.buildPhaseContext = buildPhaseContext;
3037
4322
  exports.buildTaskContext = buildTaskContext;
3038
4323
  exports.buildWorkflowContext = buildWorkflowContext;
3039
4324
  exports.canTransitionTask = canTransitionTask;
4325
+ exports.captureWorkflowOptions = captureWorkflowOptions;
4326
+ exports.cloneTaskActivity = cloneTaskActivity;
4327
+ exports.cloneWorkflowSnapshot = cloneWorkflowSnapshot;
3040
4328
  exports.collectResults = collectResults;
3041
4329
  exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
3042
4330
  exports.createDeferred = createDeferred;
@@ -3051,22 +4339,36 @@ exports.definitionToSnapshot = definitionToSnapshot;
3051
4339
  exports.deriveBoundary = deriveBoundary;
3052
4340
  exports.derivePhaseStatus = derivePhaseStatus;
3053
4341
  exports.deriveWorkflowStatus = deriveWorkflowStatus;
4342
+ exports.errorToMessage = errorToMessage;
3054
4343
  exports.failure = failure;
3055
4344
  exports.findFailure = findFailure;
4345
+ exports.hasWorkflowHandlers = hasWorkflowHandlers;
3056
4346
  exports.insertEntry = insertEntry;
4347
+ exports.isLifecycleStatus = isLifecycleStatus;
4348
+ exports.isOwnedWorkflowSnapshot = isOwnedWorkflowSnapshot;
4349
+ exports.isTaskActivity = isTaskActivity;
4350
+ exports.isTaskActivityInput = isTaskActivityInput;
4351
+ exports.isTaskFailure = isTaskFailure;
4352
+ exports.isTaskResult = isTaskResult;
3057
4353
  exports.isTerminalStatus = isTerminalStatus;
3058
4354
  exports.isWorkflowError = isWorkflowError;
3059
4355
  exports.isWorkflowSnapshot = isWorkflowSnapshot;
4356
+ exports.matchesDescription = matchesDescription;
3060
4357
  exports.moveEntry = moveEntry;
3061
4358
  exports.parkSignal = parkSignal;
3062
4359
  exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
3063
4360
  exports.phaseShape = phaseShape;
3064
4361
  exports.phaseUpdateShape = phaseUpdateShape;
4362
+ exports.recoverWorkflow = recoverWorkflow;
4363
+ exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
4364
+ exports.resolveTaskSilence = resolveTaskSilence;
3065
4365
  exports.restoreWorkflow = restoreWorkflow;
4366
+ exports.scheduleHost = scheduleHost;
3066
4367
  exports.success = success;
3067
4368
  exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
3068
4369
  exports.taskShape = taskShape;
3069
4370
  exports.taskUpdateShape = taskUpdateShape;
3070
4371
  exports.workflowShape = workflowShape;
4372
+ exports.workflowSnapshotContext = workflowSnapshotContext;
3071
4373
 
3072
4374
  //# sourceMappingURL=index.cjs.map