@multiplier-labs/stepflow 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.github/workflows/publish.yml +30 -0
  2. package/README.md +408 -0
  3. package/dist/chunk-FQCT3I2A.js +1512 -0
  4. package/dist/chunk-FQCT3I2A.js.map +1 -0
  5. package/dist/chunk-GXY66V7Q.js +111 -0
  6. package/dist/chunk-GXY66V7Q.js.map +1 -0
  7. package/dist/chunk-U5QGN3RT.js +715 -0
  8. package/dist/chunk-U5QGN3RT.js.map +1 -0
  9. package/dist/chunk-XCF4PLN7.js +575 -0
  10. package/dist/chunk-XCF4PLN7.js.map +1 -0
  11. package/dist/events/index.d.ts +346 -0
  12. package/dist/events/index.js +11 -0
  13. package/dist/events/index.js.map +1 -0
  14. package/dist/index-CPfGsmrp.d.ts +567 -0
  15. package/dist/index.d.ts +748 -0
  16. package/dist/index.js +1707 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/scheduler/index.d.ts +6 -0
  19. package/dist/scheduler/index.js +12 -0
  20. package/dist/scheduler/index.js.map +1 -0
  21. package/dist/storage/index.d.ts +402 -0
  22. package/dist/storage/index.js +13 -0
  23. package/dist/storage/index.js.map +1 -0
  24. package/dist/types-BVShq7zT.d.ts +150 -0
  25. package/dist/types-BlwFEGzj.d.ts +276 -0
  26. package/dist/types-DzkEqnQJ.d.ts +75 -0
  27. package/docs/github-packages-setup.md +97 -0
  28. package/docs/stepflow-api-reference.md +1662 -0
  29. package/package.json +85 -0
  30. package/src/core/engine.test.ts +1421 -0
  31. package/src/core/engine.ts +717 -0
  32. package/src/core/index.ts +7 -0
  33. package/src/core/orchestrator.ts +590 -0
  34. package/src/core/types.ts +242 -0
  35. package/src/events/index.ts +8 -0
  36. package/src/events/memory.test.ts +109 -0
  37. package/src/events/memory.ts +95 -0
  38. package/src/events/socketio.test.ts +428 -0
  39. package/src/events/socketio.ts +269 -0
  40. package/src/events/types.ts +121 -0
  41. package/src/events/webhook.test.ts +695 -0
  42. package/src/events/webhook.ts +591 -0
  43. package/src/index.ts +270 -0
  44. package/src/planning/index.ts +52 -0
  45. package/src/planning/planner.ts +601 -0
  46. package/src/planning/planning.test.ts +790 -0
  47. package/src/planning/registry.test.ts +445 -0
  48. package/src/planning/registry.ts +369 -0
  49. package/src/planning/types.ts +570 -0
  50. package/src/scheduler/cron.test.ts +480 -0
  51. package/src/scheduler/cron.ts +449 -0
  52. package/src/scheduler/index.ts +18 -0
  53. package/src/scheduler/postgres-persistence.test.ts +906 -0
  54. package/src/scheduler/postgres-persistence.ts +546 -0
  55. package/src/scheduler/sqlite-persistence.test.ts +235 -0
  56. package/src/scheduler/sqlite-persistence.ts +268 -0
  57. package/src/scheduler/types.ts +78 -0
  58. package/src/storage/index.ts +27 -0
  59. package/src/storage/memory.test.ts +399 -0
  60. package/src/storage/memory.ts +248 -0
  61. package/src/storage/postgres.ts +1550 -0
  62. package/src/storage/sqlite.test.ts +593 -0
  63. package/src/storage/sqlite.ts +662 -0
  64. package/src/storage/types.ts +368 -0
  65. package/src/utils/errors.test.ts +152 -0
  66. package/src/utils/errors.ts +191 -0
  67. package/src/utils/id.ts +21 -0
  68. package/src/utils/index.ts +8 -0
  69. package/src/utils/logger.test.ts +127 -0
  70. package/src/utils/logger.ts +96 -0
  71. package/src/utils/postgres-deps.ts +63 -0
  72. package/src/utils/retry.test.ts +197 -0
  73. package/src/utils/retry.ts +121 -0
  74. package/tsconfig.json +21 -0
  75. package/tsup.config.ts +14 -0
  76. package/vitest.config.ts +9 -0
@@ -0,0 +1,662 @@
1
+ /**
2
+ * SQLite storage adapter using better-sqlite3.
3
+ *
4
+ * Provides durable persistence for workflow runs, steps, and events.
5
+ */
6
+
7
+ import type Database from "better-sqlite3";
8
+ import { generateId } from "../utils/id";
9
+ import type {
10
+ StorageAdapter,
11
+ WorkflowRunRecord,
12
+ WorkflowRunStepRecord,
13
+ WorkflowEventRecord,
14
+ ListRunsOptions,
15
+ ListEventsOptions,
16
+ PaginatedResult,
17
+ } from "./types";
18
+ import type { RunStatus, StepStatus, WorkflowError } from "../core/types";
19
+ import { sanitizeErrorForStorage } from "../utils/logger";
20
+
21
+ // ============================================================================
22
+ // Schema SQL
23
+ // ============================================================================
24
+
25
+ /**
26
+ * SQL statements to create the workflow tables.
27
+ * These create the workflow tables in your database.
28
+ */
29
+ const CREATE_TABLES_SQL = `
30
+ -- Workflow runs table
31
+ CREATE TABLE IF NOT EXISTS workflow_runs (
32
+ id TEXT PRIMARY KEY,
33
+ kind TEXT NOT NULL,
34
+ status TEXT NOT NULL,
35
+ parent_run_id TEXT,
36
+ input_json TEXT NOT NULL,
37
+ metadata_json TEXT NOT NULL,
38
+ context_json TEXT NOT NULL,
39
+ error_json TEXT,
40
+ created_at TEXT NOT NULL,
41
+ started_at TEXT,
42
+ finished_at TEXT
43
+ );
44
+
45
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_kind_status ON workflow_runs(kind, status);
46
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_parent ON workflow_runs(parent_run_id);
47
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_created ON workflow_runs(created_at);
48
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
49
+
50
+ -- Workflow run steps table
51
+ CREATE TABLE IF NOT EXISTS workflow_run_steps (
52
+ id TEXT PRIMARY KEY,
53
+ run_id TEXT NOT NULL,
54
+ step_key TEXT NOT NULL,
55
+ step_name TEXT,
56
+ status TEXT NOT NULL,
57
+ attempt INTEGER NOT NULL DEFAULT 1,
58
+ result_json TEXT,
59
+ error_json TEXT,
60
+ started_at TEXT,
61
+ finished_at TEXT,
62
+ FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run ON workflow_run_steps(run_id);
66
+ CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run_key ON workflow_run_steps(run_id, step_key);
67
+
68
+ -- Workflow events table
69
+ CREATE TABLE IF NOT EXISTS workflow_events (
70
+ id TEXT PRIMARY KEY,
71
+ run_id TEXT NOT NULL,
72
+ step_key TEXT,
73
+ event_type TEXT NOT NULL,
74
+ level TEXT NOT NULL,
75
+ payload_json TEXT,
76
+ timestamp TEXT NOT NULL,
77
+ FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
78
+ );
79
+
80
+ CREATE INDEX IF NOT EXISTS idx_workflow_events_run ON workflow_events(run_id);
81
+ CREATE INDEX IF NOT EXISTS idx_workflow_events_run_ts ON workflow_events(run_id, timestamp);
82
+ `;
83
+
84
+ // ============================================================================
85
+ // Configuration
86
+ // ============================================================================
87
+
88
+ /**
89
+ * Configuration options for the SQLite storage adapter.
90
+ */
91
+ export interface SQLiteStorageConfig {
92
+ /**
93
+ * The better-sqlite3 database instance.
94
+ * Can be an in-memory database (`:memory:`) or a file path.
95
+ */
96
+ db: Database.Database;
97
+
98
+ /**
99
+ * Whether to automatically create tables on initialization.
100
+ * Default: true
101
+ */
102
+ autoCreateTables?: boolean;
103
+
104
+ /**
105
+ * @deprecated This option is not currently supported and will be ignored.
106
+ * Table names are always prefixed with 'workflow_'.
107
+ */
108
+ tablePrefix?: string;
109
+ }
110
+
111
+ // ============================================================================
112
+ // SQLite Storage Adapter
113
+ // ============================================================================
114
+
115
+ /**
116
+ * SQLite implementation of StorageAdapter using better-sqlite3.
117
+ *
118
+ * Features:
119
+ * - Synchronous operations (better-sqlite3 is sync by design)
120
+ * - Automatic table creation
121
+ * - Transaction support
122
+ * - Customizable table prefix
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * import Database from 'better-sqlite3';
127
+ * import { SQLiteStorageAdapter } from 'stepflow/storage';
128
+ *
129
+ * const db = new Database('./workflows.db');
130
+ * const storage = new SQLiteStorageAdapter({ db });
131
+ *
132
+ * const engine = new WorkflowEngine({ storage });
133
+ * ```
134
+ */
135
+ export class SQLiteStorageAdapter implements StorageAdapter {
136
+ private db: Database.Database;
137
+ private prefix: string;
138
+
139
+ // Prepared statements (cached for performance)
140
+ private stmts: {
141
+ insertRun: Database.Statement;
142
+ getRun: Database.Statement;
143
+ updateRun: Database.Statement;
144
+ listRuns: Database.Statement;
145
+ countRuns: Database.Statement;
146
+ insertStep: Database.Statement;
147
+ getStep: Database.Statement;
148
+ updateStep: Database.Statement;
149
+ getStepsForRun: Database.Statement;
150
+ insertEvent: Database.Statement;
151
+ getEventsForRun: Database.Statement;
152
+ deleteOldRuns: Database.Statement;
153
+ deleteStepsForRuns: Database.Statement;
154
+ deleteEventsForRuns: Database.Statement;
155
+ getInterruptedRuns: Database.Statement;
156
+ getLastCompletedStep: Database.Statement;
157
+ } | null = null;
158
+
159
+ constructor(config: SQLiteStorageConfig) {
160
+ this.db = config.db;
161
+ this.prefix = config.tablePrefix ?? "workflow";
162
+
163
+ // Enable foreign keys
164
+ this.db.pragma("foreign_keys = ON");
165
+
166
+ // Create tables if needed
167
+ if (config.autoCreateTables !== false) {
168
+ this.createTables();
169
+ }
170
+
171
+ // Prepare statements
172
+ this.prepareStatements();
173
+ }
174
+
175
+ /**
176
+ * Create the workflow tables if they don't exist.
177
+ */
178
+ private createTables(): void {
179
+ this.db.exec(CREATE_TABLES_SQL);
180
+ }
181
+
182
+ /**
183
+ * Prepare all SQL statements for better performance.
184
+ */
185
+ private prepareStatements(): void {
186
+ this.stmts = {
187
+ insertRun: this.db.prepare(`
188
+ INSERT INTO workflow_runs (id, kind, status, parent_run_id, input_json, metadata_json, context_json, error_json, created_at, started_at, finished_at)
189
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
190
+ `),
191
+ getRun: this.db.prepare(`
192
+ SELECT * FROM workflow_runs WHERE id = ?
193
+ `),
194
+ updateRun: this.db.prepare(`
195
+ UPDATE workflow_runs
196
+ SET status = COALESCE(?, status),
197
+ context_json = COALESCE(?, context_json),
198
+ error_json = COALESCE(?, error_json),
199
+ started_at = COALESCE(?, started_at),
200
+ finished_at = COALESCE(?, finished_at)
201
+ WHERE id = ?
202
+ `),
203
+ // json_each() is a SQLite table-valued function that expands a JSON array into rows.
204
+ // We use it here to support filtering by multiple statuses in a single prepared statement:
205
+ // the caller passes a JSON array like '["running","failed"]', and json_each unpacks it
206
+ // so the IN clause can match against each value. When status is NULL, the entire
207
+ // condition is bypassed via the (? IS NULL OR ...) guard.
208
+ listRuns: this.db.prepare(`
209
+ SELECT * FROM workflow_runs
210
+ WHERE (? IS NULL OR kind = ?)
211
+ AND (? IS NULL OR status IN (SELECT value FROM json_each(?)))
212
+ AND (? IS NULL OR parent_run_id = ?)
213
+ ORDER BY created_at DESC
214
+ LIMIT ? OFFSET ?
215
+ `),
216
+ countRuns: this.db.prepare(`
217
+ SELECT COUNT(*) as count FROM workflow_runs
218
+ WHERE (? IS NULL OR kind = ?)
219
+ AND (? IS NULL OR status IN (SELECT value FROM json_each(?)))
220
+ AND (? IS NULL OR parent_run_id = ?)
221
+ `),
222
+ insertStep: this.db.prepare(`
223
+ INSERT INTO workflow_run_steps (id, run_id, step_key, step_name, status, attempt, result_json, error_json, started_at, finished_at)
224
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
225
+ `),
226
+ getStep: this.db.prepare(`
227
+ SELECT * FROM workflow_run_steps WHERE id = ?
228
+ `),
229
+ updateStep: this.db.prepare(`
230
+ UPDATE workflow_run_steps
231
+ SET status = COALESCE(?, status),
232
+ attempt = COALESCE(?, attempt),
233
+ result_json = COALESCE(?, result_json),
234
+ error_json = COALESCE(?, error_json),
235
+ finished_at = COALESCE(?, finished_at)
236
+ WHERE id = ?
237
+ `),
238
+ getStepsForRun: this.db.prepare(`
239
+ SELECT * FROM workflow_run_steps WHERE run_id = ? ORDER BY started_at ASC
240
+ `),
241
+ insertEvent: this.db.prepare(`
242
+ INSERT INTO workflow_events (id, run_id, step_key, event_type, level, payload_json, timestamp)
243
+ VALUES (?, ?, ?, ?, ?, ?, ?)
244
+ `),
245
+ getEventsForRun: this.db.prepare(`
246
+ SELECT * FROM workflow_events
247
+ WHERE run_id = ?
248
+ AND (? IS NULL OR step_key = ?)
249
+ AND (? IS NULL OR level = ?)
250
+ ORDER BY timestamp ASC
251
+ LIMIT ? OFFSET ?
252
+ `),
253
+ deleteOldRuns: this.db.prepare(`
254
+ DELETE FROM workflow_runs WHERE created_at < ?
255
+ `),
256
+ deleteStepsForRuns: this.db.prepare(`
257
+ DELETE FROM workflow_run_steps WHERE run_id IN (SELECT id FROM workflow_runs WHERE created_at < ?)
258
+ `),
259
+ deleteEventsForRuns: this.db.prepare(`
260
+ DELETE FROM workflow_events WHERE run_id IN (SELECT id FROM workflow_runs WHERE created_at < ?)
261
+ `),
262
+ getInterruptedRuns: this.db.prepare(`
263
+ SELECT * FROM workflow_runs
264
+ WHERE status IN ('queued', 'running')
265
+ ORDER BY created_at ASC
266
+ `),
267
+ getLastCompletedStep: this.db.prepare(`
268
+ SELECT * FROM workflow_run_steps
269
+ WHERE run_id = ? AND status = 'succeeded'
270
+ ORDER BY finished_at DESC
271
+ LIMIT 1
272
+ `),
273
+ };
274
+ }
275
+
276
+ // ============================================================================
277
+ // Run Operations
278
+ // ============================================================================
279
+
280
+ async createRun(
281
+ run: Omit<WorkflowRunRecord, "id" | "createdAt">,
282
+ ): Promise<WorkflowRunRecord> {
283
+ const id = generateId();
284
+ const createdAt = new Date();
285
+
286
+ this.stmts!.insertRun.run(
287
+ id,
288
+ run.kind,
289
+ run.status,
290
+ run.parentRunId ?? null,
291
+ JSON.stringify(run.input),
292
+ JSON.stringify(run.metadata),
293
+ JSON.stringify(run.context),
294
+ run.error ? JSON.stringify(sanitizeErrorForStorage(run.error)) : null,
295
+ createdAt.toISOString(),
296
+ run.startedAt?.toISOString() ?? null,
297
+ run.finishedAt?.toISOString() ?? null,
298
+ );
299
+
300
+ return {
301
+ ...run,
302
+ id,
303
+ createdAt,
304
+ };
305
+ }
306
+
307
+ async getRun(runId: string): Promise<WorkflowRunRecord | null> {
308
+ const row = this.stmts!.getRun.get(runId) as SQLiteRunRow | undefined;
309
+ return row ? this.mapRunRow(row) : null;
310
+ }
311
+
312
+ async updateRun(
313
+ runId: string,
314
+ updates: Partial<WorkflowRunRecord>,
315
+ ): Promise<void> {
316
+ this.stmts!.updateRun.run(
317
+ updates.status ?? null,
318
+ updates.context ? JSON.stringify(updates.context) : null,
319
+ updates.error
320
+ ? JSON.stringify(sanitizeErrorForStorage(updates.error))
321
+ : null,
322
+ updates.startedAt?.toISOString() ?? null,
323
+ updates.finishedAt?.toISOString() ?? null,
324
+ runId,
325
+ );
326
+ }
327
+
328
+ async listRuns(
329
+ options: ListRunsOptions = {},
330
+ ): Promise<PaginatedResult<WorkflowRunRecord>> {
331
+ const limit = options.limit ?? 50;
332
+ const offset = options.offset ?? 0;
333
+ const kind = options.kind ?? null;
334
+ const status = options.status ?? null;
335
+ const statusJson = status
336
+ ? JSON.stringify(Array.isArray(status) ? status : [status])
337
+ : null;
338
+ const parentRunId = options.parentRunId ?? null;
339
+
340
+ const rows = this.stmts!.listRuns.all(
341
+ kind,
342
+ kind,
343
+ statusJson,
344
+ statusJson,
345
+ parentRunId,
346
+ parentRunId,
347
+ limit,
348
+ offset,
349
+ ) as SQLiteRunRow[];
350
+
351
+ const countResult = this.stmts!.countRuns.get(
352
+ kind,
353
+ kind,
354
+ statusJson,
355
+ statusJson,
356
+ parentRunId,
357
+ parentRunId,
358
+ ) as { count: number };
359
+
360
+ return {
361
+ items: rows.map((row) => this.mapRunRow(row)),
362
+ total: countResult.count,
363
+ limit,
364
+ offset,
365
+ };
366
+ }
367
+
368
+ // ============================================================================
369
+ // Step Operations
370
+ // ============================================================================
371
+
372
+ async createStep(
373
+ step: Omit<WorkflowRunStepRecord, "id">,
374
+ ): Promise<WorkflowRunStepRecord> {
375
+ const id = generateId();
376
+
377
+ this.stmts!.insertStep.run(
378
+ id,
379
+ step.runId,
380
+ step.stepKey,
381
+ step.stepName,
382
+ step.status,
383
+ step.attempt,
384
+ step.result !== undefined ? JSON.stringify(step.result) : null,
385
+ step.error ? JSON.stringify(sanitizeErrorForStorage(step.error)) : null,
386
+ step.startedAt?.toISOString() ?? null,
387
+ step.finishedAt?.toISOString() ?? null,
388
+ );
389
+
390
+ return { ...step, id };
391
+ }
392
+
393
+ async getStep(stepId: string): Promise<WorkflowRunStepRecord | null> {
394
+ const row = this.stmts!.getStep.get(stepId) as SQLiteStepRow | undefined;
395
+ return row ? this.mapStepRow(row) : null;
396
+ }
397
+
398
+ async updateStep(
399
+ stepId: string,
400
+ updates: Partial<WorkflowRunStepRecord>,
401
+ ): Promise<void> {
402
+ this.stmts!.updateStep.run(
403
+ updates.status ?? null,
404
+ updates.attempt ?? null,
405
+ updates.result !== undefined ? JSON.stringify(updates.result) : null,
406
+ updates.error
407
+ ? JSON.stringify(sanitizeErrorForStorage(updates.error))
408
+ : null,
409
+ updates.finishedAt?.toISOString() ?? null,
410
+ stepId,
411
+ );
412
+ }
413
+
414
+ async getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]> {
415
+ const rows = this.stmts!.getStepsForRun.all(runId) as SQLiteStepRow[];
416
+ return rows.map((row) => this.mapStepRow(row));
417
+ }
418
+
419
+ // ============================================================================
420
+ // Event Operations
421
+ // ============================================================================
422
+
423
+ async saveEvent(event: Omit<WorkflowEventRecord, "id">): Promise<void> {
424
+ const id = generateId();
425
+
426
+ this.stmts!.insertEvent.run(
427
+ id,
428
+ event.runId,
429
+ event.stepKey ?? null,
430
+ event.eventType,
431
+ event.level,
432
+ event.payload !== undefined ? JSON.stringify(event.payload) : null,
433
+ event.timestamp.toISOString(),
434
+ );
435
+ }
436
+
437
+ async getEventsForRun(
438
+ runId: string,
439
+ options: ListEventsOptions = {},
440
+ ): Promise<WorkflowEventRecord[]> {
441
+ const limit = options.limit ?? 1000;
442
+ const offset = options.offset ?? 0;
443
+ const stepKey = options.stepKey ?? null;
444
+ const level = options.level ?? null;
445
+
446
+ const rows = this.stmts!.getEventsForRun.all(
447
+ runId,
448
+ stepKey,
449
+ stepKey,
450
+ level,
451
+ level,
452
+ limit,
453
+ offset,
454
+ ) as SQLiteEventRow[];
455
+
456
+ return rows.map((row) => this.mapEventRow(row));
457
+ }
458
+
459
+ // ============================================================================
460
+ // Transaction Support
461
+ // ============================================================================
462
+
463
+ /**
464
+ * Execute a function within a database transaction (async interface).
465
+ *
466
+ * @deprecated Use `transactionSync()` instead. This method only works when
467
+ * the callback performs purely synchronous operations wrapped in async/await.
468
+ * If the callback awaits real async I/O (network, timers, etc.), it will
469
+ * throw an error. `transactionSync()` makes the synchronous requirement explicit.
470
+ */
471
+ async transaction<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T> {
472
+ let fnPromise!: Promise<T>;
473
+ this.transactionSync(() => {
474
+ fnPromise = fn(this);
475
+ });
476
+ return fnPromise;
477
+ }
478
+
479
+ /**
480
+ * Execute a synchronous transaction (preferred for better-sqlite3).
481
+ * Use this when you need transaction guarantees.
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * storage.transactionSync(() => {
486
+ * // Multiple operations in a single transaction
487
+ * const run = storage.createRunSync(...);
488
+ * storage.updateRunSync(run.id, ...);
489
+ * });
490
+ * ```
491
+ */
492
+ transactionSync<T>(fn: () => T): T {
493
+ return this.db.transaction(fn)();
494
+ }
495
+
496
+ // ============================================================================
497
+ // Cleanup Operations
498
+ // ============================================================================
499
+
500
+ /**
501
+ * Delete runs older than the specified date.
502
+ * Also deletes associated steps and events (via CASCADE).
503
+ */
504
+ async deleteOldRuns(olderThan: Date): Promise<number> {
505
+ const isoDate = olderThan.toISOString();
506
+
507
+ // Due to CASCADE, steps and events are deleted automatically
508
+ // But we do it explicitly for databases without CASCADE support
509
+ return this.db.transaction(() => {
510
+ this.stmts!.deleteEventsForRuns.run(isoDate);
511
+ this.stmts!.deleteStepsForRuns.run(isoDate);
512
+ const result = this.stmts!.deleteOldRuns.run(isoDate);
513
+ return result.changes;
514
+ })();
515
+ }
516
+
517
+ // ============================================================================
518
+ // Resume Support
519
+ // ============================================================================
520
+
521
+ /**
522
+ * Get all runs that were interrupted (status is 'running' or 'queued').
523
+ * These runs can potentially be resumed.
524
+ */
525
+ async getInterruptedRuns(): Promise<WorkflowRunRecord[]> {
526
+ const rows = this.stmts!.getInterruptedRuns.all() as SQLiteRunRow[];
527
+ return rows.map((row) => this.mapRunRow(row));
528
+ }
529
+
530
+ /**
531
+ * Get the last completed step for a run.
532
+ * Useful for resuming from a checkpoint.
533
+ */
534
+ async getLastCompletedStep(
535
+ runId: string,
536
+ ): Promise<WorkflowRunStepRecord | null> {
537
+ const row = this.stmts!.getLastCompletedStep.get(runId) as
538
+ | SQLiteStepRow
539
+ | undefined;
540
+ return row ? this.mapStepRow(row) : null;
541
+ }
542
+
543
+ // ============================================================================
544
+ // Row Mapping
545
+ // ============================================================================
546
+
547
+ private mapRunRow(row: SQLiteRunRow): WorkflowRunRecord {
548
+ return {
549
+ id: row.id,
550
+ kind: row.kind,
551
+ status: row.status as RunStatus,
552
+ parentRunId: row.parent_run_id ?? undefined,
553
+ input: JSON.parse(row.input_json),
554
+ metadata: JSON.parse(row.metadata_json),
555
+ context: JSON.parse(row.context_json),
556
+ error: row.error_json ? JSON.parse(row.error_json) : undefined,
557
+ createdAt: new Date(row.created_at),
558
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
559
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
560
+ };
561
+ }
562
+
563
+ private mapStepRow(row: SQLiteStepRow): WorkflowRunStepRecord {
564
+ return {
565
+ id: row.id,
566
+ runId: row.run_id,
567
+ stepKey: row.step_key,
568
+ stepName: row.step_name,
569
+ status: row.status as StepStatus,
570
+ attempt: row.attempt,
571
+ result: row.result_json ? JSON.parse(row.result_json) : undefined,
572
+ error: row.error_json ? JSON.parse(row.error_json) : undefined,
573
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
574
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
575
+ };
576
+ }
577
+
578
+ private mapEventRow(row: SQLiteEventRow): WorkflowEventRecord {
579
+ return {
580
+ id: row.id,
581
+ runId: row.run_id,
582
+ stepKey: row.step_key ?? undefined,
583
+ eventType: row.event_type,
584
+ level: row.level as "info" | "warn" | "error",
585
+ payload: row.payload_json ? JSON.parse(row.payload_json) : undefined,
586
+ timestamp: new Date(row.timestamp),
587
+ };
588
+ }
589
+
590
+ // ============================================================================
591
+ // Utility Methods
592
+ // ============================================================================
593
+
594
+ /**
595
+ * Close the database connection.
596
+ */
597
+ close(): void {
598
+ this.db.close();
599
+ }
600
+
601
+ /**
602
+ * Get database statistics.
603
+ */
604
+ getStats(): { runs: number; steps: number; events: number } {
605
+ const runsCount = this.db
606
+ .prepare("SELECT COUNT(*) as count FROM workflow_runs")
607
+ .get() as { count: number };
608
+ const stepsCount = this.db
609
+ .prepare("SELECT COUNT(*) as count FROM workflow_run_steps")
610
+ .get() as { count: number };
611
+ const eventsCount = this.db
612
+ .prepare("SELECT COUNT(*) as count FROM workflow_events")
613
+ .get() as { count: number };
614
+
615
+ return {
616
+ runs: runsCount.count,
617
+ steps: stepsCount.count,
618
+ events: eventsCount.count,
619
+ };
620
+ }
621
+ }
622
+
623
+ // ============================================================================
624
+ // SQLite Row Types
625
+ // ============================================================================
626
+
627
+ interface SQLiteRunRow {
628
+ id: string;
629
+ kind: string;
630
+ status: string;
631
+ parent_run_id: string | null;
632
+ input_json: string;
633
+ metadata_json: string;
634
+ context_json: string;
635
+ error_json: string | null;
636
+ created_at: string;
637
+ started_at: string | null;
638
+ finished_at: string | null;
639
+ }
640
+
641
+ interface SQLiteStepRow {
642
+ id: string;
643
+ run_id: string;
644
+ step_key: string;
645
+ step_name: string;
646
+ status: string;
647
+ attempt: number;
648
+ result_json: string | null;
649
+ error_json: string | null;
650
+ started_at: string | null;
651
+ finished_at: string | null;
652
+ }
653
+
654
+ interface SQLiteEventRow {
655
+ id: string;
656
+ run_id: string;
657
+ step_key: string | null;
658
+ event_type: string;
659
+ level: string;
660
+ payload_json: string | null;
661
+ timestamp: string;
662
+ }