@cascade-flow/backend-filesystem 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,1381 @@
1
+ // src/index.ts
2
+ import { mkdir, writeFile, readFile, readdir, access, unlink, open as openFile } from "node:fs/promises";
3
+ import { join, dirname } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import {
6
+ Backend,
7
+ StepRecordSchema,
8
+ eventSchema,
9
+ safeSerialize,
10
+ projectStepRecord,
11
+ projectStepState,
12
+ projectRunStateFromEvents,
13
+ extractLogsFromEvents,
14
+ getCurrentAttemptNumber,
15
+ getMicrosecondTimestamp,
16
+ computeErrorAnalysis,
17
+ computeRetryAnalysis,
18
+ computeSchedulingLatency,
19
+ computeStepDuration,
20
+ computeWorkflowDuration,
21
+ computeErrorFingerprints,
22
+ computeWorkerStability,
23
+ computeThroughput,
24
+ computeSuccessRate
25
+ } from "@cascade-flow/backend-interface";
26
+ import { projectStepState as projectStepState2, projectRunStateFromEvents as projectRunStateFromEvents2, projectStepRecord as projectStepRecord2 } from "@cascade-flow/backend-interface";
27
+
28
+ class FileSystemBackend extends Backend {
29
+ baseDir;
30
+ constructor(baseDir = "./.runs") {
31
+ super();
32
+ this.baseDir = baseDir;
33
+ }
34
+ async initialize() {}
35
+ getRunDir(workflowSlug, runId) {
36
+ return join(this.baseDir, workflowSlug, runId);
37
+ }
38
+ getWorkflowEventsDir(workflowSlug, runId) {
39
+ return join(this.getRunDir(workflowSlug, runId), "workflow-events");
40
+ }
41
+ getStepEventsDir(workflowSlug, runId) {
42
+ return join(this.getRunDir(workflowSlug, runId), "step-events");
43
+ }
44
+ getStepOutputsDir(workflowSlug, runId) {
45
+ return join(this.getRunDir(workflowSlug, runId), "step-outputs");
46
+ }
47
+ getStepOutputPath(workflowSlug, runId, stepId, attemptNumber) {
48
+ return join(this.getStepOutputsDir(workflowSlug, runId), `${stepId}-attempt-${attemptNumber}.json`);
49
+ }
50
+ getStepsDir(workflowSlug, runId) {
51
+ return join(this.getRunDir(workflowSlug, runId), "steps");
52
+ }
53
+ getStepFile(workflowSlug, runId, stepName) {
54
+ return join(this.getStepsDir(workflowSlug, runId), `${stepName}.json`);
55
+ }
56
+ getStepLogsFile(workflowSlug, runId, stepName) {
57
+ return join(this.getStepsDir(workflowSlug, runId), `${stepName}.logs.json`);
58
+ }
59
+ getIdempotencyDir() {
60
+ return join(this.baseDir, ".idempotency");
61
+ }
62
+ generateRunId() {
63
+ return `run_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
64
+ }
65
+ hashIdempotencyKey(key) {
66
+ return createHash("sha256").update(key).digest("hex");
67
+ }
68
+ generateEventId(timestamp) {
69
+ const ts = timestamp ?? getMicrosecondTimestamp();
70
+ return `${ts}`;
71
+ }
72
+ getWorkflowEventFilename(event) {
73
+ return `${event.eventId}-${event.type}.json`;
74
+ }
75
+ getStepEventFilename(event) {
76
+ return `${event.eventId}-${event.stepId}-${event.type}.json`;
77
+ }
78
+ getStepClaimLockPath(workflowSlug, runId, stepId, attemptNumber) {
79
+ return join(this.getRunDir(workflowSlug, runId), "locks", `${stepId}-attempt-${attemptNumber}.lock`);
80
+ }
81
+ async writeJsonAtomic(filePath, data) {
82
+ const dir = dirname(filePath);
83
+ await mkdir(dir, { recursive: true });
84
+ const jsonString = JSON.stringify(data, null, 2);
85
+ await writeFile(filePath, jsonString, "utf-8");
86
+ }
87
+ async readStepRecord(workflowSlug, runId, stepName) {
88
+ try {
89
+ const filePath = this.getStepFile(workflowSlug, runId, stepName);
90
+ const content = await readFile(filePath, "utf-8");
91
+ const parsed = JSON.parse(content);
92
+ return StepRecordSchema.parse(parsed);
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ createStateChange(status, message) {
98
+ return {
99
+ status,
100
+ timestamp: getMicrosecondTimestamp(),
101
+ message
102
+ };
103
+ }
104
+ async initializeRun(workflowSlug, runId) {
105
+ const workflowEventsDir = this.getWorkflowEventsDir(workflowSlug, runId);
106
+ const stepEventsDir = this.getStepEventsDir(workflowSlug, runId);
107
+ await mkdir(workflowEventsDir, { recursive: true });
108
+ await mkdir(stepEventsDir, { recursive: true });
109
+ }
110
+ async appendEvent(workflowSlug, runId, event) {
111
+ if (!event.eventId) {
112
+ event.eventId = this.generateEventId();
113
+ }
114
+ if (!event.timestampUs) {
115
+ event.timestampUs = getMicrosecondTimestamp();
116
+ }
117
+ eventSchema.parse(event);
118
+ const eventsDir = event.category === "workflow" ? this.getWorkflowEventsDir(workflowSlug, runId) : this.getStepEventsDir(workflowSlug, runId);
119
+ const getFilename = (evt) => {
120
+ if (evt.category === "workflow") {
121
+ return this.getWorkflowEventFilename(evt);
122
+ } else {
123
+ return this.getStepEventFilename(evt);
124
+ }
125
+ };
126
+ await mkdir(eventsDir, { recursive: true });
127
+ let timestamp = parseInt(event.eventId);
128
+ let filename = getFilename(event);
129
+ let filePath = join(eventsDir, filename);
130
+ while (true) {
131
+ try {
132
+ await access(filePath);
133
+ timestamp++;
134
+ event.eventId = `${timestamp}`;
135
+ event.timestampUs = timestamp;
136
+ filename = getFilename(event);
137
+ filePath = join(eventsDir, filename);
138
+ } catch {
139
+ break;
140
+ }
141
+ }
142
+ await this.writeJsonAtomic(filePath, event);
143
+ }
144
+ async loadEvents(workflowSlug, runId, options) {
145
+ try {
146
+ const events = [];
147
+ const directories = [];
148
+ if (!options?.category || options.category === "workflow") {
149
+ directories.push({
150
+ dir: this.getWorkflowEventsDir(workflowSlug, runId),
151
+ category: "workflow"
152
+ });
153
+ }
154
+ if (!options?.category || options.category === "step") {
155
+ directories.push({
156
+ dir: this.getStepEventsDir(workflowSlug, runId),
157
+ category: "step"
158
+ });
159
+ }
160
+ for (const { dir, category } of directories) {
161
+ try {
162
+ const files = await readdir(dir);
163
+ const eventFiles = files.filter((f) => f.endsWith(".json"));
164
+ for (const file of eventFiles) {
165
+ const content = await readFile(join(dir, file), "utf-8");
166
+ const event = eventSchema.parse(JSON.parse(content));
167
+ if (category === "step" && options?.stepId) {
168
+ if (event.category === "step" && event.stepId === options.stepId) {
169
+ events.push(event);
170
+ }
171
+ } else {
172
+ events.push(event);
173
+ }
174
+ }
175
+ } catch (err) {
176
+ continue;
177
+ }
178
+ }
179
+ events.sort((a, b) => {
180
+ if (a.timestampUs !== b.timestampUs) {
181
+ return a.timestampUs - b.timestampUs;
182
+ }
183
+ return a.eventId.localeCompare(b.eventId);
184
+ });
185
+ return events;
186
+ } catch {
187
+ return [];
188
+ }
189
+ }
190
+ async saveStepStart(workflowSlug, runId, stepId, workerId, metadata) {
191
+ const events = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
192
+ const attemptNumber = getCurrentAttemptNumber(events) + 1;
193
+ const timestamp = getMicrosecondTimestamp();
194
+ const event = {
195
+ category: "step",
196
+ eventId: this.generateEventId(timestamp),
197
+ timestampUs: timestamp,
198
+ workflowSlug,
199
+ runId,
200
+ stepId,
201
+ type: "StepStarted",
202
+ workerId,
203
+ dependencies: metadata.dependencies,
204
+ attemptNumber
205
+ };
206
+ await this.appendEvent(workflowSlug, runId, event);
207
+ }
208
+ async saveStepComplete(workflowSlug, runId, stepId, output, metadata, exportOutput = false) {
209
+ const events = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
210
+ const attemptNumber = getCurrentAttemptNumber(events);
211
+ if (attemptNumber === 0) {
212
+ throw new Error(`Cannot complete step that hasn't started: ${stepId}`);
213
+ }
214
+ if (metadata.logs && metadata.logs.length > 0) {
215
+ for (const log of metadata.logs) {
216
+ const logTimestamp = log.timestamp;
217
+ const logEvent = {
218
+ category: "step",
219
+ eventId: this.generateEventId(logTimestamp),
220
+ timestampUs: logTimestamp,
221
+ workflowSlug,
222
+ runId,
223
+ stepId,
224
+ type: "LogEntry",
225
+ stream: log.stream,
226
+ message: log.message,
227
+ attemptNumber
228
+ };
229
+ await this.appendEvent(workflowSlug, runId, logEvent);
230
+ }
231
+ }
232
+ const serialized = safeSerialize(output);
233
+ const outputString = serialized.success ? serialized.data : serialized.fallback;
234
+ const timestamp = getMicrosecondTimestamp();
235
+ const event = {
236
+ category: "step",
237
+ eventId: this.generateEventId(timestamp),
238
+ timestampUs: timestamp,
239
+ workflowSlug,
240
+ runId,
241
+ stepId,
242
+ type: "StepCompleted",
243
+ output: outputString,
244
+ durationUs: metadata.duration,
245
+ attemptNumber,
246
+ exportOutput
247
+ };
248
+ await this.appendEvent(workflowSlug, runId, event);
249
+ }
250
+ async saveStepFailed(workflowSlug, runId, stepId, error, metadata) {
251
+ const now = getMicrosecondTimestamp();
252
+ const event = {
253
+ category: "step",
254
+ eventId: this.generateEventId(now),
255
+ timestampUs: now,
256
+ workflowSlug,
257
+ runId,
258
+ stepId,
259
+ type: "StepFailed",
260
+ error,
261
+ errorFingerprints: computeErrorFingerprints(error, error.stack),
262
+ durationUs: metadata.duration,
263
+ attemptNumber: metadata.attemptNumber,
264
+ terminal: metadata.terminal,
265
+ nextRetryAtUs: metadata.nextRetryAt,
266
+ failureReason: metadata.failureReason
267
+ };
268
+ await this.appendEvent(workflowSlug, runId, event);
269
+ }
270
+ async saveStepFailedAndScheduleRetry(workflowSlug, runId, stepId, error, failureMetadata, scheduleMetadata) {
271
+ const failedTimestamp = getMicrosecondTimestamp();
272
+ const retryingTimestamp = failedTimestamp + 1;
273
+ const scheduledTimestamp = failedTimestamp + 2;
274
+ const failedEvent = {
275
+ category: "step",
276
+ type: "StepFailed",
277
+ eventId: this.generateEventId(failedTimestamp),
278
+ timestampUs: failedTimestamp,
279
+ workflowSlug,
280
+ runId,
281
+ stepId,
282
+ error,
283
+ errorFingerprints: computeErrorFingerprints(error, error.stack),
284
+ durationUs: failureMetadata.duration,
285
+ attemptNumber: failureMetadata.attemptNumber,
286
+ terminal: false,
287
+ nextRetryAtUs: failureMetadata.nextRetryAt,
288
+ failureReason: failureMetadata.failureReason
289
+ };
290
+ await this.appendEvent(workflowSlug, runId, failedEvent);
291
+ const retryingEvent = {
292
+ category: "step",
293
+ type: "StepRetrying",
294
+ eventId: this.generateEventId(retryingTimestamp),
295
+ timestampUs: retryingTimestamp,
296
+ workflowSlug,
297
+ runId,
298
+ stepId,
299
+ attemptNumber: failureMetadata.attemptNumber,
300
+ nextAttempt: scheduleMetadata.nextAttemptNumber,
301
+ maxRetries: scheduleMetadata.maxRetries,
302
+ error
303
+ };
304
+ await this.appendEvent(workflowSlug, runId, retryingEvent);
305
+ const scheduledEvent = {
306
+ category: "step",
307
+ type: "StepScheduled",
308
+ eventId: this.generateEventId(scheduledTimestamp),
309
+ timestampUs: scheduledTimestamp,
310
+ workflowSlug,
311
+ runId,
312
+ stepId,
313
+ availableAtUs: scheduleMetadata.availableAt,
314
+ reason: "retry",
315
+ attemptNumber: scheduleMetadata.nextAttemptNumber,
316
+ retryDelayMs: scheduleMetadata.retryDelayMs
317
+ };
318
+ await this.appendEvent(workflowSlug, runId, scheduledEvent);
319
+ }
320
+ async saveStepSkipped(workflowSlug, runId, stepId, metadata) {
321
+ const now = getMicrosecondTimestamp();
322
+ const event = {
323
+ category: "step",
324
+ eventId: this.generateEventId(now),
325
+ timestampUs: now,
326
+ workflowSlug,
327
+ runId,
328
+ stepId,
329
+ type: "StepSkipped",
330
+ skipType: metadata.skipType,
331
+ reason: metadata.reason,
332
+ metadata: metadata.metadata,
333
+ durationUs: metadata.duration,
334
+ attemptNumber: metadata.attemptNumber,
335
+ cascadedFrom: metadata.cascadedFrom
336
+ };
337
+ await this.appendEvent(workflowSlug, runId, event);
338
+ }
339
+ async saveStepScheduled(workflowSlug, runId, stepId, metadata) {
340
+ const now = getMicrosecondTimestamp();
341
+ const event = {
342
+ category: "step",
343
+ eventId: this.generateEventId(now),
344
+ timestampUs: now,
345
+ workflowSlug,
346
+ runId,
347
+ stepId,
348
+ type: "StepScheduled",
349
+ availableAtUs: metadata.availableAt,
350
+ reason: metadata.reason,
351
+ attemptNumber: metadata.attemptNumber,
352
+ retryDelayMs: metadata.retryDelayMs
353
+ };
354
+ await this.appendEvent(workflowSlug, runId, event);
355
+ }
356
+ async saveStepHeartbeat(workflowSlug, runId, stepId, workerId, attemptNumber) {
357
+ const now = getMicrosecondTimestamp();
358
+ const event = {
359
+ category: "step",
360
+ eventId: this.generateEventId(now),
361
+ timestampUs: now,
362
+ workflowSlug,
363
+ runId,
364
+ stepId,
365
+ type: "StepHeartbeat",
366
+ workerId,
367
+ attemptNumber
368
+ };
369
+ await this.appendEvent(workflowSlug, runId, event);
370
+ }
371
+ async saveStepReclaimed(workflowSlug, runId, stepId, metadata) {
372
+ const now = getMicrosecondTimestamp();
373
+ const event = {
374
+ category: "step",
375
+ eventId: this.generateEventId(now),
376
+ timestampUs: now,
377
+ workflowSlug,
378
+ runId,
379
+ stepId,
380
+ type: "StepReclaimed",
381
+ originalWorkerId: metadata.originalWorkerId,
382
+ reclaimedBy: metadata.reclaimedBy,
383
+ lastHeartbeatUs: metadata.lastHeartbeat,
384
+ staleThresholdUs: metadata.staleThreshold,
385
+ staleDurationUs: metadata.staleDuration,
386
+ attemptNumber: metadata.attemptNumber
387
+ };
388
+ await this.appendEvent(workflowSlug, runId, event);
389
+ }
390
+ async saveWorkflowComplete(workflowSlug, runId, output, metadata) {
391
+ const serialized = safeSerialize(output);
392
+ const outputString = serialized.success ? serialized.data : serialized.fallback;
393
+ const timestamp = getMicrosecondTimestamp();
394
+ const event = {
395
+ category: "workflow",
396
+ eventId: this.generateEventId(timestamp),
397
+ timestampUs: timestamp,
398
+ workflowSlug,
399
+ runId,
400
+ type: "WorkflowCompleted",
401
+ workflowAttemptNumber: metadata.workflowAttemptNumber,
402
+ output: outputString,
403
+ durationUs: metadata.duration,
404
+ totalSteps: metadata.totalSteps
405
+ };
406
+ await this.appendEvent(workflowSlug, runId, event);
407
+ }
408
+ async saveStepLogs(workflowSlug, runId, stepId, logs) {}
409
+ async loadStepLogs(workflowSlug, runId, stepId, attemptNumber) {
410
+ const events = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
411
+ let logs = extractLogsFromEvents(events);
412
+ if (attemptNumber !== undefined) {
413
+ logs = logs.filter((log) => log.attemptNumber === attemptNumber);
414
+ }
415
+ return logs.length > 0 ? logs : null;
416
+ }
417
+ async loadRun(workflowSlug, runId) {
418
+ try {
419
+ const events = await this.loadEvents(workflowSlug, runId, { category: "step" });
420
+ if (events.length === 0) {
421
+ return [];
422
+ }
423
+ const eventsByStep = new Map;
424
+ for (const event of events) {
425
+ if (event.category === "step") {
426
+ if (!eventsByStep.has(event.stepId)) {
427
+ eventsByStep.set(event.stepId, []);
428
+ }
429
+ eventsByStep.get(event.stepId).push(event);
430
+ }
431
+ }
432
+ const records = [];
433
+ for (const [stepId, stepEvents] of eventsByStep) {
434
+ const record = projectStepRecord(stepEvents);
435
+ records.push(record);
436
+ }
437
+ return records;
438
+ } catch {
439
+ return [];
440
+ }
441
+ }
442
+ async runExists(workflowSlug, runId) {
443
+ try {
444
+ const runDir = this.getRunDir(workflowSlug, runId);
445
+ await access(runDir);
446
+ return true;
447
+ } catch {
448
+ return false;
449
+ }
450
+ }
451
+ async saveWorkflowStart(workflowSlug, runId, metadata) {
452
+ const timestamp = getMicrosecondTimestamp();
453
+ const event = {
454
+ category: "workflow",
455
+ eventId: this.generateEventId(timestamp),
456
+ timestampUs: timestamp,
457
+ workflowSlug,
458
+ runId,
459
+ type: "WorkflowStarted",
460
+ workflowAttemptNumber: metadata.workflowAttemptNumber,
461
+ hasInputSchema: metadata.hasInputSchema,
462
+ hasInput: metadata.hasInput
463
+ };
464
+ await this.appendEvent(workflowSlug, runId, event);
465
+ }
466
+ async saveWorkflowInputValidation(workflowSlug, runId, result) {
467
+ const timestamp = getMicrosecondTimestamp();
468
+ const event = {
469
+ category: "workflow",
470
+ eventId: this.generateEventId(timestamp),
471
+ timestampUs: timestamp,
472
+ workflowSlug,
473
+ runId,
474
+ type: "WorkflowInputValidation",
475
+ ...result
476
+ };
477
+ await this.appendEvent(workflowSlug, runId, event);
478
+ }
479
+ async saveWorkflowFailed(workflowSlug, runId, error, metadata, failureReason) {
480
+ const timestamp = getMicrosecondTimestamp();
481
+ const event = {
482
+ category: "workflow",
483
+ eventId: this.generateEventId(timestamp),
484
+ timestampUs: timestamp,
485
+ workflowSlug,
486
+ runId,
487
+ type: "WorkflowFailed",
488
+ workflowAttemptNumber: metadata.workflowAttemptNumber,
489
+ error,
490
+ durationUs: metadata.duration,
491
+ completedSteps: metadata.completedSteps,
492
+ failedStep: metadata.failedStep,
493
+ failureReason
494
+ };
495
+ await this.appendEvent(workflowSlug, runId, event);
496
+ }
497
+ async saveWorkflowResumed(workflowSlug, runId, metadata) {
498
+ const timestamp = getMicrosecondTimestamp();
499
+ const event = {
500
+ category: "workflow",
501
+ eventId: this.generateEventId(timestamp),
502
+ timestampUs: timestamp,
503
+ workflowSlug,
504
+ runId,
505
+ type: "WorkflowResumed",
506
+ originalRunId: metadata.originalRunId,
507
+ resumedSteps: metadata.resumedSteps,
508
+ pendingSteps: metadata.pendingSteps
509
+ };
510
+ await this.appendEvent(workflowSlug, runId, event);
511
+ }
512
+ async saveWorkflowCancelled(workflowSlug, runId, metadata) {
513
+ const timestamp = getMicrosecondTimestamp();
514
+ const event = {
515
+ category: "workflow",
516
+ eventId: this.generateEventId(timestamp),
517
+ timestampUs: timestamp,
518
+ workflowSlug,
519
+ runId,
520
+ type: "WorkflowCancelled",
521
+ workflowAttemptNumber: metadata.workflowAttemptNumber,
522
+ reason: metadata.reason,
523
+ durationUs: metadata.duration,
524
+ completedSteps: metadata.completedSteps
525
+ };
526
+ await this.appendEvent(workflowSlug, runId, event);
527
+ }
528
+ async saveWorkflowRetryStarted(workflowSlug, runId, metadata) {
529
+ const timestamp = getMicrosecondTimestamp();
530
+ const event = {
531
+ category: "workflow",
532
+ eventId: this.generateEventId(timestamp),
533
+ timestampUs: timestamp,
534
+ workflowSlug,
535
+ runId,
536
+ type: "WorkflowRetryStarted",
537
+ workflowAttemptNumber: metadata.workflowAttemptNumber,
538
+ previousAttemptNumber: metadata.previousAttemptNumber,
539
+ retriedSteps: metadata.retriedSteps,
540
+ reason: metadata.reason
541
+ };
542
+ await this.appendEvent(workflowSlug, runId, event);
543
+ }
544
+ async getFailedSteps(workflowSlug, runId) {
545
+ const events = await this.loadEvents(workflowSlug, runId, {
546
+ category: "step"
547
+ });
548
+ if (events.length === 0 || !("category" in events[0])) {
549
+ return [];
550
+ }
551
+ const eventsByStep = new Map;
552
+ for (const event of events) {
553
+ if (event.category === "step") {
554
+ const stepEvents = eventsByStep.get(event.stepId) || [];
555
+ stepEvents.push(event);
556
+ eventsByStep.set(event.stepId, stepEvents);
557
+ }
558
+ }
559
+ const failedSteps = [];
560
+ for (const [stepId, stepEvents] of eventsByStep) {
561
+ const state = projectStepState(stepEvents, workflowSlug);
562
+ if (state.status === "failed" && state.terminal && state.error) {
563
+ failedSteps.push({
564
+ stepId,
565
+ error: state.error,
566
+ attemptNumber: state.attemptNumber
567
+ });
568
+ }
569
+ }
570
+ return failedSteps;
571
+ }
572
+ async saveRunSubmitted(workflowSlug, runId, metadata) {
573
+ const timestamp = getMicrosecondTimestamp();
574
+ const event = {
575
+ category: "workflow",
576
+ eventId: this.generateEventId(timestamp),
577
+ timestampUs: timestamp,
578
+ workflowSlug,
579
+ runId,
580
+ type: "RunSubmitted",
581
+ availableAtUs: metadata.availableAt,
582
+ priority: metadata.priority,
583
+ input: metadata.input,
584
+ hasInputSchema: metadata.hasInputSchema,
585
+ timeoutUs: metadata.timeout,
586
+ idempotencyKey: metadata.idempotencyKey,
587
+ metadata: metadata.metadata,
588
+ tags: metadata.tags
589
+ };
590
+ await this.appendEvent(workflowSlug, runId, event);
591
+ }
592
+ async submitRun(submission) {
593
+ if (submission.idempotencyKey) {
594
+ const hash = this.hashIdempotencyKey(submission.idempotencyKey);
595
+ const idempotencyFile = join(this.getIdempotencyDir(), `${hash}.json`);
596
+ try {
597
+ const content = await readFile(idempotencyFile, "utf-8");
598
+ const existing = JSON.parse(content);
599
+ return { runId: existing.runId, isNew: false };
600
+ } catch {}
601
+ }
602
+ const runId = submission.runId || this.generateRunId();
603
+ const now = getMicrosecondTimestamp();
604
+ const availableAt = submission.availableAt || now;
605
+ const priority = submission.priority || 0;
606
+ await this.initializeRun(submission.workflowSlug, runId);
607
+ await this.saveRunSubmitted(submission.workflowSlug, runId, {
608
+ availableAt,
609
+ priority,
610
+ input: submission.input !== undefined ? JSON.stringify(submission.input) : undefined,
611
+ hasInputSchema: false,
612
+ timeout: submission.timeout,
613
+ idempotencyKey: submission.idempotencyKey,
614
+ metadata: submission.metadata,
615
+ tags: submission.tags
616
+ });
617
+ if (submission.idempotencyKey) {
618
+ const hash = this.hashIdempotencyKey(submission.idempotencyKey);
619
+ const idempotencyDir = this.getIdempotencyDir();
620
+ await mkdir(idempotencyDir, { recursive: true });
621
+ const idempotencyFile = join(idempotencyDir, `${hash}.json`);
622
+ await this.writeJsonAtomic(idempotencyFile, { runId, createdAt: now });
623
+ }
624
+ return { runId, isNew: true };
625
+ }
626
+ async listRuns(options) {
627
+ try {
628
+ const allRuns = [];
629
+ const workflows = await readdir(this.baseDir);
630
+ for (const workflowSlug of workflows) {
631
+ if (workflowSlug.startsWith("."))
632
+ continue;
633
+ if (options?.workflowSlug && workflowSlug !== options.workflowSlug)
634
+ continue;
635
+ const workflowDir = join(this.baseDir, workflowSlug);
636
+ const runDirs = await readdir(workflowDir);
637
+ for (const runId of runDirs) {
638
+ try {
639
+ const events = await this.loadEvents(workflowSlug, runId, { category: "workflow" });
640
+ if (events.length === 0)
641
+ continue;
642
+ const state = projectRunStateFromEvents(events, workflowSlug);
643
+ if (options?.status && options.status.length > 0) {
644
+ if (!options.status.includes(state.status))
645
+ continue;
646
+ }
647
+ if (options?.tags && options.tags.length > 0) {
648
+ const stateTags = state.tags || [];
649
+ const hasAllTags = options.tags.every((tag) => stateTags.includes(tag));
650
+ if (!hasAllTags)
651
+ continue;
652
+ }
653
+ allRuns.push(state);
654
+ } catch {
655
+ continue;
656
+ }
657
+ }
658
+ }
659
+ allRuns.sort((a, b) => b.createdAt - a.createdAt);
660
+ return options?.limit ? allRuns.slice(0, options.limit) : allRuns;
661
+ } catch {
662
+ return [];
663
+ }
664
+ }
665
+ async cancelRun(runId, reason) {
666
+ const workflows = await readdir(this.baseDir);
667
+ for (const workflow of workflows) {
668
+ if (workflow.startsWith("."))
669
+ continue;
670
+ const runDir = this.getRunDir(workflow, runId);
671
+ try {
672
+ await access(runDir);
673
+ } catch {
674
+ continue;
675
+ }
676
+ const events = await this.loadEvents(workflow, runId, { category: "workflow" });
677
+ if (events.length === 0)
678
+ continue;
679
+ const state = projectRunStateFromEvents(events, workflow);
680
+ const duration = getMicrosecondTimestamp() - state.createdAt;
681
+ const stepRecords = await this.loadRun(workflow, runId);
682
+ const completedSteps = stepRecords.filter((r) => r.status === "completed").length;
683
+ await this.saveWorkflowCancelled(workflow, runId, {
684
+ workflowAttemptNumber: state.workflowAttemptNumber || 1,
685
+ reason,
686
+ duration,
687
+ completedSteps
688
+ });
689
+ return;
690
+ }
691
+ }
692
+ async getRun(runId) {
693
+ try {
694
+ const workflows = await readdir(this.baseDir);
695
+ for (const workflowSlug of workflows) {
696
+ if (workflowSlug.startsWith("."))
697
+ continue;
698
+ const runDir = this.getRunDir(workflowSlug, runId);
699
+ try {
700
+ await access(runDir);
701
+ } catch {
702
+ continue;
703
+ }
704
+ const events = await this.loadEvents(workflowSlug, runId, { category: "workflow" });
705
+ if (events.length === 0)
706
+ return null;
707
+ return projectRunStateFromEvents(events, workflowSlug);
708
+ }
709
+ return null;
710
+ } catch {
711
+ return null;
712
+ }
713
+ }
714
+ async listActiveWorkflows() {
715
+ const activeWorkflows = [];
716
+ try {
717
+ const workflows = await readdir(this.baseDir);
718
+ for (const workflowSlug of workflows) {
719
+ if (workflowSlug.startsWith("."))
720
+ continue;
721
+ const workflowDir = join(this.baseDir, workflowSlug);
722
+ const runDirs = await readdir(workflowDir);
723
+ for (const runId of runDirs) {
724
+ try {
725
+ const workflowEvents = await this.loadEvents(workflowSlug, runId, { category: "workflow" });
726
+ if (workflowEvents.length === 0)
727
+ continue;
728
+ const runState = projectRunStateFromEvents(workflowEvents, workflowSlug);
729
+ if (runState.status === "pending" || runState.status === "claimed" || runState.status === "running") {
730
+ activeWorkflows.push(workflowSlug);
731
+ break;
732
+ }
733
+ } catch {
734
+ continue;
735
+ }
736
+ }
737
+ }
738
+ return Array.from(new Set(activeWorkflows));
739
+ } catch {
740
+ return [];
741
+ }
742
+ }
743
+ async listScheduledSteps(options) {
744
+ const now = getMicrosecondTimestamp();
745
+ const availableBefore = options?.availableBefore || now;
746
+ const scheduledSteps = [];
747
+ try {
748
+ const workflows = await readdir(this.baseDir);
749
+ for (const workflowSlug of workflows) {
750
+ if (workflowSlug.startsWith("."))
751
+ continue;
752
+ if (options?.workflowSlug && workflowSlug !== options.workflowSlug)
753
+ continue;
754
+ const workflowDir = join(this.baseDir, workflowSlug);
755
+ const runDirs = await readdir(workflowDir);
756
+ for (const runId of runDirs) {
757
+ try {
758
+ const stepEventsDir = this.getStepEventsDir(workflowSlug, runId);
759
+ try {
760
+ await access(stepEventsDir);
761
+ } catch {
762
+ continue;
763
+ }
764
+ const eventFiles = await readdir(stepEventsDir);
765
+ const stepIds = new Set;
766
+ for (const file of eventFiles) {
767
+ const parts = file.replace(".json", "").split("-");
768
+ if (parts.length >= 3) {
769
+ const stepId = parts.slice(1, -1).join("-");
770
+ stepIds.add(stepId);
771
+ }
772
+ }
773
+ for (const stepId of stepIds) {
774
+ const events = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
775
+ if (events.length === 0)
776
+ continue;
777
+ const state = projectStepState(events, workflowSlug);
778
+ if (state.status === "scheduled" && state.availableAt && state.availableAt <= availableBefore) {
779
+ scheduledSteps.push({
780
+ workflowSlug,
781
+ runId,
782
+ stepId,
783
+ availableAt: state.availableAt
784
+ });
785
+ }
786
+ }
787
+ } catch {
788
+ continue;
789
+ }
790
+ }
791
+ }
792
+ scheduledSteps.sort((a, b) => a.availableAt - b.availableAt);
793
+ const limited = options?.limit ? scheduledSteps.slice(0, options.limit) : scheduledSteps;
794
+ return limited.map(({ workflowSlug, runId, stepId }) => ({ workflowSlug, runId, stepId }));
795
+ } catch {
796
+ return [];
797
+ }
798
+ }
799
+ async isStepClaimable(workflowSlug, runId, stepId) {
800
+ try {
801
+ const events = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
802
+ if (events.length === 0)
803
+ return false;
804
+ const state = projectStepState(events, workflowSlug);
805
+ const now = getMicrosecondTimestamp();
806
+ return state.status === "scheduled" && state.availableAt !== undefined && state.availableAt <= now;
807
+ } catch {
808
+ return false;
809
+ }
810
+ }
811
+ async claimScheduledStep(workflowSlug, runId, stepId, workerId, metadata) {
812
+ const initialEvents = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
813
+ if (initialEvents.length === 0) {
814
+ return null;
815
+ }
816
+ const now = getMicrosecondTimestamp();
817
+ const initialState = projectStepState(initialEvents, workflowSlug);
818
+ if (initialState.status !== "scheduled" || initialState.availableAt === undefined || initialState.availableAt > now) {
819
+ return null;
820
+ }
821
+ const attemptNumber = initialState.attemptNumber;
822
+ const lockPath = this.getStepClaimLockPath(workflowSlug, runId, stepId, attemptNumber);
823
+ await mkdir(dirname(lockPath), { recursive: true });
824
+ let lockHandle = null;
825
+ try {
826
+ lockHandle = await openFile(lockPath, "wx");
827
+ } catch (error) {
828
+ if (error.code === "EEXIST") {
829
+ return null;
830
+ }
831
+ throw error;
832
+ }
833
+ try {
834
+ const currentEvents = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
835
+ if (currentEvents.length === 0) {
836
+ return null;
837
+ }
838
+ const currentState = projectStepState(currentEvents, workflowSlug);
839
+ const claimable = currentState.status === "scheduled" && currentState.availableAt !== undefined && currentState.availableAt <= getMicrosecondTimestamp() && currentState.attemptNumber === attemptNumber;
840
+ if (!claimable) {
841
+ return null;
842
+ }
843
+ const timestamp = getMicrosecondTimestamp();
844
+ const event = {
845
+ category: "step",
846
+ eventId: this.generateEventId(timestamp),
847
+ timestampUs: timestamp,
848
+ workflowSlug,
849
+ runId,
850
+ stepId,
851
+ type: "StepStarted",
852
+ workerId,
853
+ dependencies: metadata.dependencies,
854
+ attemptNumber
855
+ };
856
+ await this.appendEvent(workflowSlug, runId, event);
857
+ return { attemptNumber };
858
+ } finally {
859
+ if (lockHandle) {
860
+ try {
861
+ await lockHandle.close();
862
+ } catch {}
863
+ try {
864
+ await unlink(lockPath);
865
+ } catch (error) {
866
+ if (error.code !== "ENOENT") {
867
+ throw error;
868
+ }
869
+ }
870
+ }
871
+ }
872
+ }
873
+ async reclaimStaleSteps(staleThreshold, reclaimedBy) {
874
+ const reclaimed = [];
875
+ const now = getMicrosecondTimestamp();
876
+ try {
877
+ const workflows = await readdir(this.baseDir);
878
+ for (const workflowSlug of workflows) {
879
+ if (workflowSlug.startsWith("."))
880
+ continue;
881
+ const workflowDir = join(this.baseDir, workflowSlug);
882
+ const runDirs = await readdir(workflowDir);
883
+ for (const runId of runDirs) {
884
+ try {
885
+ const stepEventsDir = this.getStepEventsDir(workflowSlug, runId);
886
+ try {
887
+ await access(stepEventsDir);
888
+ } catch {
889
+ continue;
890
+ }
891
+ const eventFiles = await readdir(stepEventsDir);
892
+ const stepIds = new Set;
893
+ for (const file of eventFiles) {
894
+ const parts = file.replace(".json", "").split("-");
895
+ if (parts.length >= 3) {
896
+ const stepId = parts.slice(1, -1).join("-");
897
+ stepIds.add(stepId);
898
+ }
899
+ }
900
+ for (const stepId of stepIds) {
901
+ const events = await this.loadEvents(workflowSlug, runId, { category: "step", stepId });
902
+ if (events.length === 0)
903
+ continue;
904
+ const state = projectStepState(events, workflowSlug);
905
+ if (state.status !== "running")
906
+ continue;
907
+ const lastHeartbeat = state.lastHeartbeat || state.startTime || 0;
908
+ const staleDuration = now - lastHeartbeat;
909
+ if (staleDuration > staleThreshold) {
910
+ await this.saveStepReclaimed(workflowSlug, runId, stepId, {
911
+ originalWorkerId: state.claimedBy || "unknown",
912
+ reclaimedBy,
913
+ lastHeartbeat,
914
+ staleThreshold,
915
+ staleDuration,
916
+ attemptNumber: state.attemptNumber
917
+ });
918
+ await this.saveStepScheduled(workflowSlug, runId, stepId, {
919
+ availableAt: now,
920
+ reason: "retry",
921
+ attemptNumber: state.attemptNumber,
922
+ retryDelayMs: 0
923
+ });
924
+ reclaimed.push({ workflowSlug, runId, stepId });
925
+ }
926
+ }
927
+ } catch {
928
+ continue;
929
+ }
930
+ }
931
+ }
932
+ return reclaimed;
933
+ } catch {
934
+ return reclaimed;
935
+ }
936
+ }
937
+ getRegistryDir() {
938
+ return join(this.baseDir, ".registry");
939
+ }
940
+ getWorkflowRegistryDir(slug) {
941
+ return join(this.getRegistryDir(), slug);
942
+ }
943
+ async registerWorkflow(registration) {
944
+ const registryDir = this.getWorkflowRegistryDir(registration.slug);
945
+ try {
946
+ await mkdir(registryDir, { recursive: true });
947
+ const metadataPath = join(registryDir, "metadata.json");
948
+ const metadata = {
949
+ slug: registration.slug,
950
+ name: registration.name,
951
+ location: registration.location,
952
+ inputSchemaJSON: registration.inputSchemaJSON
953
+ };
954
+ await writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
955
+ const stepsPath = join(registryDir, "steps.json");
956
+ await writeFile(stepsPath, JSON.stringify(registration.steps, null, 2), "utf-8");
957
+ } catch (error) {
958
+ console.error(`Failed to register workflow ${registration.slug}:`, error);
959
+ throw new Error(`Failed to register workflow: ${error instanceof Error ? error.message : "Unknown error"}`);
960
+ }
961
+ }
962
+ async getWorkflowMetadata(slug) {
963
+ const metadataPath = join(this.getWorkflowRegistryDir(slug), "metadata.json");
964
+ try {
965
+ const content = await readFile(metadataPath, "utf-8");
966
+ return JSON.parse(content);
967
+ } catch (error) {
968
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
969
+ return null;
970
+ }
971
+ console.error(`Failed to get workflow metadata for ${slug}:`, error);
972
+ throw new Error(`Failed to load workflow metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
973
+ }
974
+ }
975
+ async listWorkflowMetadata() {
976
+ const registryDir = this.getRegistryDir();
977
+ try {
978
+ try {
979
+ await access(registryDir);
980
+ } catch {
981
+ return [];
982
+ }
983
+ const entries = await readdir(registryDir, { withFileTypes: true });
984
+ const workflows = [];
985
+ for (const entry of entries) {
986
+ if (entry.isDirectory()) {
987
+ const metadata = await this.getWorkflowMetadata(entry.name);
988
+ if (metadata) {
989
+ workflows.push(metadata);
990
+ }
991
+ }
992
+ }
993
+ return workflows;
994
+ } catch (error) {
995
+ console.error("Failed to list workflows:", error);
996
+ throw new Error(`Failed to list workflows: ${error instanceof Error ? error.message : "Unknown error"}`);
997
+ }
998
+ }
999
+ async getWorkflowSteps(slug) {
1000
+ const stepsPath = join(this.getWorkflowRegistryDir(slug), "steps.json");
1001
+ try {
1002
+ const content = await readFile(stepsPath, "utf-8");
1003
+ return JSON.parse(content);
1004
+ } catch (error) {
1005
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
1006
+ return [];
1007
+ }
1008
+ console.error(`Failed to get workflow steps for ${slug}:`, error);
1009
+ throw new Error(`Failed to load workflow steps: ${error instanceof Error ? error.message : "Unknown error"}`);
1010
+ }
1011
+ }
1012
+ async listRunIds(workflowSlug) {
1013
+ const workflowDir = join(this.baseDir, workflowSlug);
1014
+ try {
1015
+ try {
1016
+ await access(workflowDir);
1017
+ } catch {
1018
+ return [];
1019
+ }
1020
+ const entries = await readdir(workflowDir, { withFileTypes: true });
1021
+ const runIds = [];
1022
+ for (const entry of entries) {
1023
+ if (entry.isDirectory() && entry.name !== ".registry") {
1024
+ runIds.push(entry.name);
1025
+ }
1026
+ }
1027
+ return runIds;
1028
+ } catch (error) {
1029
+ console.error(`Failed to list run IDs for workflow ${workflowSlug}:`, error);
1030
+ throw new Error(`Failed to list run IDs: ${error instanceof Error ? error.message : "Unknown error"}`);
1031
+ }
1032
+ }
1033
+ async close() {}
1034
+ async loadEventsForAnalytics(options) {
1035
+ const now = getMicrosecondTimestamp();
1036
+ const startUs = options?.startUs ?? now - 24 * 60 * 60 * 1000 * 1000;
1037
+ const endUs = options?.endUs ?? now;
1038
+ const allStepEvents = [];
1039
+ const allWorkflowEvents = [];
1040
+ const workflowsToScan = [];
1041
+ if (options?.workflowSlug) {
1042
+ workflowsToScan.push(options.workflowSlug);
1043
+ } else {
1044
+ try {
1045
+ const entries = await readdir(this.baseDir, { withFileTypes: true });
1046
+ for (const entry of entries) {
1047
+ if (entry.isDirectory() && entry.name !== ".registry") {
1048
+ workflowsToScan.push(entry.name);
1049
+ }
1050
+ }
1051
+ } catch (error) {
1052
+ return { stepEvents: [], workflowEvents: [] };
1053
+ }
1054
+ }
1055
+ for (const workflowSlug of workflowsToScan) {
1056
+ const runIds = options?.runIds ?? await this.listRunIds(workflowSlug);
1057
+ for (const runId of runIds) {
1058
+ try {
1059
+ const stepEvents = await this.loadEvents(workflowSlug, runId, {
1060
+ category: "step",
1061
+ stepId: options?.stepId
1062
+ });
1063
+ const filteredStepEvents = stepEvents.filter((e) => e.timestampUs >= startUs && e.timestampUs <= endUs);
1064
+ allStepEvents.push(...filteredStepEvents);
1065
+ if (!options?.stepId) {
1066
+ const workflowEvents = await this.loadEvents(workflowSlug, runId, {
1067
+ category: "workflow"
1068
+ });
1069
+ const filteredWorkflowEvents = workflowEvents.filter((e) => e.timestampUs >= startUs && e.timestampUs <= endUs);
1070
+ allWorkflowEvents.push(...filteredWorkflowEvents);
1071
+ }
1072
+ } catch (error) {
1073
+ continue;
1074
+ }
1075
+ }
1076
+ }
1077
+ return { stepEvents: allStepEvents, workflowEvents: allWorkflowEvents };
1078
+ }
1079
+ async getErrorAnalysis(options) {
1080
+ const { stepEvents } = await this.loadEventsForAnalytics(options);
1081
+ return computeErrorAnalysis(stepEvents, options?.workflowSlug, options?.stepId);
1082
+ }
1083
+ async getErrorsList(options) {
1084
+ const { stepEvents } = await this.loadEventsForAnalytics({
1085
+ startUs: options?.timeRange?.start,
1086
+ endUs: options?.timeRange?.end,
1087
+ workflowSlug: options?.workflowSlug
1088
+ });
1089
+ const strategy = options?.groupingStrategy || "exact";
1090
+ const limit = options?.limit || 50;
1091
+ const offset = options?.offset || 0;
1092
+ const failedEvents = stepEvents.filter((e) => e.type === "StepFailed");
1093
+ const errorGroups = new Map;
1094
+ for (const event of failedEvents) {
1095
+ const stackHash = strategy === "exact" ? event.errorFingerprints.stackExactHash : strategy === "normalized" ? event.errorFingerprints.stackNormalizedHash : event.errorFingerprints.stackPortableHash;
1096
+ const fingerprint = `${event.errorFingerprints.nameHash}:${event.errorFingerprints.messageHash}:${stackHash}`;
1097
+ const existing = errorGroups.get(fingerprint);
1098
+ if (existing) {
1099
+ existing.count++;
1100
+ existing.affectedRuns.add(event.runId);
1101
+ existing.firstSeen = Math.min(existing.firstSeen, event.timestampUs);
1102
+ existing.lastSeen = Math.max(existing.lastSeen, event.timestampUs);
1103
+ } else {
1104
+ errorGroups.set(fingerprint, {
1105
+ errorMessage: event.error.message,
1106
+ errorName: event.error.name || "Error",
1107
+ sampleStack: event.error.stack || "",
1108
+ count: 1,
1109
+ affectedRuns: new Set([event.runId]),
1110
+ firstSeen: event.timestampUs,
1111
+ lastSeen: event.timestampUs
1112
+ });
1113
+ }
1114
+ }
1115
+ const allErrors = Array.from(errorGroups.entries()).map(([fingerprint, data]) => ({
1116
+ fingerprint,
1117
+ errorMessage: data.errorMessage,
1118
+ errorName: data.errorName,
1119
+ sampleStack: data.sampleStack,
1120
+ count: data.count,
1121
+ affectedRuns: data.affectedRuns.size,
1122
+ firstSeen: data.firstSeen,
1123
+ lastSeen: data.lastSeen
1124
+ }));
1125
+ allErrors.sort((a, b) => b.count - a.count);
1126
+ const total = allErrors.length;
1127
+ const errors = allErrors.slice(offset, offset + limit);
1128
+ return { errors, total };
1129
+ }
1130
+ async getErrorDetail(fingerprint, groupingStrategy, options) {
1131
+ const parts = fingerprint.split(":");
1132
+ if (parts.length !== 3) {
1133
+ throw new Error(`Invalid fingerprint format: ${fingerprint}`);
1134
+ }
1135
+ const [nameHash, messageHash, stackHash] = parts;
1136
+ const { stepEvents } = await this.loadEventsForAnalytics({
1137
+ startUs: options?.timeRange?.start,
1138
+ endUs: options?.timeRange?.end
1139
+ });
1140
+ const limit = options?.limit || 100;
1141
+ const offset = options?.offset || 0;
1142
+ const matchingEvents = stepEvents.filter((e) => {
1143
+ if (e.type !== "StepFailed")
1144
+ return false;
1145
+ if (e.errorFingerprints.nameHash !== nameHash)
1146
+ return false;
1147
+ if (e.errorFingerprints.messageHash !== messageHash)
1148
+ return false;
1149
+ const eventStackHash = groupingStrategy === "exact" ? e.errorFingerprints.stackExactHash : groupingStrategy === "normalized" ? e.errorFingerprints.stackNormalizedHash : e.errorFingerprints.stackPortableHash;
1150
+ return eventStackHash === stackHash;
1151
+ });
1152
+ if (matchingEvents.length === 0) {
1153
+ return {
1154
+ fingerprint,
1155
+ errorMessage: "",
1156
+ errorName: "",
1157
+ sampleStack: "",
1158
+ totalCount: 0,
1159
+ affectedRuns: 0,
1160
+ firstSeen: 0,
1161
+ lastSeen: 0,
1162
+ occurrences: [],
1163
+ total: 0
1164
+ };
1165
+ }
1166
+ const affectedRunsSet = new Set(matchingEvents.map((e) => e.runId));
1167
+ const firstSeen = Math.min(...matchingEvents.map((e) => e.timestampUs));
1168
+ const lastSeen = Math.max(...matchingEvents.map((e) => e.timestampUs));
1169
+ matchingEvents.sort((a, b) => b.timestampUs - a.timestampUs);
1170
+ const paginatedEvents = matchingEvents.slice(offset, offset + limit);
1171
+ const occurrences = paginatedEvents.map((e) => ({
1172
+ workflowSlug: e.workflowSlug,
1173
+ runId: e.runId,
1174
+ stepId: e.stepId,
1175
+ attemptNumber: e.attemptNumber,
1176
+ timestampUs: e.timestampUs
1177
+ }));
1178
+ const sampleEvent = matchingEvents[0];
1179
+ return {
1180
+ fingerprint,
1181
+ errorMessage: sampleEvent.error.message,
1182
+ errorName: sampleEvent.error.name || "Error",
1183
+ sampleStack: sampleEvent.error.stack || "",
1184
+ totalCount: matchingEvents.length,
1185
+ affectedRuns: affectedRunsSet.size,
1186
+ firstSeen,
1187
+ lastSeen,
1188
+ occurrences,
1189
+ total: matchingEvents.length
1190
+ };
1191
+ }
1192
+ async getRetryAnalysis(options) {
1193
+ const { stepEvents } = await this.loadEventsForAnalytics(options);
1194
+ return computeRetryAnalysis(stepEvents);
1195
+ }
1196
+ async getSchedulingLatency(options) {
1197
+ const { stepEvents } = await this.loadEventsForAnalytics(options);
1198
+ return computeSchedulingLatency(stepEvents, options?.workflowSlug, options?.stepId);
1199
+ }
1200
+ async getStepDuration(options) {
1201
+ const { stepEvents } = await this.loadEventsForAnalytics(options);
1202
+ return computeStepDuration(stepEvents, options?.workflowSlug, options?.stepId);
1203
+ }
1204
+ async getWorkflowDuration(options) {
1205
+ const { workflowEvents } = await this.loadEventsForAnalytics(options);
1206
+ return computeWorkflowDuration(workflowEvents, options?.workflowSlug);
1207
+ }
1208
+ async getWorkerStability(options) {
1209
+ const { stepEvents } = await this.loadEventsForAnalytics(options);
1210
+ return computeWorkerStability(stepEvents);
1211
+ }
1212
+ async getThroughput(options) {
1213
+ const { stepEvents, workflowEvents } = await this.loadEventsForAnalytics(options);
1214
+ const now = getMicrosecondTimestamp();
1215
+ const startUs = options?.startUs ?? now - 24 * 60 * 60 * 1000 * 1000;
1216
+ const endUs = options?.endUs ?? now;
1217
+ const timeRangeUs = endUs - startUs;
1218
+ return computeThroughput(stepEvents, workflowEvents, timeRangeUs, options?.workflowSlug);
1219
+ }
1220
+ async getQueueDepth(options) {
1221
+ const runs = await this.listRuns({
1222
+ workflowSlug: options?.workflowSlug,
1223
+ status: ["pending", "running"]
1224
+ });
1225
+ let pendingRuns = 0;
1226
+ let runningRuns = 0;
1227
+ let scheduledSteps = 0;
1228
+ let runningSteps = 0;
1229
+ let oldestScheduledStepUs;
1230
+ let oldestPendingRunUs;
1231
+ for (const run of runs) {
1232
+ if (run.status === "pending") {
1233
+ pendingRuns++;
1234
+ if (!oldestPendingRunUs || run.createdAt < oldestPendingRunUs) {
1235
+ oldestPendingRunUs = run.createdAt;
1236
+ }
1237
+ } else if (run.status === "running") {
1238
+ runningRuns++;
1239
+ try {
1240
+ const stepEvents = await this.loadEvents(run.workflowSlug, run.runId, { category: "step" });
1241
+ const eventsByStep = new Map;
1242
+ for (const event of stepEvents) {
1243
+ const events = eventsByStep.get(event.stepId) || [];
1244
+ events.push(event);
1245
+ eventsByStep.set(event.stepId, events);
1246
+ }
1247
+ for (const [stepId, events] of eventsByStep.entries()) {
1248
+ const state = projectStepState(events, run.workflowSlug);
1249
+ if (state.status === "scheduled") {
1250
+ scheduledSteps++;
1251
+ if (state.availableAt && (!oldestScheduledStepUs || state.availableAt < oldestScheduledStepUs)) {
1252
+ oldestScheduledStepUs = state.availableAt;
1253
+ }
1254
+ } else if (state.status === "running") {
1255
+ runningSteps++;
1256
+ }
1257
+ }
1258
+ } catch (error) {
1259
+ continue;
1260
+ }
1261
+ }
1262
+ }
1263
+ return {
1264
+ workflowSlug: options?.workflowSlug,
1265
+ pendingRuns,
1266
+ runningRuns,
1267
+ scheduledSteps,
1268
+ runningSteps,
1269
+ oldestScheduledStepUs,
1270
+ oldestPendingRunUs
1271
+ };
1272
+ }
1273
+ async getQueueDepthByWorkflow() {
1274
+ const runs = await this.listRuns({ status: ["pending", "running"] });
1275
+ const workflowMap = new Map;
1276
+ for (const run of runs) {
1277
+ const existing = workflowMap.get(run.workflowSlug) || {
1278
+ pendingRuns: 0,
1279
+ scheduledSteps: 0
1280
+ };
1281
+ if (run.status === "pending") {
1282
+ existing.pendingRuns++;
1283
+ if (!existing.oldestPendingItemUs || run.createdAt < existing.oldestPendingItemUs) {
1284
+ existing.oldestPendingItemUs = run.createdAt;
1285
+ }
1286
+ } else if (run.status === "running") {
1287
+ try {
1288
+ const stepEvents = await this.loadEvents(run.workflowSlug, run.runId, {
1289
+ category: "step"
1290
+ });
1291
+ const eventsByStep = new Map;
1292
+ for (const event of stepEvents) {
1293
+ const events = eventsByStep.get(event.stepId) || [];
1294
+ events.push(event);
1295
+ eventsByStep.set(event.stepId, events);
1296
+ }
1297
+ for (const [stepId, events] of eventsByStep.entries()) {
1298
+ const state = projectStepState(events, run.workflowSlug);
1299
+ if (state.status === "scheduled") {
1300
+ existing.scheduledSteps++;
1301
+ if (state.availableAt && (!existing.oldestPendingItemUs || state.availableAt < existing.oldestPendingItemUs)) {
1302
+ existing.oldestPendingItemUs = state.availableAt;
1303
+ }
1304
+ }
1305
+ }
1306
+ } catch (error) {
1307
+ continue;
1308
+ }
1309
+ }
1310
+ workflowMap.set(run.workflowSlug, existing);
1311
+ }
1312
+ const allMetadata = await this.listWorkflowMetadata();
1313
+ const metadataMap = new Map(allMetadata.map((m) => [m.slug, m]));
1314
+ const result = Array.from(workflowMap.entries()).map(([workflowSlug, data]) => ({
1315
+ workflowSlug,
1316
+ workflowName: metadataMap.get(workflowSlug)?.name,
1317
+ pendingRuns: data.pendingRuns,
1318
+ scheduledSteps: data.scheduledSteps,
1319
+ oldestPendingItemUs: data.oldestPendingItemUs
1320
+ })).filter((item) => item.pendingRuns > 0 || item.scheduledSteps > 0).sort((a, b) => {
1321
+ const aTotal = a.pendingRuns + a.scheduledSteps;
1322
+ const bTotal = b.pendingRuns + b.scheduledSteps;
1323
+ return bTotal - aTotal;
1324
+ });
1325
+ return result;
1326
+ }
1327
+ async getSuccessRate(options) {
1328
+ const { stepEvents, workflowEvents } = await this.loadEventsForAnalytics(options);
1329
+ return computeSuccessRate(stepEvents, workflowEvents, options?.workflowSlug, options?.stepId);
1330
+ }
1331
+ async getAnalyticsSummary(options) {
1332
+ const now = getMicrosecondTimestamp();
1333
+ const startUs = options?.startUs ?? now - 24 * 60 * 60 * 1000 * 1000;
1334
+ const endUs = options?.endUs ?? now;
1335
+ const [
1336
+ errorAnalysis,
1337
+ retryAnalysis,
1338
+ schedulingLatency,
1339
+ stepDuration,
1340
+ workflowDuration,
1341
+ workerStability,
1342
+ throughput,
1343
+ queueDepth,
1344
+ successRate
1345
+ ] = await Promise.all([
1346
+ this.getErrorAnalysis(options),
1347
+ this.getRetryAnalysis(options),
1348
+ this.getSchedulingLatency(options),
1349
+ this.getStepDuration(options),
1350
+ this.getWorkflowDuration(options),
1351
+ this.getWorkerStability(options),
1352
+ this.getThroughput(options),
1353
+ this.getQueueDepth(options),
1354
+ this.getSuccessRate(options)
1355
+ ]);
1356
+ return {
1357
+ timeRange: {
1358
+ startUs,
1359
+ endUs,
1360
+ durationUs: endUs - startUs
1361
+ },
1362
+ errorAnalysis,
1363
+ retryAnalysis,
1364
+ schedulingLatency,
1365
+ stepDuration,
1366
+ workflowDuration,
1367
+ workerStability,
1368
+ throughput,
1369
+ queueDepth,
1370
+ successRate
1371
+ };
1372
+ }
1373
+ }
1374
+ export {
1375
+ projectStepState2 as projectStepState,
1376
+ projectStepRecord2 as projectStepRecord,
1377
+ projectRunStateFromEvents2 as projectRunStateFromEvents,
1378
+ FileSystemBackend
1379
+ };
1380
+
1381
+ //# debugId=0253EE6D5A19700564756E2164756E21