@smithers-orchestrator/scheduler 0.25.1 → 0.25.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/scheduler",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "description": "Pure decision engine: session, scheduler, and task state management for Smithers workflows",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -176,8 +176,8 @@
176
176
  ],
177
177
  "dependencies": {
178
178
  "effect": "^3.21.1",
179
- "@smithers-orchestrator/errors": "0.25.1",
180
- "@smithers-orchestrator/graph": "0.25.1"
179
+ "@smithers-orchestrator/errors": "0.25.2",
180
+ "@smithers-orchestrator/graph": "0.25.2"
181
181
  },
182
182
  "devDependencies": {
183
183
  "@types/bun": "latest",
@@ -597,14 +597,23 @@ export function makeWorkflowSession(options = {}) {
597
597
  };
598
598
  }
599
599
  /**
600
- * @param {number} [depth] recursion depth; guarded at 10 to catch decision cycles
600
+ * @param {number} [depth] recursion depth; a safety net for a true decision
601
+ * cycle (a non-monotonic transition bug)
601
602
  * @returns {EngineDecision}
602
603
  */
603
604
  function decide(depth = 0) {
604
- if (depth > 10) {
605
+ // Each recursion below only fires when `changed` is true, i.e. at least
606
+ // one task moved to a terminal/in-progress/waiting state — monotonic
607
+ // forward progress. A legitimate chain can therefore be as long as the
608
+ // number of tasks: e.g. a <Sequence> of N skipIf steps yields exactly one
609
+ // skip per pass (#bug: 11+ such steps tripped a hard constant-10 guard and
610
+ // failed a perfectly valid run). Bound by the task count + slack instead;
611
+ // a genuine cycle keeps recursing past the point where every task settled.
612
+ const maxDecideDepth = state.descriptors.size + 10;
613
+ if (depth > maxDecideDepth) {
605
614
  return {
606
615
  _tag: "Failed",
607
- error: new SmithersError("SCHEDULER_ERROR", "Exceeded scheduler decide() depth guard.", { depth }),
616
+ error: new SmithersError("SCHEDULER_ERROR", "Exceeded scheduler decide() depth guard.", { depth, maxDepth: maxDecideDepth }),
608
617
  };
609
618
  }
610
619
  if (state.cancelled) {
@@ -398,8 +398,20 @@ export function scheduleTasks(plan, states, descriptors, ralphState, retryWait,
398
398
  const status = inspect(child, {
399
399
  includeContinuedFailures: true,
400
400
  });
401
- if (!status.terminal)
401
+ if (!status.terminal) {
402
+ // A failure already present in this still-running action
403
+ // subtree (e.g. a failed task in a <Parallel> whose sibling
404
+ // is still in flight) must be recorded as recoverable now.
405
+ // Otherwise decide()'s unhandled-failure check fails the run
406
+ // before the action region settles and the saga's
407
+ // compensation can run — an order-dependent bug that only
408
+ // bites when the failing task settles before its sibling.
409
+ const before = failureRecoveryKeys.size;
410
+ collectFailureKeys(child, { includeContinuedFailures: true });
411
+ if (failureRecoveryKeys.size > before)
412
+ failureRecoveryActive = true;
402
413
  return walk(child);
414
+ }
403
415
  if (status.failed) {
404
416
  failed = true;
405
417
  break;
@@ -448,8 +460,19 @@ export function scheduleTasks(plan, states, descriptors, ralphState, retryWait,
448
460
  const status = inspect(child, {
449
461
  includeContinuedFailures: true,
450
462
  });
451
- if (!status.terminal)
463
+ if (!status.terminal) {
464
+ // A failure already present in this still-running try child
465
+ // (e.g. a failed task in a <Parallel> whose sibling is still
466
+ // in flight) must be recorded as recoverable now, or decide()
467
+ // fails the run before the try region settles — skipping
468
+ // catch AND finally. Deferring here lets the region finish so
469
+ // catch/finally run regardless of which task settles first.
470
+ const before = failureRecoveryKeys.size;
471
+ collectFailureKeys(child, { includeContinuedFailures: true });
472
+ if (failureRecoveryKeys.size > before)
473
+ failureRecoveryActive = true;
452
474
  return walk(child);
475
+ }
453
476
  if (status.failed) {
454
477
  tryFailed = true;
455
478
  break;