@donkeylabs/server 2.0.20 → 2.0.22
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/docs/workflows.md +73 -7
- package/package.json +2 -2
- package/src/admin/dashboard.ts +74 -3
- package/src/admin/routes.ts +62 -0
- package/src/core/cron.ts +17 -10
- package/src/core/index.ts +28 -0
- package/src/core/jobs.ts +8 -2
- package/src/core/logger.ts +14 -0
- package/src/core/logs-adapter-kysely.ts +287 -0
- package/src/core/logs-transport.ts +83 -0
- package/src/core/logs.ts +398 -0
- package/src/core/workflow-executor.ts +116 -337
- package/src/core/workflow-socket.ts +2 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +399 -0
- package/src/core/workflows.ts +300 -909
- package/src/core.ts +2 -0
- package/src/harness.ts +4 -0
- package/src/index.ts +10 -0
- package/src/server.ts +44 -5
- /package/{CLAUDE.md → agents.md} +0 -0
package/src/core/workflows.ts
CHANGED
|
@@ -12,9 +12,10 @@ import type { Events } from "./events";
|
|
|
12
12
|
import type { Jobs } from "./jobs";
|
|
13
13
|
import type { SSE } from "./sse";
|
|
14
14
|
import type { z } from "zod";
|
|
15
|
+
import { sql } from "kysely";
|
|
15
16
|
import type { CoreServices } from "../core";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
19
|
import {
|
|
19
20
|
createWorkflowSocketServer,
|
|
20
21
|
type WorkflowSocketServer,
|
|
@@ -22,6 +23,32 @@ import {
|
|
|
22
23
|
type ProxyRequest,
|
|
23
24
|
} from "./workflow-socket";
|
|
24
25
|
import { isProcessAlive } from "./external-jobs";
|
|
26
|
+
import { WorkflowStateMachine, type StateMachineEvents } from "./workflow-state-machine";
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// Auto-detect caller module for isolated workflows
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
const WORKFLOWS_FILE = resolve(fileURLToPath(import.meta.url));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk the call stack to find the file that invoked build().
|
|
36
|
+
* Returns a file:// URL string or undefined if detection fails.
|
|
37
|
+
*/
|
|
38
|
+
function captureCallerUrl(): string | undefined {
|
|
39
|
+
const stack = new Error().stack ?? "";
|
|
40
|
+
for (const line of stack.split("\n").slice(1)) {
|
|
41
|
+
const match = line.match(/at\s+(?:.*?\s+\(?)?([^\s():]+):\d+:\d+/);
|
|
42
|
+
if (match) {
|
|
43
|
+
let filePath = match[1];
|
|
44
|
+
if (filePath.startsWith("file://")) filePath = fileURLToPath(filePath);
|
|
45
|
+
if (filePath.startsWith("native")) continue;
|
|
46
|
+
filePath = resolve(filePath);
|
|
47
|
+
if (filePath !== WORKFLOWS_FILE) return pathToFileURL(filePath).href;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
25
52
|
|
|
26
53
|
// Type helper for Zod schema inference
|
|
27
54
|
type ZodSchema = z.ZodTypeAny;
|
|
@@ -143,6 +170,8 @@ export interface WorkflowDefinition {
|
|
|
143
170
|
* Set to false for lightweight workflows that benefit from inline execution.
|
|
144
171
|
*/
|
|
145
172
|
isolated?: boolean;
|
|
173
|
+
/** Auto-detected module URL where this workflow was built. Used as fallback for isolated execution. */
|
|
174
|
+
sourceModule?: string;
|
|
146
175
|
}
|
|
147
176
|
|
|
148
177
|
// ============================================
|
|
@@ -575,6 +604,7 @@ export class WorkflowBuilder {
|
|
|
575
604
|
timeout: this._timeout,
|
|
576
605
|
defaultRetry: this._defaultRetry,
|
|
577
606
|
isolated: this._isolated,
|
|
607
|
+
sourceModule: captureCallerUrl(),
|
|
578
608
|
};
|
|
579
609
|
}
|
|
580
610
|
}
|
|
@@ -616,10 +646,15 @@ export interface WorkflowsConfig {
|
|
|
616
646
|
export interface WorkflowRegisterOptions {
|
|
617
647
|
/**
|
|
618
648
|
* Module path for isolated workflows.
|
|
619
|
-
*
|
|
620
|
-
*
|
|
649
|
+
* Auto-detected from the call site of `build()` in most cases.
|
|
650
|
+
* Only needed if the workflow definition is re-exported from a different
|
|
651
|
+
* module than the one that calls `build()`.
|
|
621
652
|
*
|
|
622
653
|
* @example
|
|
654
|
+
* // Usually not needed — auto-detected:
|
|
655
|
+
* workflows.register(myWorkflow);
|
|
656
|
+
*
|
|
657
|
+
* // Override when re-exporting from another module:
|
|
623
658
|
* workflows.register(myWorkflow, { modulePath: import.meta.url });
|
|
624
659
|
*/
|
|
625
660
|
modulePath?: string;
|
|
@@ -648,6 +683,8 @@ export interface Workflows {
|
|
|
648
683
|
stop(): Promise<void>;
|
|
649
684
|
/** Set core services (called after initialization to resolve circular dependency) */
|
|
650
685
|
setCore(core: CoreServices): void;
|
|
686
|
+
/** Resolve dbPath from the database instance (call after setCore, before resume) */
|
|
687
|
+
resolveDbPath(): Promise<void>;
|
|
651
688
|
/** Set plugin services (called after plugins are initialized) */
|
|
652
689
|
setPlugins(plugins: Record<string, any>): void;
|
|
653
690
|
/** Update metadata for a workflow instance (used by isolated workflows) */
|
|
@@ -655,7 +692,7 @@ export interface Workflows {
|
|
|
655
692
|
}
|
|
656
693
|
|
|
657
694
|
// ============================================
|
|
658
|
-
// Workflow Service Implementation
|
|
695
|
+
// Workflow Service Implementation (Supervisor)
|
|
659
696
|
// ============================================
|
|
660
697
|
|
|
661
698
|
interface IsolatedProcessInfo {
|
|
@@ -667,13 +704,13 @@ interface IsolatedProcessInfo {
|
|
|
667
704
|
|
|
668
705
|
class WorkflowsImpl implements Workflows {
|
|
669
706
|
private adapter: WorkflowAdapter;
|
|
670
|
-
private
|
|
707
|
+
private eventsService?: Events;
|
|
671
708
|
private jobs?: Jobs;
|
|
672
709
|
private sse?: SSE;
|
|
673
710
|
private core?: CoreServices;
|
|
674
711
|
private plugins: Record<string, any> = {};
|
|
675
712
|
private definitions = new Map<string, WorkflowDefinition>();
|
|
676
|
-
private running = new Map<string, { timeout?: ReturnType<typeof setTimeout
|
|
713
|
+
private running = new Map<string, { timeout?: ReturnType<typeof setTimeout>; sm?: WorkflowStateMachine }>();
|
|
677
714
|
private pollInterval: number;
|
|
678
715
|
|
|
679
716
|
// Isolated execution state
|
|
@@ -687,7 +724,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
687
724
|
|
|
688
725
|
constructor(config: WorkflowsConfig = {}) {
|
|
689
726
|
this.adapter = config.adapter ?? new MemoryWorkflowAdapter();
|
|
690
|
-
this.
|
|
727
|
+
this.eventsService = config.events;
|
|
691
728
|
this.jobs = config.jobs;
|
|
692
729
|
this.sse = config.sse;
|
|
693
730
|
this.core = config.core;
|
|
@@ -727,19 +764,21 @@ class WorkflowsImpl implements Workflows {
|
|
|
727
764
|
|
|
728
765
|
setCore(core: CoreServices): void {
|
|
729
766
|
this.core = core;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async resolveDbPath(): Promise<void> {
|
|
770
|
+
if (this.dbPath) return;
|
|
771
|
+
if (!this.core?.db) return;
|
|
772
|
+
|
|
773
|
+
// Use PRAGMA database_list to get the file path — works with any SQLite dialect
|
|
774
|
+
try {
|
|
775
|
+
const result = await sql<{ name: string; file: string }>`PRAGMA database_list`.execute(this.core.db);
|
|
776
|
+
const main = result.rows.find((r) => r.name === "main");
|
|
777
|
+
if (main?.file && main.file !== "" && main.file !== ":memory:") {
|
|
778
|
+
this.dbPath = main.file;
|
|
742
779
|
}
|
|
780
|
+
} catch {
|
|
781
|
+
// Not a SQLite database or PRAGMA not supported — dbPath stays unset
|
|
743
782
|
}
|
|
744
783
|
}
|
|
745
784
|
|
|
@@ -760,26 +799,15 @@ class WorkflowsImpl implements Workflows {
|
|
|
760
799
|
throw new Error(`Workflow "${definition.name}" is already registered`);
|
|
761
800
|
}
|
|
762
801
|
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
// Store module path for isolated workflows
|
|
776
|
-
if (options?.modulePath) {
|
|
777
|
-
this.workflowModulePaths.set(definition.name, options.modulePath);
|
|
802
|
+
// Resolve module path: explicit option > auto-detected sourceModule
|
|
803
|
+
const modulePath = options?.modulePath ?? definition.sourceModule;
|
|
804
|
+
if (modulePath) {
|
|
805
|
+
this.workflowModulePaths.set(definition.name, modulePath);
|
|
778
806
|
} else if (definition.isolated !== false) {
|
|
779
|
-
// Warn if
|
|
807
|
+
// Warn only if neither explicit nor auto-detected path is available
|
|
780
808
|
console.warn(
|
|
781
|
-
`[Workflows] Workflow "${definition.name}" is isolated but no modulePath
|
|
782
|
-
`
|
|
809
|
+
`[Workflows] Workflow "${definition.name}" is isolated but no modulePath could be detected. ` +
|
|
810
|
+
`Pass { modulePath: import.meta.url } to register().`
|
|
783
811
|
);
|
|
784
812
|
}
|
|
785
813
|
|
|
@@ -829,13 +857,18 @@ class WorkflowsImpl implements Workflows {
|
|
|
829
857
|
// Execute in isolated subprocess
|
|
830
858
|
this.executeIsolatedWorkflow(instance.id, definition, input, modulePath);
|
|
831
859
|
} else {
|
|
832
|
-
// Execute inline
|
|
860
|
+
// Execute inline using state machine
|
|
833
861
|
if (isIsolated && !modulePath) {
|
|
834
862
|
console.warn(
|
|
835
863
|
`[Workflows] Workflow "${workflowName}" falling back to inline execution (no modulePath)`
|
|
836
864
|
);
|
|
865
|
+
} else if (isIsolated && modulePath && !this.dbPath) {
|
|
866
|
+
console.warn(
|
|
867
|
+
`[Workflows] Workflow "${workflowName}" falling back to inline execution (dbPath could not be auto-detected). ` +
|
|
868
|
+
`Set workflows.dbPath in your server config to enable isolated execution.`
|
|
869
|
+
);
|
|
837
870
|
}
|
|
838
|
-
this.
|
|
871
|
+
this.startInlineWorkflow(instance.id, definition);
|
|
839
872
|
}
|
|
840
873
|
|
|
841
874
|
return instance.id;
|
|
@@ -865,8 +898,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
865
898
|
await this.getSocketServer().closeSocket(instanceId);
|
|
866
899
|
}
|
|
867
900
|
|
|
868
|
-
//
|
|
901
|
+
// Cancel inline state machine if running
|
|
869
902
|
const runInfo = this.running.get(instanceId);
|
|
903
|
+
if (runInfo?.sm) {
|
|
904
|
+
runInfo.sm.cancel(instanceId);
|
|
905
|
+
}
|
|
870
906
|
if (runInfo?.timeout) {
|
|
871
907
|
clearTimeout(runInfo.timeout);
|
|
872
908
|
}
|
|
@@ -918,7 +954,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
918
954
|
if (isIsolated && modulePath && this.dbPath) {
|
|
919
955
|
this.executeIsolatedWorkflow(instance.id, definition, instance.input, modulePath);
|
|
920
956
|
} else {
|
|
921
|
-
this.
|
|
957
|
+
this.startInlineWorkflow(instance.id, definition);
|
|
922
958
|
}
|
|
923
959
|
}
|
|
924
960
|
}
|
|
@@ -942,8 +978,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
942
978
|
this.socketServer = undefined;
|
|
943
979
|
}
|
|
944
980
|
|
|
945
|
-
// Clear all inline timeouts
|
|
981
|
+
// Clear all inline timeouts and cancel state machines
|
|
946
982
|
for (const [instanceId, runInfo] of this.running) {
|
|
983
|
+
if (runInfo.sm) {
|
|
984
|
+
runInfo.sm.cancel(instanceId);
|
|
985
|
+
}
|
|
947
986
|
if (runInfo.timeout) {
|
|
948
987
|
clearTimeout(runInfo.timeout);
|
|
949
988
|
}
|
|
@@ -957,761 +996,177 @@ class WorkflowsImpl implements Workflows {
|
|
|
957
996
|
}
|
|
958
997
|
|
|
959
998
|
// ============================================
|
|
960
|
-
// Execution
|
|
999
|
+
// Inline Execution via State Machine
|
|
961
1000
|
// ============================================
|
|
962
1001
|
|
|
963
|
-
private
|
|
1002
|
+
private startInlineWorkflow(
|
|
964
1003
|
instanceId: string,
|
|
965
|
-
definition: WorkflowDefinition
|
|
966
|
-
):
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
});
|
|
976
|
-
}
|
|
1004
|
+
definition: WorkflowDefinition,
|
|
1005
|
+
): void {
|
|
1006
|
+
const sm = new WorkflowStateMachine({
|
|
1007
|
+
adapter: this.adapter,
|
|
1008
|
+
core: this.core,
|
|
1009
|
+
plugins: this.plugins,
|
|
1010
|
+
events: this.createInlineEventHandler(instanceId),
|
|
1011
|
+
jobs: this.jobs,
|
|
1012
|
+
pollInterval: this.pollInterval,
|
|
1013
|
+
});
|
|
977
1014
|
|
|
978
1015
|
// Set up workflow timeout
|
|
1016
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
979
1017
|
if (definition.timeout) {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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 },
|
|
1018
|
+
timeout = setTimeout(async () => {
|
|
1019
|
+
sm.cancel(instanceId);
|
|
1020
|
+
await this.adapter.updateInstance(instanceId, {
|
|
1021
|
+
status: "failed",
|
|
1022
|
+
error: "Workflow timed out",
|
|
1023
|
+
completedAt: new Date(),
|
|
1024
|
+
});
|
|
1025
|
+
await this.emitEvent("workflow.failed", {
|
|
1026
|
+
instanceId,
|
|
1027
|
+
workflowName: definition.name,
|
|
1028
|
+
error: "Workflow timed out",
|
|
1029
|
+
});
|
|
1030
|
+
if (this.sse) {
|
|
1031
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: "Workflow timed out" });
|
|
1032
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1033
|
+
instanceId,
|
|
1034
|
+
workflowName: definition.name,
|
|
1035
|
+
error: "Workflow timed out",
|
|
1145
1036
|
});
|
|
1146
1037
|
}
|
|
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");
|
|
1038
|
+
this.running.delete(instanceId);
|
|
1039
|
+
}, definition.timeout);
|
|
1167
1040
|
}
|
|
1168
1041
|
|
|
1169
|
-
|
|
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
|
-
}
|
|
1042
|
+
this.running.set(instanceId, { timeout, sm });
|
|
1185
1043
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1044
|
+
// Run the state machine (fire and forget - events handle communication)
|
|
1045
|
+
sm.run(instanceId, definition).then(() => {
|
|
1046
|
+
// Clean up timeout on completion
|
|
1047
|
+
const runInfo = this.running.get(instanceId);
|
|
1048
|
+
if (runInfo?.timeout) {
|
|
1049
|
+
clearTimeout(runInfo.timeout);
|
|
1189
1050
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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);
|
|
1051
|
+
this.running.delete(instanceId);
|
|
1052
|
+
}).catch(() => {
|
|
1053
|
+
// State machine already persisted the failure - just clean up
|
|
1054
|
+
const runInfo = this.running.get(instanceId);
|
|
1055
|
+
if (runInfo?.timeout) {
|
|
1056
|
+
clearTimeout(runInfo.timeout);
|
|
1209
1057
|
}
|
|
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
|
-
},
|
|
1058
|
+
this.running.delete(instanceId);
|
|
1257
1059
|
});
|
|
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
1060
|
}
|
|
1288
1061
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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 },
|
|
1062
|
+
/**
|
|
1063
|
+
* Create an event handler that bridges state machine events to Events service + SSE
|
|
1064
|
+
*/
|
|
1065
|
+
private createInlineEventHandler(instanceId: string): StateMachineEvents {
|
|
1066
|
+
return {
|
|
1067
|
+
onStepStarted: (id, stepName, stepType) => {
|
|
1068
|
+
this.emitEvent("workflow.step.started", {
|
|
1069
|
+
instanceId: id,
|
|
1070
|
+
stepName,
|
|
1071
|
+
stepType,
|
|
1072
|
+
});
|
|
1073
|
+
if (this.sse) {
|
|
1074
|
+
this.sse.broadcast(`workflow:${id}`, "step.started", { stepName });
|
|
1075
|
+
this.sse.broadcast("workflows:all", "workflow.step.started", {
|
|
1076
|
+
instanceId: id,
|
|
1077
|
+
stepName,
|
|
1324
1078
|
});
|
|
1325
|
-
|
|
1326
|
-
// Execute next step
|
|
1327
|
-
await this.executeStep(instanceId, definition);
|
|
1328
|
-
return choice.next;
|
|
1329
1079
|
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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 },
|
|
1080
|
+
},
|
|
1081
|
+
onStepCompleted: (id, stepName, output, nextStep) => {
|
|
1082
|
+
this.emitEvent("workflow.step.completed", {
|
|
1083
|
+
instanceId: id,
|
|
1084
|
+
stepName,
|
|
1085
|
+
output,
|
|
1086
|
+
});
|
|
1087
|
+
if (this.sse) {
|
|
1088
|
+
this.sse.broadcast(`workflow:${id}`, "step.completed", { stepName, output });
|
|
1089
|
+
this.sse.broadcast("workflows:all", "workflow.step.completed", {
|
|
1090
|
+
instanceId: id,
|
|
1091
|
+
stepName,
|
|
1351
1092
|
});
|
|
1352
1093
|
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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;
|
|
1094
|
+
},
|
|
1095
|
+
onStepFailed: (id, stepName, error, attempts) => {
|
|
1096
|
+
this.emitEvent("workflow.step.failed", {
|
|
1097
|
+
instanceId: id,
|
|
1098
|
+
stepName,
|
|
1099
|
+
error,
|
|
1100
|
+
attempts,
|
|
1101
|
+
});
|
|
1102
|
+
if (this.sse) {
|
|
1103
|
+
this.sse.broadcast(`workflow:${id}`, "step.failed", { stepName, error });
|
|
1104
|
+
this.sse.broadcast("workflows:all", "workflow.step.failed", {
|
|
1105
|
+
instanceId: id,
|
|
1106
|
+
stepName,
|
|
1107
|
+
error,
|
|
1108
|
+
});
|
|
1402
1109
|
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1110
|
+
},
|
|
1111
|
+
onStepRetry: (id, stepName, attempt, max, delayMs) => {
|
|
1112
|
+
this.emitEvent("workflow.step.retry", {
|
|
1113
|
+
instanceId: id,
|
|
1114
|
+
stepName,
|
|
1115
|
+
attempt,
|
|
1116
|
+
maxAttempts: max,
|
|
1117
|
+
delay: delayMs,
|
|
1118
|
+
});
|
|
1119
|
+
},
|
|
1120
|
+
onProgress: (id, progress, currentStep, completed, total) => {
|
|
1121
|
+
this.emitEvent("workflow.progress", {
|
|
1122
|
+
instanceId: id,
|
|
1123
|
+
progress,
|
|
1124
|
+
currentStep,
|
|
1125
|
+
completedSteps: completed,
|
|
1126
|
+
totalSteps: total,
|
|
1127
|
+
});
|
|
1128
|
+
if (this.sse) {
|
|
1129
|
+
this.sse.broadcast(`workflow:${id}`, "progress", {
|
|
1130
|
+
progress,
|
|
1131
|
+
currentStep,
|
|
1132
|
+
completedSteps: completed,
|
|
1133
|
+
totalSteps: total,
|
|
1134
|
+
});
|
|
1135
|
+
this.sse.broadcast("workflows:all", "workflow.progress", {
|
|
1136
|
+
instanceId: id,
|
|
1137
|
+
progress,
|
|
1138
|
+
currentStep,
|
|
1412
1139
|
});
|
|
1413
|
-
if (completedSteps.length > 0) {
|
|
1414
|
-
prev = completedSteps[0][1].output;
|
|
1415
1140
|
}
|
|
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
1141
|
},
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
// Update local snapshot
|
|
1435
|
-
metadata[key] = value;
|
|
1436
|
-
// Persist to database
|
|
1437
|
-
await this.adapter.updateInstance(instance.id, {
|
|
1438
|
-
metadata: { ...metadata },
|
|
1142
|
+
onCompleted: (id, output) => {
|
|
1143
|
+
this.emitEvent("workflow.completed", {
|
|
1144
|
+
instanceId: id,
|
|
1145
|
+
output,
|
|
1439
1146
|
});
|
|
1147
|
+
if (this.sse) {
|
|
1148
|
+
this.sse.broadcast(`workflow:${id}`, "completed", { output });
|
|
1149
|
+
this.sse.broadcast("workflows:all", "workflow.completed", {
|
|
1150
|
+
instanceId: id,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1440
1153
|
},
|
|
1441
|
-
|
|
1442
|
-
|
|
1154
|
+
onFailed: (id, error) => {
|
|
1155
|
+
this.emitEvent("workflow.failed", {
|
|
1156
|
+
instanceId: id,
|
|
1157
|
+
error,
|
|
1158
|
+
});
|
|
1159
|
+
if (this.sse) {
|
|
1160
|
+
this.sse.broadcast(`workflow:${id}`, "failed", { error });
|
|
1161
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1162
|
+
instanceId: id,
|
|
1163
|
+
error,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1443
1166
|
},
|
|
1444
1167
|
};
|
|
1445
1168
|
}
|
|
1446
1169
|
|
|
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
1170
|
// ============================================
|
|
1716
1171
|
// Isolated Execution Engine
|
|
1717
1172
|
// ============================================
|
|
@@ -1799,13 +1254,33 @@ class WorkflowsImpl implements Workflows {
|
|
|
1799
1254
|
const instance = await this.adapter.getInstance(instanceId);
|
|
1800
1255
|
if (instance && instance.status === "running") {
|
|
1801
1256
|
console.error(`[Workflows] Isolated workflow ${instanceId} crashed with exit code ${exitCode}`);
|
|
1802
|
-
await this.
|
|
1257
|
+
await this.adapter.updateInstance(instanceId, {
|
|
1258
|
+
status: "failed",
|
|
1259
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1260
|
+
completedAt: new Date(),
|
|
1261
|
+
});
|
|
1262
|
+
await this.emitEvent("workflow.failed", {
|
|
1263
|
+
instanceId,
|
|
1264
|
+
workflowName: instance.workflowName,
|
|
1265
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1266
|
+
});
|
|
1267
|
+
if (this.sse) {
|
|
1268
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", {
|
|
1269
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1270
|
+
});
|
|
1271
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1272
|
+
instanceId,
|
|
1273
|
+
workflowName: instance.workflowName,
|
|
1274
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1803
1277
|
}
|
|
1804
1278
|
});
|
|
1805
1279
|
}
|
|
1806
1280
|
|
|
1807
1281
|
/**
|
|
1808
|
-
* Handle events from isolated workflow subprocess
|
|
1282
|
+
* Handle events from isolated workflow subprocess.
|
|
1283
|
+
* The subprocess owns persistence via its own adapter - we only forward events to SSE/Events.
|
|
1809
1284
|
*/
|
|
1810
1285
|
private async handleIsolatedEvent(event: WorkflowEvent): Promise<void> {
|
|
1811
1286
|
const { instanceId, type } = event;
|
|
@@ -1819,42 +1294,22 @@ class WorkflowsImpl implements Workflows {
|
|
|
1819
1294
|
|
|
1820
1295
|
switch (type) {
|
|
1821
1296
|
case "started":
|
|
1822
|
-
// Already marked as running in executeIsolatedWorkflow
|
|
1823
|
-
break;
|
|
1824
|
-
|
|
1825
1297
|
case "heartbeat":
|
|
1826
|
-
//
|
|
1298
|
+
// No-op: heartbeat handled above, started is handled by executeIsolatedWorkflow
|
|
1827
1299
|
break;
|
|
1828
1300
|
|
|
1829
1301
|
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
1302
|
await this.emitEvent("workflow.step.started", {
|
|
1846
1303
|
instanceId,
|
|
1847
|
-
workflowName: instance?.workflowName,
|
|
1848
1304
|
stepName: event.stepName,
|
|
1305
|
+
stepType: event.stepType,
|
|
1849
1306
|
});
|
|
1850
|
-
// Broadcast via SSE
|
|
1851
1307
|
if (this.sse) {
|
|
1852
1308
|
this.sse.broadcast(`workflow:${instanceId}`, "step.started", {
|
|
1853
1309
|
stepName: event.stepName,
|
|
1854
1310
|
});
|
|
1855
1311
|
this.sse.broadcast("workflows:all", "workflow.step.started", {
|
|
1856
1312
|
instanceId,
|
|
1857
|
-
workflowName: instance?.workflowName,
|
|
1858
1313
|
stepName: event.stepName,
|
|
1859
1314
|
});
|
|
1860
1315
|
}
|
|
@@ -1862,32 +1317,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
1862
1317
|
}
|
|
1863
1318
|
|
|
1864
1319
|
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
1320
|
await this.emitEvent("workflow.step.completed", {
|
|
1885
1321
|
instanceId,
|
|
1886
|
-
workflowName: instance?.workflowName,
|
|
1887
1322
|
stepName: event.stepName,
|
|
1888
1323
|
output: event.output,
|
|
1889
1324
|
});
|
|
1890
|
-
// Broadcast via SSE
|
|
1891
1325
|
if (this.sse) {
|
|
1892
1326
|
this.sse.broadcast(`workflow:${instanceId}`, "step.completed", {
|
|
1893
1327
|
stepName: event.stepName,
|
|
@@ -1895,7 +1329,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
1895
1329
|
});
|
|
1896
1330
|
this.sse.broadcast("workflows:all", "workflow.step.completed", {
|
|
1897
1331
|
instanceId,
|
|
1898
|
-
workflowName: instance?.workflowName,
|
|
1899
1332
|
stepName: event.stepName,
|
|
1900
1333
|
output: event.output,
|
|
1901
1334
|
});
|
|
@@ -1904,31 +1337,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
1904
1337
|
}
|
|
1905
1338
|
|
|
1906
1339
|
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
1340
|
await this.emitEvent("workflow.step.failed", {
|
|
1926
1341
|
instanceId,
|
|
1927
|
-
workflowName: instance?.workflowName,
|
|
1928
1342
|
stepName: event.stepName,
|
|
1929
1343
|
error: event.error,
|
|
1930
1344
|
});
|
|
1931
|
-
// Broadcast via SSE
|
|
1932
1345
|
if (this.sse) {
|
|
1933
1346
|
this.sse.broadcast(`workflow:${instanceId}`, "step.failed", {
|
|
1934
1347
|
stepName: event.stepName,
|
|
@@ -1936,7 +1349,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
1936
1349
|
});
|
|
1937
1350
|
this.sse.broadcast("workflows:all", "workflow.step.failed", {
|
|
1938
1351
|
instanceId,
|
|
1939
|
-
workflowName: instance?.workflowName,
|
|
1940
1352
|
stepName: event.stepName,
|
|
1941
1353
|
error: event.error,
|
|
1942
1354
|
});
|
|
@@ -1945,15 +1357,12 @@ class WorkflowsImpl implements Workflows {
|
|
|
1945
1357
|
}
|
|
1946
1358
|
|
|
1947
1359
|
case "progress": {
|
|
1948
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1949
1360
|
await this.emitEvent("workflow.progress", {
|
|
1950
1361
|
instanceId,
|
|
1951
|
-
workflowName: instance?.workflowName,
|
|
1952
1362
|
progress: event.progress,
|
|
1953
1363
|
completedSteps: event.completedSteps,
|
|
1954
1364
|
totalSteps: event.totalSteps,
|
|
1955
1365
|
});
|
|
1956
|
-
// Broadcast via SSE
|
|
1957
1366
|
if (this.sse) {
|
|
1958
1367
|
this.sse.broadcast(`workflow:${instanceId}`, "progress", {
|
|
1959
1368
|
progress: event.progress,
|
|
@@ -1962,7 +1371,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
1962
1371
|
});
|
|
1963
1372
|
this.sse.broadcast("workflows:all", "workflow.progress", {
|
|
1964
1373
|
instanceId,
|
|
1965
|
-
workflowName: instance?.workflowName,
|
|
1966
1374
|
progress: event.progress,
|
|
1967
1375
|
completedSteps: event.completedSteps,
|
|
1968
1376
|
totalSteps: event.totalSteps,
|
|
@@ -1971,13 +1379,40 @@ class WorkflowsImpl implements Workflows {
|
|
|
1971
1379
|
break;
|
|
1972
1380
|
}
|
|
1973
1381
|
|
|
1974
|
-
case "completed":
|
|
1975
|
-
|
|
1382
|
+
case "completed": {
|
|
1383
|
+
// Clean up isolated process tracking
|
|
1384
|
+
this.cleanupIsolatedProcess(instanceId);
|
|
1385
|
+
|
|
1386
|
+
// Subprocess already persisted state - just emit events
|
|
1387
|
+
await this.emitEvent("workflow.completed", {
|
|
1388
|
+
instanceId,
|
|
1389
|
+
output: event.output,
|
|
1390
|
+
});
|
|
1391
|
+
if (this.sse) {
|
|
1392
|
+
this.sse.broadcast(`workflow:${instanceId}`, "completed", { output: event.output });
|
|
1393
|
+
this.sse.broadcast("workflows:all", "workflow.completed", { instanceId });
|
|
1394
|
+
}
|
|
1976
1395
|
break;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
case "failed": {
|
|
1399
|
+
// Clean up isolated process tracking
|
|
1400
|
+
this.cleanupIsolatedProcess(instanceId);
|
|
1977
1401
|
|
|
1978
|
-
|
|
1979
|
-
await this.
|
|
1402
|
+
// Subprocess already persisted state - just emit events
|
|
1403
|
+
await this.emitEvent("workflow.failed", {
|
|
1404
|
+
instanceId,
|
|
1405
|
+
error: event.error,
|
|
1406
|
+
});
|
|
1407
|
+
if (this.sse) {
|
|
1408
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: event.error });
|
|
1409
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1410
|
+
instanceId,
|
|
1411
|
+
error: event.error,
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1980
1414
|
break;
|
|
1415
|
+
}
|
|
1981
1416
|
}
|
|
1982
1417
|
}
|
|
1983
1418
|
|
|
@@ -2015,6 +1450,18 @@ class WorkflowsImpl implements Workflows {
|
|
|
2015
1450
|
}
|
|
2016
1451
|
}
|
|
2017
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* Clean up isolated process tracking
|
|
1455
|
+
*/
|
|
1456
|
+
private cleanupIsolatedProcess(instanceId: string): void {
|
|
1457
|
+
const info = this.isolatedProcesses.get(instanceId);
|
|
1458
|
+
if (info) {
|
|
1459
|
+
if (info.timeout) clearTimeout(info.timeout);
|
|
1460
|
+
if (info.heartbeatTimeout) clearTimeout(info.heartbeatTimeout);
|
|
1461
|
+
this.isolatedProcesses.delete(instanceId);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
2018
1465
|
/**
|
|
2019
1466
|
* Reset heartbeat timeout for an isolated workflow
|
|
2020
1467
|
*/
|
|
@@ -2060,85 +1507,29 @@ class WorkflowsImpl implements Workflows {
|
|
|
2060
1507
|
await this.getSocketServer().closeSocket(instanceId);
|
|
2061
1508
|
|
|
2062
1509
|
// 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
1510
|
await this.adapter.updateInstance(instanceId, {
|
|
2121
1511
|
status: "failed",
|
|
2122
|
-
error,
|
|
1512
|
+
error: "Workflow timed out",
|
|
2123
1513
|
completedAt: new Date(),
|
|
2124
1514
|
});
|
|
2125
|
-
|
|
2126
1515
|
await this.emitEvent("workflow.failed", {
|
|
2127
1516
|
instanceId,
|
|
2128
|
-
|
|
2129
|
-
error,
|
|
1517
|
+
error: "Workflow timed out",
|
|
2130
1518
|
});
|
|
2131
|
-
|
|
2132
|
-
// Broadcast via SSE
|
|
2133
1519
|
if (this.sse) {
|
|
2134
|
-
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error });
|
|
1520
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: "Workflow timed out" });
|
|
2135
1521
|
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
2136
1522
|
instanceId,
|
|
2137
|
-
|
|
2138
|
-
error,
|
|
1523
|
+
error: "Workflow timed out",
|
|
2139
1524
|
});
|
|
2140
1525
|
}
|
|
2141
1526
|
}
|
|
1527
|
+
|
|
1528
|
+
private async emitEvent(event: string, data: any): Promise<void> {
|
|
1529
|
+
if (this.eventsService) {
|
|
1530
|
+
await this.eventsService.emit(event, data);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
2142
1533
|
}
|
|
2143
1534
|
|
|
2144
1535
|
// ============================================
|