@kb-labs/workflow-engine 2.119.0 → 2.119.1-canary.1e87213c2
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.d.ts +73 -1
- package/dist/index.js +75 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,30 @@ declare class WorkflowLoader {
|
|
|
43
43
|
private validate;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* How long a daemon's liveness claim on this shared state store stays valid
|
|
48
|
+
* without a heartbeat renewal. This answers a completely different question
|
|
49
|
+
* from `RUN_TTL_MS` above: not "is this run's data still around" but "is the
|
|
50
|
+
* daemon that owns it still alive". `cleanupStaleRuns` uses ONLY this lease —
|
|
51
|
+
* never a run's or job's own elapsed running time — to decide whether a
|
|
52
|
+
* previous daemon instance is genuinely gone. A lease still on file this
|
|
53
|
+
* recently can only mean some process wrote it within the last
|
|
54
|
+
* DAEMON_LEASE_TTL_MS; if that process isn't the one calling
|
|
55
|
+
* `cleanupStaleRuns`, force-failing "running"/"queued" jobs would very likely
|
|
56
|
+
* be abandoning work a still-live sibling instance owns, not cleaning up
|
|
57
|
+
* after a dead one. Kept well short of RUN_TTL_MS on purpose — a run/job can
|
|
58
|
+
* legitimately run for tens of minutes, but no legitimate single daemon
|
|
59
|
+
* instance should ever go this long without renewing its own lease.
|
|
60
|
+
*/
|
|
61
|
+
declare const DAEMON_LEASE_TTL_MS = 45000;
|
|
62
|
+
/** How often a live daemon should call `writeDaemonLease` to keep its lease
|
|
63
|
+
* fresh — comfortably under `DAEMON_LEASE_TTL_MS` so a normal GC pause or a
|
|
64
|
+
* slow cache round-trip never lets the lease lapse on its own. */
|
|
65
|
+
declare const DAEMON_LEASE_HEARTBEAT_INTERVAL_MS = 15000;
|
|
66
|
+
interface DaemonLease {
|
|
67
|
+
instanceId: string;
|
|
68
|
+
heartbeatAt: string;
|
|
69
|
+
}
|
|
46
70
|
declare class StateStore {
|
|
47
71
|
private readonly logger;
|
|
48
72
|
private readonly cache;
|
|
@@ -51,6 +75,22 @@ declare class StateStore {
|
|
|
51
75
|
getRun(runId: string): Promise<WorkflowRun | null>;
|
|
52
76
|
deleteRun(runId: string): Promise<void>;
|
|
53
77
|
getAllRunIds(): Promise<string[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Read the current daemon liveness lease, if one is still on file. Returns
|
|
80
|
+
* null both when no daemon has ever written one and when the last writer's
|
|
81
|
+
* lease has expired (per `DAEMON_LEASE_TTL_MS`) — the cache backend itself
|
|
82
|
+
* enforces that expiry, so a non-null result here is proof some process
|
|
83
|
+
* heartbeated within the last `DAEMON_LEASE_TTL_MS`.
|
|
84
|
+
*/
|
|
85
|
+
getDaemonLease(): Promise<DaemonLease | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Claim (or renew) the daemon liveness lease under `instanceId`, valid for
|
|
88
|
+
* `DAEMON_LEASE_TTL_MS`. Called once at startup after `cleanupStaleRuns`
|
|
89
|
+
* decides this instance is safe to proceed as the active daemon, and then
|
|
90
|
+
* periodically for as long as the daemon stays up, so its lease never
|
|
91
|
+
* lapses while it's genuinely alive.
|
|
92
|
+
*/
|
|
93
|
+
writeDaemonLease(instanceId: string): Promise<void>;
|
|
54
94
|
/**
|
|
55
95
|
* Holds an exclusive per-run lock (see `withLock`) for the whole
|
|
56
96
|
* read-modify-write, so `mutator` runs exactly once per call — no other
|
|
@@ -234,11 +274,21 @@ interface WorkflowEngineOptions {
|
|
|
234
274
|
snapshotManager?: ISnapshotManager;
|
|
235
275
|
/** Workspace root (monorepo root) - used for plugin execution context */
|
|
236
276
|
workspaceRoot?: string;
|
|
277
|
+
/**
|
|
278
|
+
* Unique identifier for this daemon/engine process, used by
|
|
279
|
+
* `cleanupStaleRuns` to tell "genuinely a different daemon instance" apart
|
|
280
|
+
* from "this same instance calling in again". Defaults to a fresh
|
|
281
|
+
* `randomUUID()` per engine — override only in tests that need to assert
|
|
282
|
+
* on a known id or simulate two instances against the same store.
|
|
283
|
+
*/
|
|
284
|
+
instanceId?: string;
|
|
237
285
|
}
|
|
238
286
|
declare class WorkflowEngine {
|
|
239
287
|
private readonly options;
|
|
240
288
|
readonly loader: WorkflowLoader;
|
|
241
289
|
readonly maxWorkflowDepth: number;
|
|
290
|
+
/** See `WorkflowEngineOptions.instanceId`. */
|
|
291
|
+
readonly instanceId: string;
|
|
242
292
|
private readonly logger;
|
|
243
293
|
private readonly analytics?;
|
|
244
294
|
private readonly stateStore;
|
|
@@ -372,6 +422,14 @@ declare class WorkflowEngine {
|
|
|
372
422
|
* Get the scheduler for direct access (used by worker for gate re-enqueue).
|
|
373
423
|
*/
|
|
374
424
|
getScheduler(): Scheduler;
|
|
425
|
+
/**
|
|
426
|
+
* Renew this instance's daemon liveness lease. Call once cleanupStaleRuns
|
|
427
|
+
* has decided this instance is safe to proceed, then periodically (see
|
|
428
|
+
* `DAEMON_LEASE_HEARTBEAT_INTERVAL_MS`) for as long as the daemon stays
|
|
429
|
+
* up — a live daemon's lease must never lapse, since `cleanupStaleRuns`
|
|
430
|
+
* treats a lapsed lease as proof its owner is genuinely gone.
|
|
431
|
+
*/
|
|
432
|
+
renewDaemonLease(): Promise<void>;
|
|
375
433
|
/**
|
|
376
434
|
* Mark stale running/queued jobs as failed on daemon startup — their
|
|
377
435
|
* executor process is gone, so they're unrecoverable. The run itself is
|
|
@@ -379,6 +437,20 @@ declare class WorkflowEngine {
|
|
|
379
437
|
* a run with one abandoned job and one job legitimately parked on a human
|
|
380
438
|
* approval or a child workflow stays 'running' (only the abandoned job is
|
|
381
439
|
* failed) until the parked one resolves.
|
|
440
|
+
*
|
|
441
|
+
* Before touching anything, this checks the shared daemon liveness lease
|
|
442
|
+
* (see `StateStore.getDaemonLease`/`writeDaemonLease`). Being CALLED is not
|
|
443
|
+
* by itself evidence of a restart — that's exactly what let a stray or
|
|
444
|
+
* duplicate daemon process (e.g. a second launch racing a daemon that never
|
|
445
|
+
* actually restarted) force-fail a genuinely in-flight run out from under
|
|
446
|
+
* a still-alive sibling instance, purely because it, too, executed its own
|
|
447
|
+
* one-time startup sweep against the same shared state store. A lease still
|
|
448
|
+
* fresh under a DIFFERENT instanceId is real, recent (within
|
|
449
|
+
* `DAEMON_LEASE_TTL_MS`) evidence that some other process is currently
|
|
450
|
+
* alive and heartbeating — in that case this pass must not run at all, no
|
|
451
|
+
* matter how long any individual job has been "running": elapsed job
|
|
452
|
+
* runtime is never a substitute for genuine restart evidence, and is not
|
|
453
|
+
* even consulted here.
|
|
382
454
|
*/
|
|
383
455
|
cleanupStaleRuns(): Promise<void>;
|
|
384
456
|
/**
|
|
@@ -1058,4 +1130,4 @@ declare class JobManager implements IJobScheduler {
|
|
|
1058
1130
|
private jobRecordToHandle;
|
|
1059
1131
|
}
|
|
1060
1132
|
|
|
1061
|
-
export { type AcquireOptions, ArtifactMerger, type ArtifactMergerOptions, ConcurrencyManager, type CreateRunInput, type EngineLogger, EnvSecretProvider, type EnvSecretProviderOptions, EventBusBridge, JobManager, type JobManagerConfig, type JobQueueEntry, ManifestConverter, ManifestScanner, type ManifestScannerOptions, type RetryDecision, type RunContext, RunCoordinator, type RunCoordinatorOptions, type RunSnapshot, RunSnapshotStorage, Scheduler, type SchedulerOptions, type SecretProvider, StateStore, type ValidationResult, WorkflowEngine, type WorkflowEngineOptions, type WorkflowEvent, type WorkflowHandlerInfo, type WorkflowListOptions, WorkflowLoader, type WorkflowLoaderOptions, type WorkflowLoaderResult, WorkflowRegistry, type WorkflowRegistryEntry, type WorkflowRegistryOptions, WorkflowRepository, type WorkflowRepositoryOptions, type WorkflowRuntime, type WorkflowSchedule, WorkflowService, type WorkflowServiceListOptions, type WorkflowServiceOptions, type WorkflowStats, type WorkflowTrigger, type WorkflowTriggerType, calculateBackoff, createDefaultSecretProvider, shouldRetry };
|
|
1133
|
+
export { type AcquireOptions, ArtifactMerger, type ArtifactMergerOptions, ConcurrencyManager, type CreateRunInput, DAEMON_LEASE_HEARTBEAT_INTERVAL_MS, DAEMON_LEASE_TTL_MS, type DaemonLease, type EngineLogger, EnvSecretProvider, type EnvSecretProviderOptions, EventBusBridge, JobManager, type JobManagerConfig, type JobQueueEntry, ManifestConverter, ManifestScanner, type ManifestScannerOptions, type RetryDecision, type RunContext, RunCoordinator, type RunCoordinatorOptions, type RunSnapshot, RunSnapshotStorage, Scheduler, type SchedulerOptions, type SecretProvider, StateStore, type ValidationResult, WorkflowEngine, type WorkflowEngineOptions, type WorkflowEvent, type WorkflowHandlerInfo, type WorkflowListOptions, WorkflowLoader, type WorkflowLoaderOptions, type WorkflowLoaderResult, WorkflowRegistry, type WorkflowRegistryEntry, type WorkflowRegistryOptions, WorkflowRepository, type WorkflowRepositoryOptions, type WorkflowRuntime, type WorkflowSchedule, WorkflowService, type WorkflowServiceListOptions, type WorkflowServiceOptions, type WorkflowStats, type WorkflowTrigger, type WorkflowTriggerType, calculateBackoff, createDefaultSecretProvider, shouldRetry };
|
package/dist/index.js
CHANGED
|
@@ -121,6 +121,9 @@ async function withLock(cache, lockKey, fn) {
|
|
|
121
121
|
|
|
122
122
|
// src/state-store.ts
|
|
123
123
|
var RUN_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
124
|
+
var DAEMON_LEASE_TTL_MS = 45e3;
|
|
125
|
+
var DAEMON_LEASE_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
126
|
+
var DAEMON_LEASE_KEY = "kb:daemon:lease";
|
|
124
127
|
var StateStore = class {
|
|
125
128
|
constructor(cache, logger) {
|
|
126
129
|
this.logger = logger;
|
|
@@ -159,6 +162,36 @@ var StateStore = class {
|
|
|
159
162
|
const runIds = await this.cache.zrangebyscore("workflow:runs:index", -Infinity, Infinity);
|
|
160
163
|
return runIds ?? [];
|
|
161
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Read the current daemon liveness lease, if one is still on file. Returns
|
|
167
|
+
* null both when no daemon has ever written one and when the last writer's
|
|
168
|
+
* lease has expired (per `DAEMON_LEASE_TTL_MS`) — the cache backend itself
|
|
169
|
+
* enforces that expiry, so a non-null result here is proof some process
|
|
170
|
+
* heartbeated within the last `DAEMON_LEASE_TTL_MS`.
|
|
171
|
+
*/
|
|
172
|
+
async getDaemonLease() {
|
|
173
|
+
const raw = await this.cache.get(DAEMON_LEASE_KEY);
|
|
174
|
+
if (!raw) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(raw);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
this.logger.error("Failed to parse stored daemon lease", error instanceof Error ? error : void 0);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Claim (or renew) the daemon liveness lease under `instanceId`, valid for
|
|
186
|
+
* `DAEMON_LEASE_TTL_MS`. Called once at startup after `cleanupStaleRuns`
|
|
187
|
+
* decides this instance is safe to proceed as the active daemon, and then
|
|
188
|
+
* periodically for as long as the daemon stays up, so its lease never
|
|
189
|
+
* lapses while it's genuinely alive.
|
|
190
|
+
*/
|
|
191
|
+
async writeDaemonLease(instanceId) {
|
|
192
|
+
const lease = { instanceId, heartbeatAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
193
|
+
await this.cache.set(DAEMON_LEASE_KEY, JSON.stringify(lease), DAEMON_LEASE_TTL_MS);
|
|
194
|
+
}
|
|
162
195
|
/**
|
|
163
196
|
* Holds an exclusive per-run lock (see `withLock`) for the whole
|
|
164
197
|
* read-modify-write, so `mutator` runs exactly once per call — no other
|
|
@@ -794,6 +827,7 @@ var WorkflowEngine = class {
|
|
|
794
827
|
this.options = options;
|
|
795
828
|
this.logger = options.logger;
|
|
796
829
|
this.analytics = options.analytics;
|
|
830
|
+
this.instanceId = options.instanceId ?? randomUUID();
|
|
797
831
|
this.stateStore = new StateStore(options.cache, this.logger);
|
|
798
832
|
this.concurrency = new ConcurrencyManager(
|
|
799
833
|
options.cache,
|
|
@@ -816,6 +850,8 @@ var WorkflowEngine = class {
|
|
|
816
850
|
options;
|
|
817
851
|
loader;
|
|
818
852
|
maxWorkflowDepth;
|
|
853
|
+
/** See `WorkflowEngineOptions.instanceId`. */
|
|
854
|
+
instanceId;
|
|
819
855
|
logger;
|
|
820
856
|
analytics;
|
|
821
857
|
stateStore;
|
|
@@ -1523,6 +1559,16 @@ var WorkflowEngine = class {
|
|
|
1523
1559
|
getScheduler() {
|
|
1524
1560
|
return this.scheduler;
|
|
1525
1561
|
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Renew this instance's daemon liveness lease. Call once cleanupStaleRuns
|
|
1564
|
+
* has decided this instance is safe to proceed, then periodically (see
|
|
1565
|
+
* `DAEMON_LEASE_HEARTBEAT_INTERVAL_MS`) for as long as the daemon stays
|
|
1566
|
+
* up — a live daemon's lease must never lapse, since `cleanupStaleRuns`
|
|
1567
|
+
* treats a lapsed lease as proof its owner is genuinely gone.
|
|
1568
|
+
*/
|
|
1569
|
+
async renewDaemonLease() {
|
|
1570
|
+
await this.stateStore.writeDaemonLease(this.instanceId);
|
|
1571
|
+
}
|
|
1526
1572
|
/**
|
|
1527
1573
|
* Mark stale running/queued jobs as failed on daemon startup — their
|
|
1528
1574
|
* executor process is gone, so they're unrecoverable. The run itself is
|
|
@@ -1530,8 +1576,36 @@ var WorkflowEngine = class {
|
|
|
1530
1576
|
* a run with one abandoned job and one job legitimately parked on a human
|
|
1531
1577
|
* approval or a child workflow stays 'running' (only the abandoned job is
|
|
1532
1578
|
* failed) until the parked one resolves.
|
|
1579
|
+
*
|
|
1580
|
+
* Before touching anything, this checks the shared daemon liveness lease
|
|
1581
|
+
* (see `StateStore.getDaemonLease`/`writeDaemonLease`). Being CALLED is not
|
|
1582
|
+
* by itself evidence of a restart — that's exactly what let a stray or
|
|
1583
|
+
* duplicate daemon process (e.g. a second launch racing a daemon that never
|
|
1584
|
+
* actually restarted) force-fail a genuinely in-flight run out from under
|
|
1585
|
+
* a still-alive sibling instance, purely because it, too, executed its own
|
|
1586
|
+
* one-time startup sweep against the same shared state store. A lease still
|
|
1587
|
+
* fresh under a DIFFERENT instanceId is real, recent (within
|
|
1588
|
+
* `DAEMON_LEASE_TTL_MS`) evidence that some other process is currently
|
|
1589
|
+
* alive and heartbeating — in that case this pass must not run at all, no
|
|
1590
|
+
* matter how long any individual job has been "running": elapsed job
|
|
1591
|
+
* runtime is never a substitute for genuine restart evidence, and is not
|
|
1592
|
+
* even consulted here.
|
|
1533
1593
|
*/
|
|
1534
1594
|
async cleanupStaleRuns() {
|
|
1595
|
+
const priorLease = await this.stateStore.getDaemonLease();
|
|
1596
|
+
if (priorLease && priorLease.instanceId !== this.instanceId) {
|
|
1597
|
+
this.logger.error(
|
|
1598
|
+
"cleanupStaleRuns: a daemon liveness lease held by a different instance is still fresh \u2014 skipping stale-run cleanup entirely to avoid abandoning runs that may genuinely still be in flight under that instance. If no other workflow daemon is actually running, this lease is simply stale past its own restart window and the next daemon start will clean up normally.",
|
|
1599
|
+
void 0,
|
|
1600
|
+
{
|
|
1601
|
+
thisInstanceId: this.instanceId,
|
|
1602
|
+
otherInstanceId: priorLease.instanceId,
|
|
1603
|
+
otherHeartbeatAt: priorLease.heartbeatAt
|
|
1604
|
+
}
|
|
1605
|
+
);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
await this.stateStore.writeDaemonLease(this.instanceId);
|
|
1535
1609
|
const runIds = await this.stateStore.getAllRunIds();
|
|
1536
1610
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1537
1611
|
let count = 0;
|
|
@@ -3463,6 +3537,6 @@ var JobManager = class {
|
|
|
3463
3537
|
}
|
|
3464
3538
|
};
|
|
3465
3539
|
|
|
3466
|
-
export { ArtifactMerger, ConcurrencyManager, EnvSecretProvider, EventBusBridge, JobManager, ManifestConverter, ManifestScanner, RunCoordinator, RunSnapshotStorage, Scheduler, StateStore, WorkflowEngine, WorkflowLoader, WorkflowRegistry, WorkflowRepository, WorkflowService, calculateBackoff, createDefaultSecretProvider, shouldRetry };
|
|
3540
|
+
export { ArtifactMerger, ConcurrencyManager, DAEMON_LEASE_HEARTBEAT_INTERVAL_MS, DAEMON_LEASE_TTL_MS, EnvSecretProvider, EventBusBridge, JobManager, ManifestConverter, ManifestScanner, RunCoordinator, RunSnapshotStorage, Scheduler, StateStore, WorkflowEngine, WorkflowLoader, WorkflowRegistry, WorkflowRepository, WorkflowService, calculateBackoff, createDefaultSecretProvider, shouldRetry };
|
|
3467
3541
|
//# sourceMappingURL=index.js.map
|
|
3468
3542
|
//# sourceMappingURL=index.js.map
|