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