@orkestrel/workflow 0.0.6 → 0.0.8

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.
@@ -65,17 +65,18 @@ var Scheduler = class {
65
65
  #sleep(ms, signal) {
66
66
  if (signal?.aborted === true) return Promise.reject(signal.reason);
67
67
  return new Promise((resolve, reject) => {
68
- const onAbort = () => {
69
- clearTimeout(handle);
70
- reject(signal?.reason);
71
- };
72
68
  const handle = setTimeout(() => {
73
69
  signal?.removeEventListener("abort", onAbort);
74
70
  resolve();
75
71
  }, ms);
72
+ const onAbort = this.#abort.bind(this, handle, reject, signal);
76
73
  signal?.addEventListener("abort", onAbort, { once: true });
77
74
  });
78
75
  }
76
+ #abort(handle, reject, signal) {
77
+ clearTimeout(handle);
78
+ reject(signal?.reason);
79
+ }
79
80
  };
80
81
  //#endregion
81
82
  //#region src/core/constants.ts
@@ -178,21 +179,20 @@ var TASK_TRANSITIONS = Object.freeze({
178
179
  * phase may still DECLARE a larger explicit `concurrency` and pays that allocation knowingly.
179
180
  */
180
181
  var DEFAULT_PHASE_CONCURRENCY = 1024;
182
+ /**
183
+ * The largest delay representable by the host timer APIs without overflow or clamping.
184
+ */
185
+ var MAX_TIMER_MS = 2147483647;
181
186
  //#endregion
182
187
  //#region src/core/errors.ts
183
188
  /**
184
- * An error thrown by the workflow entity + W-c2 recursion layer.
189
+ * An error raised by the workflow runtime.
185
190
  *
186
191
  * @remarks
187
192
  * Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
188
- * offending node id / status. Thrown for an illegal lifecycle transition
193
+ * offending node id / status. Raised for an illegal lifecycle transition
189
194
  * (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
190
- * passed to {@link import('./factories.js').restoreWorkflow} (`RESTORE`), an over-deep /
191
- * cyclic nested-workflow dispatch (`DEPTH`), and a malformed workflow-authoring-tool args
192
- * blob (`TOOL`). `DEPTH` and `TOOL` are public type surface constructed by the
193
- * `@orkestrel/tool` package's workflow-tool / agent-function adapters; on that seam the
194
- * throw is ISOLATED by its `ToolManager` into the tool result's top-level `error`
195
- * (AGENTS §14 — the universal tool-handler contract).
195
+ * boundary (`RESTORE`), or a refused structural/activity edit (`MUTATION`).
196
196
  */
197
197
  var WorkflowError = class extends Error {
198
198
  code;
@@ -201,7 +201,7 @@ var WorkflowError = class extends Error {
201
201
  super(message);
202
202
  this.name = "WorkflowError";
203
203
  this.code = code;
204
- this.context = context;
204
+ if (context !== void 0) this.context = context;
205
205
  }
206
206
  };
207
207
  /**
@@ -220,7 +220,11 @@ var WorkflowError = class extends Error {
220
220
  * ```
221
221
  */
222
222
  function isWorkflowError(value) {
223
- return value instanceof WorkflowError;
223
+ try {
224
+ return value instanceof WorkflowError;
225
+ } catch {
226
+ return false;
227
+ }
224
228
  }
225
229
  //#endregion
226
230
  //#region src/core/helpers.ts
@@ -357,6 +361,17 @@ function canTransitionTask(from, to) {
357
361
  return TASK_TRANSITIONS[from].includes(to);
358
362
  }
359
363
  /**
364
+ * Resolve a task's runtime silence window against its workflow default.
365
+ *
366
+ * @param value - The task-level override; any present non-positive or non-finite value disables
367
+ * @param fallback - The workflow-level default
368
+ * @returns A host-safe effective window (`1..MAX_TIMER_MS`), or `undefined`
369
+ */
370
+ function resolveTaskSilence(value, fallback) {
371
+ if (value !== void 0) return Number.isFinite(value) && value > 0 && value <= 2147483647 ? value : void 0;
372
+ return fallback !== void 0 && Number.isFinite(fallback) && fallback > 0 && fallback <= 2147483647 ? fallback : void 0;
373
+ }
374
+ /**
360
375
  * Box a value as a {@link Success} — the graceful outcome half of a {@link Result}.
361
376
  *
362
377
  * @typeParam T - The boxed value's type
@@ -393,6 +408,20 @@ function failure(error) {
393
408
  };
394
409
  }
395
410
  /**
411
+ * Normalize an unknown thrown value to a non-empty persistence-safe message.
412
+ *
413
+ * @param error - The caught value
414
+ * @returns A non-empty message without stack or cause data
415
+ */
416
+ function errorToMessage(error) {
417
+ try {
418
+ const message = error instanceof Error ? error.message : String(error);
419
+ return typeof message === "string" && message.length > 0 ? message : "unknown failure";
420
+ } catch {
421
+ return "unknown failure";
422
+ }
423
+ }
424
+ /**
396
425
  * Find the first {@link TaskResult} in a positional list whose boxed outcome is a
397
426
  * `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
398
427
  * `fail`-event lookup.
@@ -429,11 +458,11 @@ function findFailure(results) {
429
458
  * @returns The {@link WorkflowContext}
430
459
  */
431
460
  function buildWorkflowContext(node) {
432
- return {
461
+ return Object.freeze({
433
462
  id: node.id,
434
463
  name: node.name,
435
464
  ...node.description === void 0 ? {} : { description: node.description }
436
- };
465
+ });
437
466
  }
438
467
  /**
439
468
  * Build a {@link PhaseContext} — a phase's own identity plus a back-reference to its
@@ -444,10 +473,10 @@ function buildWorkflowContext(node) {
444
473
  * @returns The {@link PhaseContext}
445
474
  */
446
475
  function buildPhaseContext(workflow, node) {
447
- return {
476
+ return Object.freeze({
448
477
  ...buildWorkflowContext(node),
449
- workflow
450
- };
478
+ workflow: buildWorkflowContext(workflow)
479
+ });
451
480
  }
452
481
  /**
453
482
  * Build a {@link TaskContext} — a task's own identity plus a back-reference to its phase
@@ -459,31 +488,10 @@ function buildPhaseContext(workflow, node) {
459
488
  * @returns The {@link TaskContext}
460
489
  */
461
490
  function buildTaskContext(phase, node) {
462
- return {
491
+ return Object.freeze({
463
492
  ...buildWorkflowContext(node),
464
- phase
465
- };
466
- }
467
- /**
468
- * Narrow an `unknown` to a {@link WorkflowSnapshot} — the AGENTS §14 boundary guard for an
469
- * UNTRUSTED snapshot read (a storage row a {@link import('./stores/DatabaseWorkflowStore.js').DatabaseWorkflowStore}
470
- * reads back from its opaque JSON column, a snapshot loaded from disk).
471
- *
472
- * @remarks
473
- * A total guard (it NEVER throws — adversarial input returns `false`, AGENTS §14). It checks the
474
- * snapshot's SHAPE — `id` / `name` / `status` strings, a `boolean` `bail`, an array of `phases`,
475
- * `created` / `updated` numbers — enough to safely impose the {@link WorkflowSnapshot} type at a
476
- * storage boundary WITHOUT a cast. It is complementary to
477
- * {@link import('./factories.js').assertSnapshot}, which validates the DEEPER invariant (every
478
- * node's status / override drawn from the lifecycle vocabulary) and THROWS a `RESTORE`
479
- * {@link import('./errors.js').WorkflowError} — the deep gate a {@link import('./factories.js').restoreWorkflow}
480
- * applies. A boundary read narrows shape with this guard; a restore validates vocabulary with `assertSnapshot`.
481
- *
482
- * @param value - The value to test (an opaque storage read)
483
- * @returns `true` when `value` has the structural shape of a {@link WorkflowSnapshot}
484
- */
485
- function isWorkflowSnapshot(value) {
486
- 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);
493
+ phase: buildPhaseContext(phase.workflow, phase)
494
+ });
487
495
  }
488
496
  /**
489
497
  * Convert a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
@@ -573,12 +581,91 @@ function taskDefinitionToSnapshot(task) {
573
581
  ...task.description === void 0 ? {} : { description: task.description },
574
582
  status: "pending",
575
583
  metadata: {},
584
+ attempts: 0,
576
585
  ...task.run === void 0 ? {} : { run: task.run },
577
586
  ...task.retries === void 0 ? {} : { retries: task.retries },
578
587
  ...task.timeout === void 0 ? {} : { timeout: task.timeout }
579
588
  };
580
589
  }
581
590
  /**
591
+ * Convert interrupted running work into a recoverable pending suffix or an
592
+ * exhausted recovery failure without replenishing attempts.
593
+ *
594
+ * @param snapshot - A fully validated owned snapshot with no terminal overrides
595
+ * @returns The recovery projection
596
+ */
597
+ function recoverWorkflowSnapshot(snapshot) {
598
+ const phases = [];
599
+ let halted = false;
600
+ const now = Math.max(Date.now(), snapshot.updated);
601
+ const workflow = buildWorkflowContext(snapshot);
602
+ for (const phase of snapshot.phases) {
603
+ const exhausted = /* @__PURE__ */ new Set();
604
+ for (const task of phase.tasks) {
605
+ const budget = (task.retries ?? 0) + 1;
606
+ if (task.status === "running" && task.attempts >= budget) exhausted.add(task.id);
607
+ }
608
+ const strict = phase.bail && (exhausted.size > 0 || phase.tasks.some((task) => task.status === "failed"));
609
+ const tasks = [];
610
+ for (const task of phase.tasks) {
611
+ const eligible = task.status === "pending" || task.status === "running";
612
+ if ((halted || strict) && eligible && !exhausted.has(task.id)) {
613
+ tasks.push({
614
+ ...task,
615
+ status: "skipped"
616
+ });
617
+ continue;
618
+ }
619
+ if (!exhausted.has(task.id)) {
620
+ if (task.status === "running") {
621
+ const { activity: _activity, ...pending } = task;
622
+ tasks.push({
623
+ ...pending,
624
+ status: "pending"
625
+ });
626
+ } else tasks.push(task);
627
+ continue;
628
+ }
629
+ const phaseContext = buildPhaseContext(workflow, phase);
630
+ const result = {
631
+ task: buildTaskContext(phaseContext, task),
632
+ phase: phaseContext,
633
+ workflow,
634
+ status: "failed",
635
+ result: {
636
+ success: false,
637
+ error: {
638
+ origin: "recovery",
639
+ message: `task '${task.id}' exhausted its retry budget during recovery`
640
+ }
641
+ },
642
+ timestamp: now
643
+ };
644
+ tasks.push({
645
+ ...task,
646
+ status: "failed",
647
+ result
648
+ });
649
+ }
650
+ const status = derivePhaseStatus(tasks.map((task) => task.status));
651
+ phases.push({
652
+ ...phase,
653
+ status,
654
+ tasks
655
+ });
656
+ if (strict) halted = true;
657
+ }
658
+ return {
659
+ ...snapshot,
660
+ status: deriveWorkflowStatus(phases.map((phase) => ({
661
+ status: phase.status,
662
+ bail: phase.bail
663
+ }))),
664
+ phases,
665
+ updated: now
666
+ };
667
+ }
668
+ /**
582
669
  * Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
583
670
  * — the workflow tier of the result tree, built from each phase's `results()`.
584
671
  *
@@ -661,16 +748,7 @@ function moveEntry(entries, key, index) {
661
748
  * @returns A deferred `promise` plus its `resolve` / `reject`
662
749
  */
663
750
  function createDeferred() {
664
- let resolve = () => {};
665
- let reject = () => {};
666
- return {
667
- promise: new Promise((res, rej) => {
668
- resolve = res;
669
- reject = rej;
670
- }),
671
- resolve,
672
- reject
673
- };
751
+ return Promise.withResolvers();
674
752
  }
675
753
  /**
676
754
  * Park until `signal` aborts — a promise-parked wait (AGENTS §21), never a timer or
@@ -700,6 +778,302 @@ function parkSignal(signal) {
700
778
  });
701
779
  }
702
780
  //#endregion
781
+ //#region src/core/validators.ts
782
+ /** Test the workflow lifecycle vocabulary. */
783
+ function isLifecycleStatus(value) {
784
+ return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
785
+ }
786
+ /** Test a normalized persisted task failure. */
787
+ function isTaskFailure(value) {
788
+ try {
789
+ 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);
790
+ } catch {
791
+ return false;
792
+ }
793
+ }
794
+ /** Compare two optional description values. */
795
+ function matchesDescription(left, right) {
796
+ return left === right && (left === void 0 || typeof left === "string");
797
+ }
798
+ /** Test a result's lineage against its containing snapshot nodes. */
799
+ function isTaskResult(value, workflow, phase, task) {
800
+ try {
801
+ 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;
802
+ 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;
803
+ 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;
804
+ 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;
805
+ 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);
806
+ 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);
807
+ return false;
808
+ } catch {
809
+ return false;
810
+ }
811
+ }
812
+ /**
813
+ * Validate a safe owned JSON graph as a coherent workflow snapshot.
814
+ *
815
+ * @remarks
816
+ * Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
817
+ * graph first so this semantic pass never observes accessors or prototypes.
818
+ */
819
+ function isOwnedWorkflowSnapshot(value) {
820
+ try {
821
+ 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;
822
+ const phaseIds = /* @__PURE__ */ new Set();
823
+ const derivations = [];
824
+ let frontier = false;
825
+ let running = false;
826
+ let vacuous = true;
827
+ for (const phase of value.phases) {
828
+ 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;
829
+ const forced = phase.override === "skipped" || phase.override === "stopped";
830
+ const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
831
+ if (!forced && frontier && started || phase.status === "running" && running) return false;
832
+ if (phase.status === "running") running = true;
833
+ if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
834
+ phaseIds.add(phase.id);
835
+ const taskIds = /* @__PURE__ */ new Set();
836
+ const statuses = [];
837
+ if (phase.tasks.length > 0) vacuous = false;
838
+ for (const task of phase.tasks) {
839
+ 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;
840
+ const budget = (task.retries ?? 0) + 1;
841
+ if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
842
+ if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
843
+ if (task.status === "running" || task.status === "completed" || task.status === "failed") {
844
+ if (task.attempts < 1 || task.activity === void 0) return false;
845
+ }
846
+ if (task.status === "pending" && task.activity !== void 0) return false;
847
+ if (task.status === "completed" || task.status === "failed") {
848
+ if (!isTaskResult(task.result, value, phase, task)) return false;
849
+ } else if (task.result !== void 0) return false;
850
+ taskIds.add(task.id);
851
+ statuses.push(task.status);
852
+ }
853
+ const derived = derivePhaseStatus(statuses);
854
+ if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
855
+ derivations.push({
856
+ status: phase.status,
857
+ bail: phase.bail
858
+ });
859
+ }
860
+ const derived = deriveWorkflowStatus(derivations);
861
+ if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
862
+ return value.status === (value.override ?? derived);
863
+ } catch {
864
+ return false;
865
+ }
866
+ }
867
+ /** Total hostile-boundary workflow snapshot guard. */
868
+ function isWorkflowSnapshot(value) {
869
+ const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
870
+ return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
871
+ }
872
+ /** Test that every present behavior reference resolves before dispatch. */
873
+ function hasWorkflowHandlers(snapshot, functions) {
874
+ for (const phase of snapshot.phases) for (const task of phase.tasks) if (task.run !== void 0 && functions?.[task.run] === void 0) return false;
875
+ return true;
876
+ }
877
+ /** Locate the nearest identifiable node for an inconsistent owned snapshot. */
878
+ function workflowSnapshotContext(value) {
879
+ if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
880
+ for (const phase of value.phases) {
881
+ if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
882
+ const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
883
+ 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;
884
+ for (const task of phase.tasks) {
885
+ if (!(0, _orkestrel_contract.isRecord)(task)) continue;
886
+ 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 {
887
+ ...phaseContext ?? {},
888
+ ...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
889
+ };
890
+ }
891
+ }
892
+ }
893
+ /**
894
+ * Test whether an unknown value is a valid whole-frame activity report.
895
+ */
896
+ function isTaskActivityInput(value) {
897
+ try {
898
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
899
+ const prototype = Object.getPrototypeOf(value);
900
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
901
+ const note = value.note;
902
+ const progress = value.progress;
903
+ const operations = value.operations;
904
+ const constraints = value.constraints;
905
+ if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
906
+ if (progress !== void 0) {
907
+ if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
908
+ const progressPrototype = Object.getPrototypeOf(progress);
909
+ if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
910
+ const current = progress.current;
911
+ const total = progress.total;
912
+ const unit = progress.unit;
913
+ 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;
914
+ }
915
+ if (operations !== void 0) {
916
+ if (!(0, _orkestrel_contract.isArray)(operations)) return false;
917
+ const ids = /* @__PURE__ */ new Set();
918
+ for (const operation of operations) {
919
+ if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
920
+ const operationPrototype = Object.getPrototypeOf(operation);
921
+ if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
922
+ const id = operation.id;
923
+ const name = operation.name;
924
+ const started = operation.started;
925
+ if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
926
+ ids.add(id);
927
+ }
928
+ }
929
+ if (constraints !== void 0) {
930
+ if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
931
+ const ids = /* @__PURE__ */ new Set();
932
+ for (const constraint of constraints) {
933
+ if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
934
+ const constraintPrototype = Object.getPrototypeOf(constraint);
935
+ if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
936
+ const id = constraint.id;
937
+ const name = constraint.name;
938
+ const started = constraint.started;
939
+ if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
940
+ ids.add(id);
941
+ }
942
+ }
943
+ return true;
944
+ } catch {
945
+ return false;
946
+ }
947
+ }
948
+ /**
949
+ * Test whether an unknown value is valid persisted task activity.
950
+ */
951
+ function isTaskActivity(value) {
952
+ try {
953
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
954
+ const prototype = Object.getPrototypeOf(value);
955
+ if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
956
+ const note = value.note;
957
+ const progress = value.progress;
958
+ const operations = value.operations;
959
+ const constraints = value.constraints;
960
+ const updated = value.updated;
961
+ if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
962
+ return isTaskActivityInput({
963
+ ...note === void 0 ? {} : { note },
964
+ ...progress === void 0 ? {} : { progress },
965
+ operations,
966
+ constraints
967
+ });
968
+ } catch {
969
+ return false;
970
+ }
971
+ }
972
+ //#endregion
973
+ //#region src/core/cloners.ts
974
+ /**
975
+ * Validate and own a workflow snapshot before live construction.
976
+ *
977
+ * @param input - The hostile snapshot boundary
978
+ * @returns A deeply owned frozen snapshot
979
+ */
980
+ function cloneWorkflowSnapshot(input) {
981
+ try {
982
+ const cloned = (0, _orkestrel_contract.cloneJSONValue)(input);
983
+ if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", workflowSnapshotContext(cloned));
984
+ return cloned;
985
+ } catch (error) {
986
+ if (isWorkflowError(error)) throw error;
987
+ if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
988
+ throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
989
+ }
990
+ }
991
+ /**
992
+ * Validate and clone one complete task activity frame.
993
+ *
994
+ * @remarks
995
+ * This is the hostile boundary behind task reports and snapshot hydration. Supplying
996
+ * `updated` stamps an input frame without reading an `updated` property from it; omitting
997
+ * `updated` restores a stored frame and reads its persisted timestamp exactly once. Every
998
+ * untrusted property is captured once inside one protected boundary. The returned frame,
999
+ * collections, progress, operations, and constraints are copied and frozen.
1000
+ *
1001
+ * @param input - The untrusted complete activity frame
1002
+ * @param updated - An optional accepted timestamp used instead of a persisted `updated`
1003
+ * @returns An immutable cloned {@link TaskActivity}
1004
+ * @throws {WorkflowError} With `MUTATION` when the frame cannot be read or validated
1005
+ */
1006
+ function cloneTaskActivity(input, updated) {
1007
+ try {
1008
+ if (!(0, _orkestrel_contract.isRecord)(input)) throw new WorkflowError("MUTATION", "task activity must be a record");
1009
+ const inputPrototype = Object.getPrototypeOf(input);
1010
+ 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");
1011
+ const note = input.note;
1012
+ const progressInput = input.progress;
1013
+ const operationsInput = input.operations;
1014
+ const constraintsInput = input.constraints;
1015
+ const accepted = updated === void 0 ? input.updated : updated;
1016
+ const operationInputs = operationsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(operationsInput) ? [...operationsInput] : void 0;
1017
+ if (operationInputs === void 0) throw new WorkflowError("MUTATION", "task activity operations must be an array");
1018
+ const operations = [];
1019
+ for (const operation of operationInputs) {
1020
+ if (!(0, _orkestrel_contract.isRecord)(operation)) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
1021
+ const operationPrototype = Object.getPrototypeOf(operation);
1022
+ 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");
1023
+ const id = operation.id;
1024
+ const name = operation.name;
1025
+ const started = operation.started;
1026
+ operations.push(Object.freeze({
1027
+ id,
1028
+ name,
1029
+ started
1030
+ }));
1031
+ }
1032
+ let progress;
1033
+ if (progressInput !== void 0) {
1034
+ if (!(0, _orkestrel_contract.isRecord)(progressInput)) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
1035
+ const progressPrototype = Object.getPrototypeOf(progressInput);
1036
+ 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");
1037
+ const current = progressInput.current;
1038
+ const total = progressInput.total;
1039
+ const unit = progressInput.unit;
1040
+ progress = Object.freeze({
1041
+ current,
1042
+ ...total === void 0 ? {} : { total },
1043
+ ...unit === void 0 ? {} : { unit }
1044
+ });
1045
+ }
1046
+ const constraintInputs = constraintsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(constraintsInput) ? [...constraintsInput] : void 0;
1047
+ if (constraintInputs === void 0) throw new WorkflowError("MUTATION", "task activity constraints must be an array");
1048
+ const constraints = [];
1049
+ for (const constraint of constraintInputs) {
1050
+ if (!(0, _orkestrel_contract.isRecord)(constraint)) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
1051
+ const constraintPrototype = Object.getPrototypeOf(constraint);
1052
+ 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");
1053
+ const id = constraint.id;
1054
+ const name = constraint.name;
1055
+ const started = constraint.started;
1056
+ constraints.push(Object.freeze({
1057
+ id,
1058
+ name,
1059
+ started
1060
+ }));
1061
+ }
1062
+ const activity = Object.freeze({
1063
+ ...note === void 0 ? {} : { note },
1064
+ ...progress === void 0 ? {} : { progress },
1065
+ operations: Object.freeze(operations),
1066
+ constraints: Object.freeze(constraints),
1067
+ updated: accepted
1068
+ });
1069
+ if (!isTaskActivity(activity)) throw new WorkflowError("MUTATION", "task activity is invalid");
1070
+ return activity;
1071
+ } catch (error) {
1072
+ if (isWorkflowError(error)) throw error;
1073
+ throw new WorkflowError("MUTATION", "task activity could not be read safely");
1074
+ }
1075
+ }
1076
+ //#endregion
703
1077
  //#region src/core/shapers.ts
704
1078
  /**
705
1079
  * The shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
@@ -726,6 +1100,7 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
726
1100
  })),
727
1101
  timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
728
1102
  min: 0,
1103
+ max: MAX_TIMER_MS,
729
1104
  description: "Per-attempt deadline in milliseconds; overrides the phase default. Omitted means no deadline."
730
1105
  }))
731
1106
  });
@@ -872,13 +1247,14 @@ var DatabaseWorkflowStore = class {
872
1247
  async get(id) {
873
1248
  const row = await this.#table.get(id);
874
1249
  if (row === void 0) return void 0;
875
- return isWorkflowSnapshot(row.snapshot) ? row.snapshot : void 0;
1250
+ return cloneWorkflowSnapshot(row.snapshot);
876
1251
  }
877
1252
  /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
878
1253
  async set(snapshot) {
1254
+ const owned = cloneWorkflowSnapshot(snapshot);
879
1255
  await this.#table.set({
880
- id: snapshot.id,
881
- snapshot
1256
+ id: owned.id,
1257
+ snapshot: owned
882
1258
  });
883
1259
  }
884
1260
  /** Drop a snapshot by id; an absent id is a no-op (no throw). */
@@ -929,10 +1305,12 @@ var DatabaseWorkflowStore = class {
929
1305
  var MemoryWorkflowStore = class {
930
1306
  #snapshots = /* @__PURE__ */ new Map();
931
1307
  get(id) {
932
- return Promise.resolve(this.#snapshots.get(id));
1308
+ const snapshot = this.#snapshots.get(id);
1309
+ return Promise.resolve(snapshot === void 0 ? void 0 : cloneWorkflowSnapshot(snapshot));
933
1310
  }
934
1311
  set(snapshot) {
935
- this.#snapshots.set(snapshot.id, snapshot);
1312
+ const owned = cloneWorkflowSnapshot(snapshot);
1313
+ this.#snapshots.set(owned.id, owned);
936
1314
  return Promise.resolve();
937
1315
  }
938
1316
  delete(id) {
@@ -955,9 +1333,8 @@ var MemoryWorkflowStore = class {
955
1333
  * `TRANSITION` {@link WorkflowError} on an illegal move (e.g. completing a non-`running`
956
1334
  * task) — the legal graph is the single source of truth, so the leaf can never reach an
957
1335
  * impossible state.
958
- * - **Override (snapshot fidelity).** `skip` / `stop` set `#override` to the forced terminal
959
- * status, so a RESTORE can tell a forced leaf (`skipped` / `stopped`) from a run-produced
960
- * one and reinstate it AS an override — preserving the round-trip.
1336
+ * - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
1337
+ * statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
961
1338
  * - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
962
1339
  * OWN event, THEN calls the parent phase's `#recompute` (injected at construction) so the
963
1340
  * transition propagates UP (Task → Phase → Workflow re-derive). The own-event-before-cascade
@@ -972,7 +1349,8 @@ var MemoryWorkflowStore = class {
972
1349
  * matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
973
1350
  * is the RUNTIME-ONLY counterpart — `run` resolved ONCE at construction against the
974
1351
  * workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
975
- * NEVER persisted; `undefined` when `run` is omitted or unregistered (the no-handler rule).
1352
+ * NEVER persisted; `undefined` when `run` is omitted or unregistered. Only omission is a
1353
+ * deliberate no-op; unresolved named work is rejected before dispatch.
976
1354
  */
977
1355
  var Task = class {
978
1356
  #context;
@@ -984,29 +1362,57 @@ var Task = class {
984
1362
  #status;
985
1363
  #result;
986
1364
  #name;
987
- #description;
988
1365
  #run;
989
1366
  #retries;
990
1367
  #timeout;
1368
+ #attempts;
991
1369
  #handler;
992
- constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, handler) {
993
- this.#context = context;
1370
+ #abort;
1371
+ #silence;
1372
+ #onSilence;
1373
+ #liveness;
1374
+ #activity;
1375
+ #paused;
1376
+ #gate;
1377
+ #timerSignal;
1378
+ constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
1379
+ this.#context = buildTaskContext(context.phase, context);
994
1380
  this.#phase = phase;
995
1381
  this.#workflow = workflow;
996
1382
  this.#recompute = recompute;
997
- this.#metadata = options?.metadata ?? {};
1383
+ try {
1384
+ this.#metadata = (0, _orkestrel_contract.cloneJSONRecord)(options?.metadata ?? metadata);
1385
+ } catch (error) {
1386
+ if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `task '${context.id}' metadata could not be read safely: ${error.message}`, { task: context.id });
1387
+ throw new WorkflowError("RESTORE", `task '${context.id}' metadata could not be read safely`, { task: context.id });
1388
+ }
998
1389
  this.#emitter = new _orkestrel_emitter.Emitter({
999
- on: options?.on,
1000
- error: options?.error
1390
+ ...options?.on === void 0 ? {} : { on: options.on },
1391
+ ...options?.error === void 0 ? {} : { error: options.error }
1001
1392
  });
1002
1393
  this.#status = status;
1003
1394
  this.#result = result;
1004
1395
  this.#name = context.name;
1005
- this.#description = context.description;
1396
+ if (context.description !== void 0) Object.defineProperty(this, "description", {
1397
+ configurable: true,
1398
+ value: context.description
1399
+ });
1006
1400
  this.#run = run;
1007
1401
  this.#retries = retries;
1008
1402
  this.#timeout = timeout;
1403
+ this.#attempts = attempts;
1009
1404
  this.#handler = handler;
1405
+ this.#abort = (0, _orkestrel_abort.createAbort)();
1406
+ this.#silence = resolveTaskSilence(options?.silence, silence);
1407
+ this.#onSilence = this.#expire.bind(this);
1408
+ this.#liveness = this.#silence === void 0 ? void 0 : (0, _orkestrel_timeout.createTimeout)({
1409
+ ms: this.#silence,
1410
+ signal: this.#abort.signal
1411
+ });
1412
+ this.#activity = activity === void 0 ? void 0 : cloneTaskActivity(activity);
1413
+ this.#paused = false;
1414
+ this.#gate = void 0;
1415
+ this.#timerSignal = void 0;
1010
1416
  }
1011
1417
  get emitter() {
1012
1418
  return this.#emitter;
@@ -1017,9 +1423,6 @@ var Task = class {
1017
1423
  get name() {
1018
1424
  return this.#name;
1019
1425
  }
1020
- get description() {
1021
- return this.#description;
1022
- }
1023
1426
  get context() {
1024
1427
  return this.#context;
1025
1428
  }
@@ -1035,6 +1438,9 @@ var Task = class {
1035
1438
  get result() {
1036
1439
  return this.#result;
1037
1440
  }
1441
+ get attempts() {
1442
+ return this.#attempts;
1443
+ }
1038
1444
  get run() {
1039
1445
  return this.#run;
1040
1446
  }
@@ -1047,40 +1453,120 @@ var Task = class {
1047
1453
  get timeout() {
1048
1454
  return this.#timeout;
1049
1455
  }
1456
+ get activity() {
1457
+ return this.#activity;
1458
+ }
1459
+ get silence() {
1460
+ return this.#silence;
1461
+ }
1462
+ get silent() {
1463
+ return this.#status === "running" && this.#liveness?.expired === true;
1464
+ }
1465
+ get paused() {
1466
+ return this.#paused;
1467
+ }
1468
+ get signal() {
1469
+ return this.#abort.signal;
1470
+ }
1050
1471
  start() {
1051
- this.#transition("running");
1472
+ const budget = Math.max(0, this.#retries ?? 0) + 1;
1473
+ if (this.#status !== "pending" && this.#status !== "running" || this.#attempts >= budget) throw new WorkflowError("TRANSITION", `task '${this.id}' cannot start another attempt`, {
1474
+ task: this.id,
1475
+ status: this.#status,
1476
+ attempts: this.#attempts,
1477
+ budget
1478
+ });
1479
+ if (this.#status === "pending") this.#transition("running");
1480
+ this.#attempts += 1;
1481
+ this.#activity = cloneTaskActivity({}, this.#stamp());
1482
+ this.#arm();
1052
1483
  this.#emitter.emit("start", this.id);
1053
1484
  this.#escalate();
1054
1485
  }
1055
1486
  complete(value) {
1487
+ let owned;
1488
+ try {
1489
+ owned = (0, _orkestrel_contract.cloneJSONValue)(value);
1490
+ } catch (error) {
1491
+ if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `task '${this.id}' result could not be read safely: ${error.message}`, { task: this.id });
1492
+ throw new WorkflowError("RESTORE", `task '${this.id}' result could not be read safely`, { task: this.id });
1493
+ }
1056
1494
  this.#transition("completed");
1057
- const result = this.#record("completed", {
1495
+ this.#finish();
1496
+ const result = this.#record("completed", Object.freeze({
1058
1497
  success: true,
1059
- value
1060
- });
1498
+ value: owned
1499
+ }));
1061
1500
  this.#emitter.emit("complete", result);
1062
1501
  this.#escalate();
1063
1502
  }
1064
1503
  fail(error) {
1504
+ const origin = error.origin === "handler" || error.origin === "timeout" || error.origin === "recovery" ? error.origin : "handler";
1505
+ const message = typeof error.message === "string" && error.message.length > 0 ? error.message : "unknown failure";
1065
1506
  this.#transition("failed");
1066
- const reason = error instanceof Error ? error : new Error(String(error), { cause: error });
1067
- const result = this.#record("failed", {
1507
+ this.#finish();
1508
+ const result = this.#record("failed", Object.freeze({
1068
1509
  success: false,
1069
- error: reason
1070
- });
1510
+ error: Object.freeze({
1511
+ origin,
1512
+ message
1513
+ })
1514
+ }));
1071
1515
  this.#emitter.emit("fail", result);
1072
1516
  this.#escalate();
1073
1517
  }
1074
1518
  skip() {
1075
1519
  this.#transition("skipped");
1520
+ this.#finish();
1521
+ this.#abort.abort();
1076
1522
  this.#emitter.emit("skip");
1077
1523
  this.#escalate();
1078
1524
  }
1079
1525
  stop() {
1080
1526
  this.#transition("stopped");
1527
+ this.#finish();
1528
+ this.#abort.abort();
1081
1529
  this.#emitter.emit("stop");
1082
1530
  this.#escalate();
1083
1531
  }
1532
+ report(input) {
1533
+ if (this.#status !== "running") return failure(new WorkflowError("TRANSITION", `task '${this.id}' cannot report while '${this.#status}'`, {
1534
+ task: this.id,
1535
+ status: this.#status
1536
+ }));
1537
+ try {
1538
+ const activity = cloneTaskActivity(input, this.#stamp());
1539
+ this.#activity = activity;
1540
+ this.#arm();
1541
+ this.#emitter.emit("report", activity);
1542
+ return success(activity);
1543
+ } catch (error) {
1544
+ return failure(error instanceof WorkflowError ? error : new WorkflowError("MUTATION", "task activity report was refused", { task: this.id }));
1545
+ }
1546
+ }
1547
+ pulse() {
1548
+ if (this.#status !== "running" || this.#activity === void 0) return false;
1549
+ this.#touch();
1550
+ this.#arm();
1551
+ const activity = this.#activity;
1552
+ this.#emitter.emit("pulse", activity);
1553
+ return true;
1554
+ }
1555
+ pause() {
1556
+ if (this.#paused || this.#status !== "pending" && this.#status !== "running") return;
1557
+ this.#paused = true;
1558
+ this.#gate = createDeferred();
1559
+ this.#emitter.emit("pause");
1560
+ }
1561
+ resume() {
1562
+ if (!this.#paused) return;
1563
+ this.#paused = false;
1564
+ this.#release();
1565
+ this.#emitter.emit("resume");
1566
+ }
1567
+ wait() {
1568
+ return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
1569
+ }
1084
1570
  /**
1085
1571
  * Apply a validated declarative patch to SELF (`name` / `description`).
1086
1572
  *
@@ -1102,7 +1588,10 @@ var Task = class {
1102
1588
  status: this.#status
1103
1589
  });
1104
1590
  if (value.name !== void 0) this.#name = value.name;
1105
- if (value.description !== void 0) this.#description = value.description;
1591
+ if (value.description !== void 0) Object.defineProperty(this, "description", {
1592
+ configurable: true,
1593
+ value: value.description
1594
+ });
1106
1595
  }
1107
1596
  snapshot() {
1108
1597
  return {
@@ -1112,9 +1601,11 @@ var Task = class {
1112
1601
  status: this.#status,
1113
1602
  ...this.#result === void 0 ? {} : { result: this.#result },
1114
1603
  metadata: this.#metadata,
1604
+ attempts: this.#attempts,
1115
1605
  ...this.#run === void 0 ? {} : { run: this.#run },
1116
1606
  ...this.#retries === void 0 ? {} : { retries: this.#retries },
1117
- ...this.#timeout === void 0 ? {} : { timeout: this.#timeout }
1607
+ ...this.#timeout === void 0 ? {} : { timeout: this.#timeout },
1608
+ ...this.#activity === void 0 ? {} : { activity: this.#activity }
1118
1609
  };
1119
1610
  }
1120
1611
  #transition(to) {
@@ -1134,12 +1625,53 @@ var Task = class {
1134
1625
  ...result === void 0 ? {} : { result },
1135
1626
  timestamp: Date.now()
1136
1627
  };
1137
- this.#result = record;
1138
- return record;
1628
+ const frozen = Object.freeze(record);
1629
+ this.#result = frozen;
1630
+ return frozen;
1139
1631
  }
1140
1632
  #escalate() {
1141
1633
  this.#recompute();
1142
1634
  }
1635
+ #touch() {
1636
+ if (this.#activity === void 0) return;
1637
+ this.#activity = Object.freeze({
1638
+ ...this.#activity,
1639
+ updated: this.#stamp()
1640
+ });
1641
+ }
1642
+ #stamp() {
1643
+ return Math.max(Date.now(), this.#activity?.updated ?? 0);
1644
+ }
1645
+ #finish() {
1646
+ this.#clear();
1647
+ this.#paused = false;
1648
+ this.#release();
1649
+ }
1650
+ #arm() {
1651
+ this.#clear();
1652
+ const liveness = this.#liveness;
1653
+ if (liveness === void 0 || this.#status !== "running") return;
1654
+ liveness.start();
1655
+ const signal = liveness.signal;
1656
+ this.#timerSignal = signal;
1657
+ signal.addEventListener("abort", this.#onSilence, { once: true });
1658
+ }
1659
+ #clear() {
1660
+ const signal = this.#timerSignal;
1661
+ if (signal !== void 0) signal.removeEventListener("abort", this.#onSilence);
1662
+ this.#timerSignal = void 0;
1663
+ this.#liveness?.clear();
1664
+ }
1665
+ #expire() {
1666
+ this.#timerSignal = void 0;
1667
+ if (this.#status !== "running" || this.#liveness?.expired !== true) return;
1668
+ this.#emitter.emit("silence");
1669
+ }
1670
+ #release() {
1671
+ if (this.#gate === void 0) return;
1672
+ this.#gate.resolve();
1673
+ this.#gate = void 0;
1674
+ }
1143
1675
  };
1144
1676
  //#endregion
1145
1677
  //#region src/core/tasks/TaskManager.ts
@@ -1240,9 +1772,11 @@ var TaskManager = class {
1240
1772
  * `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
1241
1773
  * tree); `workflow` navigates UP to the live parent.
1242
1774
  * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link PhaseEventMap}) fires
1243
- * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE, strictly AFTER the
1244
- * recompute + escalate; the emitter isolates a listener throw and routes it to its `error`
1245
- * handler (the `error` option); `fail` carries the failing task's {@link TaskResult}.
1775
+ * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
1776
+ * corresponding status or runtime-gate change. Status events fire after the phase recomputes
1777
+ * and before it escalates to the workflow, preserving child/phase cause before parent effect.
1778
+ * The emitter isolates a listener throw and routes it to its `error` handler (the `error`
1779
+ * option); `fail` carries the failing task's {@link TaskResult}.
1246
1780
  * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1247
1781
  * delegating to {@link tasks} (the manager gates the target's own existence/status/id/
1248
1782
  * bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
@@ -1262,7 +1796,8 @@ var TaskManager = class {
1262
1796
  * {@link import('../types.js').WorkflowFunctions} registry (threaded from
1263
1797
  * {@link import('../types.js').WorkflowOptions.functions}) resolves each task's `run` name into
1264
1798
  * its runtime {@link import('../types.js').TaskInterface.handler} ONCE; a `run` that is omitted
1265
- * or unregistered resolves to no handler (the no-handler rule).
1799
+ * or unregistered resolves to no handler; only an omitted `run` is a no-op, while an
1800
+ * unresolved present name makes the containing tree non-drivable.
1266
1801
  * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
1267
1802
  * quartet, scoped to this phase — a driving
1268
1803
  * {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
@@ -1275,11 +1810,11 @@ var TaskManager = class {
1275
1810
  var Phase = class {
1276
1811
  #id;
1277
1812
  #name;
1278
- #description;
1279
1813
  #workflow;
1280
1814
  #escalateUp;
1281
1815
  #tasks = new TaskManager();
1282
1816
  #functions;
1817
+ #silence;
1283
1818
  #bail;
1284
1819
  #concurrency;
1285
1820
  #emitter;
@@ -1287,18 +1822,22 @@ var Phase = class {
1287
1822
  #override;
1288
1823
  #paused;
1289
1824
  #gate;
1290
- constructor(snapshot, workflow, escalate, options, bail, functions) {
1825
+ constructor(snapshot, workflow, escalate, options, bail, functions, silence) {
1291
1826
  this.#id = snapshot.id;
1292
1827
  this.#name = snapshot.name;
1293
- this.#description = snapshot.description;
1828
+ if (snapshot.description !== void 0) Object.defineProperty(this, "description", {
1829
+ configurable: true,
1830
+ value: snapshot.description
1831
+ });
1294
1832
  this.#workflow = workflow;
1295
1833
  this.#escalateUp = escalate;
1296
1834
  this.#functions = functions;
1835
+ this.#silence = silence;
1297
1836
  this.#bail = bail ?? snapshot.bail;
1298
1837
  this.#concurrency = snapshot.concurrency;
1299
1838
  this.#emitter = new _orkestrel_emitter.Emitter({
1300
- on: options?.on,
1301
- error: options?.error
1839
+ ...options?.on === void 0 ? {} : { on: options.on },
1840
+ ...options?.error === void 0 ? {} : { error: options.error }
1302
1841
  });
1303
1842
  for (const task of snapshot.tasks) this.#append(task, options);
1304
1843
  this.#override = snapshot.override;
@@ -1315,14 +1854,11 @@ var Phase = class {
1315
1854
  get name() {
1316
1855
  return this.#name;
1317
1856
  }
1318
- get description() {
1319
- return this.#description;
1320
- }
1321
1857
  get context() {
1322
1858
  return buildPhaseContext(this.#workflow.context, {
1323
1859
  id: this.#id,
1324
1860
  name: this.#name,
1325
- ...this.#description === void 0 ? {} : { description: this.#description }
1861
+ ...this.description === void 0 ? {} : { description: this.description }
1326
1862
  });
1327
1863
  }
1328
1864
  get workflow() {
@@ -1365,11 +1901,13 @@ var Phase = class {
1365
1901
  if (this.#paused || isTerminalStatus(this.status)) return;
1366
1902
  this.#paused = true;
1367
1903
  this.#gate = createDeferred();
1904
+ this.#emitter.emit("pause");
1368
1905
  }
1369
1906
  resume() {
1370
1907
  if (!this.#paused) return;
1371
1908
  this.#paused = false;
1372
1909
  this.#release();
1910
+ this.#emitter.emit("resume");
1373
1911
  }
1374
1912
  wait() {
1375
1913
  return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
@@ -1424,7 +1962,10 @@ var Phase = class {
1424
1962
  status: this.status
1425
1963
  });
1426
1964
  if (value.name !== void 0) this.#name = value.name;
1427
- if (value.description !== void 0) this.#description = value.description;
1965
+ if (value.description !== void 0) Object.defineProperty(this, "description", {
1966
+ configurable: true,
1967
+ value: value.description
1968
+ });
1428
1969
  if (value.concurrency !== void 0) this.#concurrency = value.concurrency;
1429
1970
  if (value.bail !== void 0) this.#bail = value.bail;
1430
1971
  }
@@ -1447,6 +1988,10 @@ var Phase = class {
1447
1988
  return;
1448
1989
  }
1449
1990
  this.#status = next;
1991
+ if (isTerminalStatus(next)) {
1992
+ this.#paused = false;
1993
+ this.#release();
1994
+ }
1450
1995
  this.#emitFor(next);
1451
1996
  this.#escalateUp();
1452
1997
  }
@@ -1458,6 +2003,7 @@ var Phase = class {
1458
2003
  if (status === "running") this.#emitter.emit("start", this.id);
1459
2004
  else if (status === "completed") this.#emitter.emit("complete");
1460
2005
  else if (status === "failed") this.#emitter.emit("fail", this.#failure());
2006
+ else if (status === "skipped") this.#emitter.emit("skip");
1461
2007
  else if (status === "stopped") this.#emitter.emit("stop");
1462
2008
  }
1463
2009
  #failure() {
@@ -1482,7 +2028,7 @@ var Phase = class {
1482
2028
  #create(snapshot, options) {
1483
2029
  const context = buildTaskContext(this.context, snapshot);
1484
2030
  const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
1485
- return new Task(context, this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, handler);
2031
+ return new Task(context, this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, snapshot.metadata, snapshot.attempts, snapshot.activity, handler, this.#silence);
1486
2032
  }
1487
2033
  #mint(definition) {
1488
2034
  return this.#create(taskDefinitionToSnapshot(definition), void 0);
@@ -1586,18 +2132,20 @@ var PhaseManager = class {
1586
2132
  * reachable ONLY under `bail: true` (a single failed task halts the workflow); under
1587
2133
  * `bail: false` a failed phase folds into `completed`. {@link #recompute} diffs on each phase
1588
2134
  * change; a CHANGE emits.
1589
- * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; the override is PERSISTED in the
1590
- * snapshot's own `override` field and restored DIRECTLY (no divergence guess). The snapshot also
1591
- * persists `bail`, so a restore re-derives status identically without a silent policy default.
2135
+ * - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; an executed task-free pending tree
2136
+ * may also be force-completed vacuously. The override is PERSISTED in the snapshot's own
2137
+ * `override` field and restored DIRECTLY (no divergence guess). The snapshot also persists
2138
+ * `bail`, so a restore re-derives status identically without a silent policy default.
1592
2139
  * - **Result tree.** `results()` flattens every phase's `results()` ({@link collectResults}) — the
1593
2140
  * workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
1594
2141
  * navigate UP.
1595
2142
  * - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
1596
2143
  * JSON); {@link import('./factories.js').restoreWorkflow} rebuilds an equivalent live tree.
1597
2144
  * - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
1598
- * `start` / `complete` / `fail` / `stop` on a derived-status CHANGE; the emitter isolates a
1599
- * listener throw and routes it to its `error` handler (the `error` option); `fail` carries
1600
- * the failing task's {@link TaskResult}.
2145
+ * `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
2146
+ * corresponding status or runtime-gate change; the emitter isolates a listener throw and
2147
+ * routes it to its `error` handler (the `error` option); `fail` carries the failing task's
2148
+ * {@link TaskResult}.
1601
2149
  * - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
1602
2150
  * delegating to {@link phases} (the manager gates the target's own existence/status/id/
1603
2151
  * bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
@@ -1609,16 +2157,17 @@ var PhaseManager = class {
1609
2157
  * naturally accepted.
1610
2158
  * - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` gate execution at the runner's
1611
2159
  * phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
1612
- * persisted. `destroy` is a terminal teardown: it aborts {@link signal}, `stop`s every
1613
- * non-terminal live phase (so an engine parked on a phase's own gate unparks and the tree
1614
- * lands coherent), forces the `stop` override on THIS workflow when not already terminal,
1615
- * releases any parked {@link wait} waiter, and marks {@link destroyed} — all four idempotent.
2160
+ * persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
2161
+ * phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
2162
+ * workflow `stop` override when needed, releases its parked waiter, and marks
2163
+ * {@link destroyed} — all idempotent.
1616
2164
  */
1617
2165
  var Workflow = class {
1618
2166
  #context;
1619
2167
  #bail;
1620
2168
  #bailOverride;
1621
2169
  #functions;
2170
+ #silence;
1622
2171
  #phases = new PhaseManager();
1623
2172
  #emitter;
1624
2173
  #created;
@@ -1631,12 +2180,14 @@ var Workflow = class {
1631
2180
  #destroyed;
1632
2181
  constructor(snapshot, options) {
1633
2182
  this.#context = buildWorkflowContext(snapshot);
2183
+ if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
1634
2184
  this.#bail = options?.bail ?? snapshot.bail;
1635
2185
  this.#bailOverride = options?.bail;
1636
2186
  this.#functions = options?.functions;
2187
+ this.#silence = options?.silence;
1637
2188
  this.#emitter = new _orkestrel_emitter.Emitter({
1638
- on: options?.on,
1639
- error: options?.error
2189
+ ...options?.on === void 0 ? {} : { on: options.on },
2190
+ ...options?.error === void 0 ? {} : { error: options.error }
1640
2191
  });
1641
2192
  this.#created = snapshot.created;
1642
2193
  this.#updated = snapshot.updated;
@@ -1657,9 +2208,6 @@ var Workflow = class {
1657
2208
  get name() {
1658
2209
  return this.#context.name;
1659
2210
  }
1660
- get description() {
1661
- return this.#context.description;
1662
- }
1663
2211
  get context() {
1664
2212
  return this.#context;
1665
2213
  }
@@ -1698,26 +2246,35 @@ var Workflow = class {
1698
2246
  this.#release();
1699
2247
  }
1700
2248
  complete() {
1701
- if (this.status === "pending") this.#force("completed");
2249
+ if (this.status === "pending" && this.#phases.phases().every((phase) => phase.tasks.count === 0)) this.#force("completed");
1702
2250
  }
1703
2251
  pause() {
1704
2252
  if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
1705
2253
  this.#paused = true;
1706
2254
  this.#gate = createDeferred();
2255
+ this.#emitter.emit("pause");
1707
2256
  }
1708
2257
  resume() {
1709
2258
  if (!this.#paused) return;
1710
2259
  this.#paused = false;
1711
2260
  this.#release();
2261
+ this.#emitter.emit("resume");
1712
2262
  }
1713
2263
  destroy() {
1714
2264
  if (this.#destroyed) return;
1715
2265
  this.#destroyed = true;
1716
- this.#abort.abort();
1717
- for (const phase of this.#phases.phases()) if (!isTerminalStatus(phase.status)) phase.stop();
2266
+ const phases = this.#phases.phases();
1718
2267
  if (!isTerminalStatus(this.status)) this.stop();
2268
+ for (const phase of phases) phase.stop();
2269
+ for (const phase of phases) for (const task of phase.tasks.tasks()) if (!isTerminalStatus(task.status)) task.stop();
1719
2270
  this.#paused = false;
1720
2271
  this.#release();
2272
+ this.#abort.abort();
2273
+ for (const phase of phases) {
2274
+ for (const task of phase.tasks.tasks()) task.emitter.destroy();
2275
+ phase.emitter.destroy();
2276
+ }
2277
+ this.#emitter.destroy();
1721
2278
  }
1722
2279
  wait() {
1723
2280
  return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
@@ -1779,7 +2336,7 @@ var Workflow = class {
1779
2336
  return result;
1780
2337
  }
1781
2338
  snapshot() {
1782
- return {
2339
+ return cloneWorkflowSnapshot({
1783
2340
  id: this.id,
1784
2341
  name: this.name,
1785
2342
  ...this.description === void 0 ? {} : { description: this.description },
@@ -1789,13 +2346,17 @@ var Workflow = class {
1789
2346
  phases: this.#phases.phases().map((phase) => phase.snapshot()),
1790
2347
  created: this.#created,
1791
2348
  updated: this.#updated
1792
- };
2349
+ });
1793
2350
  }
1794
2351
  #recompute() {
1795
2352
  const next = this.status;
1796
2353
  if (next === this.#status) return;
1797
2354
  this.#status = next;
1798
- this.#updated = Date.now();
2355
+ if (isTerminalStatus(next)) {
2356
+ this.#paused = false;
2357
+ this.#release();
2358
+ }
2359
+ this.#updated = Math.max(Date.now(), this.#updated);
1799
2360
  this.#emitFor(next);
1800
2361
  }
1801
2362
  #force(status) {
@@ -1806,6 +2367,7 @@ var Workflow = class {
1806
2367
  if (status === "running") this.#emitter.emit("start", this.id);
1807
2368
  else if (status === "completed") this.#emitter.emit("complete");
1808
2369
  else if (status === "failed") this.#emitter.emit("fail", this.#failure());
2370
+ else if (status === "skipped") this.#emitter.emit("skip");
1809
2371
  else if (status === "stopped") this.#emitter.emit("stop");
1810
2372
  }
1811
2373
  #addTo(phase, index, at) {
@@ -1825,11 +2387,11 @@ var Workflow = class {
1825
2387
  return found;
1826
2388
  }
1827
2389
  #append(phase, options) {
1828
- const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride, this.#functions);
2390
+ const created = new Phase(phase, this, () => this.#recompute(), options?.phases?.[phase.id], this.#bailOverride, this.#functions, this.#silence);
1829
2391
  this.#phases.append(created);
1830
2392
  }
1831
2393
  #mint(definition) {
1832
- return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions);
2394
+ return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions, this.#silence);
1833
2395
  }
1834
2396
  #release() {
1835
2397
  if (this.#gate === void 0) return;
@@ -1896,7 +2458,7 @@ var WorkflowManager = class {
1896
2458
  return [...this.#workflows.values()];
1897
2459
  }
1898
2460
  add(definition) {
1899
- const workflow = createWorkflow(definition, { functions: this.#functions });
2461
+ const workflow = createWorkflow(definition, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
1900
2462
  this.#workflows.set(workflow.id, workflow);
1901
2463
  return workflow;
1902
2464
  }
@@ -1906,7 +2468,7 @@ var WorkflowManager = class {
1906
2468
  if (this.#store === void 0) return void 0;
1907
2469
  const snapshot = await this.#store.get(id);
1908
2470
  if (snapshot === void 0) return void 0;
1909
- const workflow = restoreWorkflow(snapshot, { functions: this.#functions });
2471
+ const workflow = restoreWorkflow(snapshot, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
1910
2472
  this.#workflows.set(workflow.id, workflow);
1911
2473
  return workflow;
1912
2474
  }
@@ -2056,14 +2618,14 @@ var Runner = class {
2056
2618
  this.#handler = options.handler;
2057
2619
  this.#entries = options.entries;
2058
2620
  this.#emitter = new _orkestrel_emitter.Emitter({
2059
- on: options?.on,
2060
- error: options?.error
2621
+ ...options.on === void 0 ? {} : { on: options.on },
2622
+ ...options.error === void 0 ? {} : { error: options.error }
2061
2623
  });
2062
2624
  this.#queue = (0, _orkestrel_queue.createQueue)({
2063
- handler: (unit, execution) => this.#dispatch(unit, execution),
2064
- concurrency: options.concurrency,
2065
- retries: options.retries,
2066
- timeout: options.timeout
2625
+ handler: this.#dispatch.bind(this),
2626
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
2627
+ ...options.retries === void 0 ? {} : { retries: options.retries },
2628
+ ...options.timeout === void 0 ? {} : { timeout: options.timeout }
2067
2629
  });
2068
2630
  }
2069
2631
  get emitter() {
@@ -2249,18 +2811,17 @@ var Runner = class {
2249
2811
  //#endregion
2250
2812
  //#region src/core/tasks/TaskController.ts
2251
2813
  /**
2252
- * The lean per-task handle a {@link import('./types.js').WorkflowFunction} receives — the
2253
- * running task's folded cancellation, its input, its lineage, and read-UP access to the
2254
- * result tree.
2814
+ * The attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
2255
2815
  *
2256
2816
  * @remarks
2257
2817
  * - **A leaf handle, NOT the runner `Controller`.** A workflow task is a leaf of the
2258
- * declarative W-b tree, not a fan-out unit, so this carries none of the runner
2259
- * `Controller`'s `spawn` / `wait` only what a leaf needs.
2260
- * - **Folded signal.** `signal` is the cancellation the runner folds for THIS run: it fires
2261
- * on a workflow-level abort / timeout / budget ceiling, or under `bail: true` — when a
2262
- * sibling task fails (the runner aborts the in-flight siblings via the substrate's
2263
- * fail-fast). A handler races its work against it; `aborted` reads it.
2818
+ * declarative W-b tree, not a fan-out unit, so it has no `spawn`; its `wait` instead
2819
+ * checkpoints the workflow, phase, and task cooperative gates.
2820
+ * - **Folded signal.** `signal` is the cancellation folded for THIS attempt: its per-attempt
2821
+ * deadline, task stop/skip, workflow abort/timeout/budget/destroy, or a sibling fail-fast.
2822
+ * A handler races its work against it; `aborted` reads it.
2823
+ * - **Attempt ownership.** `report` / `pulse` are closures supplied by the runner and refuse
2824
+ * after this signal aborts or a retry token supersedes this handle.
2264
2825
  * - **Input + lineage.** `input` is the task's open `metadata` bag (its
2265
2826
  * {@link import('./types.js').TaskInput} payload, `{}` when none); `task` is the full
2266
2827
  * {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate UP the lineage.
@@ -2276,19 +2837,273 @@ var TaskController = class {
2276
2837
  signal;
2277
2838
  input;
2278
2839
  task;
2840
+ attempt;
2841
+ #entity;
2842
+ #report;
2843
+ #pulse;
2279
2844
  #results;
2280
- constructor(signal, input, task, results) {
2845
+ constructor(signal, input, task, attempt, results, report, pulse) {
2281
2846
  this.signal = signal;
2282
2847
  this.input = input;
2283
- this.task = task;
2848
+ this.task = task.context;
2849
+ this.attempt = attempt;
2850
+ this.#entity = task;
2284
2851
  this.#results = results;
2852
+ this.#report = report;
2853
+ this.#pulse = pulse;
2285
2854
  }
2286
2855
  get aborted() {
2287
2856
  return this.signal.aborted;
2288
2857
  }
2858
+ get paused() {
2859
+ if (this.#ancestorTerminal()) return false;
2860
+ return this.#entity.workflow.paused || this.#entity.phase.paused || !isTerminalStatus(this.#entity.status) && this.#entity.paused;
2861
+ }
2862
+ report(input) {
2863
+ return this.#report(input);
2864
+ }
2865
+ pulse() {
2866
+ return this.#pulse();
2867
+ }
2868
+ async wait() {
2869
+ while (this.paused && !this.signal.aborted) await this.#race(this.#gates());
2870
+ }
2289
2871
  results() {
2290
2872
  return this.#results();
2291
2873
  }
2874
+ #gates() {
2875
+ if (this.#ancestorTerminal()) return [];
2876
+ const gates = [];
2877
+ if (this.#entity.workflow.paused) gates.push(this.#entity.workflow.wait());
2878
+ if (this.#entity.phase.paused) gates.push(this.#entity.phase.wait());
2879
+ if (!isTerminalStatus(this.#entity.status) && this.#entity.paused) gates.push(this.#entity.wait());
2880
+ return gates;
2881
+ }
2882
+ async #race(gates) {
2883
+ if (this.signal.aborted || gates.length === 0) return;
2884
+ const deferred = Promise.withResolvers();
2885
+ const onAbort = this.#resolve.bind(this, deferred);
2886
+ const onTerminal = this.#resolve.bind(this, deferred);
2887
+ this.signal.addEventListener("abort", onAbort, { once: true });
2888
+ this.#entity.workflow.emitter.on("skip", onTerminal);
2889
+ this.#entity.workflow.emitter.on("stop", onTerminal);
2890
+ this.#entity.phase.emitter.on("skip", onTerminal);
2891
+ this.#entity.phase.emitter.on("stop", onTerminal);
2892
+ try {
2893
+ if (this.#ancestorTerminal()) deferred.resolve();
2894
+ await Promise.race([Promise.all(gates), deferred.promise]);
2895
+ } finally {
2896
+ this.signal.removeEventListener("abort", onAbort);
2897
+ this.#entity.workflow.emitter.off("skip", onTerminal);
2898
+ this.#entity.workflow.emitter.off("stop", onTerminal);
2899
+ this.#entity.phase.emitter.off("skip", onTerminal);
2900
+ this.#entity.phase.emitter.off("stop", onTerminal);
2901
+ }
2902
+ }
2903
+ #ancestorTerminal() {
2904
+ return isTerminalStatus(this.#entity.workflow.status) || isTerminalStatus(this.#entity.phase.status);
2905
+ }
2906
+ #resolve(deferred) {
2907
+ deferred.resolve();
2908
+ }
2909
+ };
2910
+ //#endregion
2911
+ //#region src/core/WorkflowPersistence.ts
2912
+ /**
2913
+ * Advanced run-local snapshot persistence with one writer and one coalesced latest obligation.
2914
+ *
2915
+ * @remarks
2916
+ * Normally composed by `WorkflowRunner.execute({ store })`; exported for hosts that need to
2917
+ * coordinate the same required boundaries around their own runner integration.
2918
+ */
2919
+ var WorkflowPersistence = class {
2920
+ #workflow;
2921
+ #store;
2922
+ #phases = /* @__PURE__ */ new Set();
2923
+ #tasks = /* @__PURE__ */ new Set();
2924
+ #onWorkflowChange;
2925
+ #onWorkflowAdd;
2926
+ #onWorkflowRemove;
2927
+ #onPhaseChange;
2928
+ #onPhaseAdd;
2929
+ #onPhaseRemove;
2930
+ #onTaskChange;
2931
+ #writing;
2932
+ #error;
2933
+ #fault;
2934
+ #attached = true;
2935
+ #revision = 0;
2936
+ #stored = 0;
2937
+ constructor(workflow, store) {
2938
+ this.#workflow = workflow;
2939
+ this.#store = store;
2940
+ this.#onWorkflowChange = this.#change.bind(this);
2941
+ this.#onWorkflowAdd = this.#addPhase.bind(this);
2942
+ this.#onWorkflowRemove = this.#removePhase.bind(this);
2943
+ this.#onPhaseChange = this.#change.bind(this);
2944
+ this.#onPhaseAdd = this.#addTask.bind(this);
2945
+ this.#onPhaseRemove = this.#removeTask.bind(this);
2946
+ this.#onTaskChange = this.#change.bind(this);
2947
+ this.#attachWorkflow();
2948
+ }
2949
+ get fault() {
2950
+ return this.#fault;
2951
+ }
2952
+ /**
2953
+ * Persist every change through this required boundary.
2954
+ *
2955
+ * @param checkpoint - The boundary being made durable
2956
+ * @param task - The task owning an attempt or settlement
2957
+ * @param attempt - The persisted attempt number
2958
+ * @returns Whether the latest state reached the store
2959
+ */
2960
+ async checkpoint(checkpoint, task, attempt) {
2961
+ const revision = this.#mark();
2962
+ while (this.#stored < revision) await this.#flush();
2963
+ if (this.#error === void 0) return true;
2964
+ if (this.#fault === void 0) this.#fault = Object.freeze({
2965
+ origin: "persistence",
2966
+ checkpoint,
2967
+ message: this.#error,
2968
+ ...task === void 0 ? {} : { task: task.id },
2969
+ ...attempt === void 0 ? {} : { attempt }
2970
+ });
2971
+ return false;
2972
+ }
2973
+ /**
2974
+ * Stop observing the live tree and persist its final state.
2975
+ *
2976
+ * @returns Whether the final snapshot reached the store
2977
+ */
2978
+ async finalize() {
2979
+ this.detach();
2980
+ return this.checkpoint("final");
2981
+ }
2982
+ /** Stop observing the live tree. */
2983
+ detach() {
2984
+ if (!this.#attached) return;
2985
+ this.#attached = false;
2986
+ this.#workflow.emitter.off("start", this.#onWorkflowChange);
2987
+ this.#workflow.emitter.off("complete", this.#onWorkflowChange);
2988
+ this.#workflow.emitter.off("fail", this.#onWorkflowChange);
2989
+ this.#workflow.emitter.off("skip", this.#onWorkflowChange);
2990
+ this.#workflow.emitter.off("stop", this.#onWorkflowChange);
2991
+ this.#workflow.emitter.off("move", this.#onWorkflowChange);
2992
+ this.#workflow.emitter.off("update", this.#onWorkflowChange);
2993
+ this.#workflow.emitter.off("add", this.#onWorkflowAdd);
2994
+ this.#workflow.emitter.off("remove", this.#onWorkflowRemove);
2995
+ for (const phase of this.#phases) this.#detachPhase(phase);
2996
+ }
2997
+ #attachWorkflow() {
2998
+ this.#workflow.emitter.on("start", this.#onWorkflowChange);
2999
+ this.#workflow.emitter.on("complete", this.#onWorkflowChange);
3000
+ this.#workflow.emitter.on("fail", this.#onWorkflowChange);
3001
+ this.#workflow.emitter.on("skip", this.#onWorkflowChange);
3002
+ this.#workflow.emitter.on("stop", this.#onWorkflowChange);
3003
+ this.#workflow.emitter.on("move", this.#onWorkflowChange);
3004
+ this.#workflow.emitter.on("update", this.#onWorkflowChange);
3005
+ this.#workflow.emitter.on("add", this.#onWorkflowAdd);
3006
+ this.#workflow.emitter.on("remove", this.#onWorkflowRemove);
3007
+ for (const phase of this.#workflow.phases.phases()) this.#attachPhase(phase);
3008
+ }
3009
+ #attachPhase(phase) {
3010
+ if (this.#phases.has(phase)) return;
3011
+ this.#phases.add(phase);
3012
+ phase.emitter.on("start", this.#onPhaseChange);
3013
+ phase.emitter.on("complete", this.#onPhaseChange);
3014
+ phase.emitter.on("fail", this.#onPhaseChange);
3015
+ phase.emitter.on("skip", this.#onPhaseChange);
3016
+ phase.emitter.on("stop", this.#onPhaseChange);
3017
+ phase.emitter.on("move", this.#onPhaseChange);
3018
+ phase.emitter.on("update", this.#onPhaseChange);
3019
+ phase.emitter.on("add", this.#onPhaseAdd);
3020
+ phase.emitter.on("remove", this.#onPhaseRemove);
3021
+ for (const task of phase.tasks.tasks()) this.#attachTask(task);
3022
+ }
3023
+ #detachPhase(phase) {
3024
+ if (!this.#phases.delete(phase)) return;
3025
+ phase.emitter.off("start", this.#onPhaseChange);
3026
+ phase.emitter.off("complete", this.#onPhaseChange);
3027
+ phase.emitter.off("fail", this.#onPhaseChange);
3028
+ phase.emitter.off("skip", this.#onPhaseChange);
3029
+ phase.emitter.off("stop", this.#onPhaseChange);
3030
+ phase.emitter.off("move", this.#onPhaseChange);
3031
+ phase.emitter.off("update", this.#onPhaseChange);
3032
+ phase.emitter.off("add", this.#onPhaseAdd);
3033
+ phase.emitter.off("remove", this.#onPhaseRemove);
3034
+ for (const task of phase.tasks.tasks()) this.#detachTask(task);
3035
+ }
3036
+ #attachTask(task) {
3037
+ if (this.#tasks.has(task)) return;
3038
+ this.#tasks.add(task);
3039
+ task.emitter.on("start", this.#onTaskChange);
3040
+ task.emitter.on("complete", this.#onTaskChange);
3041
+ task.emitter.on("fail", this.#onTaskChange);
3042
+ task.emitter.on("skip", this.#onTaskChange);
3043
+ task.emitter.on("stop", this.#onTaskChange);
3044
+ task.emitter.on("report", this.#onTaskChange);
3045
+ task.emitter.on("pulse", this.#onTaskChange);
3046
+ }
3047
+ #detachTask(task) {
3048
+ if (!this.#tasks.delete(task)) return;
3049
+ task.emitter.off("start", this.#onTaskChange);
3050
+ task.emitter.off("complete", this.#onTaskChange);
3051
+ task.emitter.off("fail", this.#onTaskChange);
3052
+ task.emitter.off("skip", this.#onTaskChange);
3053
+ task.emitter.off("stop", this.#onTaskChange);
3054
+ task.emitter.off("report", this.#onTaskChange);
3055
+ task.emitter.off("pulse", this.#onTaskChange);
3056
+ }
3057
+ #addPhase(phase) {
3058
+ this.#attachPhase(phase);
3059
+ this.#change();
3060
+ }
3061
+ #removePhase(phase) {
3062
+ this.#detachPhase(phase);
3063
+ this.#change();
3064
+ }
3065
+ #addTask(task) {
3066
+ this.#attachTask(task);
3067
+ this.#change();
3068
+ }
3069
+ #removeTask(task) {
3070
+ this.#detachTask(task);
3071
+ this.#change();
3072
+ }
3073
+ #change() {
3074
+ this.#mark();
3075
+ this.#flush();
3076
+ }
3077
+ async #flush() {
3078
+ if (this.#writing !== void 0) {
3079
+ await this.#writing;
3080
+ return;
3081
+ }
3082
+ const writing = this.#drain();
3083
+ this.#writing = writing;
3084
+ try {
3085
+ await writing;
3086
+ } finally {
3087
+ if (this.#writing === writing) this.#writing = void 0;
3088
+ if (this.#stored < this.#revision) this.#flush();
3089
+ }
3090
+ }
3091
+ async #drain() {
3092
+ while (this.#stored < this.#revision) {
3093
+ const revision = this.#revision;
3094
+ try {
3095
+ await this.#store.set(this.#workflow.snapshot());
3096
+ this.#error = void 0;
3097
+ } catch (error) {
3098
+ this.#error = errorToMessage(error);
3099
+ }
3100
+ this.#stored = revision;
3101
+ }
3102
+ }
3103
+ #mark() {
3104
+ this.#revision += 1;
3105
+ return this.#revision;
3106
+ }
2292
3107
  };
2293
3108
  //#endregion
2294
3109
  //#region src/core/WorkflowRunner.ts
@@ -2305,17 +3120,16 @@ var TaskController = class {
2305
3120
  * `AbortSignal.any` (exactly as the agent runtime folds its bounds); pacing is the shipped
2306
3121
  * {@link SchedulerInterface}. The runner writes ZERO concurrency / retry / abort logic of
2307
3122
  * its own — it only sequences phases, dispatches a task's own handler, and drives the live
2308
- * entity.
2309
- * - **Pure engine no registries, no tool/agent knowledge.** The runner carries no
2310
- * `functions` / `tools` / `agents` registry: each live {@link TaskInterface} already
3123
+ * entity. The workflow layer owns per-task deadlines because timeout settlement must
3124
+ * update the live leaf under the phase's `bail` policy before the substrate unit settles.
3125
+ * - **Pure engine no integration registry.** The runner carries no behavior or provider
3126
+ * registry: each live {@link TaskInterface} already
2311
3127
  * resolved its own {@link import('./types.js').WorkflowFunction} into
2312
3128
  * {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
2313
3129
  * or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
2314
- * dispatch is simply "invoke the task's own handler". Static tool / agent calling is an
2315
- * OPT-IN concern of the `@orkestrel/tool` package's adapter factories — plain
2316
- * {@link import('./types.js').WorkflowFunction}s a caller wires into
2317
- * {@link WorkflowOptions.functions} like any other behavior. This module never imports
2318
- * any tool/agent package.
3130
+ * dispatch is simply "invoke the task's own handler". Provider, protocol, and tool
3131
+ * integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
3132
+ * composed into {@link WorkflowOptions.functions}. This module imports none of them.
2319
3133
  * - **Two `execute` forms, one engine.** `execute(definition, options)` BUILDS the live tree
2320
3134
  * from a {@link WorkflowDefinition} (single source of truth for the `run` / `concurrency`
2321
3135
  * metadata); `execute(workflow, options)` DRIVES a caller-owned, ALREADY-BUILT
@@ -2335,10 +3149,9 @@ var TaskController = class {
2335
3149
  * for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
2336
3150
  * phase always reaches a coherent terminal state.
2337
3151
  * - **Dispatch by handler.** `#runTask` invokes the live task's own
2338
- * {@link import('./types.js').TaskInterface.handler} directly: `undefined` (an omitted `run`,
2339
- * or a `run` name absent from the {@link WorkflowOptions.functions} registry it was resolved
2340
- * against) AUTO-COMPLETES the ROADMAP no-handler rule; otherwise the handler runs with the
2341
- * task's {@link import('./types.js').TaskControllerInterface} handle.
3152
+ * {@link import('./types.js').TaskInterface.handler} directly. An omitted `run` deliberately
3153
+ * auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
3154
+ * execution claim and never false-completes.
2342
3155
  * - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
2343
3156
  * THEN re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
2344
3157
  * (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
@@ -2346,11 +3159,12 @@ var TaskController = class {
2346
3159
  * Under `bail: false` (graceful) a failure `fail`s the leaf and RESOLVES (never throws), so
2347
3160
  * the Runner settles every unit (allSettled) and the run finishes (the workflow derives
2348
3161
  * `completed`, the failure recorded in the result tree).
2349
- * - **Pause / stop / destroy gates.** `workflow.pause()` is honoured at exactly two points
2350
- * the next phase boundary (workflow-only) and each task's own pre-dispatch (before
2351
- * `task.start()`, workflow gate FIRST then this task's own `phase.pause()`) by parking on
2352
- * {@link WorkflowInterface.wait} / {@link PhaseInterface.wait}; an in-flight task body is
2353
- * NEVER suspended mid-flight. A GRACEFUL `workflow.stop()` (no signal involved) is caught at
3162
+ * - **Pause / stop / destroy gates.** Workflow, phase, and task gates are checked before
3163
+ * dispatch, and a running handler can checkpoint their folded state through
3164
+ * {@link import('./types.js').TaskControllerInterface.wait}. Because the substrate acquires
3165
+ * concurrency before this handler gate, a paused task occupies one phase slot until resume;
3166
+ * already-running siblings continue and its per-attempt timeout keeps counting. A GRACEFUL
3167
+ * `workflow.stop()` (no signal involved) is caught at
2354
3168
  * those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
2355
3169
  * HARD `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
2356
3170
  * into the run's composed signal — so it cancels the active phase Runner (and every
@@ -2367,37 +3181,49 @@ var TaskController = class {
2367
3181
  * {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
2368
3182
  * `runSignal`, so a handler observes either cause directly.
2369
3183
  * - **Re-entrant-safe.** No shared per-run mutable field: the active-Runner holder is LOCAL to
2370
- * each `#execute`, so a nested `execute` (a bound workflow-tool handler re-entering this
2371
- * instance while the outer run is suspended awaiting it) cannot clobber the outer run's state.
3184
+ * each `#execute`, so a nested application-level `execute` cannot clobber the outer run's
3185
+ * state.
2372
3186
  */
2373
- var WorkflowRunner = class {
3187
+ var WorkflowRunner = class WorkflowRunner {
3188
+ static #executions = /* @__PURE__ */ new WeakSet();
2374
3189
  #scheduler;
2375
3190
  constructor(scheduler) {
2376
3191
  this.#scheduler = scheduler;
2377
3192
  }
2378
3193
  execute(target, options) {
2379
3194
  if (this.#isWorkflow(target)) {
2380
- if (target.status !== "pending" || target.destroyed) throw new WorkflowError("TRANSITION", `workflow '${target.id}' is not drivable`, {
2381
- id: target.id,
2382
- status: target.status,
2383
- destroyed: target.destroyed
2384
- });
3195
+ this.#acquire(target);
2385
3196
  return this.#execute(target, options);
2386
3197
  }
2387
3198
  const workflow = new Workflow(definitionToSnapshot(target, options?.bail ?? target.bail ?? false), options);
3199
+ this.#acquire(workflow);
2388
3200
  return this.#execute(workflow, options);
2389
3201
  }
3202
+ #acquire(workflow) {
3203
+ const tasks = workflow.phases.phases().flatMap((phase) => phase.tasks.tasks());
3204
+ if (!((workflow.status === "pending" || workflow.status === "running") && tasks.every((task) => task.status !== "running") && (tasks.length === 0 || tasks.some((task) => task.status === "pending")) && tasks.every((task) => task.run === void 0 || task.handler !== void 0)) || workflow.destroyed || WorkflowRunner.#executions.has(workflow)) throw new WorkflowError("TRANSITION", `workflow '${workflow.id}' is not drivable`, {
3205
+ id: workflow.id,
3206
+ status: workflow.status,
3207
+ destroyed: workflow.destroyed
3208
+ });
3209
+ WorkflowRunner.#executions.add(workflow);
3210
+ }
2390
3211
  async #execute(workflow, options) {
2391
3212
  const ms = options?.timeout;
2392
- const timeout = ms !== void 0 && ms > 0 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
3213
+ const timeout = ms !== void 0 && Number.isFinite(ms) && ms > 0 && ms <= 2147483647 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
2393
3214
  timeout?.start();
2394
3215
  options?.budget?.start();
2395
3216
  const runSignal = this.#fold(workflow, options, timeout);
3217
+ const persistence = options?.store === void 0 ? void 0 : new WorkflowPersistence(workflow, options.store);
2396
3218
  const holder = { runner: void 0 };
2397
- const onCancel = () => holder.runner?.abort(runSignal.reason);
3219
+ const onCancel = this.#abortActive.bind(this, holder, runSignal);
2398
3220
  if (runSignal.aborted) onCancel();
2399
3221
  else runSignal.addEventListener("abort", onCancel, { once: true });
2400
3222
  try {
3223
+ if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
3224
+ if (this.#stoppable(workflow)) workflow.stop();
3225
+ this.#skipFrom(workflow.phases.phases(), 0);
3226
+ }
2401
3227
  let index = 0;
2402
3228
  for (;;) {
2403
3229
  const phases = workflow.phases.phases();
@@ -2411,12 +3237,17 @@ var WorkflowRunner = class {
2411
3237
  this.#haltFrom(phases, index, workflow, runSignal);
2412
3238
  break;
2413
3239
  }
2414
- if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
3240
+ if (workflow.paused) await this.#raceWait(workflow.wait(), runSignal, void 0, workflow);
2415
3241
  if (this.#cancelled(runSignal) || this.#halted(workflow)) {
2416
3242
  this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
2417
3243
  break;
2418
3244
  }
2419
- if (await this.#runPhase(workflow, phase, runSignal, holder)) {
3245
+ if (phase.status === "skipped" || phase.status === "stopped") {
3246
+ this.#skipFrom([phase], 0);
3247
+ index += 1;
3248
+ continue;
3249
+ }
3250
+ if (await this.#runPhase(workflow, phase, runSignal, holder, persistence)) {
2420
3251
  this.#skipFrom(workflow.phases.phases(), index + 1);
2421
3252
  break;
2422
3253
  }
@@ -2430,24 +3261,28 @@ var WorkflowRunner = class {
2430
3261
  }
2431
3262
  if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
2432
3263
  else if (this.#completable(workflow)) workflow.complete();
3264
+ const durable = await persistence?.finalize();
2433
3265
  return {
2434
3266
  workflow,
2435
3267
  status: workflow.status,
2436
- results: workflow.results()
3268
+ results: workflow.results(),
3269
+ ...durable === void 0 ? {} : { durable },
3270
+ ...persistence?.fault === void 0 ? {} : { fault: persistence.fault }
2437
3271
  };
3272
+ } catch (error) {
3273
+ if (this.#stoppable(workflow)) workflow.stop();
3274
+ this.#skipFrom(workflow.phases.phases(), 0);
3275
+ await persistence?.finalize();
3276
+ throw error;
2438
3277
  } finally {
3278
+ persistence?.detach();
2439
3279
  timeout?.clear();
2440
3280
  runSignal.removeEventListener("abort", onCancel);
2441
3281
  }
2442
3282
  }
2443
- async #runPhase(workflow, phase, runSignal, holder) {
3283
+ async #runPhase(workflow, phase, runSignal, holder, persistence) {
2444
3284
  const launched = /* @__PURE__ */ new Set();
2445
- let runner;
2446
- const onAdd = (task) => {
2447
- if (launched.has(task.id)) return;
2448
- launched.add(task.id);
2449
- runner?.spawn(task);
2450
- };
3285
+ const onAdd = this.#spawnAdded.bind(this, launched, holder);
2451
3286
  phase.emitter.on("add", onAdd);
2452
3287
  try {
2453
3288
  const tasks = phase.tasks.tasks();
@@ -2456,15 +3291,13 @@ var WorkflowRunner = class {
2456
3291
  const bail = phase.bail;
2457
3292
  const concurrency = phase.concurrency !== void 0 && phase.concurrency > 0 ? phase.concurrency : DEFAULT_PHASE_CONCURRENCY;
2458
3293
  const attempts = /* @__PURE__ */ new Map();
3294
+ for (const task of tasks) attempts.set(task.id, task.attempts);
3295
+ const owners = /* @__PURE__ */ new Map();
2459
3296
  const created = new Runner({
2460
3297
  concurrency,
2461
- entries: (task) => ({
2462
- retries: task.retries,
2463
- timeout: task.timeout
2464
- }),
2465
- handler: (controller) => this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts)
3298
+ entries: this.#entry.bind(this),
3299
+ handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts, owners, persistence)
2466
3300
  });
2467
- runner = created;
2468
3301
  holder.runner = created;
2469
3302
  try {
2470
3303
  await created.execute(tasks);
@@ -2481,74 +3314,215 @@ var WorkflowRunner = class {
2481
3314
  for (const task of phase.tasks.tasks()) this.#skip(task);
2482
3315
  }
2483
3316
  }
2484
- async #runTask(workflow, task, controller, runSignal, bail, attempts) {
2485
- const signal = this.#taskSignal(controller.signal, runSignal);
3317
+ #abortActive(holder, runSignal) {
3318
+ holder.runner?.abort(runSignal.reason);
3319
+ }
3320
+ #spawnAdded(launched, holder, task) {
3321
+ if (launched.has(task.id)) return;
3322
+ launched.add(task.id);
3323
+ holder.runner?.spawn(task);
3324
+ }
3325
+ #entry(task) {
3326
+ const retries = Math.max(0, (task.retries ?? 0) - task.attempts);
3327
+ return retries === 0 ? {} : { retries };
3328
+ }
3329
+ #runUnit(workflow, runSignal, bail, attempts, owners, persistence, controller) {
3330
+ return this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts, owners, persistence);
3331
+ }
3332
+ async #runTask(workflow, task, controller, runSignal, bail, attempts, owners, persistence) {
2486
3333
  const attempt = (attempts.get(task.id) ?? 0) + 1;
2487
3334
  attempts.set(task.id, attempt);
2488
3335
  const last = attempt > Math.max(0, task.retries ?? 0);
2489
- if (workflow.paused) await this.#raceWait(() => workflow.wait(), runSignal);
2490
- if (task.phase.paused) await this.#raceWait(() => task.phase.wait(), runSignal);
2491
- if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2492
- this.#skipCancelled(task, workflow, runSignal);
3336
+ if (task.status !== "pending" && task.status !== "running") return;
3337
+ if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
3338
+ this.#settleCancelled(task, workflow, runSignal);
2493
3339
  return;
2494
3340
  }
2495
- if (task.status === "pending") task.start();
2496
- if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
2497
- this.#skipCancelled(task, workflow, runSignal);
2498
- return;
2499
- }
2500
- const handle = new TaskController(signal, task.snapshot().metadata, task.context, () => workflow.results());
3341
+ const ms = task.timeout;
3342
+ const deadline = ms !== void 0 && Number.isFinite(ms) && ms > 0 && ms <= 2147483647 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
3343
+ const signal = this.#taskSignal(task, controller.signal, runSignal, deadline);
2501
3344
  try {
2502
- const value = task.handler === void 0 ? void 0 : await task.handler(handle);
2503
- if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2504
- this.#skipCancelled(task, workflow, runSignal);
3345
+ task.start();
3346
+ if (task.attempts !== attempt) return;
3347
+ owners.set(task.id, attempt);
3348
+ deadline?.start();
3349
+ const durable = persistence === void 0 ? true : await persistence.checkpoint("attempt", task, attempt);
3350
+ if (!this.#owns(owners, task, attempt)) return;
3351
+ if (!durable) {
3352
+ if (this.#stoppable(workflow)) workflow.stop();
2505
3353
  return;
2506
3354
  }
2507
- if (signal.aborted) {
2508
- this.#timedOut(task, last);
3355
+ if (task.run !== void 0 && task.handler === void 0) {
3356
+ const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved run '${task.run}'`, {
3357
+ task: task.id,
3358
+ run: task.run
3359
+ });
3360
+ task.fail({
3361
+ origin: "handler",
3362
+ message: error.message
3363
+ });
3364
+ if (bail) throw error;
2509
3365
  return;
2510
3366
  }
2511
- task.complete(value);
2512
- } catch (error) {
2513
- if (task.status !== "running" || this.#skipping(controller, runSignal)) {
2514
- this.#skipCancelled(task, workflow, runSignal);
3367
+ if (await this.#gate(workflow.paused ? workflow.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3368
+ if (await this.#gate(task.phase.paused ? task.phase.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3369
+ if (await this.#gate(task.paused ? task.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
3370
+ if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
3371
+ this.#settleCancelled(task, workflow, runSignal);
3372
+ return;
3373
+ }
3374
+ if (task.status !== "running") return;
3375
+ 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`, {
3376
+ task: task.id,
3377
+ attempt
3378
+ })), () => this.#owns(owners, task, attempt) && !signal.aborted && task.pulse());
3379
+ let outcome;
3380
+ try {
3381
+ outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, this.#skipping.bind(this, task, controller, runSignal));
3382
+ } catch (error) {
3383
+ if (!this.#owns(owners, task, attempt)) return;
3384
+ if (task.status !== "running" || this.#skipping(task, controller, runSignal)) {
3385
+ this.#settleCancelled(task, workflow, runSignal);
3386
+ return;
3387
+ }
3388
+ if (signal.aborted) {
3389
+ this.#timedOut(owners, task, attempt, last, bail);
3390
+ return;
3391
+ }
3392
+ this.#failed(owners, task, attempt, error, last, bail);
3393
+ return;
3394
+ }
3395
+ if (!this.#owns(owners, task, attempt)) return;
3396
+ if (!outcome[0]) {
3397
+ this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, outcome[2]);
3398
+ return;
3399
+ }
3400
+ if (task.status !== "running") return;
3401
+ if (this.#skipping(task, controller, runSignal)) {
3402
+ this.#settleCancelled(task, workflow, runSignal);
2515
3403
  return;
2516
3404
  }
2517
3405
  if (signal.aborted) {
2518
- this.#timedOut(task, last);
3406
+ this.#timedOut(owners, task, attempt, last, bail);
2519
3407
  return;
2520
3408
  }
2521
- if (!last) throw error;
2522
- task.fail(error);
2523
- if (bail) throw error;
3409
+ if (!this.#owns(owners, task, attempt)) return;
3410
+ try {
3411
+ task.complete(outcome[1]);
3412
+ } catch (error) {
3413
+ if (!this.#owns(owners, task, attempt)) return;
3414
+ if (task.status !== "running") throw error;
3415
+ this.#failed(owners, task, attempt, error, last, bail);
3416
+ }
3417
+ } finally {
3418
+ deadline?.clear();
3419
+ if (persistence !== void 0 && this.#owns(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && this.#stoppable(workflow)) workflow.stop();
3420
+ this.#revoke(owners, task.id, attempt);
2524
3421
  }
2525
3422
  }
2526
- #timedOut(task, last) {
2527
- if (!last) return;
2528
- task.fail(/* @__PURE__ */ new Error(`task '${task.id}' timed out`));
3423
+ async #gate(wait, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail) {
3424
+ const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, this.#skipping.bind(this, task, controller, runSignal), workflow, task.phase);
3425
+ return this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine);
3426
+ }
3427
+ #settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine) {
3428
+ if (attempts.get(task.id) !== attempt || !this.#owns(owners, task, attempt)) return true;
3429
+ if (signal.aborted) {
3430
+ if (genuine ?? this.#skipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
3431
+ else this.#timedOut(owners, task, attempt, last, bail);
3432
+ return true;
3433
+ }
3434
+ if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
3435
+ this.#settleCancelled(task, workflow, runSignal);
3436
+ return true;
3437
+ }
3438
+ return task.status !== "running";
3439
+ }
3440
+ async #raceHandler(handler, signal, cancelled) {
3441
+ if (signal.aborted) return [
3442
+ false,
3443
+ void 0,
3444
+ cancelled()
3445
+ ];
3446
+ const deferred = Promise.withResolvers();
3447
+ const onAbort = this.#resolveHandlerAbort.bind(this, deferred, cancelled);
3448
+ signal.addEventListener("abort", onAbort, { once: true });
3449
+ try {
3450
+ return await Promise.race([handler.then((value) => [true, value]), deferred.promise]);
3451
+ } finally {
3452
+ signal.removeEventListener("abort", onAbort);
3453
+ }
2529
3454
  }
2530
- async #raceWait(wait, runSignal) {
2531
- if (runSignal.aborted) return;
2532
- let onAbort;
2533
- const cancelled = new Promise((resolve) => {
2534
- onAbort = () => resolve();
2535
- runSignal.addEventListener("abort", onAbort, { once: true });
3455
+ #resolveHandlerAbort(deferred, cancelled) {
3456
+ deferred.resolve([
3457
+ false,
3458
+ void 0,
3459
+ cancelled()
3460
+ ]);
3461
+ }
3462
+ #timedOut(owners, task, attempt, last, bail) {
3463
+ if (!this.#owns(owners, task, attempt)) return;
3464
+ const error = /* @__PURE__ */ new Error(`task '${task.id}' timed out`);
3465
+ if (last) task.fail({
3466
+ origin: "timeout",
3467
+ message: error.message
3468
+ });
3469
+ if (!last || bail) throw error;
3470
+ }
3471
+ #failed(owners, task, attempt, error, last, bail) {
3472
+ if (!this.#owns(owners, task, attempt)) return;
3473
+ if (!last) throw error;
3474
+ task.fail({
3475
+ origin: "handler",
3476
+ message: errorToMessage(error)
2536
3477
  });
3478
+ if (bail) throw error;
3479
+ }
3480
+ #owns(owners, task, attempt) {
3481
+ return owners.get(task.id) === attempt && task.attempts === attempt;
3482
+ }
3483
+ #revoke(owners, id, attempt) {
3484
+ if (owners.get(id) === attempt) owners.delete(id);
3485
+ }
3486
+ async #raceWait(wait, signal, cancelled, workflow, phase) {
3487
+ if (signal.aborted) return cancelled?.();
3488
+ const deferred = Promise.withResolvers();
3489
+ const onAbort = this.#resolveWaitAbort.bind(this, deferred, cancelled);
3490
+ const onTerminal = this.#resolveWaitAbort.bind(this, deferred, void 0);
3491
+ signal.addEventListener("abort", onAbort, { once: true });
3492
+ workflow?.emitter.on("skip", onTerminal);
3493
+ workflow?.emitter.on("stop", onTerminal);
3494
+ phase?.emitter.on("skip", onTerminal);
3495
+ phase?.emitter.on("stop", onTerminal);
2537
3496
  try {
2538
- await Promise.race([wait(), cancelled]);
3497
+ if (workflow !== void 0 && this.#halted(workflow, phase)) deferred.resolve(void 0);
3498
+ const outcome = await Promise.race([wait, deferred.promise]);
3499
+ return typeof outcome === "boolean" ? outcome : void 0;
2539
3500
  } finally {
2540
- if (onAbort !== void 0) runSignal.removeEventListener("abort", onAbort);
3501
+ signal.removeEventListener("abort", onAbort);
3502
+ workflow?.emitter.off("skip", onTerminal);
3503
+ workflow?.emitter.off("stop", onTerminal);
3504
+ phase?.emitter.off("skip", onTerminal);
3505
+ phase?.emitter.off("stop", onTerminal);
2541
3506
  }
2542
3507
  }
2543
- #taskSignal(unitSignal, runSignal) {
2544
- return AbortSignal.any([unitSignal, runSignal]);
3508
+ #resolveWaitAbort(deferred, cancelled) {
3509
+ deferred.resolve(cancelled?.());
3510
+ }
3511
+ #taskSignal(task, unitSignal, runSignal, timeout) {
3512
+ const signals = [
3513
+ task.signal,
3514
+ unitSignal,
3515
+ runSignal
3516
+ ];
3517
+ if (timeout !== void 0) signals.push(timeout.signal);
3518
+ return AbortSignal.any(signals);
2545
3519
  }
2546
3520
  #fold(workflow, options, timeout) {
2547
3521
  const signals = [workflow.signal];
2548
3522
  if (options?.signal !== void 0) signals.push(options.signal);
2549
3523
  if (timeout !== void 0) signals.push(timeout.signal);
2550
3524
  if (options?.budget !== void 0) signals.push(options.budget.signal);
2551
- return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
3525
+ return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
2552
3526
  }
2553
3527
  #haltFrom(phases, index, workflow, runSignal) {
2554
3528
  if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
@@ -2561,22 +3535,22 @@ var WorkflowRunner = class {
2561
3535
  for (const task of phase.tasks.tasks()) this.#skip(task);
2562
3536
  }
2563
3537
  }
2564
- #skipCancelled(task, workflow, runSignal) {
3538
+ #settleCancelled(task, workflow, runSignal) {
2565
3539
  if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
2566
3540
  this.#skip(task);
2567
3541
  }
2568
3542
  #skip(task) {
2569
3543
  if (task.status === "pending" || task.status === "running") task.skip();
2570
3544
  }
2571
- #skipping(controller, runSignal) {
2572
- return controller.aborted || runSignal.aborted;
3545
+ #skipping(task, controller, runSignal) {
3546
+ return task.signal.aborted || controller.aborted || runSignal.aborted;
2573
3547
  }
2574
3548
  #cancelled(runSignal) {
2575
3549
  return runSignal.aborted;
2576
3550
  }
2577
- #halted(workflow) {
3551
+ #halted(workflow, phase) {
2578
3552
  const status = workflow.status;
2579
- return status === "failed" || status === "skipped" || status === "stopped";
3553
+ return status === "failed" || status === "skipped" || status === "stopped" || phase?.status === "skipped" || phase?.status === "stopped";
2580
3554
  }
2581
3555
  #stoppable(workflow) {
2582
3556
  const status = workflow.status;
@@ -2619,14 +3593,7 @@ var WorkflowRunner = class {
2619
3593
  * ```
2620
3594
  */
2621
3595
  function createWorkflowContract() {
2622
- const contract = (0, _orkestrel_contract.createContract)(workflowShape);
2623
- return {
2624
- schema: contract.schema,
2625
- is: contract.is,
2626
- generate: (random) => contract.generate(random),
2627
- parse: (value) => contract.parse(value),
2628
- explain: (value) => contract.explain(value)
2629
- };
3596
+ return (0, _orkestrel_contract.createContract)(workflowShape);
2630
3597
  }
2631
3598
  /**
2632
3599
  * Build the live W-b entity tree from a {@link WorkflowDefinition} — the whole
@@ -2646,8 +3613,8 @@ function createWorkflowContract() {
2646
3613
  *
2647
3614
  * `options.functions` is the {@link import('./types.js').WorkflowFunctions} registry each live
2648
3615
  * task's `run` name resolves against ONCE at construction into its runtime
2649
- * {@link import('./types.js').TaskInterface.handler} a name omitted or absent from the
2650
- * registry resolves to no handler (the no-handler rule).
3616
+ * {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
3617
+ * an unresolved present name remains inspectable but is rejected if execution is attempted.
2651
3618
  *
2652
3619
  * @param definition - The workflow definition to bring to life
2653
3620
  * @param options - Runtime options (initial listeners, `bail` override, per-node options)
@@ -2681,6 +3648,9 @@ function createWorkflow(definition, options) {
2681
3648
  * still wins when supplied (to deliberately re-run under a different policy). A structurally
2682
3649
  * invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
2683
3650
  * non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
3651
+ * Runtime handlers are optional: without a matching `functions` entry, a persisted `run`
3652
+ * remains visible with an undefined `handler` so the exact state is inspectable. The runner
3653
+ * rejects that unresolved tree if execution is attempted.
2684
3654
  *
2685
3655
  * @param snapshot - The snapshot to restore (carries its own `bail` + `override`)
2686
3656
  * @param options - Runtime options (initial listeners, an optional `bail` override, per-node options)
@@ -2695,8 +3665,20 @@ function createWorkflow(definition, options) {
2695
3665
  * ```
2696
3666
  */
2697
3667
  function restoreWorkflow(snapshot, options) {
2698
- assertSnapshot(snapshot);
2699
- return new Workflow(snapshot, options);
3668
+ return new Workflow(cloneWorkflowSnapshot(snapshot), options);
3669
+ }
3670
+ /**
3671
+ * Rebuild an interrupted workflow at its remaining retry budget.
3672
+ *
3673
+ * @param snapshot - The hostile persisted snapshot
3674
+ * @param options - Runtime handlers and entity options
3675
+ * @returns A recoverable live workflow
3676
+ */
3677
+ function recoverWorkflow(snapshot, options) {
3678
+ const owned = cloneWorkflowSnapshot(snapshot);
3679
+ if (owned.override !== void 0 || owned.phases.some((phase) => phase.override !== void 0)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has a terminal override`, { workflow: owned.id });
3680
+ if (!hasWorkflowHandlers(owned, options?.functions)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved run`, { workflow: owned.id });
3681
+ return new Workflow(cloneWorkflowSnapshot(recoverWorkflowSnapshot(owned)), options);
2700
3682
  }
2701
3683
  /**
2702
3684
  * Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
@@ -2719,54 +3701,7 @@ function restoreWorkflow(snapshot, options) {
2719
3701
  * @param snapshot - The snapshot to validate
2720
3702
  */
2721
3703
  function assertSnapshot(snapshot) {
2722
- if (typeof snapshot.bail !== "boolean") throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has a non-boolean bail`, {
2723
- workflow: snapshot.id,
2724
- bail: snapshot.bail
2725
- });
2726
- if (!WORKFLOW_STATUSES.includes(snapshot.status)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid status`, {
2727
- workflow: snapshot.id,
2728
- status: snapshot.status
2729
- });
2730
- if (snapshot.override !== void 0 && !WORKFLOW_STATUSES.includes(snapshot.override)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid override`, {
2731
- workflow: snapshot.id,
2732
- override: snapshot.override
2733
- });
2734
- for (const phase of snapshot.phases) {
2735
- if (typeof phase.bail !== "boolean") throw new WorkflowError("RESTORE", `phase '${phase.id}' has a non-boolean bail`, {
2736
- phase: phase.id,
2737
- bail: phase.bail
2738
- });
2739
- if (!PHASE_STATUSES.includes(phase.status)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid status`, {
2740
- phase: phase.id,
2741
- status: phase.status
2742
- });
2743
- if (phase.override !== void 0 && !PHASE_STATUSES.includes(phase.override)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid override`, {
2744
- phase: phase.id,
2745
- override: phase.override
2746
- });
2747
- if (phase.concurrency !== void 0 && (!Number.isInteger(phase.concurrency) || phase.concurrency < 1)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid concurrency`, {
2748
- phase: phase.id,
2749
- concurrency: phase.concurrency
2750
- });
2751
- for (const task of phase.tasks) {
2752
- if (!TASK_STATUSES.includes(task.status)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid status`, {
2753
- task: task.id,
2754
- status: task.status
2755
- });
2756
- if (task.run !== void 0 && task.run.length < 1) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid run`, {
2757
- task: task.id,
2758
- run: task.run
2759
- });
2760
- if (task.retries !== void 0 && (!Number.isInteger(task.retries) || task.retries < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid retries`, {
2761
- task: task.id,
2762
- retries: task.retries
2763
- });
2764
- if (task.timeout !== void 0 && (!Number.isInteger(task.timeout) || task.timeout < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid timeout`, {
2765
- task: task.id,
2766
- timeout: task.timeout
2767
- });
2768
- }
2769
- }
3704
+ cloneWorkflowSnapshot(snapshot);
2770
3705
  }
2771
3706
  /**
2772
3707
  * Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
@@ -2833,12 +3768,13 @@ function createMemoryWorkflowStore() {
2833
3768
  * ```
2834
3769
  */
2835
3770
  function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
3771
+ const columns = {
3772
+ id: (0, _orkestrel_contract.stringShape)(),
3773
+ snapshot: (0, _orkestrel_contract.rawShape)({})
3774
+ };
2836
3775
  return new DatabaseWorkflowStore((0, _orkestrel_database.createDatabase)({
2837
3776
  driver,
2838
- tables: { snapshots: {
2839
- id: (0, _orkestrel_contract.stringShape)(),
2840
- snapshot: (0, _orkestrel_contract.rawShape)({})
2841
- } }
3777
+ tables: { snapshots: columns }
2842
3778
  }).table("snapshots"));
2843
3779
  }
2844
3780
  /**
@@ -2848,7 +3784,7 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
2848
3784
  *
2849
3785
  * @remarks
2850
3786
  * The runner is a PURE engine — it re-implements no concurrency / retry / abort logic, AND it
2851
- * carries no `functions` / `tools` / `agents` registry of its own: each live task already
3787
+ * carries no behavior or provider registry of its own: each live task already
2852
3788
  * resolved its own {@link import('./types.js').WorkflowFunction} into
2853
3789
  * {@link import('./types.js').TaskInterface.handler} ONCE at construction, from the
2854
3790
  * {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
@@ -2862,11 +3798,10 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
2862
3798
  * the live entity (`start` → `complete` / `fail`), and resolves a
2863
3799
  * {@link import('./types.js').WorkflowResult}.
2864
3800
  *
2865
- * Static tool / agent calling is OPT-IN: a caller wires a plain
2866
- * {@link import('./types.js').WorkflowFunction} into its OWN {@link WorkflowOptions.functions}
2867
- * registry, same as any other behavior the `@orkestrel/tool` package ships the
2868
- * tool/agent adapter factories for that. A task with no resolved handler AUTO-COMPLETES
2869
- * (the ROADMAP no-handler rule).
3801
+ * External integrations remain application-owned: a caller wires an ordinary
3802
+ * {@link import('./types.js').WorkflowFunction} into its own {@link WorkflowOptions.functions}
3803
+ * registry. Only a task that omits `run` auto-completes; unresolved named work is rejected
3804
+ * before dispatch.
2870
3805
  *
2871
3806
  * @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
2872
3807
  * See {@link WorkflowRunnerOptions}.
@@ -3020,6 +3955,7 @@ exports.Controller = Controller;
3020
3955
  exports.DEFAULT_BAIL = DEFAULT_BAIL;
3021
3956
  exports.DEFAULT_PHASE_CONCURRENCY = DEFAULT_PHASE_CONCURRENCY;
3022
3957
  exports.DatabaseWorkflowStore = DatabaseWorkflowStore;
3958
+ exports.MAX_TIMER_MS = MAX_TIMER_MS;
3023
3959
  exports.MemoryWorkflowStore = MemoryWorkflowStore;
3024
3960
  exports.PHASE_STATUSES = PHASE_STATUSES;
3025
3961
  exports.Phase = Phase;
@@ -3036,12 +3972,15 @@ exports.WORKFLOW_STATUSES = WORKFLOW_STATUSES;
3036
3972
  exports.Workflow = Workflow;
3037
3973
  exports.WorkflowError = WorkflowError;
3038
3974
  exports.WorkflowManager = WorkflowManager;
3975
+ exports.WorkflowPersistence = WorkflowPersistence;
3039
3976
  exports.WorkflowRunner = WorkflowRunner;
3040
3977
  exports.assertSnapshot = assertSnapshot;
3041
3978
  exports.buildPhaseContext = buildPhaseContext;
3042
3979
  exports.buildTaskContext = buildTaskContext;
3043
3980
  exports.buildWorkflowContext = buildWorkflowContext;
3044
3981
  exports.canTransitionTask = canTransitionTask;
3982
+ exports.cloneTaskActivity = cloneTaskActivity;
3983
+ exports.cloneWorkflowSnapshot = cloneWorkflowSnapshot;
3045
3984
  exports.collectResults = collectResults;
3046
3985
  exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
3047
3986
  exports.createDeferred = createDeferred;
@@ -3056,22 +3995,35 @@ exports.definitionToSnapshot = definitionToSnapshot;
3056
3995
  exports.deriveBoundary = deriveBoundary;
3057
3996
  exports.derivePhaseStatus = derivePhaseStatus;
3058
3997
  exports.deriveWorkflowStatus = deriveWorkflowStatus;
3998
+ exports.errorToMessage = errorToMessage;
3059
3999
  exports.failure = failure;
3060
4000
  exports.findFailure = findFailure;
4001
+ exports.hasWorkflowHandlers = hasWorkflowHandlers;
3061
4002
  exports.insertEntry = insertEntry;
4003
+ exports.isLifecycleStatus = isLifecycleStatus;
4004
+ exports.isOwnedWorkflowSnapshot = isOwnedWorkflowSnapshot;
4005
+ exports.isTaskActivity = isTaskActivity;
4006
+ exports.isTaskActivityInput = isTaskActivityInput;
4007
+ exports.isTaskFailure = isTaskFailure;
4008
+ exports.isTaskResult = isTaskResult;
3062
4009
  exports.isTerminalStatus = isTerminalStatus;
3063
4010
  exports.isWorkflowError = isWorkflowError;
3064
4011
  exports.isWorkflowSnapshot = isWorkflowSnapshot;
4012
+ exports.matchesDescription = matchesDescription;
3065
4013
  exports.moveEntry = moveEntry;
3066
4014
  exports.parkSignal = parkSignal;
3067
4015
  exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
3068
4016
  exports.phaseShape = phaseShape;
3069
4017
  exports.phaseUpdateShape = phaseUpdateShape;
4018
+ exports.recoverWorkflow = recoverWorkflow;
4019
+ exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
4020
+ exports.resolveTaskSilence = resolveTaskSilence;
3070
4021
  exports.restoreWorkflow = restoreWorkflow;
3071
4022
  exports.success = success;
3072
4023
  exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
3073
4024
  exports.taskShape = taskShape;
3074
4025
  exports.taskUpdateShape = taskUpdateShape;
3075
4026
  exports.workflowShape = workflowShape;
4027
+ exports.workflowSnapshotContext = workflowSnapshotContext;
3076
4028
 
3077
4029
  //# sourceMappingURL=index.cjs.map