@donkeylabs/server 2.0.20 → 2.0.21

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.
@@ -22,6 +22,7 @@ import {
22
22
  type ProxyRequest,
23
23
  } from "./workflow-socket";
24
24
  import { isProcessAlive } from "./external-jobs";
25
+ import { WorkflowStateMachine, type StateMachineEvents } from "./workflow-state-machine";
25
26
 
26
27
  // Type helper for Zod schema inference
27
28
  type ZodSchema = z.ZodTypeAny;
@@ -655,7 +656,7 @@ export interface Workflows {
655
656
  }
656
657
 
657
658
  // ============================================
658
- // Workflow Service Implementation
659
+ // Workflow Service Implementation (Supervisor)
659
660
  // ============================================
660
661
 
661
662
  interface IsolatedProcessInfo {
@@ -667,13 +668,13 @@ interface IsolatedProcessInfo {
667
668
 
668
669
  class WorkflowsImpl implements Workflows {
669
670
  private adapter: WorkflowAdapter;
670
- private events?: Events;
671
+ private eventsService?: Events;
671
672
  private jobs?: Jobs;
672
673
  private sse?: SSE;
673
674
  private core?: CoreServices;
674
675
  private plugins: Record<string, any> = {};
675
676
  private definitions = new Map<string, WorkflowDefinition>();
676
- private running = new Map<string, { timeout?: ReturnType<typeof setTimeout> }>();
677
+ private running = new Map<string, { timeout?: ReturnType<typeof setTimeout>; sm?: WorkflowStateMachine }>();
677
678
  private pollInterval: number;
678
679
 
679
680
  // Isolated execution state
@@ -687,7 +688,7 @@ class WorkflowsImpl implements Workflows {
687
688
 
688
689
  constructor(config: WorkflowsConfig = {}) {
689
690
  this.adapter = config.adapter ?? new MemoryWorkflowAdapter();
690
- this.events = config.events;
691
+ this.eventsService = config.events;
691
692
  this.jobs = config.jobs;
692
693
  this.sse = config.sse;
693
694
  this.core = config.core;
@@ -760,18 +761,6 @@ class WorkflowsImpl implements Workflows {
760
761
  throw new Error(`Workflow "${definition.name}" is already registered`);
761
762
  }
762
763
 
763
- // Validate isolated workflows don't use unsupported step types
764
- if (definition.isolated !== false) {
765
- for (const [stepName, step] of definition.steps) {
766
- if (step.type === "choice" || step.type === "parallel") {
767
- throw new Error(
768
- `Workflow "${definition.name}" uses ${step.type} step "${stepName}" ` +
769
- `which is not supported in isolated mode. Use .isolated(false) to run inline.`
770
- );
771
- }
772
- }
773
- }
774
-
775
764
  // Store module path for isolated workflows
776
765
  if (options?.modulePath) {
777
766
  this.workflowModulePaths.set(definition.name, options.modulePath);
@@ -829,13 +818,13 @@ class WorkflowsImpl implements Workflows {
829
818
  // Execute in isolated subprocess
830
819
  this.executeIsolatedWorkflow(instance.id, definition, input, modulePath);
831
820
  } else {
832
- // Execute inline (existing behavior)
821
+ // Execute inline using state machine
833
822
  if (isIsolated && !modulePath) {
834
823
  console.warn(
835
824
  `[Workflows] Workflow "${workflowName}" falling back to inline execution (no modulePath)`
836
825
  );
837
826
  }
838
- this.executeWorkflow(instance.id, definition);
827
+ this.startInlineWorkflow(instance.id, definition);
839
828
  }
840
829
 
841
830
  return instance.id;
@@ -865,8 +854,11 @@ class WorkflowsImpl implements Workflows {
865
854
  await this.getSocketServer().closeSocket(instanceId);
866
855
  }
867
856
 
868
- // Clear inline timeout
857
+ // Cancel inline state machine if running
869
858
  const runInfo = this.running.get(instanceId);
859
+ if (runInfo?.sm) {
860
+ runInfo.sm.cancel(instanceId);
861
+ }
870
862
  if (runInfo?.timeout) {
871
863
  clearTimeout(runInfo.timeout);
872
864
  }
@@ -918,7 +910,7 @@ class WorkflowsImpl implements Workflows {
918
910
  if (isIsolated && modulePath && this.dbPath) {
919
911
  this.executeIsolatedWorkflow(instance.id, definition, instance.input, modulePath);
920
912
  } else {
921
- this.executeWorkflow(instance.id, definition);
913
+ this.startInlineWorkflow(instance.id, definition);
922
914
  }
923
915
  }
924
916
  }
@@ -942,8 +934,11 @@ class WorkflowsImpl implements Workflows {
942
934
  this.socketServer = undefined;
943
935
  }
944
936
 
945
- // Clear all inline timeouts
937
+ // Clear all inline timeouts and cancel state machines
946
938
  for (const [instanceId, runInfo] of this.running) {
939
+ if (runInfo.sm) {
940
+ runInfo.sm.cancel(instanceId);
941
+ }
947
942
  if (runInfo.timeout) {
948
943
  clearTimeout(runInfo.timeout);
949
944
  }
@@ -957,761 +952,177 @@ class WorkflowsImpl implements Workflows {
957
952
  }
958
953
 
959
954
  // ============================================
960
- // Execution Engine
955
+ // Inline Execution via State Machine
961
956
  // ============================================
962
957
 
963
- private async executeWorkflow(
958
+ private startInlineWorkflow(
964
959
  instanceId: string,
965
- definition: WorkflowDefinition
966
- ): Promise<void> {
967
- const instance = await this.adapter.getInstance(instanceId);
968
- if (!instance) return;
969
-
970
- // Mark as running
971
- if (instance.status === "pending") {
972
- await this.adapter.updateInstance(instanceId, {
973
- status: "running",
974
- startedAt: new Date(),
975
- });
976
- }
960
+ definition: WorkflowDefinition,
961
+ ): void {
962
+ const sm = new WorkflowStateMachine({
963
+ adapter: this.adapter,
964
+ core: this.core,
965
+ plugins: this.plugins,
966
+ events: this.createInlineEventHandler(instanceId),
967
+ jobs: this.jobs,
968
+ pollInterval: this.pollInterval,
969
+ });
977
970
 
978
971
  // Set up workflow timeout
972
+ let timeout: ReturnType<typeof setTimeout> | undefined;
979
973
  if (definition.timeout) {
980
- const timeout = setTimeout(async () => {
981
- await this.failWorkflow(instanceId, "Workflow timed out");
982
- }, definition.timeout);
983
- this.running.set(instanceId, { timeout });
984
- } else {
985
- this.running.set(instanceId, {});
986
- }
987
-
988
- // Execute current step
989
- await this.executeStep(instanceId, definition);
990
- }
991
-
992
- private async executeStep(
993
- instanceId: string,
994
- definition: WorkflowDefinition
995
- ): Promise<void> {
996
- const instance = await this.adapter.getInstance(instanceId);
997
- if (!instance || instance.status !== "running") return;
998
-
999
- const stepName = instance.currentStep;
1000
- if (!stepName) {
1001
- await this.completeWorkflow(instanceId);
1002
- return;
1003
- }
1004
-
1005
- const step = definition.steps.get(stepName);
1006
- if (!step) {
1007
- await this.failWorkflow(instanceId, `Step "${stepName}" not found`);
1008
- return;
1009
- }
1010
-
1011
- // Build context
1012
- const ctx = this.buildContext(instance, definition);
1013
-
1014
- // Emit step started event
1015
- await this.emitEvent("workflow.step.started", {
1016
- instanceId,
1017
- workflowName: instance.workflowName,
1018
- stepName,
1019
- stepType: step.type,
1020
- });
1021
-
1022
- // Broadcast via SSE
1023
- if (this.sse) {
1024
- this.sse.broadcast(`workflow:${instanceId}`, "step.started", { stepName });
1025
- this.sse.broadcast("workflows:all", "workflow.step.started", {
1026
- instanceId,
1027
- workflowName: instance.workflowName,
1028
- stepName,
1029
- });
1030
- }
1031
-
1032
- // Update step result as running
1033
- const stepResult: StepResult = {
1034
- stepName,
1035
- status: "running",
1036
- startedAt: new Date(),
1037
- attempts: (instance.stepResults[stepName]?.attempts ?? 0) + 1,
1038
- };
1039
- await this.adapter.updateInstance(instanceId, {
1040
- stepResults: { ...instance.stepResults, [stepName]: stepResult },
1041
- });
1042
-
1043
- try {
1044
- let output: any;
1045
-
1046
- switch (step.type) {
1047
- case "task":
1048
- output = await this.executeTaskStep(instanceId, step, ctx, definition);
1049
- break;
1050
- case "parallel":
1051
- output = await this.executeParallelStep(instanceId, step, ctx, definition);
1052
- break;
1053
- case "choice":
1054
- output = await this.executeChoiceStep(instanceId, step, ctx, definition);
1055
- break;
1056
- case "pass":
1057
- output = await this.executePassStep(instanceId, step, ctx);
1058
- break;
1059
- }
1060
-
1061
- // Step completed successfully
1062
- await this.completeStep(instanceId, stepName, output, step, definition);
1063
- } catch (error) {
1064
- const errorMsg = error instanceof Error ? error.message : String(error);
1065
- await this.handleStepError(instanceId, stepName, errorMsg, step, definition);
1066
- }
1067
- }
1068
-
1069
- private async executeTaskStep(
1070
- instanceId: string,
1071
- step: TaskStepDefinition,
1072
- ctx: WorkflowContext,
1073
- definition: WorkflowDefinition
1074
- ): Promise<any> {
1075
- // Determine which API is being used
1076
- const useInlineHandler = !!step.handler;
1077
-
1078
- if (useInlineHandler) {
1079
- // === NEW API: Inline handler with Zod schemas ===
1080
- let input: any;
1081
-
1082
- if (step.inputSchema) {
1083
- if (typeof step.inputSchema === "function") {
1084
- // inputSchema is a mapper function: (prev, workflowInput) => input
1085
- input = step.inputSchema(ctx.prev, ctx.input);
1086
- } else {
1087
- // inputSchema is a Zod schema - validate workflow input
1088
- const parseResult = step.inputSchema.safeParse(ctx.input);
1089
- if (!parseResult.success) {
1090
- throw new Error(`Input validation failed: ${parseResult.error.message}`);
1091
- }
1092
- input = parseResult.data;
1093
- }
1094
- } else {
1095
- // No input schema, use workflow input directly
1096
- input = ctx.input;
1097
- }
1098
-
1099
- // Update step with input
1100
- const instance = await this.adapter.getInstance(instanceId);
1101
- if (instance) {
1102
- const stepResult = instance.stepResults[step.name];
1103
- if (stepResult) {
1104
- stepResult.input = input;
1105
- await this.adapter.updateInstance(instanceId, {
1106
- stepResults: { ...instance.stepResults, [step.name]: stepResult },
1107
- });
1108
- }
1109
- }
1110
-
1111
- // Execute the inline handler
1112
- let result = await step.handler!(input, ctx);
1113
-
1114
- // Validate output if schema provided
1115
- if (step.outputSchema) {
1116
- const parseResult = step.outputSchema.safeParse(result);
1117
- if (!parseResult.success) {
1118
- throw new Error(`Output validation failed: ${parseResult.error.message}`);
1119
- }
1120
- result = parseResult.data;
1121
- }
1122
-
1123
- return result;
1124
- } else {
1125
- // === LEGACY API: Job-based execution ===
1126
- if (!this.jobs) {
1127
- throw new Error("Jobs service not configured");
1128
- }
1129
-
1130
- if (!step.job) {
1131
- throw new Error("Task step requires either 'handler' or 'job'");
1132
- }
1133
-
1134
- // Prepare job input
1135
- const jobInput = step.input ? step.input(ctx) : ctx.input;
1136
-
1137
- // Update step with input
1138
- const instance = await this.adapter.getInstance(instanceId);
1139
- if (instance) {
1140
- const stepResult = instance.stepResults[step.name];
1141
- if (stepResult) {
1142
- stepResult.input = jobInput;
1143
- await this.adapter.updateInstance(instanceId, {
1144
- stepResults: { ...instance.stepResults, [step.name]: stepResult },
974
+ timeout = setTimeout(async () => {
975
+ sm.cancel(instanceId);
976
+ await this.adapter.updateInstance(instanceId, {
977
+ status: "failed",
978
+ error: "Workflow timed out",
979
+ completedAt: new Date(),
980
+ });
981
+ await this.emitEvent("workflow.failed", {
982
+ instanceId,
983
+ workflowName: definition.name,
984
+ error: "Workflow timed out",
985
+ });
986
+ if (this.sse) {
987
+ this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: "Workflow timed out" });
988
+ this.sse.broadcast("workflows:all", "workflow.failed", {
989
+ instanceId,
990
+ workflowName: definition.name,
991
+ error: "Workflow timed out",
1145
992
  });
1146
993
  }
1147
- }
1148
-
1149
- // Enqueue the job
1150
- const jobId = await this.jobs.enqueue(step.job, {
1151
- ...jobInput,
1152
- _workflowInstanceId: instanceId,
1153
- _workflowStepName: step.name,
1154
- });
1155
-
1156
- // Wait for job completion
1157
- const result = await this.waitForJob(jobId, step.timeout);
1158
-
1159
- // Transform output if needed
1160
- return step.output ? step.output(result, ctx) : result;
1161
- }
1162
- }
1163
-
1164
- private async waitForJob(jobId: string, timeout?: number): Promise<any> {
1165
- if (!this.jobs) {
1166
- throw new Error("Jobs service not configured");
994
+ this.running.delete(instanceId);
995
+ }, definition.timeout);
1167
996
  }
1168
997
 
1169
- const startTime = Date.now();
1170
-
1171
- while (true) {
1172
- const job = await this.jobs.get(jobId);
1173
-
1174
- if (!job) {
1175
- throw new Error(`Job ${jobId} not found`);
1176
- }
1177
-
1178
- if (job.status === "completed") {
1179
- return job.result;
1180
- }
1181
-
1182
- if (job.status === "failed") {
1183
- throw new Error(job.error ?? "Job failed");
1184
- }
998
+ this.running.set(instanceId, { timeout, sm });
1185
999
 
1186
- // Check timeout
1187
- if (timeout && Date.now() - startTime > timeout) {
1188
- throw new Error("Job timed out");
1000
+ // Run the state machine (fire and forget - events handle communication)
1001
+ sm.run(instanceId, definition).then(() => {
1002
+ // Clean up timeout on completion
1003
+ const runInfo = this.running.get(instanceId);
1004
+ if (runInfo?.timeout) {
1005
+ clearTimeout(runInfo.timeout);
1189
1006
  }
1190
-
1191
- // Wait before polling again
1192
- await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
1193
- }
1194
- }
1195
-
1196
- private async executeParallelStep(
1197
- instanceId: string,
1198
- step: ParallelStepDefinition,
1199
- ctx: WorkflowContext,
1200
- definition: WorkflowDefinition
1201
- ): Promise<any> {
1202
- const branchPromises: Promise<{ name: string; result: any }>[] = [];
1203
- const branchInstanceIds: string[] = [];
1204
-
1205
- for (const branchDef of step.branches) {
1206
- // Register branch workflow if not already
1207
- if (!this.definitions.has(branchDef.name)) {
1208
- this.definitions.set(branchDef.name, branchDef);
1007
+ this.running.delete(instanceId);
1008
+ }).catch(() => {
1009
+ // State machine already persisted the failure - just clean up
1010
+ const runInfo = this.running.get(instanceId);
1011
+ if (runInfo?.timeout) {
1012
+ clearTimeout(runInfo.timeout);
1209
1013
  }
1210
-
1211
- // Start branch as sub-workflow
1212
- const branchInstanceId = await this.adapter.createInstance({
1213
- workflowName: branchDef.name,
1214
- status: "pending",
1215
- currentStep: branchDef.startAt,
1216
- input: ctx.input,
1217
- stepResults: {},
1218
- createdAt: new Date(),
1219
- parentId: instanceId,
1220
- branchName: branchDef.name,
1221
- });
1222
-
1223
- branchInstanceIds.push(branchInstanceId.id);
1224
-
1225
- // Execute branch
1226
- const branchPromise = (async () => {
1227
- await this.executeWorkflow(branchInstanceId.id, branchDef);
1228
-
1229
- // Wait for branch completion
1230
- while (true) {
1231
- const branchInstance = await this.adapter.getInstance(branchInstanceId.id);
1232
- if (!branchInstance) {
1233
- throw new Error(`Branch instance ${branchInstanceId.id} not found`);
1234
- }
1235
-
1236
- if (branchInstance.status === "completed") {
1237
- return { name: branchDef.name, result: branchInstance.output };
1238
- }
1239
-
1240
- if (branchInstance.status === "failed") {
1241
- throw new Error(branchInstance.error ?? `Branch ${branchDef.name} failed`);
1242
- }
1243
-
1244
- await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
1245
- }
1246
- })();
1247
-
1248
- branchPromises.push(branchPromise);
1249
- }
1250
-
1251
- // Track branch instances
1252
- await this.adapter.updateInstance(instanceId, {
1253
- branchInstances: {
1254
- ...((await this.adapter.getInstance(instanceId))?.branchInstances ?? {}),
1255
- [step.name]: branchInstanceIds,
1256
- },
1014
+ this.running.delete(instanceId);
1257
1015
  });
1258
-
1259
- // Wait for all branches
1260
- if (step.onError === "wait-all") {
1261
- const results = await Promise.allSettled(branchPromises);
1262
- const output: Record<string, any> = {};
1263
- const errors: string[] = [];
1264
-
1265
- for (const result of results) {
1266
- if (result.status === "fulfilled") {
1267
- output[result.value.name] = result.value.result;
1268
- } else {
1269
- errors.push(result.reason?.message ?? "Branch failed");
1270
- }
1271
- }
1272
-
1273
- if (errors.length > 0) {
1274
- throw new Error(`Parallel branches failed: ${errors.join(", ")}`);
1275
- }
1276
-
1277
- return output;
1278
- } else {
1279
- // fail-fast (default)
1280
- const results = await Promise.all(branchPromises);
1281
- const output: Record<string, any> = {};
1282
- for (const result of results) {
1283
- output[result.name] = result.result;
1284
- }
1285
- return output;
1286
- }
1287
1016
  }
1288
1017
 
1289
- private async executeChoiceStep(
1290
- instanceId: string,
1291
- step: ChoiceStepDefinition,
1292
- ctx: WorkflowContext,
1293
- definition: WorkflowDefinition
1294
- ): Promise<string> {
1295
- // Evaluate conditions in order
1296
- for (const choice of step.choices) {
1297
- try {
1298
- if (choice.condition(ctx)) {
1299
- // Update current step and continue
1300
- await this.adapter.updateInstance(instanceId, {
1301
- currentStep: choice.next,
1302
- });
1303
-
1304
- // Mark choice step as complete
1305
- const instance = await this.adapter.getInstance(instanceId);
1306
- if (instance) {
1307
- const stepResult = instance.stepResults[step.name];
1308
- if (stepResult) {
1309
- stepResult.status = "completed";
1310
- stepResult.output = { chosen: choice.next };
1311
- stepResult.completedAt = new Date();
1312
- await this.adapter.updateInstance(instanceId, {
1313
- stepResults: { ...instance.stepResults, [step.name]: stepResult },
1314
- });
1315
- }
1316
- }
1317
-
1318
- // Emit progress
1319
- await this.emitEvent("workflow.step.completed", {
1320
- instanceId,
1321
- workflowName: (await this.adapter.getInstance(instanceId))?.workflowName,
1322
- stepName: step.name,
1323
- output: { chosen: choice.next },
1018
+ /**
1019
+ * Create an event handler that bridges state machine events to Events service + SSE
1020
+ */
1021
+ private createInlineEventHandler(instanceId: string): StateMachineEvents {
1022
+ return {
1023
+ onStepStarted: (id, stepName, stepType) => {
1024
+ this.emitEvent("workflow.step.started", {
1025
+ instanceId: id,
1026
+ stepName,
1027
+ stepType,
1028
+ });
1029
+ if (this.sse) {
1030
+ this.sse.broadcast(`workflow:${id}`, "step.started", { stepName });
1031
+ this.sse.broadcast("workflows:all", "workflow.step.started", {
1032
+ instanceId: id,
1033
+ stepName,
1324
1034
  });
1325
-
1326
- // Execute next step
1327
- await this.executeStep(instanceId, definition);
1328
- return choice.next;
1329
1035
  }
1330
- } catch {
1331
- // Condition threw, try next
1332
- }
1333
- }
1334
-
1335
- // No condition matched, use default
1336
- if (step.default) {
1337
- await this.adapter.updateInstance(instanceId, {
1338
- currentStep: step.default,
1339
- });
1340
-
1341
- // Mark choice step as complete
1342
- const instance = await this.adapter.getInstance(instanceId);
1343
- if (instance) {
1344
- const stepResult = instance.stepResults[step.name];
1345
- if (stepResult) {
1346
- stepResult.status = "completed";
1347
- stepResult.output = { chosen: step.default };
1348
- stepResult.completedAt = new Date();
1349
- await this.adapter.updateInstance(instanceId, {
1350
- stepResults: { ...instance.stepResults, [step.name]: stepResult },
1036
+ },
1037
+ onStepCompleted: (id, stepName, output, nextStep) => {
1038
+ this.emitEvent("workflow.step.completed", {
1039
+ instanceId: id,
1040
+ stepName,
1041
+ output,
1042
+ });
1043
+ if (this.sse) {
1044
+ this.sse.broadcast(`workflow:${id}`, "step.completed", { stepName, output });
1045
+ this.sse.broadcast("workflows:all", "workflow.step.completed", {
1046
+ instanceId: id,
1047
+ stepName,
1351
1048
  });
1352
1049
  }
1353
- }
1354
-
1355
- await this.emitEvent("workflow.step.completed", {
1356
- instanceId,
1357
- workflowName: instance?.workflowName,
1358
- stepName: step.name,
1359
- output: { chosen: step.default },
1360
- });
1361
-
1362
- await this.executeStep(instanceId, definition);
1363
- return step.default;
1364
- }
1365
-
1366
- throw new Error("No choice condition matched and no default specified");
1367
- }
1368
-
1369
- private async executePassStep(
1370
- instanceId: string,
1371
- step: PassStepDefinition,
1372
- ctx: WorkflowContext
1373
- ): Promise<any> {
1374
- if (step.result !== undefined) {
1375
- return step.result;
1376
- }
1377
-
1378
- if (step.transform) {
1379
- return step.transform(ctx);
1380
- }
1381
-
1382
- return ctx.input;
1383
- }
1384
-
1385
- private buildContext(instance: WorkflowInstance, definition: WorkflowDefinition): WorkflowContext {
1386
- // Build steps object with outputs
1387
- const steps: Record<string, any> = {};
1388
- for (const [name, result] of Object.entries(instance.stepResults)) {
1389
- if (result.status === "completed" && result.output !== undefined) {
1390
- steps[name] = result.output;
1391
- }
1392
- }
1393
-
1394
- // Find the previous step's output by tracing the workflow path
1395
- let prev: any = undefined;
1396
- if (instance.currentStep) {
1397
- // Find which step comes before current step
1398
- for (const [stepName, stepDef] of definition.steps) {
1399
- if (stepDef.next === instance.currentStep && steps[stepName] !== undefined) {
1400
- prev = steps[stepName];
1401
- break;
1050
+ },
1051
+ onStepFailed: (id, stepName, error, attempts) => {
1052
+ this.emitEvent("workflow.step.failed", {
1053
+ instanceId: id,
1054
+ stepName,
1055
+ error,
1056
+ attempts,
1057
+ });
1058
+ if (this.sse) {
1059
+ this.sse.broadcast(`workflow:${id}`, "step.failed", { stepName, error });
1060
+ this.sse.broadcast("workflows:all", "workflow.step.failed", {
1061
+ instanceId: id,
1062
+ stepName,
1063
+ error,
1064
+ });
1402
1065
  }
1403
- }
1404
- // If no explicit next found, use most recent completed step output
1405
- if (prev === undefined) {
1406
- const completedSteps = Object.entries(instance.stepResults)
1407
- .filter(([, r]) => r.status === "completed" && r.output !== undefined)
1408
- .sort((a, b) => {
1409
- const aTime = a[1].completedAt?.getTime() ?? 0;
1410
- const bTime = b[1].completedAt?.getTime() ?? 0;
1411
- return bTime - aTime;
1066
+ },
1067
+ onStepRetry: (id, stepName, attempt, max, delayMs) => {
1068
+ this.emitEvent("workflow.step.retry", {
1069
+ instanceId: id,
1070
+ stepName,
1071
+ attempt,
1072
+ maxAttempts: max,
1073
+ delay: delayMs,
1074
+ });
1075
+ },
1076
+ onProgress: (id, progress, currentStep, completed, total) => {
1077
+ this.emitEvent("workflow.progress", {
1078
+ instanceId: id,
1079
+ progress,
1080
+ currentStep,
1081
+ completedSteps: completed,
1082
+ totalSteps: total,
1083
+ });
1084
+ if (this.sse) {
1085
+ this.sse.broadcast(`workflow:${id}`, "progress", {
1086
+ progress,
1087
+ currentStep,
1088
+ completedSteps: completed,
1089
+ totalSteps: total,
1090
+ });
1091
+ this.sse.broadcast("workflows:all", "workflow.progress", {
1092
+ instanceId: id,
1093
+ progress,
1094
+ currentStep,
1412
1095
  });
1413
- if (completedSteps.length > 0) {
1414
- prev = completedSteps[0][1].output;
1415
1096
  }
1416
- }
1417
- }
1418
-
1419
- // Metadata snapshot (mutable reference for setMetadata updates)
1420
- const metadata = { ...(instance.metadata ?? {}) };
1421
-
1422
- return {
1423
- input: instance.input,
1424
- steps,
1425
- prev,
1426
- instance,
1427
- getStepResult: <T = any>(stepName: string): T | undefined => {
1428
- return steps[stepName] as T | undefined;
1429
1097
  },
1430
- core: this.core!,
1431
- plugins: this.plugins,
1432
- metadata,
1433
- setMetadata: async (key: string, value: any): Promise<void> => {
1434
- // Update local snapshot
1435
- metadata[key] = value;
1436
- // Persist to database
1437
- await this.adapter.updateInstance(instance.id, {
1438
- metadata: { ...metadata },
1098
+ onCompleted: (id, output) => {
1099
+ this.emitEvent("workflow.completed", {
1100
+ instanceId: id,
1101
+ output,
1439
1102
  });
1103
+ if (this.sse) {
1104
+ this.sse.broadcast(`workflow:${id}`, "completed", { output });
1105
+ this.sse.broadcast("workflows:all", "workflow.completed", {
1106
+ instanceId: id,
1107
+ });
1108
+ }
1440
1109
  },
1441
- getMetadata: <T = any>(key: string): T | undefined => {
1442
- return metadata[key] as T | undefined;
1110
+ onFailed: (id, error) => {
1111
+ this.emitEvent("workflow.failed", {
1112
+ instanceId: id,
1113
+ error,
1114
+ });
1115
+ if (this.sse) {
1116
+ this.sse.broadcast(`workflow:${id}`, "failed", { error });
1117
+ this.sse.broadcast("workflows:all", "workflow.failed", {
1118
+ instanceId: id,
1119
+ error,
1120
+ });
1121
+ }
1443
1122
  },
1444
1123
  };
1445
1124
  }
1446
1125
 
1447
- private async completeStep(
1448
- instanceId: string,
1449
- stepName: string,
1450
- output: any,
1451
- step: StepDefinition,
1452
- definition: WorkflowDefinition
1453
- ): Promise<void> {
1454
- const instance = await this.adapter.getInstance(instanceId);
1455
- if (!instance) return;
1456
-
1457
- // Check if workflow is still running (not cancelled/failed/timed out)
1458
- if (instance.status !== "running") {
1459
- console.log(`[Workflows] Ignoring step completion for ${instanceId}, status is ${instance.status}`);
1460
- return;
1461
- }
1462
-
1463
- // Update step result
1464
- const stepResult = instance.stepResults[stepName] ?? {
1465
- stepName,
1466
- status: "pending",
1467
- attempts: 0,
1468
- };
1469
- stepResult.status = "completed";
1470
- stepResult.output = output;
1471
- stepResult.completedAt = new Date();
1472
-
1473
- await this.adapter.updateInstance(instanceId, {
1474
- stepResults: { ...instance.stepResults, [stepName]: stepResult },
1475
- });
1476
-
1477
- // Emit step completed event
1478
- await this.emitEvent("workflow.step.completed", {
1479
- instanceId,
1480
- workflowName: instance.workflowName,
1481
- stepName,
1482
- output,
1483
- });
1484
-
1485
- // Broadcast step completed via SSE
1486
- if (this.sse) {
1487
- this.sse.broadcast(`workflow:${instanceId}`, "step.completed", {
1488
- stepName,
1489
- output,
1490
- });
1491
- this.sse.broadcast("workflows:all", "workflow.step.completed", {
1492
- instanceId,
1493
- workflowName: instance.workflowName,
1494
- stepName,
1495
- });
1496
- }
1497
-
1498
- // Calculate and emit progress
1499
- const totalSteps = definition.steps.size;
1500
- const completedSteps = Object.values(instance.stepResults).filter(
1501
- (r) => r.status === "completed"
1502
- ).length + 1; // +1 for current step
1503
- const progress = Math.round((completedSteps / totalSteps) * 100);
1504
-
1505
- await this.emitEvent("workflow.progress", {
1506
- instanceId,
1507
- workflowName: instance.workflowName,
1508
- progress,
1509
- currentStep: stepName,
1510
- completedSteps,
1511
- totalSteps,
1512
- });
1513
-
1514
- // Broadcast progress via SSE
1515
- if (this.sse) {
1516
- this.sse.broadcast(`workflow:${instanceId}`, "progress", {
1517
- progress,
1518
- currentStep: stepName,
1519
- completedSteps,
1520
- totalSteps,
1521
- });
1522
- this.sse.broadcast("workflows:all", "workflow.progress", {
1523
- instanceId,
1524
- workflowName: instance.workflowName,
1525
- progress,
1526
- currentStep: stepName,
1527
- });
1528
- }
1529
-
1530
- // Move to next step or complete
1531
- if (step.end) {
1532
- await this.completeWorkflow(instanceId, output);
1533
- } else if (step.next) {
1534
- await this.adapter.updateInstance(instanceId, {
1535
- currentStep: step.next,
1536
- });
1537
- await this.executeStep(instanceId, definition);
1538
- } else {
1539
- // No next step, complete
1540
- await this.completeWorkflow(instanceId, output);
1541
- }
1542
- }
1543
-
1544
- private async handleStepError(
1545
- instanceId: string,
1546
- stepName: string,
1547
- error: string,
1548
- step: StepDefinition,
1549
- definition: WorkflowDefinition
1550
- ): Promise<void> {
1551
- const instance = await this.adapter.getInstance(instanceId);
1552
- if (!instance) return;
1553
-
1554
- const stepResult = instance.stepResults[stepName] ?? {
1555
- stepName,
1556
- status: "pending",
1557
- attempts: 0,
1558
- };
1559
-
1560
- // Check retry config
1561
- const retry = step.retry ?? definition.defaultRetry;
1562
- if (retry && stepResult.attempts < retry.maxAttempts) {
1563
- // Retry with backoff
1564
- const backoffRate = retry.backoffRate ?? 2;
1565
- const intervalMs = retry.intervalMs ?? 1000;
1566
- const maxIntervalMs = retry.maxIntervalMs ?? 30000;
1567
- const delay = Math.min(
1568
- intervalMs * Math.pow(backoffRate, stepResult.attempts - 1),
1569
- maxIntervalMs
1570
- );
1571
-
1572
- console.log(
1573
- `[Workflows] Retrying step ${stepName} in ${delay}ms (attempt ${stepResult.attempts}/${retry.maxAttempts})`
1574
- );
1575
-
1576
- await this.emitEvent("workflow.step.retry", {
1577
- instanceId,
1578
- workflowName: instance.workflowName,
1579
- stepName,
1580
- attempt: stepResult.attempts,
1581
- maxAttempts: retry.maxAttempts,
1582
- delay,
1583
- error,
1584
- });
1585
-
1586
- // Update step result
1587
- stepResult.error = error;
1588
- await this.adapter.updateInstance(instanceId, {
1589
- stepResults: { ...instance.stepResults, [stepName]: stepResult },
1590
- });
1591
-
1592
- // Retry after delay
1593
- setTimeout(() => {
1594
- this.executeStep(instanceId, definition);
1595
- }, delay);
1596
-
1597
- return;
1598
- }
1599
-
1600
- // No more retries, fail the step
1601
- stepResult.status = "failed";
1602
- stepResult.error = error;
1603
- stepResult.completedAt = new Date();
1604
-
1605
- await this.adapter.updateInstance(instanceId, {
1606
- stepResults: { ...instance.stepResults, [stepName]: stepResult },
1607
- });
1608
-
1609
- await this.emitEvent("workflow.step.failed", {
1610
- instanceId,
1611
- workflowName: instance.workflowName,
1612
- stepName,
1613
- error,
1614
- attempts: stepResult.attempts,
1615
- });
1616
-
1617
- // Broadcast step failed via SSE
1618
- if (this.sse) {
1619
- this.sse.broadcast(`workflow:${instanceId}`, "step.failed", {
1620
- stepName,
1621
- error,
1622
- });
1623
- this.sse.broadcast("workflows:all", "workflow.step.failed", {
1624
- instanceId,
1625
- workflowName: instance.workflowName,
1626
- stepName,
1627
- error,
1628
- });
1629
- }
1630
-
1631
- // Fail the workflow
1632
- await this.failWorkflow(instanceId, `Step "${stepName}" failed: ${error}`);
1633
- }
1634
-
1635
- private async completeWorkflow(instanceId: string, output?: any): Promise<void> {
1636
- const instance = await this.adapter.getInstance(instanceId);
1637
- if (!instance) return;
1638
-
1639
- // Check if workflow is still running (not cancelled/failed/timed out)
1640
- if (instance.status !== "running") {
1641
- console.log(`[Workflows] Ignoring workflow completion for ${instanceId}, status is ${instance.status}`);
1642
- return;
1643
- }
1644
-
1645
- // Clear timeout
1646
- const runInfo = this.running.get(instanceId);
1647
- if (runInfo?.timeout) {
1648
- clearTimeout(runInfo.timeout);
1649
- }
1650
- this.running.delete(instanceId);
1651
-
1652
- await this.adapter.updateInstance(instanceId, {
1653
- status: "completed",
1654
- output,
1655
- completedAt: new Date(),
1656
- currentStep: undefined,
1657
- });
1658
-
1659
- await this.emitEvent("workflow.completed", {
1660
- instanceId,
1661
- workflowName: instance.workflowName,
1662
- output,
1663
- });
1664
-
1665
- // Broadcast via SSE
1666
- if (this.sse) {
1667
- this.sse.broadcast(`workflow:${instanceId}`, "completed", { output });
1668
- this.sse.broadcast("workflows:all", "workflow.completed", {
1669
- instanceId,
1670
- workflowName: instance.workflowName,
1671
- });
1672
- }
1673
- }
1674
-
1675
- private async failWorkflow(instanceId: string, error: string): Promise<void> {
1676
- const instance = await this.adapter.getInstance(instanceId);
1677
- if (!instance) return;
1678
-
1679
- // Clear timeout
1680
- const runInfo = this.running.get(instanceId);
1681
- if (runInfo?.timeout) {
1682
- clearTimeout(runInfo.timeout);
1683
- }
1684
- this.running.delete(instanceId);
1685
-
1686
- await this.adapter.updateInstance(instanceId, {
1687
- status: "failed",
1688
- error,
1689
- completedAt: new Date(),
1690
- });
1691
-
1692
- await this.emitEvent("workflow.failed", {
1693
- instanceId,
1694
- workflowName: instance.workflowName,
1695
- error,
1696
- });
1697
-
1698
- // Broadcast via SSE
1699
- if (this.sse) {
1700
- this.sse.broadcast(`workflow:${instanceId}`, "failed", { error });
1701
- this.sse.broadcast("workflows:all", "workflow.failed", {
1702
- instanceId,
1703
- workflowName: instance.workflowName,
1704
- error,
1705
- });
1706
- }
1707
- }
1708
-
1709
- private async emitEvent(event: string, data: any): Promise<void> {
1710
- if (this.events) {
1711
- await this.events.emit(event, data);
1712
- }
1713
- }
1714
-
1715
1126
  // ============================================
1716
1127
  // Isolated Execution Engine
1717
1128
  // ============================================
@@ -1799,13 +1210,33 @@ class WorkflowsImpl implements Workflows {
1799
1210
  const instance = await this.adapter.getInstance(instanceId);
1800
1211
  if (instance && instance.status === "running") {
1801
1212
  console.error(`[Workflows] Isolated workflow ${instanceId} crashed with exit code ${exitCode}`);
1802
- await this.failWorkflow(instanceId, `Subprocess crashed with exit code ${exitCode}`);
1213
+ await this.adapter.updateInstance(instanceId, {
1214
+ status: "failed",
1215
+ error: `Subprocess crashed with exit code ${exitCode}`,
1216
+ completedAt: new Date(),
1217
+ });
1218
+ await this.emitEvent("workflow.failed", {
1219
+ instanceId,
1220
+ workflowName: instance.workflowName,
1221
+ error: `Subprocess crashed with exit code ${exitCode}`,
1222
+ });
1223
+ if (this.sse) {
1224
+ this.sse.broadcast(`workflow:${instanceId}`, "failed", {
1225
+ error: `Subprocess crashed with exit code ${exitCode}`,
1226
+ });
1227
+ this.sse.broadcast("workflows:all", "workflow.failed", {
1228
+ instanceId,
1229
+ workflowName: instance.workflowName,
1230
+ error: `Subprocess crashed with exit code ${exitCode}`,
1231
+ });
1232
+ }
1803
1233
  }
1804
1234
  });
1805
1235
  }
1806
1236
 
1807
1237
  /**
1808
- * Handle events from isolated workflow subprocess
1238
+ * Handle events from isolated workflow subprocess.
1239
+ * The subprocess owns persistence via its own adapter - we only forward events to SSE/Events.
1809
1240
  */
1810
1241
  private async handleIsolatedEvent(event: WorkflowEvent): Promise<void> {
1811
1242
  const { instanceId, type } = event;
@@ -1819,42 +1250,22 @@ class WorkflowsImpl implements Workflows {
1819
1250
 
1820
1251
  switch (type) {
1821
1252
  case "started":
1822
- // Already marked as running in executeIsolatedWorkflow
1823
- break;
1824
-
1825
1253
  case "heartbeat":
1826
- // Heartbeat handled above
1254
+ // No-op: heartbeat handled above, started is handled by executeIsolatedWorkflow
1827
1255
  break;
1828
1256
 
1829
1257
  case "step.started": {
1830
- const instance = await this.adapter.getInstance(instanceId);
1831
- if (!instance) break;
1832
-
1833
- // Update current step and step results in DB
1834
- const stepResult = {
1835
- stepName: event.stepName!,
1836
- status: "running" as const,
1837
- startedAt: new Date(),
1838
- attempts: (instance.stepResults[event.stepName!]?.attempts ?? 0) + 1,
1839
- };
1840
- await this.adapter.updateInstance(instanceId, {
1841
- currentStep: event.stepName,
1842
- stepResults: { ...instance.stepResults, [event.stepName!]: stepResult },
1843
- });
1844
-
1845
1258
  await this.emitEvent("workflow.step.started", {
1846
1259
  instanceId,
1847
- workflowName: instance?.workflowName,
1848
1260
  stepName: event.stepName,
1261
+ stepType: event.stepType,
1849
1262
  });
1850
- // Broadcast via SSE
1851
1263
  if (this.sse) {
1852
1264
  this.sse.broadcast(`workflow:${instanceId}`, "step.started", {
1853
1265
  stepName: event.stepName,
1854
1266
  });
1855
1267
  this.sse.broadcast("workflows:all", "workflow.step.started", {
1856
1268
  instanceId,
1857
- workflowName: instance?.workflowName,
1858
1269
  stepName: event.stepName,
1859
1270
  });
1860
1271
  }
@@ -1862,32 +1273,11 @@ class WorkflowsImpl implements Workflows {
1862
1273
  }
1863
1274
 
1864
1275
  case "step.completed": {
1865
- const instance = await this.adapter.getInstance(instanceId);
1866
- if (!instance) break;
1867
-
1868
- // Update step results in DB
1869
- const stepResult = instance.stepResults[event.stepName!] ?? {
1870
- stepName: event.stepName!,
1871
- status: "pending" as const,
1872
- startedAt: new Date(),
1873
- attempts: 0,
1874
- };
1875
- stepResult.status = "completed";
1876
- stepResult.output = event.output;
1877
- stepResult.completedAt = new Date();
1878
-
1879
- await this.adapter.updateInstance(instanceId, {
1880
- stepResults: { ...instance.stepResults, [event.stepName!]: stepResult },
1881
- currentStep: event.nextStep,
1882
- });
1883
-
1884
1276
  await this.emitEvent("workflow.step.completed", {
1885
1277
  instanceId,
1886
- workflowName: instance?.workflowName,
1887
1278
  stepName: event.stepName,
1888
1279
  output: event.output,
1889
1280
  });
1890
- // Broadcast via SSE
1891
1281
  if (this.sse) {
1892
1282
  this.sse.broadcast(`workflow:${instanceId}`, "step.completed", {
1893
1283
  stepName: event.stepName,
@@ -1895,7 +1285,6 @@ class WorkflowsImpl implements Workflows {
1895
1285
  });
1896
1286
  this.sse.broadcast("workflows:all", "workflow.step.completed", {
1897
1287
  instanceId,
1898
- workflowName: instance?.workflowName,
1899
1288
  stepName: event.stepName,
1900
1289
  output: event.output,
1901
1290
  });
@@ -1904,31 +1293,11 @@ class WorkflowsImpl implements Workflows {
1904
1293
  }
1905
1294
 
1906
1295
  case "step.failed": {
1907
- const instance = await this.adapter.getInstance(instanceId);
1908
- if (!instance) break;
1909
-
1910
- // Update step results in DB
1911
- const stepResult = instance.stepResults[event.stepName!] ?? {
1912
- stepName: event.stepName!,
1913
- status: "pending" as const,
1914
- startedAt: new Date(),
1915
- attempts: 0,
1916
- };
1917
- stepResult.status = "failed";
1918
- stepResult.error = event.error;
1919
- stepResult.completedAt = new Date();
1920
-
1921
- await this.adapter.updateInstance(instanceId, {
1922
- stepResults: { ...instance.stepResults, [event.stepName!]: stepResult },
1923
- });
1924
-
1925
1296
  await this.emitEvent("workflow.step.failed", {
1926
1297
  instanceId,
1927
- workflowName: instance?.workflowName,
1928
1298
  stepName: event.stepName,
1929
1299
  error: event.error,
1930
1300
  });
1931
- // Broadcast via SSE
1932
1301
  if (this.sse) {
1933
1302
  this.sse.broadcast(`workflow:${instanceId}`, "step.failed", {
1934
1303
  stepName: event.stepName,
@@ -1936,7 +1305,6 @@ class WorkflowsImpl implements Workflows {
1936
1305
  });
1937
1306
  this.sse.broadcast("workflows:all", "workflow.step.failed", {
1938
1307
  instanceId,
1939
- workflowName: instance?.workflowName,
1940
1308
  stepName: event.stepName,
1941
1309
  error: event.error,
1942
1310
  });
@@ -1945,15 +1313,12 @@ class WorkflowsImpl implements Workflows {
1945
1313
  }
1946
1314
 
1947
1315
  case "progress": {
1948
- const instance = await this.adapter.getInstance(instanceId);
1949
1316
  await this.emitEvent("workflow.progress", {
1950
1317
  instanceId,
1951
- workflowName: instance?.workflowName,
1952
1318
  progress: event.progress,
1953
1319
  completedSteps: event.completedSteps,
1954
1320
  totalSteps: event.totalSteps,
1955
1321
  });
1956
- // Broadcast via SSE
1957
1322
  if (this.sse) {
1958
1323
  this.sse.broadcast(`workflow:${instanceId}`, "progress", {
1959
1324
  progress: event.progress,
@@ -1962,7 +1327,6 @@ class WorkflowsImpl implements Workflows {
1962
1327
  });
1963
1328
  this.sse.broadcast("workflows:all", "workflow.progress", {
1964
1329
  instanceId,
1965
- workflowName: instance?.workflowName,
1966
1330
  progress: event.progress,
1967
1331
  completedSteps: event.completedSteps,
1968
1332
  totalSteps: event.totalSteps,
@@ -1971,13 +1335,40 @@ class WorkflowsImpl implements Workflows {
1971
1335
  break;
1972
1336
  }
1973
1337
 
1974
- case "completed":
1975
- await this.completeWorkflowIsolated(instanceId, event.output);
1338
+ case "completed": {
1339
+ // Clean up isolated process tracking
1340
+ this.cleanupIsolatedProcess(instanceId);
1341
+
1342
+ // Subprocess already persisted state - just emit events
1343
+ await this.emitEvent("workflow.completed", {
1344
+ instanceId,
1345
+ output: event.output,
1346
+ });
1347
+ if (this.sse) {
1348
+ this.sse.broadcast(`workflow:${instanceId}`, "completed", { output: event.output });
1349
+ this.sse.broadcast("workflows:all", "workflow.completed", { instanceId });
1350
+ }
1976
1351
  break;
1352
+ }
1353
+
1354
+ case "failed": {
1355
+ // Clean up isolated process tracking
1356
+ this.cleanupIsolatedProcess(instanceId);
1977
1357
 
1978
- case "failed":
1979
- await this.failWorkflowIsolated(instanceId, event.error ?? "Unknown error");
1358
+ // Subprocess already persisted state - just emit events
1359
+ await this.emitEvent("workflow.failed", {
1360
+ instanceId,
1361
+ error: event.error,
1362
+ });
1363
+ if (this.sse) {
1364
+ this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: event.error });
1365
+ this.sse.broadcast("workflows:all", "workflow.failed", {
1366
+ instanceId,
1367
+ error: event.error,
1368
+ });
1369
+ }
1980
1370
  break;
1371
+ }
1981
1372
  }
1982
1373
  }
1983
1374
 
@@ -2015,6 +1406,18 @@ class WorkflowsImpl implements Workflows {
2015
1406
  }
2016
1407
  }
2017
1408
 
1409
+ /**
1410
+ * Clean up isolated process tracking
1411
+ */
1412
+ private cleanupIsolatedProcess(instanceId: string): void {
1413
+ const info = this.isolatedProcesses.get(instanceId);
1414
+ if (info) {
1415
+ if (info.timeout) clearTimeout(info.timeout);
1416
+ if (info.heartbeatTimeout) clearTimeout(info.heartbeatTimeout);
1417
+ this.isolatedProcesses.delete(instanceId);
1418
+ }
1419
+ }
1420
+
2018
1421
  /**
2019
1422
  * Reset heartbeat timeout for an isolated workflow
2020
1423
  */
@@ -2060,85 +1463,29 @@ class WorkflowsImpl implements Workflows {
2060
1463
  await this.getSocketServer().closeSocket(instanceId);
2061
1464
 
2062
1465
  // Fail the workflow
2063
- await this.failWorkflow(instanceId, "Workflow timed out");
2064
- }
2065
-
2066
- /**
2067
- * Complete an isolated workflow (called from event handler)
2068
- */
2069
- private async completeWorkflowIsolated(instanceId: string, output?: any): Promise<void> {
2070
- const instance = await this.adapter.getInstance(instanceId);
2071
- if (!instance) return;
2072
-
2073
- // Clean up isolated process tracking (process should have exited)
2074
- const info = this.isolatedProcesses.get(instanceId);
2075
- if (info) {
2076
- if (info.timeout) clearTimeout(info.timeout);
2077
- if (info.heartbeatTimeout) clearTimeout(info.heartbeatTimeout);
2078
- this.isolatedProcesses.delete(instanceId);
2079
- }
2080
-
2081
- await this.adapter.updateInstance(instanceId, {
2082
- status: "completed",
2083
- output,
2084
- completedAt: new Date(),
2085
- currentStep: undefined,
2086
- });
2087
-
2088
- await this.emitEvent("workflow.completed", {
2089
- instanceId,
2090
- workflowName: instance.workflowName,
2091
- output,
2092
- });
2093
-
2094
- // Broadcast via SSE
2095
- if (this.sse) {
2096
- this.sse.broadcast(`workflow:${instanceId}`, "completed", { output });
2097
- this.sse.broadcast("workflows:all", "workflow.completed", {
2098
- instanceId,
2099
- workflowName: instance.workflowName,
2100
- output,
2101
- });
2102
- }
2103
- }
2104
-
2105
- /**
2106
- * Fail an isolated workflow (called from event handler)
2107
- */
2108
- private async failWorkflowIsolated(instanceId: string, error: string): Promise<void> {
2109
- const instance = await this.adapter.getInstance(instanceId);
2110
- if (!instance) return;
2111
-
2112
- // Clean up isolated process tracking
2113
- const info = this.isolatedProcesses.get(instanceId);
2114
- if (info) {
2115
- if (info.timeout) clearTimeout(info.timeout);
2116
- if (info.heartbeatTimeout) clearTimeout(info.heartbeatTimeout);
2117
- this.isolatedProcesses.delete(instanceId);
2118
- }
2119
-
2120
1466
  await this.adapter.updateInstance(instanceId, {
2121
1467
  status: "failed",
2122
- error,
1468
+ error: "Workflow timed out",
2123
1469
  completedAt: new Date(),
2124
1470
  });
2125
-
2126
1471
  await this.emitEvent("workflow.failed", {
2127
1472
  instanceId,
2128
- workflowName: instance.workflowName,
2129
- error,
1473
+ error: "Workflow timed out",
2130
1474
  });
2131
-
2132
- // Broadcast via SSE
2133
1475
  if (this.sse) {
2134
- this.sse.broadcast(`workflow:${instanceId}`, "failed", { error });
1476
+ this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: "Workflow timed out" });
2135
1477
  this.sse.broadcast("workflows:all", "workflow.failed", {
2136
1478
  instanceId,
2137
- workflowName: instance.workflowName,
2138
- error,
1479
+ error: "Workflow timed out",
2139
1480
  });
2140
1481
  }
2141
1482
  }
1483
+
1484
+ private async emitEvent(event: string, data: any): Promise<void> {
1485
+ if (this.eventsService) {
1486
+ await this.eventsService.emit(event, data);
1487
+ }
1488
+ }
2142
1489
  }
2143
1490
 
2144
1491
  // ============================================