@bratsos/workflow-engine 0.5.0 → 0.6.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.
Files changed (41) hide show
  1. package/README.md +67 -13
  2. package/dist/{chunk-RZY5YRGL.js → chunk-2MWO6UVR.js} +2 -2
  3. package/dist/chunk-2MWO6UVR.js.map +1 -0
  4. package/dist/{chunk-PHLNTR5Z.js → chunk-DIADEUGZ.js} +21 -3
  5. package/dist/chunk-DIADEUGZ.js.map +1 -0
  6. package/dist/{chunk-ZYMT2PAO.js → chunk-HKGZ2WHJ.js} +2 -30
  7. package/dist/chunk-HKGZ2WHJ.js.map +1 -0
  8. package/dist/{chunk-3NEGRI4M.js → chunk-HOGDFLCG.js} +342 -114
  9. package/dist/chunk-HOGDFLCG.js.map +1 -0
  10. package/dist/{client-oLD5ilXp.d.ts → client-llB6XpHS.d.ts} +10 -81
  11. package/dist/client.d.ts +4 -3
  12. package/dist/client.js +1 -1
  13. package/dist/events-D_P24UaY.d.ts +105 -0
  14. package/dist/{index-CVkkGnxx.d.ts → index-sGgV8JNu.d.ts} +5 -1
  15. package/dist/index.d.ts +184 -32
  16. package/dist/index.js +41 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/{interface-TsryH4d7.d.ts → interface-DCdddCe0.d.ts} +7 -2
  19. package/dist/kernel/index.d.ts +6 -5
  20. package/dist/kernel/index.js +2 -1
  21. package/dist/kernel/testing/index.d.ts +3 -2
  22. package/dist/persistence/index.d.ts +2 -2
  23. package/dist/persistence/index.js +2 -2
  24. package/dist/persistence/prisma/index.d.ts +2 -2
  25. package/dist/persistence/prisma/index.js +2 -2
  26. package/dist/{plugins-DW266bhT.d.ts → plugins-Oyo_iu0l.d.ts} +16 -10
  27. package/dist/{ports-855bktyD.d.ts → ports-ChGnJcn2.d.ts} +5 -106
  28. package/dist/{stage-BPw7m9Wx.d.ts → stage-_7BKqqUG.d.ts} +2 -2
  29. package/dist/testing/index.d.ts +2 -1
  30. package/dist/testing/index.js +23 -5
  31. package/dist/testing/index.js.map +1 -1
  32. package/package.json +1 -1
  33. package/skills/workflow-engine/SKILL.md +31 -12
  34. package/skills/workflow-engine/references/02-workflow-builder.md +2 -0
  35. package/skills/workflow-engine/references/03-runtime-setup.md +1 -1
  36. package/skills/workflow-engine/references/08-common-patterns.md +17 -3
  37. package/skills/workflow-engine/references/09-troubleshooting.md +12 -3
  38. package/dist/chunk-3NEGRI4M.js.map +0 -1
  39. package/dist/chunk-PHLNTR5Z.js.map +0 -1
  40. package/dist/chunk-RZY5YRGL.js.map +0 -1
  41. package/dist/chunk-ZYMT2PAO.js.map +0 -1
@@ -1,6 +1,6 @@
1
+ import { StaleVersionError } from './chunk-2MWO6UVR.js';
1
2
  import { z } from 'zod';
2
3
 
3
- // src/core/types.ts
4
4
  z.object({
5
5
  batchId: z.string(),
6
6
  statusUrl: z.string().optional(),
@@ -78,15 +78,44 @@ function createStorageShim(workflowRunId, workflowType, deps) {
78
78
  };
79
79
  }
80
80
 
81
+ // src/kernel/helpers/resolve-execution-group-output.ts
82
+ function resolveExecutionGroupOutput(workflow, groupIndex, workflowContext) {
83
+ const stages = workflow.getStagesInExecutionGroup(groupIndex);
84
+ if (stages.length === 0) return void 0;
85
+ if (stages.length === 1) {
86
+ return workflowContext[stages[0].id];
87
+ }
88
+ const merged = {};
89
+ for (const stage of stages) {
90
+ if (workflowContext[stage.id] !== void 0) {
91
+ merged[stage.id] = workflowContext[stage.id];
92
+ }
93
+ }
94
+ return merged;
95
+ }
96
+
97
+ // src/kernel/helpers/save-stage-artifacts.ts
98
+ async function saveStageArtifacts(runId, workflowType, stageId, artifacts, deps) {
99
+ const artifactKeys = {};
100
+ for (const [artifactName, artifact] of Object.entries(artifacts)) {
101
+ const encodedName = encodeURIComponent(artifactName);
102
+ const key = `workflow-v2/${workflowType}/${runId}/${stageId}/artifacts/${encodedName}.json`;
103
+ await deps.blobStore.put(key, artifact);
104
+ artifactKeys[artifactName] = key;
105
+ }
106
+ return artifactKeys;
107
+ }
108
+
81
109
  // src/kernel/handlers/job-execute.ts
82
110
  function resolveStageInput(workflow, stageId, workflowRun, workflowContext) {
83
111
  const groupIndex = workflow.getExecutionGroupIndex(stageId);
84
- if (groupIndex === 0) return workflowRun.input;
85
- const prevStageId = workflow.getPreviousStageId(stageId);
86
- if (prevStageId && workflowContext[prevStageId] !== void 0) {
87
- return workflowContext[prevStageId];
88
- }
89
- return workflowRun.input;
112
+ if (groupIndex <= 1) return workflowRun.input;
113
+ const prevOutput = resolveExecutionGroupOutput(
114
+ workflow,
115
+ groupIndex - 1,
116
+ workflowContext
117
+ );
118
+ return prevOutput ?? workflowRun.input;
90
119
  }
91
120
  function toOutboxEvents(workflowRunId, causationId, events) {
92
121
  return events.map((event) => ({
@@ -112,6 +141,7 @@ async function handleJobExecute(command, deps) {
112
141
  if (workflowRun.status !== "RUNNING") {
113
142
  return {
114
143
  outcome: "failed",
144
+ ghost: true,
115
145
  error: `Run ${workflowRunId} is ${workflowRun.status}, expected RUNNING \u2014 ghost job discarded`,
116
146
  _events: []
117
147
  };
@@ -202,6 +232,15 @@ async function handleJobExecute(command, deps) {
202
232
  workflowContext
203
233
  };
204
234
  const result = await stageDef.execute(context);
235
+ const currentRunStatus = await deps.persistence.getRunStatus(workflowRunId);
236
+ if (currentRunStatus !== "RUNNING") {
237
+ return {
238
+ outcome: "failed",
239
+ ghost: true,
240
+ error: `Run ${workflowRunId} was ${currentRunStatus} after stage execution \u2014 result discarded`,
241
+ _events: []
242
+ };
243
+ }
205
244
  if (isSuspendedResult(result)) {
206
245
  const { state, pollConfig, metrics } = result;
207
246
  const nextPollAt = new Date(
@@ -241,12 +280,22 @@ async function handleJobExecute(command, deps) {
241
280
  result.output,
242
281
  deps
243
282
  );
283
+ const artifactKeys = result.artifacts && Object.keys(result.artifacts).length > 0 ? await saveStageArtifacts(
284
+ workflowRunId,
285
+ workflowRun.workflowType,
286
+ stageId,
287
+ result.artifacts,
288
+ deps
289
+ ) : void 0;
244
290
  await deps.persistence.withTransaction(async (tx) => {
245
291
  await tx.updateStage(stageRecord.id, {
246
292
  status: "COMPLETED",
247
293
  completedAt: deps.clock.now(),
248
294
  duration,
249
- outputData: { _artifactKey: outputKey },
295
+ outputData: {
296
+ _artifactKey: outputKey,
297
+ ...artifactKeys ? { _artifactKeys: artifactKeys } : {}
298
+ },
250
299
  metrics: result.metrics,
251
300
  embeddingInfo: result.embeddings
252
301
  });
@@ -362,6 +411,17 @@ async function handleRunCancel(command, deps) {
362
411
  status: "CANCELLED",
363
412
  completedAt: deps.clock.now()
364
413
  });
414
+ const stages = await deps.persistence.getStagesByRun(command.workflowRunId);
415
+ for (const stage of stages) {
416
+ if (!TERMINAL_STATUSES.has(stage.status)) {
417
+ await deps.persistence.updateStage(stage.id, {
418
+ status: "CANCELLED",
419
+ completedAt: deps.clock.now(),
420
+ nextPollAt: null
421
+ });
422
+ }
423
+ }
424
+ await deps.jobTransport.cancelByRun(command.workflowRunId);
365
425
  return {
366
426
  cancelled: true,
367
427
  _events: [
@@ -644,9 +704,10 @@ async function handleRunRerunFrom(command, deps) {
644
704
  );
645
705
  const deletedStageIds = stagesToDelete.map((s) => s.stageId);
646
706
  for (const stage of stagesToDelete) {
647
- const outputRef = stage.outputData;
648
- if (outputRef?._artifactKey) {
649
- await deps.blobStore.delete(outputRef._artifactKey).catch(() => {
707
+ const prefix = `workflow-v2/${run.workflowType}/${workflowRunId}/${stage.stageId}/`;
708
+ const keys = await deps.blobStore.list(prefix).catch(() => []);
709
+ for (const key of keys) {
710
+ await deps.blobStore.delete(key).catch(() => {
650
711
  });
651
712
  }
652
713
  }
@@ -655,7 +716,12 @@ async function handleRunRerunFrom(command, deps) {
655
716
  }
656
717
  await deps.persistence.updateRun(workflowRunId, {
657
718
  status: "RUNNING",
658
- completedAt: null
719
+ startedAt: deps.clock.now(),
720
+ completedAt: null,
721
+ duration: null,
722
+ output: null,
723
+ totalCost: 0,
724
+ totalTokens: 0
659
725
  });
660
726
  const targetStages = workflow.getStagesInExecutionGroup(targetGroup);
661
727
  for (const stage of targetStages) {
@@ -694,6 +760,19 @@ async function handleRunRerunFrom(command, deps) {
694
760
  // src/kernel/handlers/run-transition.ts
695
761
  var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "CANCELLED"]);
696
762
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["RUNNING", "PENDING", "SUSPENDED"]);
763
+ async function claimRunTransition(run, deps) {
764
+ try {
765
+ await deps.persistence.updateRun(run.id, {
766
+ expectedVersion: run.version
767
+ });
768
+ return true;
769
+ } catch (error) {
770
+ if (error instanceof StaleVersionError) {
771
+ return false;
772
+ }
773
+ throw error;
774
+ }
775
+ }
697
776
  async function enqueueExecutionGroup(run, workflow, groupIndex, deps) {
698
777
  const stages = workflow.getStagesInExecutionGroup(groupIndex);
699
778
  const stagesToEnqueue = [];
@@ -742,6 +821,9 @@ async function handleRunTransition(command, deps) {
742
821
  }
743
822
  const stages = await deps.persistence.getStagesByRun(command.workflowRunId);
744
823
  if (stages.length === 0) {
824
+ if (!await claimRunTransition(run, deps)) {
825
+ return { action: "noop", _events: [] };
826
+ }
745
827
  await enqueueExecutionGroup(run, workflow, 1, deps);
746
828
  events.push({
747
829
  type: "workflow:started",
@@ -756,6 +838,9 @@ async function handleRunTransition(command, deps) {
756
838
  }
757
839
  const failedStage = stages.find((s) => s.status === "FAILED");
758
840
  if (failedStage) {
841
+ if (!await claimRunTransition(run, deps)) {
842
+ return { action: "noop", _events: [] };
843
+ }
759
844
  await deps.persistence.updateRun(command.workflowRunId, {
760
845
  status: "FAILED",
761
846
  completedAt: deps.clock.now()
@@ -774,6 +859,9 @@ async function handleRunTransition(command, deps) {
774
859
  );
775
860
  const nextGroupStages = workflow.getStagesInExecutionGroup(maxGroup + 1);
776
861
  if (nextGroupStages.length > 0) {
862
+ if (!await claimRunTransition(run, deps)) {
863
+ return { action: "noop", _events: [] };
864
+ }
777
865
  await enqueueExecutionGroup(run, workflow, maxGroup + 1, deps);
778
866
  return {
779
867
  action: "advanced",
@@ -791,10 +879,23 @@ async function handleRunTransition(command, deps) {
791
879
  }
792
880
  }
793
881
  const duration = deps.clock.now().getTime() - run.createdAt.getTime();
882
+ const workflowContext = await loadWorkflowContext(
883
+ command.workflowRunId,
884
+ deps
885
+ );
886
+ const output = resolveExecutionGroupOutput(
887
+ workflow,
888
+ maxGroup,
889
+ workflowContext
890
+ );
891
+ if (!await claimRunTransition(run, deps)) {
892
+ return { action: "noop", _events: [] };
893
+ }
794
894
  await deps.persistence.updateRun(command.workflowRunId, {
795
895
  status: "COMPLETED",
796
896
  completedAt: deps.clock.now(),
797
897
  duration,
898
+ output,
798
899
  totalCost,
799
900
  totalTokens
800
901
  });
@@ -803,6 +904,7 @@ async function handleRunTransition(command, deps) {
803
904
  timestamp: deps.clock.now(),
804
905
  workflowRunId: command.workflowRunId,
805
906
  duration,
907
+ output,
806
908
  totalCost,
807
909
  totalTokens
808
910
  });
@@ -810,8 +912,40 @@ async function handleRunTransition(command, deps) {
810
912
  }
811
913
 
812
914
  // src/kernel/handlers/stage-poll-suspended.ts
915
+ function toOutboxEvents2(workflowRunId, events) {
916
+ const causationId = crypto.randomUUID();
917
+ return events.map((event) => ({
918
+ workflowRunId,
919
+ eventType: event.type,
920
+ payload: event,
921
+ causationId,
922
+ occurredAt: event.timestamp
923
+ }));
924
+ }
925
+ async function markStageCancelled(stageId, deps) {
926
+ await deps.persistence.updateStage(stageId, {
927
+ status: "CANCELLED",
928
+ completedAt: deps.clock.now(),
929
+ nextPollAt: null
930
+ });
931
+ }
932
+ async function withClaimedRun(workflowRunId, expectedVersion, deps, fn) {
933
+ try {
934
+ const value = await deps.persistence.withTransaction(async (tx) => {
935
+ await tx.updateRun(workflowRunId, {
936
+ expectedVersion
937
+ });
938
+ return fn(tx);
939
+ });
940
+ return { status: "claimed", value };
941
+ } catch (error) {
942
+ if (error instanceof StaleVersionError) {
943
+ return { status: "stale" };
944
+ }
945
+ throw error;
946
+ }
947
+ }
813
948
  async function handleStagePollSuspended(command, deps) {
814
- const events = [];
815
949
  const maxChecks = command.maxChecks ?? 50;
816
950
  const suspendedStages = await deps.persistence.getSuspendedStages(
817
951
  deps.clock.now()
@@ -825,41 +959,57 @@ async function handleStagePollSuspended(command, deps) {
825
959
  checked++;
826
960
  const run = await deps.persistence.getRun(stageRecord.workflowRunId);
827
961
  if (!run) continue;
962
+ if (run.status === "CANCELLED") {
963
+ await markStageCancelled(stageRecord.id, deps);
964
+ continue;
965
+ }
828
966
  const workflow = deps.registry.getWorkflow(run.workflowId);
829
967
  if (!workflow) {
830
- await deps.persistence.updateStage(stageRecord.id, {
831
- status: "FAILED",
832
- completedAt: deps.clock.now(),
833
- errorMessage: `Workflow ${run.workflowId} not found in registry`
968
+ await deps.persistence.withTransaction(async (tx) => {
969
+ await tx.updateStage(stageRecord.id, {
970
+ status: "FAILED",
971
+ completedAt: deps.clock.now(),
972
+ errorMessage: `Workflow ${run.workflowId} not found in registry`
973
+ });
974
+ await tx.appendOutboxEvents(
975
+ toOutboxEvents2(stageRecord.workflowRunId, [
976
+ {
977
+ type: "stage:failed",
978
+ timestamp: deps.clock.now(),
979
+ workflowRunId: stageRecord.workflowRunId,
980
+ stageId: stageRecord.stageId,
981
+ stageName: stageRecord.stageName,
982
+ error: `Workflow ${run.workflowId} not found in registry`
983
+ }
984
+ ])
985
+ );
834
986
  });
835
987
  failed++;
836
- events.push({
837
- type: "stage:failed",
838
- timestamp: deps.clock.now(),
839
- workflowRunId: stageRecord.workflowRunId,
840
- stageId: stageRecord.stageId,
841
- stageName: stageRecord.stageName,
842
- error: `Workflow ${run.workflowId} not found in registry`
843
- });
844
988
  continue;
845
989
  }
846
990
  const stageDef = workflow.getStage(stageRecord.stageId);
847
991
  if (!stageDef || !stageDef.checkCompletion) {
848
992
  const errorMsg = !stageDef ? `Stage ${stageRecord.stageId} not found in workflow ${run.workflowId}` : `Stage ${stageRecord.stageId} does not support checkCompletion`;
849
- await deps.persistence.updateStage(stageRecord.id, {
850
- status: "FAILED",
851
- completedAt: deps.clock.now(),
852
- errorMessage: errorMsg
993
+ await deps.persistence.withTransaction(async (tx) => {
994
+ await tx.updateStage(stageRecord.id, {
995
+ status: "FAILED",
996
+ completedAt: deps.clock.now(),
997
+ errorMessage: errorMsg
998
+ });
999
+ await tx.appendOutboxEvents(
1000
+ toOutboxEvents2(stageRecord.workflowRunId, [
1001
+ {
1002
+ type: "stage:failed",
1003
+ timestamp: deps.clock.now(),
1004
+ workflowRunId: stageRecord.workflowRunId,
1005
+ stageId: stageRecord.stageId,
1006
+ stageName: stageRecord.stageName,
1007
+ error: errorMsg
1008
+ }
1009
+ ])
1010
+ );
853
1011
  });
854
1012
  failed++;
855
- events.push({
856
- type: "stage:failed",
857
- timestamp: deps.clock.now(),
858
- workflowRunId: stageRecord.workflowRunId,
859
- stageId: stageRecord.stageId,
860
- stageName: stageRecord.stageName,
861
- error: errorMsg
862
- });
863
1013
  continue;
864
1014
  }
865
1015
  const storage = createStorageShim(
@@ -892,31 +1042,51 @@ async function handleStagePollSuspended(command, deps) {
892
1042
  checkContext
893
1043
  );
894
1044
  if (checkResult.error) {
895
- await deps.persistence.updateStage(stageRecord.id, {
896
- status: "FAILED",
897
- completedAt: deps.clock.now(),
898
- errorMessage: checkResult.error,
899
- nextPollAt: null
900
- });
901
- await deps.persistence.updateRun(stageRecord.workflowRunId, {
902
- status: "FAILED",
903
- completedAt: deps.clock.now()
904
- });
1045
+ const claimResult = await withClaimedRun(
1046
+ stageRecord.workflowRunId,
1047
+ run.version,
1048
+ deps,
1049
+ async (tx) => {
1050
+ await tx.updateStage(stageRecord.id, {
1051
+ status: "FAILED",
1052
+ completedAt: deps.clock.now(),
1053
+ errorMessage: checkResult.error,
1054
+ nextPollAt: null
1055
+ });
1056
+ await tx.updateRun(stageRecord.workflowRunId, {
1057
+ status: "FAILED",
1058
+ completedAt: deps.clock.now()
1059
+ });
1060
+ await tx.appendOutboxEvents(
1061
+ toOutboxEvents2(stageRecord.workflowRunId, [
1062
+ {
1063
+ type: "stage:failed",
1064
+ timestamp: deps.clock.now(),
1065
+ workflowRunId: stageRecord.workflowRunId,
1066
+ stageId: stageRecord.stageId,
1067
+ stageName: stageRecord.stageName,
1068
+ error: checkResult.error
1069
+ },
1070
+ {
1071
+ type: "workflow:failed",
1072
+ timestamp: deps.clock.now(),
1073
+ workflowRunId: stageRecord.workflowRunId,
1074
+ error: checkResult.error
1075
+ }
1076
+ ])
1077
+ );
1078
+ }
1079
+ );
1080
+ if (claimResult.status === "stale") {
1081
+ const latestStatus = await deps.persistence.getRunStatus(
1082
+ stageRecord.workflowRunId
1083
+ );
1084
+ if (latestStatus === "CANCELLED") {
1085
+ await markStageCancelled(stageRecord.id, deps);
1086
+ }
1087
+ continue;
1088
+ }
905
1089
  failed++;
906
- events.push({
907
- type: "stage:failed",
908
- timestamp: deps.clock.now(),
909
- workflowRunId: stageRecord.workflowRunId,
910
- stageId: stageRecord.stageId,
911
- stageName: stageRecord.stageName,
912
- error: checkResult.error
913
- });
914
- events.push({
915
- type: "workflow:failed",
916
- timestamp: deps.clock.now(),
917
- workflowRunId: stageRecord.workflowRunId,
918
- error: checkResult.error
919
- });
920
1090
  } else if (checkResult.ready) {
921
1091
  let outputRef;
922
1092
  if (checkResult.output !== void 0) {
@@ -935,59 +1105,115 @@ async function handleStagePollSuspended(command, deps) {
935
1105
  outputRef = { _artifactKey: outputKey };
936
1106
  }
937
1107
  const duration = deps.clock.now().getTime() - (stageRecord.startedAt?.getTime() ?? deps.clock.now().getTime());
938
- await deps.persistence.updateStage(stageRecord.id, {
939
- status: "COMPLETED",
940
- completedAt: deps.clock.now(),
941
- duration,
942
- outputData: outputRef,
943
- nextPollAt: null,
944
- metrics: checkResult.metrics,
945
- embeddingInfo: checkResult.embeddings
946
- });
1108
+ const claimResult = await withClaimedRun(
1109
+ stageRecord.workflowRunId,
1110
+ run.version,
1111
+ deps,
1112
+ async (tx) => {
1113
+ await tx.updateStage(stageRecord.id, {
1114
+ status: "COMPLETED",
1115
+ completedAt: deps.clock.now(),
1116
+ duration,
1117
+ outputData: outputRef,
1118
+ nextPollAt: null,
1119
+ metrics: checkResult.metrics,
1120
+ embeddingInfo: checkResult.embeddings
1121
+ });
1122
+ await tx.appendOutboxEvents(
1123
+ toOutboxEvents2(stageRecord.workflowRunId, [
1124
+ {
1125
+ type: "stage:completed",
1126
+ timestamp: deps.clock.now(),
1127
+ workflowRunId: stageRecord.workflowRunId,
1128
+ stageId: stageRecord.stageId,
1129
+ stageName: stageRecord.stageName,
1130
+ duration
1131
+ }
1132
+ ])
1133
+ );
1134
+ }
1135
+ );
1136
+ if (claimResult.status === "stale") {
1137
+ const latestStatus = await deps.persistence.getRunStatus(
1138
+ stageRecord.workflowRunId
1139
+ );
1140
+ if (latestStatus === "CANCELLED") {
1141
+ await markStageCancelled(stageRecord.id, deps);
1142
+ }
1143
+ continue;
1144
+ }
947
1145
  resumed++;
948
1146
  resumedWorkflowRunIds.add(stageRecord.workflowRunId);
949
- events.push({
950
- type: "stage:completed",
951
- timestamp: deps.clock.now(),
952
- workflowRunId: stageRecord.workflowRunId,
953
- stageId: stageRecord.stageId,
954
- stageName: stageRecord.stageName,
955
- duration
956
- });
957
1147
  } else {
958
1148
  const pollInterval = checkResult.nextCheckIn ?? stageRecord.pollInterval ?? 6e4;
959
1149
  const nextPollAt = new Date(deps.clock.now().getTime() + pollInterval);
960
- await deps.persistence.updateStage(stageRecord.id, {
961
- nextPollAt
962
- });
1150
+ const claimResult = await withClaimedRun(
1151
+ stageRecord.workflowRunId,
1152
+ run.version,
1153
+ deps,
1154
+ async (tx) => {
1155
+ await tx.updateStage(stageRecord.id, {
1156
+ nextPollAt
1157
+ });
1158
+ }
1159
+ );
1160
+ if (claimResult.status === "stale") {
1161
+ const latestStatus = await deps.persistence.getRunStatus(
1162
+ stageRecord.workflowRunId
1163
+ );
1164
+ if (latestStatus === "CANCELLED") {
1165
+ await markStageCancelled(stageRecord.id, deps);
1166
+ }
1167
+ continue;
1168
+ }
963
1169
  }
964
1170
  } catch (error) {
965
1171
  const errorMessage = error instanceof Error ? error.message : String(error);
966
- await deps.persistence.updateStage(stageRecord.id, {
967
- status: "FAILED",
968
- completedAt: deps.clock.now(),
969
- errorMessage,
970
- nextPollAt: null
971
- });
972
- await deps.persistence.updateRun(stageRecord.workflowRunId, {
973
- status: "FAILED",
974
- completedAt: deps.clock.now()
975
- });
1172
+ const claimResult = await withClaimedRun(
1173
+ stageRecord.workflowRunId,
1174
+ run.version,
1175
+ deps,
1176
+ async (tx) => {
1177
+ await tx.updateStage(stageRecord.id, {
1178
+ status: "FAILED",
1179
+ completedAt: deps.clock.now(),
1180
+ errorMessage,
1181
+ nextPollAt: null
1182
+ });
1183
+ await tx.updateRun(stageRecord.workflowRunId, {
1184
+ status: "FAILED",
1185
+ completedAt: deps.clock.now()
1186
+ });
1187
+ await tx.appendOutboxEvents(
1188
+ toOutboxEvents2(stageRecord.workflowRunId, [
1189
+ {
1190
+ type: "stage:failed",
1191
+ timestamp: deps.clock.now(),
1192
+ workflowRunId: stageRecord.workflowRunId,
1193
+ stageId: stageRecord.stageId,
1194
+ stageName: stageRecord.stageName,
1195
+ error: errorMessage
1196
+ },
1197
+ {
1198
+ type: "workflow:failed",
1199
+ timestamp: deps.clock.now(),
1200
+ workflowRunId: stageRecord.workflowRunId,
1201
+ error: errorMessage
1202
+ }
1203
+ ])
1204
+ );
1205
+ }
1206
+ );
1207
+ if (claimResult.status === "stale") {
1208
+ const latestStatus = await deps.persistence.getRunStatus(
1209
+ stageRecord.workflowRunId
1210
+ );
1211
+ if (latestStatus === "CANCELLED") {
1212
+ await markStageCancelled(stageRecord.id, deps);
1213
+ }
1214
+ continue;
1215
+ }
976
1216
  failed++;
977
- events.push({
978
- type: "stage:failed",
979
- timestamp: deps.clock.now(),
980
- workflowRunId: stageRecord.workflowRunId,
981
- stageId: stageRecord.stageId,
982
- stageName: stageRecord.stageName,
983
- error: errorMessage
984
- });
985
- events.push({
986
- type: "workflow:failed",
987
- timestamp: deps.clock.now(),
988
- workflowRunId: stageRecord.workflowRunId,
989
- error: errorMessage
990
- });
991
1217
  }
992
1218
  }
993
1219
  return {
@@ -995,7 +1221,7 @@ async function handleStagePollSuspended(command, deps) {
995
1221
  resumed,
996
1222
  failed,
997
1223
  resumedWorkflowRunIds: [...resumedWorkflowRunIds],
998
- _events: events
1224
+ _events: []
999
1225
  };
1000
1226
  }
1001
1227
 
@@ -1041,6 +1267,14 @@ function createKernel(config) {
1041
1267
  const { _events: _, ...publicResult } = result;
1042
1268
  return publicResult;
1043
1269
  }
1270
+ if (command.type === "stage.pollSuspended") {
1271
+ const result = await handleStagePollSuspended(
1272
+ command,
1273
+ deps
1274
+ );
1275
+ const { _events: _, ...publicResult } = result;
1276
+ return publicResult;
1277
+ }
1044
1278
  if (command.type === "job.execute") {
1045
1279
  const jobCommand = command;
1046
1280
  const jobIdempotencyKey = jobCommand.idempotencyKey;
@@ -1121,12 +1355,6 @@ function createKernel(config) {
1121
1355
  txDeps
1122
1356
  );
1123
1357
  break;
1124
- case "stage.pollSuspended":
1125
- result = await handleStagePollSuspended(
1126
- command,
1127
- txDeps
1128
- );
1129
- break;
1130
1358
  case "lease.reapStale":
1131
1359
  result = await handleLeaseReapStale(
1132
1360
  command,
@@ -1209,5 +1437,5 @@ function createPluginRunner(config) {
1209
1437
  }
1210
1438
 
1211
1439
  export { IdempotencyInProgressError, createKernel, createPluginRunner, definePlugin, loadWorkflowContext, saveStageOutput };
1212
- //# sourceMappingURL=chunk-3NEGRI4M.js.map
1213
- //# sourceMappingURL=chunk-3NEGRI4M.js.map
1440
+ //# sourceMappingURL=chunk-HOGDFLCG.js.map
1441
+ //# sourceMappingURL=chunk-HOGDFLCG.js.map