@cascade-flow/worker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,841 @@
1
+ // src/step-worker.ts
2
+ import { FileSystemBackend } from "@cascade-flow/backend-filesystem";
3
+ import { getMicrosecondTimestamp as getMicrosecondTimestamp5, MICROSECONDS_PER_MILLISECOND, convertZodToJSONSchema, projectStepState as projectStepState6, serializeError } from "@cascade-flow/backend-interface";
4
+ import { hostname } from "node:os";
5
+ import { join } from "node:path";
6
+ import { discoverSteps, discoverWorkflows, executeStepInProcess, calculateWorkflowHash, getGitInfo } from "@cascade-flow/runner";
7
+ import { Skip } from "@cascade-flow/workflow";
8
+
9
+ // src/execution/dependency-resolver.ts
10
+ import { projectStepState } from "@cascade-flow/backend-interface";
11
+ import { isOptional } from "@cascade-flow/workflow";
12
+ async function checkDependenciesSatisfied(step, backend, workflowSlug, runId) {
13
+ for (const [depKey, dep] of Object.entries(step.dependencies)) {
14
+ const depEvents = await backend.loadEvents(workflowSlug, runId, {
15
+ category: "step",
16
+ stepId: dep.id
17
+ });
18
+ if (depEvents.length === 0) {
19
+ const isOptionalDep2 = isOptional(step.dependencies[depKey]);
20
+ if (!isOptionalDep2) {
21
+ return false;
22
+ }
23
+ continue;
24
+ }
25
+ const depState = projectStepState(depEvents, workflowSlug);
26
+ const isOptionalDep = isOptional(step.dependencies[depKey]);
27
+ if (depState.status === "skipped" && isOptionalDep) {
28
+ continue;
29
+ }
30
+ if (depState.status !== "completed") {
31
+ return false;
32
+ }
33
+ }
34
+ return true;
35
+ }
36
+ async function loadDependencyOutputs(step, backend, workflowSlug, runId) {
37
+ const dependencyOutputs = {};
38
+ for (const [depKey, depStep] of Object.entries(step.dependencies)) {
39
+ const depEvents = await backend.loadEvents(workflowSlug, runId, {
40
+ category: "step",
41
+ stepId: depStep.id
42
+ });
43
+ if (depEvents.length === 0) {
44
+ const isOptionalDep2 = isOptional(step.dependencies[depKey]);
45
+ if (!isOptionalDep2) {
46
+ throw new Error(`Required dependency ${depStep.name} has no events`);
47
+ }
48
+ dependencyOutputs[depKey] = undefined;
49
+ continue;
50
+ }
51
+ const depState = projectStepState(depEvents, workflowSlug);
52
+ const isOptionalDep = isOptional(step.dependencies[depKey]);
53
+ if (depState.status === "skipped" && isOptionalDep) {
54
+ dependencyOutputs[depKey] = undefined;
55
+ continue;
56
+ }
57
+ if (depState.status !== "completed" || !depState.output) {
58
+ throw new Error(`Dependency ${depStep.name} is not completed`);
59
+ }
60
+ dependencyOutputs[depKey] = JSON.parse(depState.output);
61
+ }
62
+ return dependencyOutputs;
63
+ }
64
+ function findDependentSteps(targetStepId, allSteps, requiredOnly = false) {
65
+ return allSteps.filter((step) => {
66
+ const deps = Object.entries(step.dependencies);
67
+ return deps.some(([alias, dep]) => {
68
+ const dependsOnTarget = dep.id === targetStepId;
69
+ if (!dependsOnTarget)
70
+ return false;
71
+ if (requiredOnly) {
72
+ const isOptionalDep = isOptional(step.dependencies[alias]);
73
+ return !isOptionalDep;
74
+ }
75
+ return true;
76
+ });
77
+ });
78
+ }
79
+
80
+ // src/scheduling/skip-handler.ts
81
+ import { getMicrosecondTimestamp, projectStepState as projectStepState2 } from "@cascade-flow/backend-interface";
82
+ async function handleStepSkip(backend, workflowSlug, runId, stepId, skipError, duration, attemptNumber) {
83
+ await backend.saveStepSkipped(workflowSlug, runId, stepId, {
84
+ skipType: "primary",
85
+ reason: skipError.reason || skipError.message.replace("Step skipped: ", ""),
86
+ metadata: skipError.metadata,
87
+ duration,
88
+ attemptNumber
89
+ });
90
+ }
91
+ async function cascadeSkip(backend, workflowSlug, runId, skippedStepId, allSteps, workerId) {
92
+ const dependentSteps = findDependentSteps(skippedStepId, allSteps, true);
93
+ for (const dependentStep of dependentSteps) {
94
+ const stepEvents = await backend.loadEvents(workflowSlug, runId, {
95
+ category: "step",
96
+ stepId: dependentStep.id
97
+ });
98
+ const hasEvents = stepEvents.length > 0;
99
+ if (!hasEvents) {
100
+ const now = getMicrosecondTimestamp();
101
+ await backend.saveStepStart(workflowSlug, runId, dependentStep.id, workerId, {
102
+ dependencies: Object.keys(dependentStep.dependencies),
103
+ timestamp: now,
104
+ attemptNumber: 1
105
+ });
106
+ await backend.saveStepSkipped(workflowSlug, runId, dependentStep.id, {
107
+ skipType: "cascade",
108
+ reason: `Dependency '${skippedStepId}' was skipped`,
109
+ duration: 0,
110
+ attemptNumber: 1,
111
+ cascadedFrom: skippedStepId
112
+ });
113
+ await cascadeSkip(backend, workflowSlug, runId, dependentStep.id, allSteps, workerId);
114
+ } else {
115
+ const depState = projectStepState2(stepEvents, workflowSlug);
116
+ if (depState.status !== "completed" && depState.status !== "failed" && depState.status !== "skipped") {
117
+ await backend.saveStepSkipped(workflowSlug, runId, dependentStep.id, {
118
+ skipType: "cascade",
119
+ reason: `Dependency '${skippedStepId}' was skipped`,
120
+ duration: getMicrosecondTimestamp() - (depState.startTime || getMicrosecondTimestamp()),
121
+ attemptNumber: depState.attemptNumber,
122
+ cascadedFrom: skippedStepId
123
+ });
124
+ await cascadeSkip(backend, workflowSlug, runId, dependentStep.id, allSteps, workerId);
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ // src/scheduling/step-scheduler.ts
131
+ import { getMicrosecondTimestamp as getMicrosecondTimestamp2, projectStepState as projectStepState3 } from "@cascade-flow/backend-interface";
132
+ async function scheduleStep(backend, workflowSlug, runId, stepId, reason, attemptNumber, options) {
133
+ const now = getMicrosecondTimestamp2();
134
+ await backend.saveStepScheduled(workflowSlug, runId, stepId, {
135
+ availableAt: options?.availableAt ?? now,
136
+ reason,
137
+ attemptNumber,
138
+ retryDelayMs: options?.retryDelayMs
139
+ });
140
+ }
141
+ async function scheduleDownstreamSteps(backend, workflowSlug, runId, completedStepId, allSteps) {
142
+ const scheduledSteps = [];
143
+ const dependentSteps = findDependentSteps(completedStepId, allSteps, false);
144
+ for (const dependentStep of dependentSteps) {
145
+ const allDepsCompleted = await checkDependenciesSatisfied(dependentStep, backend, workflowSlug, runId);
146
+ if (allDepsCompleted) {
147
+ const stepEvents = await backend.loadEvents(workflowSlug, runId, {
148
+ category: "step",
149
+ stepId: dependentStep.id
150
+ });
151
+ if (stepEvents.length === 0) {
152
+ await scheduleStep(backend, workflowSlug, runId, dependentStep.id, "dependency-satisfied", 1);
153
+ scheduledSteps.push(dependentStep.name);
154
+ } else {
155
+ const state = projectStepState3(stepEvents, workflowSlug);
156
+ if (state.status === "pending") {
157
+ await scheduleStep(backend, workflowSlug, runId, dependentStep.id, "dependency-satisfied", 1);
158
+ scheduledSteps.push(dependentStep.name);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ return scheduledSteps;
164
+ }
165
+
166
+ // src/utils/step-key.ts
167
+ function createStepKey(workflowSlug, runId, stepId) {
168
+ return `${workflowSlug}/${runId}/${stepId}`;
169
+ }
170
+ function extractStepId(stepKey) {
171
+ const parts = stepKey.split("/");
172
+ return parts.slice(2).join("/");
173
+ }
174
+
175
+ // src/scheduling/workflow-lifecycle.ts
176
+ import { getMicrosecondTimestamp as getMicrosecondTimestamp4, projectStepState as projectStepState5 } from "@cascade-flow/backend-interface";
177
+
178
+ // src/utils/event-helpers.ts
179
+ import { getMicrosecondTimestamp as getMicrosecondTimestamp3, projectStepState as projectStepState4 } from "@cascade-flow/backend-interface";
180
+ async function loadWorkflowEvents(backend, workflowSlug, runId) {
181
+ return backend.loadEvents(workflowSlug, runId, { category: "workflow" });
182
+ }
183
+ function getCurrentWorkflowAttemptNumber(events) {
184
+ const retryEvents = events.filter((e) => e.type === "WorkflowRetryStarted");
185
+ if (retryEvents.length === 0)
186
+ return 1;
187
+ const mostRecentRetry = retryEvents[retryEvents.length - 1];
188
+ if (!mostRecentRetry)
189
+ return 1;
190
+ return mostRecentRetry.workflowAttemptNumber;
191
+ }
192
+ function getWorkflowTerminalState(events) {
193
+ const currentAttempt = getCurrentWorkflowAttemptNumber(events);
194
+ return {
195
+ hasCompleted: events.some((e) => e.type === "WorkflowCompleted" && e.workflowAttemptNumber === currentAttempt),
196
+ hasFailed: events.some((e) => e.type === "WorkflowFailed" && e.workflowAttemptNumber === currentAttempt),
197
+ hasCancelled: events.some((e) => e.type === "WorkflowCancelled" && e.workflowAttemptNumber === currentAttempt)
198
+ };
199
+ }
200
+ function calculateWorkflowDuration(events) {
201
+ const currentAttempt = getCurrentWorkflowAttemptNumber(events);
202
+ const workflowStartEvent = events.find((e) => e.type === "WorkflowStarted" && e.workflowAttemptNumber === currentAttempt);
203
+ const startTime = workflowStartEvent ? workflowStartEvent.timestampUs : 0;
204
+ const now = getMicrosecondTimestamp3();
205
+ return now - startTime;
206
+ }
207
+ function hasWorkflowStarted(events) {
208
+ const currentAttempt = getCurrentWorkflowAttemptNumber(events);
209
+ return events.some((e) => e.type === "WorkflowStarted" && e.workflowAttemptNumber === currentAttempt);
210
+ }
211
+ function hasWorkflowSubmitted(events) {
212
+ return events.some((e) => e.type === "RunSubmitted");
213
+ }
214
+ function hasWorkflowValidatedInput(events) {
215
+ const currentAttempt = getCurrentWorkflowAttemptNumber(events);
216
+ return events.some((e) => e.type === "WorkflowInputValidation" && e.workflowAttemptNumber === currentAttempt);
217
+ }
218
+ function getRunSubmittedEvent(events) {
219
+ return events.find((e) => e.type === "RunSubmitted");
220
+ }
221
+ function getWorkflowInputValidationEvent(events) {
222
+ const validationEvents = events.filter((e) => e.type === "WorkflowInputValidation");
223
+ return validationEvents.length > 0 ? validationEvents[validationEvents.length - 1] : undefined;
224
+ }
225
+
226
+ // src/scheduling/workflow-lifecycle.ts
227
+ async function handleWorkflowStart(backend, workflowSlug, runId, metadata, workflowEvents) {
228
+ const workflowAttemptNumber = getCurrentWorkflowAttemptNumber(workflowEvents);
229
+ await backend.saveWorkflowStart(workflowSlug, runId, {
230
+ versionId: metadata.versionId,
231
+ workflowAttemptNumber,
232
+ hasInputSchema: metadata.hasInputSchema,
233
+ hasInput: metadata.hasInput
234
+ });
235
+ }
236
+ async function validateWorkflowInput(backend, workflowSlug, runId, workflowInput, inputSchema, workflowEvents) {
237
+ const workflowAttemptNumber = getCurrentWorkflowAttemptNumber(workflowEvents);
238
+ if (!inputSchema) {
239
+ if (workflowInput !== undefined) {
240
+ await backend.saveWorkflowInputValidation(workflowSlug, runId, {
241
+ workflowAttemptNumber,
242
+ hasSchema: false,
243
+ success: true
244
+ });
245
+ }
246
+ return { success: true };
247
+ }
248
+ const parseResult = inputSchema.safeParse(workflowInput ?? {});
249
+ if (!parseResult.success) {
250
+ const validationErrors = parseResult.error.issues.map((e) => ({
251
+ path: e.path.join("."),
252
+ message: e.message
253
+ }));
254
+ const errorMessage = validationErrors.map((e) => ` ${e.path}: ${e.message}`).join(`
255
+ `);
256
+ const error = {
257
+ name: "ValidationError",
258
+ message: `Invalid workflow input:
259
+ ${errorMessage}`
260
+ };
261
+ await backend.saveWorkflowInputValidation(workflowSlug, runId, {
262
+ workflowAttemptNumber,
263
+ hasSchema: true,
264
+ success: false,
265
+ error,
266
+ validationErrors
267
+ });
268
+ const duration = calculateWorkflowDuration(workflowEvents);
269
+ await backend.saveWorkflowFailed(workflowSlug, runId, error, {
270
+ workflowAttemptNumber,
271
+ duration,
272
+ completedSteps: 0
273
+ }, "step-failed");
274
+ return { success: false, error, validationErrors };
275
+ }
276
+ await backend.saveWorkflowInputValidation(workflowSlug, runId, {
277
+ workflowAttemptNumber,
278
+ hasSchema: true,
279
+ success: true
280
+ });
281
+ return { success: true };
282
+ }
283
+ async function handleWorkflowCompletion(backend, workflowSlug, runId, steps, workflowEvents) {
284
+ const workflowAttemptNumber = getCurrentWorkflowAttemptNumber(workflowEvents);
285
+ const duration = calculateWorkflowDuration(workflowEvents);
286
+ const now = getMicrosecondTimestamp4();
287
+ const output = {};
288
+ for (const step of steps) {
289
+ const stepEvents = await backend.loadEvents(workflowSlug, runId, {
290
+ category: "step",
291
+ stepId: step.id
292
+ });
293
+ const state = projectStepState5(stepEvents, workflowSlug);
294
+ if (state.exportOutput && state.output) {
295
+ output[step.name] = JSON.parse(state.output);
296
+ }
297
+ }
298
+ await backend.saveWorkflowComplete(workflowSlug, runId, output, {
299
+ workflowAttemptNumber,
300
+ timestamp: now,
301
+ duration,
302
+ totalSteps: steps.length
303
+ });
304
+ }
305
+ async function handleWorkflowFailure(backend, workflowSlug, runId, steps, workflowEvents, failedStepName) {
306
+ const workflowAttemptNumber = getCurrentWorkflowAttemptNumber(workflowEvents);
307
+ const duration = calculateWorkflowDuration(workflowEvents);
308
+ let completedCount = 0;
309
+ for (const step of steps) {
310
+ const stepEvents = await backend.loadEvents(workflowSlug, runId, {
311
+ category: "step",
312
+ stepId: step.id
313
+ });
314
+ if (stepEvents.length > 0) {
315
+ const state = projectStepState5(stepEvents, workflowSlug);
316
+ if (state.status === "completed") {
317
+ completedCount++;
318
+ }
319
+ }
320
+ }
321
+ await backend.saveWorkflowFailed(workflowSlug, runId, {
322
+ message: failedStepName ? `Step ${failedStepName} failed` : "Workflow failed",
323
+ name: "StepFailure"
324
+ }, {
325
+ workflowAttemptNumber,
326
+ duration,
327
+ completedSteps: completedCount,
328
+ failedStep: failedStepName
329
+ }, "step-failed");
330
+ }
331
+ function parseWorkflowInput(workflowEvents) {
332
+ const runSubmittedEvent = getRunSubmittedEvent(workflowEvents);
333
+ if (runSubmittedEvent && runSubmittedEvent.input) {
334
+ return JSON.parse(runSubmittedEvent.input);
335
+ }
336
+ return;
337
+ }
338
+
339
+ // src/step-worker.ts
340
+ class StepWorker {
341
+ workerId;
342
+ backend;
343
+ options;
344
+ running = false;
345
+ activeSteps = new Map;
346
+ stats;
347
+ workflowCache = new Map;
348
+ constructor(backend, options = {}) {
349
+ this.workerId = options.workerId || `step-worker-${hostname()}-${process.pid}-${getMicrosecondTimestamp5()}`;
350
+ this.backend = backend || new FileSystemBackend(options.baseDir || "./.runs");
351
+ this.options = {
352
+ workerId: this.workerId,
353
+ mode: options.mode ?? "unified",
354
+ concurrency: options.concurrency ?? 1,
355
+ pollInterval: options.pollInterval ?? 1000,
356
+ heartbeatInterval: options.heartbeatInterval ?? 5000,
357
+ staleThreshold: options.staleThreshold ?? 30000,
358
+ staleCheckInterval: options.staleCheckInterval ?? 1e4,
359
+ schedulerInterval: options.schedulerInterval ?? 5000,
360
+ shutdownTimeout: options.shutdownTimeout ?? 30000,
361
+ workflowsDir: options.workflowsDir || "./workflows",
362
+ baseDir: options.baseDir || "./.runs"
363
+ };
364
+ this.stats = {
365
+ workerId: this.workerId,
366
+ mode: this.options.mode,
367
+ startedAt: getMicrosecondTimestamp5(),
368
+ totalStepsProcessed: 0,
369
+ currentlyRunning: 0,
370
+ failedSteps: 0,
371
+ reclaimedSteps: 0
372
+ };
373
+ }
374
+ getStats() {
375
+ return { ...this.stats };
376
+ }
377
+ async start() {
378
+ if (this.running) {
379
+ throw new Error("Worker is already running");
380
+ }
381
+ this.running = true;
382
+ console.log(`[StepWorker ${this.workerId}] Starting in ${this.options.mode} mode...`);
383
+ this.setupSignalHandlers();
384
+ console.log(`[StepWorker ${this.workerId}] Loading workflows from ${this.options.workflowsDir}...`);
385
+ await this.loadWorkflows();
386
+ console.log(`[StepWorker ${this.workerId}] Registering workflows with backend...`);
387
+ await this.registerWorkflows();
388
+ const loops = [];
389
+ if (this.options.mode === "unified" || this.options.mode === "executor") {
390
+ loops.push(this.executorLoop());
391
+ console.log(`[StepWorker ${this.workerId}] Executor loop started (concurrency: ${this.options.concurrency})`);
392
+ }
393
+ if (this.options.mode === "unified" || this.options.mode === "scheduler") {
394
+ loops.push(this.schedulerLoop());
395
+ console.log(`[StepWorker ${this.workerId}] Scheduler loop started (interval: ${this.options.schedulerInterval}ms)`);
396
+ loops.push(this.reclamationLoop());
397
+ console.log(`[StepWorker ${this.workerId}] Reclamation loop started (threshold: ${this.options.staleThreshold}ms)`);
398
+ }
399
+ console.log(`[StepWorker ${this.workerId}] Started successfully`);
400
+ try {
401
+ await Promise.all(loops);
402
+ } finally {
403
+ clearInterval(this.heartbeatTimer);
404
+ console.log(`[StepWorker ${this.workerId}] Stopped`);
405
+ }
406
+ }
407
+ async stop() {
408
+ if (!this.running) {
409
+ return;
410
+ }
411
+ console.log(`[StepWorker ${this.workerId}] Stopping... (${this.activeSteps.size} active steps)`);
412
+ this.running = false;
413
+ const shutdownStart = Date.now();
414
+ while (this.activeSteps.size > 0 && Date.now() - shutdownStart < this.options.shutdownTimeout) {
415
+ await new Promise((resolve) => setTimeout(resolve, 100));
416
+ }
417
+ if (this.activeSteps.size > 0) {
418
+ console.warn(`[StepWorker ${this.workerId}] Forced shutdown with ${this.activeSteps.size} active steps`);
419
+ }
420
+ console.log(`[StepWorker ${this.workerId}] Stopped`);
421
+ }
422
+ async loadWorkflows() {
423
+ try {
424
+ const workflows = await discoverWorkflows(this.options.workflowsDir);
425
+ for (const workflow of workflows) {
426
+ const steps = await discoverSteps(workflow.stepsDir);
427
+ this.workflowCache.set(workflow.slug, {
428
+ metadata: workflow,
429
+ steps,
430
+ inputSchema: workflow.inputSchema
431
+ });
432
+ }
433
+ console.log(`[StepWorker ${this.workerId}] Loaded ${workflows.length} workflows`);
434
+ } catch (error) {
435
+ console.error(`[StepWorker ${this.workerId}] Failed to load workflows:`, error);
436
+ throw error;
437
+ }
438
+ }
439
+ async registerWorkflows() {
440
+ try {
441
+ for (const [slug, { metadata, steps, inputSchema }] of this.workflowCache) {
442
+ let inputSchemaJSON = undefined;
443
+ if (inputSchema) {
444
+ try {
445
+ inputSchemaJSON = convertZodToJSONSchema(inputSchema);
446
+ } catch (error) {
447
+ console.warn(`[StepWorker ${this.workerId}] Failed to convert schema for workflow ${slug}: ${error}`);
448
+ }
449
+ }
450
+ const stepDefinitions = steps.map((step) => ({
451
+ id: step.id,
452
+ name: step.name,
453
+ dependencies: Object.values(step.dependencies).map((dep) => dep.id),
454
+ exportOutput: step.exportOutput ?? false
455
+ }));
456
+ await this.backend.registerWorkflow({
457
+ slug,
458
+ name: metadata.name,
459
+ location: metadata.dir,
460
+ inputSchemaJSON,
461
+ steps: stepDefinitions
462
+ });
463
+ const versionId = await calculateWorkflowHash(metadata);
464
+ const git = await getGitInfo(metadata.dir);
465
+ const stepManifest = steps.map((s) => s.id);
466
+ await this.backend.createWorkflowVersion({
467
+ workflowSlug: slug,
468
+ versionId,
469
+ createdAt: getMicrosecondTimestamp5(),
470
+ stepManifest,
471
+ totalSteps: steps.length,
472
+ git
473
+ });
474
+ }
475
+ console.log(`[StepWorker ${this.workerId}] Registered ${this.workflowCache.size} workflows`);
476
+ } catch (error) {
477
+ console.error(`[StepWorker ${this.workerId}] Failed to register workflows:`, error);
478
+ throw error;
479
+ }
480
+ }
481
+ async executorLoop() {
482
+ while (this.running) {
483
+ try {
484
+ if (this.activeSteps.size >= this.options.concurrency) {
485
+ await new Promise((resolve) => setTimeout(resolve, this.options.pollInterval));
486
+ continue;
487
+ }
488
+ const limit = this.options.concurrency - this.activeSteps.size;
489
+ const scheduledSteps = await this.backend.listScheduledSteps({ limit });
490
+ if (scheduledSteps.length === 0) {
491
+ await new Promise((resolve) => setTimeout(resolve, this.options.pollInterval));
492
+ continue;
493
+ }
494
+ for (const { workflowSlug, runId, stepId } of scheduledSteps) {
495
+ if (this.activeSteps.size >= this.options.concurrency) {
496
+ break;
497
+ }
498
+ const isClaimable = await this.backend.isStepClaimable(workflowSlug, runId, stepId);
499
+ if (!isClaimable) {
500
+ continue;
501
+ }
502
+ this.executeStep(workflowSlug, runId, stepId).catch((err) => {
503
+ console.error(`[StepWorker ${this.workerId}] Error executing ${createStepKey(workflowSlug, runId, stepId)}:`, err);
504
+ });
505
+ }
506
+ } catch (error) {
507
+ console.error(`[StepWorker ${this.workerId}] Error in executor loop:`, error);
508
+ await new Promise((resolve) => setTimeout(resolve, this.options.pollInterval));
509
+ }
510
+ }
511
+ }
512
+ async schedulerLoop() {
513
+ while (this.running) {
514
+ try {
515
+ const activeWorkflows = await this.backend.listActiveWorkflows();
516
+ for (const workflowSlug of activeWorkflows) {
517
+ const workflowData = this.workflowCache.get(workflowSlug);
518
+ if (!workflowData) {
519
+ console.warn(`[StepWorker ${this.workerId}] Workflow ${workflowSlug} not in cache, skipping`);
520
+ continue;
521
+ }
522
+ let runIds = [];
523
+ try {
524
+ runIds = await this.backend.listRunIds(workflowSlug);
525
+ } catch {
526
+ continue;
527
+ }
528
+ for (const runId of runIds) {
529
+ try {
530
+ const workflowEvents = await loadWorkflowEvents(this.backend, workflowSlug, runId);
531
+ if (workflowEvents.length === 0)
532
+ continue;
533
+ const { hasCompleted, hasFailed, hasCancelled } = getWorkflowTerminalState(workflowEvents);
534
+ if (hasCompleted || hasFailed || hasCancelled) {
535
+ continue;
536
+ }
537
+ const hasSubmitted = hasWorkflowSubmitted(workflowEvents);
538
+ const hasStarted = hasWorkflowStarted(workflowEvents);
539
+ if (!hasSubmitted) {
540
+ continue;
541
+ }
542
+ if (!hasStarted) {
543
+ const runSubmittedEvent = getRunSubmittedEvent(workflowEvents);
544
+ if (!runSubmittedEvent) {
545
+ throw new Error(`No RunSubmitted event found for ${workflowSlug}/${runId}`);
546
+ }
547
+ await handleWorkflowStart(this.backend, workflowSlug, runId, {
548
+ versionId: runSubmittedEvent.versionId,
549
+ hasInputSchema: workflowData.inputSchema !== undefined,
550
+ hasInput: !!(runSubmittedEvent && runSubmittedEvent.input !== undefined)
551
+ }, workflowEvents);
552
+ console.log(`[StepWorker ${this.workerId}] Started workflow ${workflowSlug}/${runId}`);
553
+ }
554
+ const hasValidatedInput = hasWorkflowValidatedInput(workflowEvents);
555
+ if (!hasValidatedInput) {
556
+ const workflowInput = parseWorkflowInput(workflowEvents);
557
+ const validationResult = await validateWorkflowInput(this.backend, workflowSlug, runId, workflowInput, workflowData.inputSchema, workflowEvents);
558
+ if (!validationResult.success) {
559
+ console.log(`[StepWorker ${this.workerId}] Workflow ${workflowSlug}/${runId} failed (input validation)`);
560
+ continue;
561
+ }
562
+ }
563
+ let allStepsCompleted = true;
564
+ let anyStepFailed = false;
565
+ let failedStepName;
566
+ for (const step of workflowData.steps) {
567
+ const stepEvents = await this.backend.loadEvents(workflowSlug, runId, {
568
+ category: "step",
569
+ stepId: step.id
570
+ });
571
+ if (stepEvents.length === 0) {
572
+ allStepsCompleted = false;
573
+ const canSchedule = await checkDependenciesSatisfied(step, this.backend, workflowSlug, runId);
574
+ if (canSchedule) {
575
+ await scheduleStep(this.backend, workflowSlug, runId, step.id, Object.keys(step.dependencies).length === 0 ? "initial" : "dependency-satisfied", 1);
576
+ console.log(`[StepWorker ${this.workerId}] Scheduled ${step.name} in ${workflowSlug}/${runId}`);
577
+ }
578
+ } else {
579
+ const state = projectStepState6(stepEvents, workflowSlug);
580
+ if (state.status === "pending") {
581
+ allStepsCompleted = false;
582
+ const canSchedule = await checkDependenciesSatisfied(step, this.backend, workflowSlug, runId);
583
+ if (canSchedule) {
584
+ await scheduleStep(this.backend, workflowSlug, runId, step.id, "dependency-satisfied", 1);
585
+ console.log(`[StepWorker ${this.workerId}] Scheduled pending step ${step.name} in ${workflowSlug}/${runId}`);
586
+ }
587
+ } else if (state.status === "failed" && state.terminal) {
588
+ anyStepFailed = true;
589
+ failedStepName = step.name;
590
+ allStepsCompleted = false;
591
+ } else if (state.status !== "completed" && state.status !== "skipped") {
592
+ allStepsCompleted = false;
593
+ }
594
+ }
595
+ }
596
+ if (allStepsCompleted) {
597
+ await handleWorkflowCompletion(this.backend, workflowSlug, runId, workflowData.steps, workflowEvents);
598
+ console.log(`[StepWorker ${this.workerId}] Workflow ${workflowSlug}/${runId} completed`);
599
+ } else if (anyStepFailed) {
600
+ await handleWorkflowFailure(this.backend, workflowSlug, runId, workflowData.steps, workflowEvents, failedStepName);
601
+ console.log(`[StepWorker ${this.workerId}] Workflow ${workflowSlug}/${runId} failed (step: ${failedStepName})`);
602
+ }
603
+ } catch (error) {
604
+ console.error(`[StepWorker ${this.workerId}] Error processing run ${runId}:`, error);
605
+ }
606
+ }
607
+ }
608
+ await new Promise((resolve) => setTimeout(resolve, this.options.schedulerInterval));
609
+ } catch (error) {
610
+ console.error(`[StepWorker ${this.workerId}] Error in scheduler loop:`, error);
611
+ await new Promise((resolve) => setTimeout(resolve, this.options.schedulerInterval));
612
+ }
613
+ }
614
+ }
615
+ async reclamationLoop() {
616
+ while (this.running) {
617
+ try {
618
+ const reclaimed = await this.backend.reclaimStaleSteps(this.options.staleThreshold * 1000, this.workerId);
619
+ if (reclaimed.length > 0) {
620
+ console.log(`[StepWorker ${this.workerId}] Reclaimed ${reclaimed.length} stale steps`);
621
+ this.stats.reclaimedSteps += reclaimed.length;
622
+ }
623
+ await new Promise((resolve) => setTimeout(resolve, this.options.staleCheckInterval));
624
+ } catch (error) {
625
+ console.error(`[StepWorker ${this.workerId}] Error in reclamation loop:`, error);
626
+ await new Promise((resolve) => setTimeout(resolve, this.options.staleCheckInterval));
627
+ }
628
+ }
629
+ }
630
+ async executeStep(workflowSlug, runId, stepId) {
631
+ const stepKey = createStepKey(workflowSlug, runId, stepId);
632
+ let startTime = getMicrosecondTimestamp5();
633
+ let attemptNumber = null;
634
+ let heartbeatTimer = null;
635
+ try {
636
+ const workflowData = this.workflowCache.get(workflowSlug);
637
+ if (!workflowData) {
638
+ throw new Error(`Workflow ${workflowSlug} not found in cache`);
639
+ }
640
+ const step = workflowData.steps.find((s) => s.id === stepId);
641
+ if (!step) {
642
+ throw new Error(`Step ${stepId} not found in workflow ${workflowSlug}`);
643
+ }
644
+ const claimResult = await this.backend.claimScheduledStep(workflowSlug, runId, stepId, this.workerId, {
645
+ dependencies: Object.keys(step.dependencies),
646
+ timestamp: startTime,
647
+ attemptNumber: 1
648
+ });
649
+ if (!claimResult) {
650
+ console.log(`[StepWorker ${this.workerId}] Skipped ${stepKey} (already claimed)`);
651
+ return;
652
+ }
653
+ attemptNumber = claimResult.attemptNumber;
654
+ startTime = getMicrosecondTimestamp5();
655
+ heartbeatTimer = setInterval(() => {
656
+ if (attemptNumber === null)
657
+ return;
658
+ this.backend.saveStepHeartbeat(workflowSlug, runId, stepId, this.workerId, attemptNumber).catch((err) => {
659
+ console.error(`[StepWorker ${this.workerId}] Error sending heartbeat for ${stepKey}:`, err);
660
+ });
661
+ }, this.options.heartbeatInterval);
662
+ const trackedTimer = heartbeatTimer;
663
+ this.activeSteps.set(stepKey, {
664
+ workflowSlug,
665
+ runId,
666
+ stepId,
667
+ attemptNumber,
668
+ heartbeatTimer: trackedTimer
669
+ });
670
+ this.stats.currentlyRunning++;
671
+ console.log(`[StepWorker ${this.workerId}] Executing ${step.name} (attempt ${attemptNumber})`);
672
+ const dependencyOutputs = await loadDependencyOutputs(step, this.backend, workflowSlug, runId);
673
+ const workflowEvents = await loadWorkflowEvents(this.backend, workflowSlug, runId);
674
+ const runSubmittedEvent = getRunSubmittedEvent(workflowEvents);
675
+ let workflowInput = undefined;
676
+ if (runSubmittedEvent && runSubmittedEvent.input !== undefined) {
677
+ workflowInput = JSON.parse(runSubmittedEvent.input);
678
+ }
679
+ if (workflowData.inputSchema) {
680
+ const validationEvent = getWorkflowInputValidationEvent(workflowEvents);
681
+ if (validationEvent && validationEvent.success) {
682
+ const parseResult = workflowData.inputSchema.safeParse(workflowInput ?? {});
683
+ if (parseResult.success) {
684
+ workflowInput = parseResult.data;
685
+ }
686
+ }
687
+ }
688
+ const ctx = {
689
+ runId,
690
+ workflow: {
691
+ slug: workflowSlug,
692
+ name: workflowData.metadata.name
693
+ },
694
+ input: workflowInput,
695
+ log: (...args) => {
696
+ console.log(`[${step.name}]`, ...args);
697
+ }
698
+ };
699
+ const stepFile = join(step.dir, "step.ts");
700
+ const submissionTimeoutMs = runSubmittedEvent && runSubmittedEvent.timeoutUs ? Math.floor(runSubmittedEvent.timeoutUs / 1000) : undefined;
701
+ const timeout = step.timeoutMs ?? submissionTimeoutMs ?? 300000;
702
+ const abortController = new AbortController;
703
+ const executionPromise = executeStepInProcess(stepFile, step.id, dependencyOutputs, ctx, attemptNumber, this.backend, async (log) => {
704
+ const logEvent = {
705
+ workflowSlug,
706
+ runId,
707
+ category: "step",
708
+ type: "LogEntry",
709
+ stepId,
710
+ stream: log.stream,
711
+ message: log.message,
712
+ attemptNumber,
713
+ timestampUs: log.timestamp
714
+ };
715
+ await this.backend.appendEvent(workflowSlug, runId, logEvent);
716
+ }, {
717
+ signal: abortController.signal
718
+ });
719
+ const timeoutTimer = setTimeout(() => {
720
+ abortController.abort(new Error(`Step timeout after ${timeout}ms`));
721
+ }, timeout);
722
+ const { result: output } = await executionPromise.finally(() => {
723
+ clearTimeout(timeoutTimer);
724
+ });
725
+ const endTime = getMicrosecondTimestamp5();
726
+ const duration = endTime - startTime;
727
+ await this.backend.saveStepComplete(workflowSlug, runId, stepId, output, {
728
+ timestamp: endTime,
729
+ duration,
730
+ attemptNumber,
731
+ output
732
+ }, step.exportOutput ?? false);
733
+ console.log(`[StepWorker ${this.workerId}] Completed ${step.name}`);
734
+ if (heartbeatTimer) {
735
+ clearInterval(heartbeatTimer);
736
+ heartbeatTimer = null;
737
+ }
738
+ this.activeSteps.delete(stepKey);
739
+ this.stats.currentlyRunning--;
740
+ this.stats.totalStepsProcessed++;
741
+ if (this.options.mode === "unified") {
742
+ const workflowData2 = this.workflowCache.get(workflowSlug);
743
+ if (workflowData2) {
744
+ const scheduled = await scheduleDownstreamSteps(this.backend, workflowSlug, runId, stepId, workflowData2.steps);
745
+ if (scheduled.length > 0) {
746
+ console.log(`[StepWorker ${this.workerId}] Scheduled downstream steps: ${scheduled.join(", ")}`);
747
+ }
748
+ }
749
+ }
750
+ } catch (error) {
751
+ const failureContext = attemptNumber === null ? "Failed to claim" : "Failed";
752
+ console.error(`[StepWorker ${this.workerId}] ${failureContext} ${stepKey}:`, error);
753
+ const activeStep = this.activeSteps.get(stepKey);
754
+ if (activeStep) {
755
+ clearInterval(activeStep.heartbeatTimer);
756
+ this.activeSteps.delete(stepKey);
757
+ this.stats.currentlyRunning--;
758
+ } else if (heartbeatTimer) {
759
+ clearInterval(heartbeatTimer);
760
+ heartbeatTimer = null;
761
+ }
762
+ if (attemptNumber === null) {
763
+ return;
764
+ }
765
+ this.stats.failedSteps++;
766
+ const workflowData = this.workflowCache.get(workflowSlug);
767
+ if (!workflowData) {
768
+ console.error(`[StepWorker ${this.workerId}] Workflow ${workflowSlug} not found in cache`);
769
+ return;
770
+ }
771
+ const stepId2 = extractStepId(stepKey);
772
+ if (!stepId2) {
773
+ console.error(`[StepWorker ${this.workerId}] Invalid stepKey format: ${stepKey}`);
774
+ return;
775
+ }
776
+ const step = workflowData.steps.find((s) => s.id === stepId2);
777
+ if (!step) {
778
+ console.error(`[StepWorker ${this.workerId}] Step ${stepId2} not found in workflow ${workflowSlug}`);
779
+ return;
780
+ }
781
+ const events = await this.backend.loadEvents(workflowSlug, runId, { category: "step", stepId: stepId2 });
782
+ const state = projectStepState6(events, workflowSlug);
783
+ const currentAttemptNumber = state.attemptNumber;
784
+ const endTime = getMicrosecondTimestamp5();
785
+ const duration = endTime - (state.startTime || startTime);
786
+ const stepError = serializeError(error);
787
+ if (error instanceof Error && (error.name === "Skip" || error instanceof Skip)) {
788
+ const skipError = error;
789
+ await handleStepSkip(this.backend, workflowSlug, runId, stepId2, skipError, duration, currentAttemptNumber);
790
+ console.log(`[StepWorker ${this.workerId}] Skipped ${step.name}: ${skipError.reason || skipError.message}`);
791
+ if (this.options.mode === "unified") {
792
+ const workflowData2 = this.workflowCache.get(workflowSlug);
793
+ if (workflowData2) {
794
+ await cascadeSkip(this.backend, workflowSlug, runId, stepId2, workflowData2.steps, this.workerId);
795
+ }
796
+ }
797
+ return;
798
+ }
799
+ const maxRetries = step.maxRetries ?? 0;
800
+ if (currentAttemptNumber <= maxRetries) {
801
+ const retryDelayMs = step.retryDelayMs ?? 0;
802
+ const nextRetryAt = getMicrosecondTimestamp5() + retryDelayMs * MICROSECONDS_PER_MILLISECOND;
803
+ await this.backend.saveStepFailedAndScheduleRetry(workflowSlug, runId, stepId2, stepError, {
804
+ duration,
805
+ attemptNumber: currentAttemptNumber,
806
+ nextRetryAt,
807
+ failureReason: "execution-error"
808
+ }, {
809
+ availableAt: nextRetryAt,
810
+ nextAttemptNumber: currentAttemptNumber + 1,
811
+ retryDelayMs,
812
+ maxRetries
813
+ });
814
+ console.log(`[StepWorker ${this.workerId}] Scheduled retry for ${step.name} (attempt ${currentAttemptNumber + 1}/${maxRetries + 1})`);
815
+ } else {
816
+ await this.backend.saveStepFailed(workflowSlug, runId, stepId2, stepError, {
817
+ duration,
818
+ attemptNumber: currentAttemptNumber,
819
+ terminal: true,
820
+ failureReason: "exhausted-retries"
821
+ });
822
+ console.log(`[StepWorker ${this.workerId}] Terminal failure for ${step.name} (retries exhausted)`);
823
+ }
824
+ }
825
+ }
826
+ setupSignalHandlers() {
827
+ const shutdown = async (signal) => {
828
+ console.log(`[StepWorker ${this.workerId}] Received ${signal}, shutting down gracefully...`);
829
+ await this.stop();
830
+ process.exit(0);
831
+ };
832
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
833
+ process.on("SIGINT", () => shutdown("SIGINT"));
834
+ }
835
+ heartbeatTimer;
836
+ }
837
+ export {
838
+ StepWorker
839
+ };
840
+
841
+ //# debugId=94F7F5AE1B4A4B8564756E2164756E21