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