@kb-labs/workflow-engine 2.107.0 → 2.110.0

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/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { WorkflowSpec, RunTrigger, IdempotencyKey, ConcurrencyGroup, WorkflowRun, JobRun, StepRun, RetryPolicy, ArtifactMergeConfig } from '@kb-labs/workflow-contracts';
2
2
  import { ILogger, ICache, IEventBus, IAnalytics, ISnapshotManager, Unsubscribe, IJobScheduler, JobDefinition, JobHandle, CronExpression, JobFilter } from '@kb-labs/core-platform';
3
3
  import { JobPriority, WorkflowEventName } from '@kb-labs/workflow-constants';
4
+ import { ClassifiedFailure, IExecutionBackend } from '@kb-labs/core-contracts';
4
5
  import { ArtifactClient } from '@kb-labs/workflow-artifacts';
5
6
  import { IEntityRegistry } from '@kb-labs/core-registry';
6
7
  import { PlatformServices, JobHandlerDecl, WorkflowHandlerDecl, CronDecl, PluginContextDescriptor } from '@kb-labs/plugin-contracts';
7
- import { IExecutionBackend } from '@kb-labs/core-contracts';
8
8
 
9
9
  /**
10
10
  * @deprecated Use ILogger from @kb-labs/core-platform instead.
@@ -229,7 +229,13 @@ declare class WorkflowEngine {
229
229
  * Mark job as failed and optionally schedule retry.
230
230
  * Implements exponential/linear backoff retry logic.
231
231
  */
232
- markJobFailed(runId: string, jobId: string, error: Error, shouldRetry?: boolean): Promise<void>;
232
+ markJobFailed(runId: string, jobId: string, error: Error, failure?: ClassifiedFailure, policyOverride?: false | {
233
+ max: number;
234
+ backoff?: 'exp' | 'lin';
235
+ initialIntervalMs?: number;
236
+ maxIntervalMs?: number;
237
+ on?: string[];
238
+ }): Promise<void>;
233
239
  /**
234
240
  * Recursively cancel jobs blocked on a failed/cancelled upstream job, so the
235
241
  * DAG does not leave dependents stuck in 'queued'. Cancellation cascades:
@@ -300,14 +306,6 @@ declare class WorkflowEngine {
300
306
  * Re-queues jobs that were interrupted during previous shutdown.
301
307
  */
302
308
  resumeInterruptedJobs(): Promise<void>;
303
- /**
304
- * Determine if job should be retried based on retry policy.
305
- */
306
- private shouldRetryJob;
307
- /**
308
- * Calculate backoff delay using exponential or linear strategy.
309
- */
310
- private calculateBackoff;
311
309
  /**
312
310
  * Move permanently failed job to Dead Letter Queue.
313
311
  */
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { parse, stringify } from 'yaml';
4
4
  import { WorkflowSpecSchema, evaluateExpression } from '@kb-labs/workflow-contracts';
5
5
  import { randomUUID } from 'crypto';
6
6
  import { WORKFLOW_REDIS_CHANNEL, EVENT_NAMES, IDEMPOTENCY_TTL_ENV, CONCURRENCY_TTL_ENV } from '@kb-labs/workflow-constants';
7
+ import { classifyFailure, decideRetry } from '@kb-labs/core-retry';
7
8
  import { createFileSystemArtifactClient } from '@kb-labs/workflow-artifacts';
8
9
  import { existsSync } from 'fs';
9
10
  import { nanoid } from 'nanoid';
@@ -794,7 +795,7 @@ var WorkflowEngine = class {
794
795
  * Mark job as failed and optionally schedule retry.
795
796
  * Implements exponential/linear backoff retry logic.
796
797
  */
797
- async markJobFailed(runId, jobId, error, shouldRetry2 = true) {
798
+ async markJobFailed(runId, jobId, error, failure, policyOverride) {
798
799
  const run = await this.stateStore.getRun(runId);
799
800
  if (!run) {
800
801
  this.logger.warn("Cannot mark job as failed: run not found", { runId, jobId });
@@ -820,13 +821,26 @@ var WorkflowEngine = class {
820
821
  jobId,
821
822
  attempt: (job.attempt || 0) + 1
822
823
  });
824
+ const classified = failure ?? classifyFailure(error, { source: "execution", phase: "response" });
825
+ const retryConfig = policyOverride !== void 0 && typeof policyOverride !== "boolean" ? {
826
+ maxAttempts: policyOverride.max + 1,
827
+ retryOn: policyOverride.on ?? ["command", "network", "timeout", "rate_limit", "server", "infrastructure"],
828
+ initialDelayMs: policyOverride.initialIntervalMs ?? 1e3,
829
+ backoff: policyOverride.backoff === "lin" ? "linear" : "exponential",
830
+ maxDelayMs: policyOverride.maxIntervalMs ?? 3e4,
831
+ respectRetryAfter: true,
832
+ requireIdempotencyForUnsafeFailures: true
833
+ } : void 0;
834
+ const retryDecision = policyOverride === false ? { retry: false, delayMs: 0 } : retryConfig ? decideRetry({ failure: classified, attempt: job.attempt || 0, policy: retryConfig }) : decideRetry({ failure: classified, attempt: job.attempt || 0 });
823
835
  this.analytics?.track("workflow.job.failed", {
824
836
  runId,
825
837
  jobId,
826
838
  jobName: job.jobName,
827
839
  attempt: (job.attempt || 0) + 1,
828
840
  errorMessage: error.message,
829
- willRetry: shouldRetry2 && this.shouldRetryJob(job)
841
+ willRetry: retryDecision.retry,
842
+ failureKind: classified.kind,
843
+ failureCode: classified.code
830
844
  }).catch(() => {
831
845
  });
832
846
  await this.events.publish({
@@ -835,8 +849,8 @@ var WorkflowEngine = class {
835
849
  jobId,
836
850
  payload: { jobName: job.jobName, error: error.message, attempt: (job.attempt || 0) + 1 }
837
851
  });
838
- if (shouldRetry2 && this.shouldRetryJob(job)) {
839
- const backoffMs = this.calculateBackoff(job.attempt || 0, job.retries);
852
+ if (retryDecision.retry) {
853
+ const backoffMs = retryDecision.delayMs;
840
854
  this.logger.info("Scheduling job retry", {
841
855
  runId,
842
856
  jobId,
@@ -1295,31 +1309,6 @@ var WorkflowEngine = class {
1295
1309
  this.logger.info("Resumed interrupted jobs", { count: resumedCount });
1296
1310
  }
1297
1311
  }
1298
- /**
1299
- * Determine if job should be retried based on retry policy.
1300
- */
1301
- shouldRetryJob(job) {
1302
- const retryPolicy = job.retries || { max: 3};
1303
- const attempt = job.attempt || 0;
1304
- return attempt < retryPolicy.max;
1305
- }
1306
- /**
1307
- * Calculate backoff delay using exponential or linear strategy.
1308
- */
1309
- calculateBackoff(attempt, policy) {
1310
- const config = {
1311
- backoff: policy?.backoff || "exp",
1312
- initialIntervalMs: policy?.initialIntervalMs || 1e3,
1313
- maxIntervalMs: policy?.maxIntervalMs || 6e4
1314
- };
1315
- let backoffMs;
1316
- if (config.backoff === "exp") {
1317
- backoffMs = config.initialIntervalMs * Math.pow(2, attempt);
1318
- } else {
1319
- backoffMs = config.initialIntervalMs * (attempt + 1);
1320
- }
1321
- return Math.min(backoffMs, config.maxIntervalMs);
1322
- }
1323
1312
  /**
1324
1313
  * Move permanently failed job to Dead Letter Queue.
1325
1314
  */