@multiplier-labs/stepflow 0.3.4 → 0.3.8
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/README.md +9 -0
- package/dist/{chunk-5XJBAPXF.js → chunk-4IDPWWGS.js} +15 -4
- package/dist/chunk-4IDPWWGS.js.map +1 -0
- package/dist/{chunk-U3PCVK6O.js → chunk-T6GAZBTQ.js} +70 -38
- package/dist/chunk-T6GAZBTQ.js.map +1 -0
- package/dist/{chunk-FMPJBK2N.js → chunk-TVSADUYU.js} +3 -5
- package/dist/{chunk-FMPJBK2N.js.map → chunk-TVSADUYU.js.map} +1 -1
- package/dist/events/index.d.ts +43 -3
- package/dist/events/index.js +7 -3
- package/dist/{index-Cd0IZ40L.d.ts → index-CdXDv30T.d.ts} +10 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +159 -46
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +2 -2
- package/dist/scheduler/index.js +1 -1
- package/dist/storage/index.d.ts +6 -4
- package/dist/storage/index.js +1 -1
- package/dist/{types-DSEjiLMj.d.ts → types-BfWe5yP6.d.ts} +2 -0
- package/package.json +26 -14
- package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
- package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
- package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
- package/.github/CODEOWNERS +0 -15
- package/.github/dependabot.yml +0 -23
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/publish.yml +0 -42
- package/.github/workflows/security.yml +0 -29
- package/CHANGELOG.md +0 -34
- package/SECURITY.md +0 -40
- package/dist/chunk-5XJBAPXF.js.map +0 -1
- package/dist/chunk-U3PCVK6O.js.map +0 -1
- package/docs/github-packages-setup.md +0 -97
- package/docs/stepflow-api-reference.md +0 -1663
- package/src/core/engine/concurrency.ts +0 -229
- package/src/core/engine/lifecycle.ts +0 -176
- package/src/core/engine.test.ts +0 -1435
- package/src/core/engine.ts +0 -598
- package/src/core/index.ts +0 -7
- package/src/core/orchestrator/persistence.ts +0 -165
- package/src/core/orchestrator/plan-resolution.ts +0 -140
- package/src/core/orchestrator/state-transitions.ts +0 -109
- package/src/core/orchestrator/step-runner.ts +0 -427
- package/src/core/orchestrator.ts +0 -252
- package/src/core/types.ts +0 -220
- package/src/events/index.ts +0 -8
- package/src/events/memory.test.ts +0 -107
- package/src/events/memory.ts +0 -92
- package/src/events/socketio.test.ts +0 -378
- package/src/events/socketio.ts +0 -262
- package/src/events/types.ts +0 -118
- package/src/events/webhook.test.ts +0 -949
- package/src/events/webhook.ts +0 -807
- package/src/index.ts +0 -270
- package/src/planning/evaluate-condition.test.ts +0 -153
- package/src/planning/evaluate-condition.ts +0 -105
- package/src/planning/index.ts +0 -55
- package/src/planning/planner.ts +0 -494
- package/src/planning/planning.test.ts +0 -727
- package/src/planning/registry.test.ts +0 -398
- package/src/planning/registry.ts +0 -283
- package/src/planning/types.ts +0 -566
- package/src/scheduler/cron.test.ts +0 -479
- package/src/scheduler/cron.ts +0 -438
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/postgres-persistence.ts +0 -530
- package/src/scheduler/sqlite-persistence.test.ts +0 -235
- package/src/scheduler/sqlite-persistence.ts +0 -286
- package/src/scheduler/types.ts +0 -73
- package/src/storage/index.ts +0 -27
- package/src/storage/memory.test.ts +0 -349
- package/src/storage/memory.ts +0 -227
- package/src/storage/postgres-core.ts +0 -550
- package/src/storage/postgres-migration.test.ts +0 -69
- package/src/storage/postgres-transaction-pagination.test.ts +0 -107
- package/src/storage/postgres.ts +0 -835
- package/src/storage/sqlite-core.ts +0 -151
- package/src/storage/sqlite.test.ts +0 -642
- package/src/storage/sqlite.ts +0 -717
- package/src/storage/types.ts +0 -343
- package/src/utils/errors.test.ts +0 -150
- package/src/utils/errors.ts +0 -161
- package/src/utils/id.ts +0 -21
- package/src/utils/index.ts +0 -10
- package/src/utils/logger.test.ts +0 -121
- package/src/utils/logger.ts +0 -96
- package/src/utils/pinned-fetch.test.ts +0 -84
- package/src/utils/pinned-fetch.ts +0 -118
- package/src/utils/postgres-deps.ts +0 -79
- package/src/utils/retry.test.ts +0 -177
- package/src/utils/retry.ts +0 -121
- package/src/utils/safe-json.test.ts +0 -166
- package/src/utils/safe-json.ts +0 -107
- package/src/utils/webhook-signature.test.ts +0 -111
- package/src/utils/webhook-signature.ts +0 -74
- package/tsconfig.json +0 -21
- package/tsup.config.ts +0 -14
- package/vitest.config.ts +0 -9
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Concurrency control for the WorkflowEngine.
|
|
3
|
-
*
|
|
4
|
-
* Owns:
|
|
5
|
-
* - the priority-ordered queue of pending runs (`runQueue`)
|
|
6
|
-
* - the set of in-flight run abort controllers (`activeRuns`)
|
|
7
|
-
* - the timer handles used to defer launches via setTimeout/setImmediate
|
|
8
|
-
*
|
|
9
|
-
* Responsibilities:
|
|
10
|
-
* - `hasCapacity()` against the configured `maxConcurrency` ceiling
|
|
11
|
-
* - priority-ordered enqueue (higher priority first, FIFO within a priority)
|
|
12
|
-
* - dispatch loop that drains the queue while capacity exists, with a
|
|
13
|
-
* try/catch around each dispatch so a single bad run cannot tear down
|
|
14
|
-
* the loop and strand the rest of the queue (preserves the audit
|
|
15
|
-
* 2026-04-27 C3 fix in `processQueue`).
|
|
16
|
-
* - dispatch-failure telemetry: log + emit `run.failed` + best-effort
|
|
17
|
-
* storage update with `QUEUE_DISPATCH_FAILED`.
|
|
18
|
-
*
|
|
19
|
-
* NOT responsible for:
|
|
20
|
-
* - building the `executeWorkflow` invocation (that lives in WorkflowEngine
|
|
21
|
-
* so the orchestrator/spawnChild wiring stays where it is)
|
|
22
|
-
* - storage initialization / shutdown sequencing (see `lifecycle.ts`)
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type { StorageAdapter } from '../../storage/types';
|
|
26
|
-
import type { EventTransport } from '../../events/types';
|
|
27
|
-
import type { Logger, WorkflowDefinition } from '../types';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Queued run waiting to be executed when capacity is available.
|
|
31
|
-
*/
|
|
32
|
-
export interface QueuedRun {
|
|
33
|
-
runId: string;
|
|
34
|
-
definition: WorkflowDefinition;
|
|
35
|
-
input: Record<string, unknown>;
|
|
36
|
-
metadata?: Record<string, unknown>;
|
|
37
|
-
priority: number;
|
|
38
|
-
queuedAt: Date;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type TimerHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;
|
|
42
|
-
|
|
43
|
-
export interface ConcurrencyManagerDeps {
|
|
44
|
-
storage: StorageAdapter;
|
|
45
|
-
events: EventTransport;
|
|
46
|
-
logger: Logger;
|
|
47
|
-
/** Maximum concurrent runs; undefined or <=0 means unlimited. */
|
|
48
|
-
maxConcurrency?: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export class ConcurrencyManager {
|
|
52
|
-
private readonly activeRuns = new Map<string, AbortController>();
|
|
53
|
-
private readonly runQueue: QueuedRun[] = [];
|
|
54
|
-
private readonly timerHandles = new Set<TimerHandle>();
|
|
55
|
-
private readonly storage: StorageAdapter;
|
|
56
|
-
private readonly events: EventTransport;
|
|
57
|
-
private readonly logger: Logger;
|
|
58
|
-
private readonly maxConcurrency: number | undefined;
|
|
59
|
-
|
|
60
|
-
constructor(deps: ConcurrencyManagerDeps) {
|
|
61
|
-
this.storage = deps.storage;
|
|
62
|
-
this.events = deps.events;
|
|
63
|
-
this.logger = deps.logger;
|
|
64
|
-
this.maxConcurrency = deps.maxConcurrency;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
hasCapacity(): boolean {
|
|
68
|
-
if (this.maxConcurrency === undefined || this.maxConcurrency <= 0) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
return this.activeRuns.size < this.maxConcurrency;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
getActiveCount(): number {
|
|
75
|
-
return this.activeRuns.size;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
getQueuedCount(): number {
|
|
79
|
-
return this.runQueue.length;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
isActive(runId: string): boolean {
|
|
83
|
-
return this.activeRuns.has(runId);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Insert a run in priority order (higher priority first, FIFO within same). */
|
|
87
|
-
enqueue(run: QueuedRun): void {
|
|
88
|
-
let inserted = false;
|
|
89
|
-
for (let i = 0; i < this.runQueue.length; i++) {
|
|
90
|
-
if (run.priority > this.runQueue[i].priority) {
|
|
91
|
-
this.runQueue.splice(i, 0, run);
|
|
92
|
-
inserted = true;
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (!inserted) {
|
|
97
|
-
this.runQueue.push(run);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Remove a run from the queue if present. Returns true if it was queued. */
|
|
102
|
-
removeFromQueue(runId: string): boolean {
|
|
103
|
-
const idx = this.runQueue.findIndex((q) => q.runId === runId);
|
|
104
|
-
if (idx === -1) return false;
|
|
105
|
-
this.runQueue.splice(idx, 1);
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
registerActive(runId: string, controller: AbortController): void {
|
|
110
|
-
this.activeRuns.set(runId, controller);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
unregisterActive(runId: string): void {
|
|
114
|
-
this.activeRuns.delete(runId);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
abortActive(runId: string): boolean {
|
|
118
|
-
const controller = this.activeRuns.get(runId);
|
|
119
|
-
if (!controller) return false;
|
|
120
|
-
controller.abort();
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Abort every in-flight run. `onAbort` is called for each run id before its
|
|
126
|
-
* controller is signaled (so callers can log, emit, etc.).
|
|
127
|
-
*/
|
|
128
|
-
abortAllActive(onAbort?: (runId: string) => void): void {
|
|
129
|
-
for (const [runId, controller] of this.activeRuns) {
|
|
130
|
-
if (onAbort) onAbort(runId);
|
|
131
|
-
controller.abort();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Forget all active-run bookkeeping without aborting (used during shutdown). */
|
|
136
|
-
clearActive(): void {
|
|
137
|
-
this.activeRuns.clear();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Schedule a launch via setTimeout (when `delay` is truthy) or setImmediate
|
|
142
|
-
* (otherwise). Tracks the handle so it can be cleared during shutdown.
|
|
143
|
-
*/
|
|
144
|
-
scheduleLaunch(fn: () => void, delay?: number): void {
|
|
145
|
-
if (delay) {
|
|
146
|
-
const handle = setTimeout(fn, delay);
|
|
147
|
-
this.timerHandles.add(handle);
|
|
148
|
-
} else {
|
|
149
|
-
const handle = setImmediate(fn);
|
|
150
|
-
this.timerHandles.add(handle);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
clearTimers(): void {
|
|
155
|
-
for (const handle of this.timerHandles) {
|
|
156
|
-
if (typeof handle === 'object' && 'ref' in handle) {
|
|
157
|
-
clearImmediate(handle as ReturnType<typeof setImmediate>);
|
|
158
|
-
} else {
|
|
159
|
-
clearTimeout(handle as ReturnType<typeof setTimeout>);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
this.timerHandles.clear();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Drain the queue while capacity is available, dispatching each item via
|
|
167
|
-
* `dispatch(run)`. Each iteration is wrapped in try/catch so that a failure
|
|
168
|
-
* dispatching one queued item (e.g., an event-transport that throws) cannot
|
|
169
|
-
* tear down the loop and leave remaining items stuck in the queue. Failed
|
|
170
|
-
* dispatches are logged, the run is marked as failed in storage as
|
|
171
|
-
* best-effort telemetry, and the loop advances to the next item.
|
|
172
|
-
*
|
|
173
|
-
* Emits `run.dequeued` for each dispatched run (this is what the C3
|
|
174
|
-
* resilience test exercises by having the emit throw).
|
|
175
|
-
*/
|
|
176
|
-
processQueue(dispatch: (run: QueuedRun) => void): void {
|
|
177
|
-
while (this.hasCapacity() && this.runQueue.length > 0) {
|
|
178
|
-
const next = this.runQueue.shift()!;
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
this.logger.debug(`Starting queued run ${next.runId}`);
|
|
182
|
-
|
|
183
|
-
this.events.emit({
|
|
184
|
-
runId: next.runId,
|
|
185
|
-
kind: next.definition.kind,
|
|
186
|
-
eventType: 'run.dequeued',
|
|
187
|
-
timestamp: new Date(),
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
dispatch(next);
|
|
191
|
-
} catch (err) {
|
|
192
|
-
this.recordQueueDispatchFailure(next, err);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Record a queue-dispatch failure: log it, emit a failure event, and
|
|
199
|
-
* best-effort mark the run as failed in storage. All side-effects are
|
|
200
|
-
* defensively wrapped so this method itself never throws.
|
|
201
|
-
*/
|
|
202
|
-
private recordQueueDispatchFailure(queued: QueuedRun, err: unknown): void {
|
|
203
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
-
this.logger.error(`Failed to dispatch queued run ${queued.runId}`, err);
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
this.events.emit({
|
|
208
|
-
runId: queued.runId,
|
|
209
|
-
kind: queued.definition.kind,
|
|
210
|
-
eventType: 'run.failed',
|
|
211
|
-
timestamp: new Date(),
|
|
212
|
-
payload: { error: { code: 'QUEUE_DISPATCH_FAILED', message } },
|
|
213
|
-
});
|
|
214
|
-
} catch (emitErr) {
|
|
215
|
-
this.logger.error(`Failed to emit dispatch-failure event for ${queued.runId}`, emitErr);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Fire-and-forget; we don't want to block the queue loop on storage I/O.
|
|
219
|
-
this.storage
|
|
220
|
-
.updateRun(queued.runId, {
|
|
221
|
-
status: 'failed',
|
|
222
|
-
finishedAt: new Date(),
|
|
223
|
-
error: { code: 'QUEUE_DISPATCH_FAILED', message },
|
|
224
|
-
})
|
|
225
|
-
.catch((updateErr) => {
|
|
226
|
-
this.logger.error(`Failed to record dispatch failure for ${queued.runId}`, updateErr);
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lifecycle and resume orchestration for the WorkflowEngine.
|
|
3
|
-
*
|
|
4
|
-
* Owns the cross-cutting "engine itself" lifecycle:
|
|
5
|
-
* - `initialize()` — bring up storage adapters that need explicit init
|
|
6
|
-
* (e.g., PostgresStorageAdapter)
|
|
7
|
-
* - `shutdown()` — drain timers, abort in-flight runs, close transports
|
|
8
|
-
* - `resumeRun()` / `getResumableRuns()` / `resumeAllInterrupted()` —
|
|
9
|
-
* restart interrupted runs from their persisted checkpoints
|
|
10
|
-
*
|
|
11
|
-
* NOT responsible for:
|
|
12
|
-
* - per-step execution (that lives in `orchestrator.ts`)
|
|
13
|
-
* - queue/active/timer bookkeeping (that lives in `engine/concurrency.ts`;
|
|
14
|
-
* this module delegates to a `ConcurrencyManager`)
|
|
15
|
-
* - building the `executeWorkflow` invocation (the engine still owns that
|
|
16
|
-
* so the orchestrator/spawnChild wiring stays in one place; this module
|
|
17
|
-
* calls it via the injected `launchRun` callback)
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { StorageAdapter, WorkflowRunRecord } from '../../storage/types';
|
|
21
|
-
import type { EventTransport } from '../../events/types';
|
|
22
|
-
import type { Logger, WorkflowDefinition, WorkflowKind } from '../types';
|
|
23
|
-
import { RunNotFoundError, WorkflowNotFoundError } from '../../utils/errors';
|
|
24
|
-
import type { ConcurrencyManager } from './concurrency';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Callback the engine provides for actually launching a run. Lifecycle uses
|
|
28
|
-
* it to start a resumed run with its checkpoint; the engine's implementation
|
|
29
|
-
* registers the abort controller, schedules the launch via the concurrency
|
|
30
|
-
* manager, and invokes the orchestrator.
|
|
31
|
-
*/
|
|
32
|
-
export type LaunchRunFn = (
|
|
33
|
-
runId: string,
|
|
34
|
-
definition: WorkflowDefinition,
|
|
35
|
-
input: Record<string, unknown>,
|
|
36
|
-
metadata?: Record<string, unknown>,
|
|
37
|
-
delay?: number,
|
|
38
|
-
checkpoint?: { completedStepKeys: Set<string>; results: Record<string, unknown> }
|
|
39
|
-
) => void;
|
|
40
|
-
|
|
41
|
-
export interface LifecycleManagerDeps {
|
|
42
|
-
storage: StorageAdapter;
|
|
43
|
-
events: EventTransport;
|
|
44
|
-
logger: Logger;
|
|
45
|
-
registry: Map<WorkflowKind, WorkflowDefinition>;
|
|
46
|
-
concurrency: ConcurrencyManager;
|
|
47
|
-
launchRun: LaunchRunFn;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class LifecycleManager {
|
|
51
|
-
private readonly storage: StorageAdapter;
|
|
52
|
-
private readonly events: EventTransport;
|
|
53
|
-
private readonly logger: Logger;
|
|
54
|
-
private readonly registry: Map<WorkflowKind, WorkflowDefinition>;
|
|
55
|
-
private readonly concurrency: ConcurrencyManager;
|
|
56
|
-
private readonly launchRun: LaunchRunFn;
|
|
57
|
-
|
|
58
|
-
constructor(deps: LifecycleManagerDeps) {
|
|
59
|
-
this.storage = deps.storage;
|
|
60
|
-
this.events = deps.events;
|
|
61
|
-
this.logger = deps.logger;
|
|
62
|
-
this.registry = deps.registry;
|
|
63
|
-
this.concurrency = deps.concurrency;
|
|
64
|
-
this.launchRun = deps.launchRun;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Initialize the engine's storage adapter if it requires it.
|
|
69
|
-
* Safe to call when the adapter has no `initialize` hook.
|
|
70
|
-
*/
|
|
71
|
-
async initialize(): Promise<void> {
|
|
72
|
-
if (this.storage.initialize) {
|
|
73
|
-
await this.storage.initialize();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Shutdown the engine gracefully:
|
|
79
|
-
* 1. clear pending timer handles (so no further launches fire)
|
|
80
|
-
* 2. abort all in-flight runs
|
|
81
|
-
* 3. close the event transport
|
|
82
|
-
* 4. close the storage adapter
|
|
83
|
-
* 5. drop active-run bookkeeping
|
|
84
|
-
*/
|
|
85
|
-
async shutdown(): Promise<void> {
|
|
86
|
-
this.logger.info('Shutting down workflow engine...');
|
|
87
|
-
|
|
88
|
-
this.concurrency.clearTimers();
|
|
89
|
-
|
|
90
|
-
this.concurrency.abortAllActive((runId) => {
|
|
91
|
-
this.logger.debug(`Canceling active run: ${runId}`);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (this.events.close) {
|
|
95
|
-
this.events.close();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (this.storage.close) {
|
|
99
|
-
await this.storage.close();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.concurrency.clearActive();
|
|
103
|
-
this.logger.info('Workflow engine shutdown complete');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Resume an interrupted workflow run from its checkpoint.
|
|
108
|
-
* The run must be in 'queued' or 'running' status.
|
|
109
|
-
*/
|
|
110
|
-
async resumeRun(runId: string): Promise<string> {
|
|
111
|
-
const run = await this.storage.getRun(runId);
|
|
112
|
-
if (!run) {
|
|
113
|
-
throw new RunNotFoundError(runId);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!['queued', 'running'].includes(run.status)) {
|
|
117
|
-
throw new Error(`Cannot resume run ${runId}: status is "${run.status}"`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const definition = this.registry.get(run.kind);
|
|
121
|
-
if (!definition) {
|
|
122
|
-
throw new WorkflowNotFoundError(run.kind);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (this.concurrency.isActive(runId)) {
|
|
126
|
-
this.logger.warn(`Run ${runId} is already active, skipping resume`);
|
|
127
|
-
return runId;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.logger.info(`Resuming run ${runId} from checkpoint`);
|
|
131
|
-
|
|
132
|
-
const completedStepKeys = new Set(run.completedSteps ?? []);
|
|
133
|
-
|
|
134
|
-
this.launchRun(runId, definition, run.input, run.metadata, undefined, {
|
|
135
|
-
completedStepKeys,
|
|
136
|
-
results: run.context,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
return runId;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Get all runs that were interrupted and can be resumed.
|
|
144
|
-
* Returns runs with status 'queued' or 'running'.
|
|
145
|
-
*/
|
|
146
|
-
async getResumableRuns(): Promise<WorkflowRunRecord[]> {
|
|
147
|
-
const result = await this.storage.listRuns({
|
|
148
|
-
status: ['queued', 'running'],
|
|
149
|
-
});
|
|
150
|
-
return result.items;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Resume every interrupted run whose workflow kind is currently registered.
|
|
155
|
-
* Useful for recovering after a server restart.
|
|
156
|
-
*/
|
|
157
|
-
async resumeAllInterrupted(): Promise<string[]> {
|
|
158
|
-
const resumableRuns = await this.getResumableRuns();
|
|
159
|
-
const resumedIds: string[] = [];
|
|
160
|
-
|
|
161
|
-
for (const run of resumableRuns) {
|
|
162
|
-
if (this.registry.has(run.kind)) {
|
|
163
|
-
try {
|
|
164
|
-
await this.resumeRun(run.id);
|
|
165
|
-
resumedIds.push(run.id);
|
|
166
|
-
} catch (error) {
|
|
167
|
-
this.logger.error(`Failed to resume run ${run.id}:`, error);
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
this.logger.warn(`Cannot resume run ${run.id}: workflow "${run.kind}" not registered`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return resumedIds;
|
|
175
|
-
}
|
|
176
|
-
}
|