@kb-labs/workflow-engine 2.96.0 → 2.98.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
@@ -320,6 +320,18 @@ declare class WorkflowEngine {
320
320
  private deadLetterJob;
321
321
  updateRun(runId: string, mutator: (run: WorkflowRun) => WorkflowRun | void): Promise<WorkflowRun | null>;
322
322
  finalizeRun(runId: string, status: WorkflowRun['status'], context?: Partial<RunContext>): Promise<WorkflowRun | null>;
323
+ /**
324
+ * Persist a snapshot for a run that just reached a terminal state, so it
325
+ * can be replayed from any step later (`replayRun`, used by both
326
+ * `workflow runs restart --from-step` and `rerun --failed-only`). Only
327
+ * 'success'/'failed' carry meaningful step output data worth snapshotting.
328
+ *
329
+ * Shared by finalizeRun() and checkRunCompletion() — checkRunCompletion is
330
+ * the run-completion path actually driven by the worker in production, so
331
+ * without this call here, no snapshot is ever created outside of tests
332
+ * that call finalizeRun() directly (issue #263).
333
+ */
334
+ private snapshotTerminalRun;
323
335
  nextJob(): Promise<JobQueueEntry | null>;
324
336
  rescheduleJob(entry: JobQueueEntry, delayMs: number): Promise<void>;
325
337
  publishRunEvent(type: WorkflowEventName, run: WorkflowRun): Promise<void>;
package/dist/index.js CHANGED
@@ -1038,7 +1038,7 @@ var WorkflowEngine = class {
1038
1038
  return;
1039
1039
  }
1040
1040
  if (allSuccess) {
1041
- await this.stateStore.updateRun(runId, (draft) => {
1041
+ const updated = await this.stateStore.updateRun(runId, (draft) => {
1042
1042
  draft.status = "success";
1043
1043
  draft.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1044
1044
  return draft;
@@ -1056,9 +1056,12 @@ var WorkflowEngine = class {
1056
1056
  runId,
1057
1057
  payload: { status: "success", name: run.name }
1058
1058
  });
1059
+ if (updated) {
1060
+ await this.snapshotTerminalRun(updated);
1061
+ }
1059
1062
  } else if (anyFailed) {
1060
1063
  const failedJob = run.jobs.find((j) => j.status === "failed");
1061
- await this.stateStore.updateRun(runId, (draft) => {
1064
+ const updated = await this.stateStore.updateRun(runId, (draft) => {
1062
1065
  draft.status = "failed";
1063
1066
  draft.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1064
1067
  if (failedJob?.error) {
@@ -1086,6 +1089,9 @@ var WorkflowEngine = class {
1086
1089
  runId,
1087
1090
  payload: { status: "failed", name: run.name }
1088
1091
  });
1092
+ if (updated) {
1093
+ await this.snapshotTerminalRun(updated);
1094
+ }
1089
1095
  }
1090
1096
  }
1091
1097
  /**
@@ -1370,21 +1376,40 @@ var WorkflowEngine = class {
1370
1376
  status === "failed" ? EVENT_NAMES.run.failed : status === "cancelled" ? EVENT_NAMES.run.cancelled : EVENT_NAMES.run.finished,
1371
1377
  updated
1372
1378
  );
1373
- if (status === "success" || status === "failed") {
1374
- const stepOutputs = {};
1375
- for (const job of updated.jobs) {
1376
- for (const step of job.steps) {
1377
- if (step.outputs && Object.keys(step.outputs).length > 0) {
1378
- stepOutputs[step.id] = step.outputs;
1379
- }
1380
- }
1379
+ await this.snapshotTerminalRun(updated);
1380
+ }
1381
+ return updated;
1382
+ }
1383
+ /**
1384
+ * Persist a snapshot for a run that just reached a terminal state, so it
1385
+ * can be replayed from any step later (`replayRun`, used by both
1386
+ * `workflow runs restart --from-step` and `rerun --failed-only`). Only
1387
+ * 'success'/'failed' carry meaningful step output data worth snapshotting.
1388
+ *
1389
+ * Shared by finalizeRun() and checkRunCompletion() — checkRunCompletion is
1390
+ * the run-completion path actually driven by the worker in production, so
1391
+ * without this call here, no snapshot is ever created outside of tests
1392
+ * that call finalizeRun() directly (issue #263).
1393
+ */
1394
+ async snapshotTerminalRun(run) {
1395
+ if (run.status !== "success" && run.status !== "failed") {
1396
+ return;
1397
+ }
1398
+ const stepOutputs = {};
1399
+ for (const job of run.jobs) {
1400
+ for (const step of job.steps) {
1401
+ if (step.outputs && Object.keys(step.outputs).length > 0) {
1402
+ stepOutputs[step.id] = step.outputs;
1381
1403
  }
1382
- await this.snapshotStorage.createSnapshot(updated, stepOutputs, updated.env ?? {}).catch((err) => {
1383
- this.logger.warn("Failed to create run snapshot", { runId, error: err instanceof Error ? err.message : String(err) });
1384
- });
1385
1404
  }
1386
1405
  }
1387
- return updated;
1406
+ await this.snapshotStorage.createSnapshot(run, stepOutputs, run.env ?? {}).catch((err) => {
1407
+ this.logger.error(
1408
+ "Failed to create run snapshot \u2014 restart --from-step and rerun --failed-only will not work for this run",
1409
+ err instanceof Error ? err : new Error(String(err)),
1410
+ { runId: run.id, status: run.status }
1411
+ );
1412
+ });
1388
1413
  }
1389
1414
  async nextJob() {
1390
1415
  return this.scheduler.dequeueJob();