@voltro/workflow 0.33.0 → 0.35.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/CHANGELOG.md +1968 -0
- package/dist/{cluster-CyIyBO39.js → cluster-CVyPUB9X.js} +43 -34
- package/dist/cluster.js +1 -1
- package/dist/clusterTestSuite.js +29 -29
- package/dist/index.d.ts +701 -7
- package/dist/index.js +3 -3
- package/dist/{primitives-Bp98_F5L.js → primitives-CyQZeT1t.js} +191 -92
- package/dist/primitives.d.ts +239 -5
- package/dist/primitives.js +2 -2
- package/dist/src-DPkbBZDD.js +2501 -0
- package/package.json +3 -3
- package/dist/src-CP61KZ9V.js +0 -1999
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Cause } from 'effect';
|
|
2
2
|
import { Context } from 'effect';
|
|
3
|
+
import { DataStore } from '@voltro/database';
|
|
3
4
|
import { DurableClock as durableClock } from '@effect/workflow';
|
|
4
5
|
import { DurableQueue as durableQueueModule } from '@effect/workflow';
|
|
5
6
|
import { DurableRateLimiter as durableRateLimiterModule } from '@effect/workflow';
|
|
@@ -100,6 +101,15 @@ export declare interface AdmissionDrainDeps {
|
|
|
100
101
|
readonly executionId: string;
|
|
101
102
|
readonly startedAt: number;
|
|
102
103
|
}>>;
|
|
104
|
+
/**
|
|
105
|
+
* EVERY registered workflow name — not just the controlled ones in
|
|
106
|
+
* `controls`. A `mode: 'delayed'` row is legitimate for a workflow with NO
|
|
107
|
+
* flow control, so "missing from `controls`" cannot mean "orphan" for it the
|
|
108
|
+
* way it does for a debounce row. Absent → a delayed row is assumed
|
|
109
|
+
* registered, and an unknown name surfaces as a per-row failure (attempts +
|
|
110
|
+
* lastError climb on the row) instead of a silent abandon.
|
|
111
|
+
*/
|
|
112
|
+
readonly registeredWorkflows?: ReadonlySet<string>;
|
|
103
113
|
readonly log?: DrainLogger;
|
|
104
114
|
readonly now?: () => number;
|
|
105
115
|
}
|
|
@@ -446,6 +456,54 @@ export declare interface AwaitUpdateOptions<A, B = A> {
|
|
|
446
456
|
readonly timeoutMs?: number;
|
|
447
457
|
}
|
|
448
458
|
|
|
459
|
+
export declare const BUDGET_HOLDS_TABLE = "_voltro_budget_holds";
|
|
460
|
+
|
|
461
|
+
/** Raised into the workflow when a hold outlived its ceiling. */
|
|
462
|
+
export declare class BudgetHoldExpired extends Error {
|
|
463
|
+
readonly budget: string;
|
|
464
|
+
readonly holdKey: string;
|
|
465
|
+
constructor(budget: string, holdKey: string, timeoutMs: number);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** How many holds this (execution, step, budget) has already taken. */
|
|
469
|
+
export declare const budgetHoldGeneration: (store: DataStore, input: {
|
|
470
|
+
readonly executionId: string;
|
|
471
|
+
readonly stepName: string;
|
|
472
|
+
readonly budget: string;
|
|
473
|
+
}) => Promise<number>;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The replay-stable identity of one hold within one run.
|
|
477
|
+
*
|
|
478
|
+
* The `generation` is the part that is easy to leave out and fatal to leave out.
|
|
479
|
+
* A run can be held MORE THAN ONCE at the same step: it is released, the body
|
|
480
|
+
* replays, it re-reads the counter, and the budget is still over — because the
|
|
481
|
+
* window rolled for a tenant that immediately spent again, or because an
|
|
482
|
+
* operator lifted the wrong hold. With a generation-free key the second park
|
|
483
|
+
* would await the deferred the FIRST release had already completed, get an
|
|
484
|
+
* instant resolution, and spend straight through a budget that was still
|
|
485
|
+
* exceeded — the AI-Flows constant-signal-name collision, one level up and with
|
|
486
|
+
* money on the other side of it.
|
|
487
|
+
*
|
|
488
|
+
* `generation` is the number of holds already recorded for this
|
|
489
|
+
* (execution, step, budget), which is durable, monotone, and unchanged by a
|
|
490
|
+
* crash-replay that took no new hold — so it is stable exactly when replay
|
|
491
|
+
* safety needs it to be and different exactly when correctness needs it to be.
|
|
492
|
+
*/
|
|
493
|
+
export declare const budgetHoldKey: (input: {
|
|
494
|
+
readonly executionId: string;
|
|
495
|
+
readonly stepName: string;
|
|
496
|
+
readonly budget: string;
|
|
497
|
+
readonly generation: number;
|
|
498
|
+
}) => string;
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The signal name ONE hold parks on. Derived from the hold key, so the releaser
|
|
502
|
+
* reconstructs it from the row with no shared state — the same discipline
|
|
503
|
+
* `suspendingSignalDeferredName` and `humanResponseSignalName` follow.
|
|
504
|
+
*/
|
|
505
|
+
export declare const budgetHoldSignalName: (holdKey: string) => string;
|
|
506
|
+
|
|
449
507
|
export declare const CANCEL_ON_WATERMARK = "cancelOn";
|
|
450
508
|
|
|
451
509
|
export declare interface CancelDecision {
|
|
@@ -659,6 +717,29 @@ export declare interface CompleteSuspendingSignalInput {
|
|
|
659
717
|
* engine-assigned executionId, not our row id. */
|
|
660
718
|
export declare const CurrentWorkflowExecutionId: FiberRef.FiberRef<string | undefined>;
|
|
661
719
|
|
|
720
|
+
/**
|
|
721
|
+
* FiberRef carrying the patch markers that were declared on the workflow at the
|
|
722
|
+
* moment THIS RUN STARTED — read back from `_voltro_workflow_runs.
|
|
723
|
+
* workflowPatches` on a resume, taken from the current declaration on a first
|
|
724
|
+
* start. This is what makes `patch('id')` (see `primitives.ts`) answer the
|
|
725
|
+
* Temporal question "was this marker in effect when this execution began?"
|
|
726
|
+
* rather than "is it in the code I am running now".
|
|
727
|
+
*
|
|
728
|
+
* Empty outside a recorded workflow body, so `patch()` is `false` there — the
|
|
729
|
+
* safe direction: an un-recorded context gets the PRE-patch branch.
|
|
730
|
+
*/
|
|
731
|
+
export declare const CurrentWorkflowPatches: FiberRef.FiberRef<readonly string[]>;
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* The tracker for the CURRENT body entry, or `undefined` when there is nothing
|
|
735
|
+
* to compare against.
|
|
736
|
+
*
|
|
737
|
+
* Installed by `wrapWorkflowExecuteWithRunRecording` ONLY on a re-entry (a run
|
|
738
|
+
* whose row already exists). A first entry has no prior shape, so the whole
|
|
739
|
+
* mechanism costs one `FiberRef` read per step and nothing else.
|
|
740
|
+
*/
|
|
741
|
+
export declare const CurrentWorkflowReplayShape: FiberRef.FiberRef<ReplayShapeTracker | undefined>;
|
|
742
|
+
|
|
662
743
|
/** FiberRef carrying the active workflow run's id. The outer
|
|
663
744
|
* `wrapWithRunRecording` in dev.ts sets it via
|
|
664
745
|
* `Effect.locally(CurrentWorkflowRunId, runId)`. */
|
|
@@ -697,6 +778,11 @@ declare type DecodedPayload<P> = P extends Schema.Struct.Fields ? Schema.Struct.
|
|
|
697
778
|
readonly Type: infer T;
|
|
698
779
|
} ? T : never;
|
|
699
780
|
|
|
781
|
+
/** Default ceiling on a hold — 7 days, matching the HITL park bound. The wait
|
|
782
|
+
* itself is free (no worker is held), and a run that gives up on a monthly
|
|
783
|
+
* budget after an hour is a run somebody has to re-drive by hand. */
|
|
784
|
+
export declare const DEFAULT_BUDGET_HOLD_TIMEOUT_MS: number;
|
|
785
|
+
|
|
700
786
|
/** How far back a COLD start looks when no watermark exists yet — one hour.
|
|
701
787
|
* Not the whole journal: a first boot against an old deployment would
|
|
702
788
|
* otherwise read every event ever published to decide about runs that mostly
|
|
@@ -727,6 +813,44 @@ export declare const DEFAULT_EVENT_BATCH = 500;
|
|
|
727
813
|
*/
|
|
728
814
|
export declare const DEFAULT_LEASE_MS: number;
|
|
729
815
|
|
|
816
|
+
/** Consecutive runner deaths a single run may cost before it is parked. Three,
|
|
817
|
+
* not one: a single crash is frequently the node and not the payload, and
|
|
818
|
+
* parking on it would turn every spot-instance eviction into an operator
|
|
819
|
+
* ticket. Three in a row is a property of the work. */
|
|
820
|
+
export declare const DEFAULT_MAX_RUN_RECLAIMS = 3;
|
|
821
|
+
|
|
822
|
+
/** Recorded step rows the replay-shape snapshot will load for one run. */
|
|
823
|
+
export declare const DEFAULT_REPLAY_SHAPE_LIMIT = 2000;
|
|
824
|
+
|
|
825
|
+
/** How long a live run may make no progress before it is reported.
|
|
826
|
+
*
|
|
827
|
+
* Thirty minutes, not five: the population this must not drown in is
|
|
828
|
+
* long-running steps (a big import, a slow provider), and a threshold under
|
|
829
|
+
* the longest legitimate step turns the feature into noise. Runs that wait
|
|
830
|
+
* legitimately for HOURS wait on a durable timer, which is excluded outright,
|
|
831
|
+
* so the threshold only has to clear the slowest single step. */
|
|
832
|
+
export declare const DEFAULT_STALL_AFTER_MS: number;
|
|
833
|
+
|
|
834
|
+
/** Lifecycle events read per candidate. Enough to see the timer handshake and
|
|
835
|
+
* a prior `run-stalled` on a run that is, by construction, not doing much. */
|
|
836
|
+
export declare const DEFAULT_STALL_EVENT_LOOKBACK = 20;
|
|
837
|
+
|
|
838
|
+
/** Live runs examined per tick. Bounded for the reason every sweep here is:
|
|
839
|
+
* oldest-first, so a deployment past the bound makes progress on the next
|
|
840
|
+
* tick instead of re-reading the same page. */
|
|
841
|
+
export declare const DEFAULT_STALL_RUN_PAGE = 200;
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Default threshold: a wait DECLARED longer than this suggests the suspending
|
|
845
|
+
* variant. Five minutes — well above any interactive approval, well below the
|
|
846
|
+
* hours-long HITL waits the suspending variant is for; the poll's own backoff
|
|
847
|
+
* (≤ 5 s) makes the polling cost of a shorter wait negligible.
|
|
848
|
+
*/
|
|
849
|
+
export declare const DEFAULT_SUSPEND_HINT_MS: number;
|
|
850
|
+
|
|
851
|
+
/** 24 h — see {@link WorkflowScheduleDeclaration.maxRuntime}. */
|
|
852
|
+
export declare const DEFAULT_WORKFLOW_SCHEDULE_MAX_RUNTIME_MS: number;
|
|
853
|
+
|
|
730
854
|
export declare type DeferMode = DeferringControl | 'paused';
|
|
731
855
|
|
|
732
856
|
/**
|
|
@@ -901,6 +1025,9 @@ export declare const getCurrentWorkflowRunId: () => Effect.Effect<string | undef
|
|
|
901
1025
|
* the path it took before this feature existed. */
|
|
902
1026
|
export declare const getWorkflowFlowControl: (value: unknown) => ResolvedFlowControl | undefined;
|
|
903
1027
|
|
|
1028
|
+
/** The resolved schedule attached to a workflow definition, or undefined. */
|
|
1029
|
+
export declare const getWorkflowSchedule: (value: unknown) => ResolvedWorkflowSchedule | undefined;
|
|
1030
|
+
|
|
904
1031
|
export declare const getWorkflowVersionMetadata: (value: unknown) => WorkflowVersionMetadata;
|
|
905
1032
|
|
|
906
1033
|
/** `true` when this workflow declares at least one `cancelOn` entry — i.e. when
|
|
@@ -917,6 +1044,13 @@ export declare const hasCancelOn: (control: ResolvedFlowControl | undefined) =>
|
|
|
917
1044
|
* two writes per start for a decision that is always "admit". */
|
|
918
1045
|
export declare const hasFlowControl: (control: ResolvedFlowControl | undefined) => boolean;
|
|
919
1046
|
|
|
1047
|
+
/** The distinct budgets currently holding at least one run — what a poller
|
|
1048
|
+
* iterates so it asks about nothing else. */
|
|
1049
|
+
export declare const heldBudgets: (store: DataStore) => Promise<ReadonlyArray<{
|
|
1050
|
+
readonly budget: string;
|
|
1051
|
+
readonly tenantId: string | null;
|
|
1052
|
+
}>>;
|
|
1053
|
+
|
|
920
1054
|
export declare interface InMemoryRecorder {
|
|
921
1055
|
readonly layer: Layer.Layer<WorkflowRunRecorder>;
|
|
922
1056
|
readonly readSteps: () => ReadonlyArray<RecordedStep>;
|
|
@@ -1019,10 +1153,28 @@ export declare const linkExecution: (store: AdmissionDataStore, ledgerId: string
|
|
|
1019
1153
|
|
|
1020
1154
|
export declare const makeInMemoryRecorder: () => InMemoryRecorder;
|
|
1021
1155
|
|
|
1156
|
+
/**
|
|
1157
|
+
* Build a tracker from the step rows this run recorded on EARLIER body entries.
|
|
1158
|
+
*
|
|
1159
|
+
* Pass the rows as they are: one row per step ATTEMPT, so a step that retried
|
|
1160
|
+
* three times contributes three. Counting rows rather than distinct names is
|
|
1161
|
+
* what makes the per-name sequence rule work, and the asymmetry is deliberate —
|
|
1162
|
+
* only reaching a name MORE often than recorded is a finding. Fewer is normal
|
|
1163
|
+
* (a crash mid-step records two rows for one logical reach).
|
|
1164
|
+
*/
|
|
1165
|
+
export declare const makeReplayShapeTracker: (recordedRows: ReadonlyArray<RecordedStepShape>) => ReplayShapeTracker;
|
|
1166
|
+
|
|
1022
1167
|
export declare const makeWorkflowRunRecorder: (options: WorkflowRunRecorderOptions) => WorkflowRunRecorderService;
|
|
1023
1168
|
|
|
1024
1169
|
export declare const makeWorkflowUpdateId: () => string;
|
|
1025
1170
|
|
|
1171
|
+
/**
|
|
1172
|
+
* Emit the steering hint when a polling `awaitSignal` declares a wait longer
|
|
1173
|
+
* than the threshold. Returns `true` when it hinted — the testable fact; the
|
|
1174
|
+
* once-per-name set is module state on purpose (one process, one hint).
|
|
1175
|
+
*/
|
|
1176
|
+
export declare const maybeHintSuspendingSignal: (input: SuspendHintInput) => boolean;
|
|
1177
|
+
|
|
1026
1178
|
/** The newest event instant in a batch, or `undefined` for an empty one.
|
|
1027
1179
|
* The watermark advances to THIS, never to `now`: advancing to wall-clock
|
|
1028
1180
|
* would skip anything written with an older `occurredAt` by a transaction
|
|
@@ -1051,6 +1203,23 @@ export declare const newestEventAt: (events: ReadonlyArray<DomainEventRow>) => D
|
|
|
1051
1203
|
*/
|
|
1052
1204
|
export declare const nextSlotAt: (recentAdmissions: ReadonlyArray<number>, limit: number, periodMs: number, now: number) => number | undefined;
|
|
1053
1205
|
|
|
1206
|
+
/** The `nondeterminism-suspected` event body. Kept here so the two emit sites
|
|
1207
|
+
* (the step wrapper, and the run wrapper's `settle`) cannot drift. */
|
|
1208
|
+
export declare const nondeterminismEventPayload: (finding: NondeterminismFinding) => Record<string, unknown>;
|
|
1209
|
+
|
|
1210
|
+
export declare interface NondeterminismFinding {
|
|
1211
|
+
readonly kind: NondeterminismKind;
|
|
1212
|
+
readonly stepName: string;
|
|
1213
|
+
/** How many rows this run recorded for `stepName` on EARLIER body entries. */
|
|
1214
|
+
readonly recorded: number;
|
|
1215
|
+
/** How many times the CURRENT body entry reached it. */
|
|
1216
|
+
readonly reached: number;
|
|
1217
|
+
/** One sentence an operator can act on, without opening the code. */
|
|
1218
|
+
readonly detail: string;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export declare type NondeterminismKind = 'unreached-step' | 'extra-step-occurrence';
|
|
1222
|
+
|
|
1054
1223
|
declare type NormaliseWorkflowMessages<M extends WorkflowMessageSchemas | undefined> = {
|
|
1055
1224
|
readonly signals: M extends {
|
|
1056
1225
|
readonly signals: infer Signals;
|
|
@@ -1058,13 +1227,99 @@ declare type NormaliseWorkflowMessages<M extends WorkflowMessageSchemas | undefi
|
|
|
1058
1227
|
readonly updates: M extends {
|
|
1059
1228
|
readonly updates: infer Updates;
|
|
1060
1229
|
} ? NonNullable<Updates> & Readonly<Record<string, WorkflowMessagePairSchemas>> : {};
|
|
1061
|
-
readonly queries: M extends {
|
|
1062
|
-
readonly queries: infer Queries;
|
|
1063
|
-
} ? NonNullable<Queries> & Readonly<Record<string, WorkflowMessagePairSchemas>> : {};
|
|
1064
1230
|
};
|
|
1065
1231
|
|
|
1232
|
+
/** `workflowPatches` comes back off a `json()` column, so it is whatever the
|
|
1233
|
+
* dialect's driver decoded — an array, a JSON string, or null. Anything that
|
|
1234
|
+
* is not a list of strings means "no markers", which is the safe answer:
|
|
1235
|
+
* `patch()` then reports the PRE-patch branch. */
|
|
1236
|
+
export declare const normaliseWorkflowPatches: (value: unknown) => ReadonlyArray<string> | undefined;
|
|
1237
|
+
|
|
1066
1238
|
export declare const noteIntentAttempt: (store: AdmissionDataStore, intent: PendingIntent, error: string, now: number) => Promise<void>;
|
|
1067
1239
|
|
|
1240
|
+
/**
|
|
1241
|
+
* Park a `start(name, payload, { at })` as a DURABLE pending row.
|
|
1242
|
+
*
|
|
1243
|
+
* A delayed start that only exists in a `setTimeout` dies with the process, so
|
|
1244
|
+
* it is a row in `_voltro_workflow_pending` (`mode: 'delayed'`) that the
|
|
1245
|
+
* coordinated drainer fires when `dueAt` arrives. Each call is its OWN row —
|
|
1246
|
+
* two delayed starts never collapse, because unlike debounce nothing about
|
|
1247
|
+
* `{ at }` says the second supersedes the first.
|
|
1248
|
+
*
|
|
1249
|
+
* What happens at `dueAt` is an ARRIVAL, not a bypass: the drainer hands the
|
|
1250
|
+
* row to the same `admitStart` a live `ctx.workflows.start` goes through, so a
|
|
1251
|
+
* workflow's declared debounce/singleton/rateLimit judge the start as of the
|
|
1252
|
+
* moment it comes due. `{ at }` delays the arrival; it never outranks a control.
|
|
1253
|
+
*/
|
|
1254
|
+
export declare const parkDelayedStart: (input: ParkDelayedStartInput) => Promise<{
|
|
1255
|
+
readonly intentId: string;
|
|
1256
|
+
readonly dueAt: number;
|
|
1257
|
+
}>;
|
|
1258
|
+
|
|
1259
|
+
export declare interface ParkDelayedStartInput {
|
|
1260
|
+
readonly store: AdmissionDataStore;
|
|
1261
|
+
readonly workflowName: string;
|
|
1262
|
+
readonly payload: unknown;
|
|
1263
|
+
readonly callerContext: unknown;
|
|
1264
|
+
readonly tenantId: string | null;
|
|
1265
|
+
/** Epoch ms the start becomes an ARRIVAL. Must be in the future — the facade
|
|
1266
|
+
* starts a past-`at` immediately and never calls in here. */
|
|
1267
|
+
readonly at: number;
|
|
1268
|
+
readonly now: number;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Is a named change marker in effect for the run currently executing?
|
|
1273
|
+
*
|
|
1274
|
+
* This is the escape valve for the versioning trap. `compatibleWith` is
|
|
1275
|
+
* all-or-nothing: leave an old version out and every in-flight run on it is
|
|
1276
|
+
* terminally failed; leave it in and the old runs replay against the new body
|
|
1277
|
+
* with no detection. `patch` is the third option — the body itself branches, so
|
|
1278
|
+
* runs that started BEFORE the change keep taking the old path to completion
|
|
1279
|
+
* while new runs take the new one.
|
|
1280
|
+
*
|
|
1281
|
+
* ```ts
|
|
1282
|
+
* export const Charge = workflow({
|
|
1283
|
+
* name: 'billing.charge',
|
|
1284
|
+
* payload: { orderId: Schema.String },
|
|
1285
|
+
* idempotencyKey: ({ orderId }) => `billing.charge:${orderId}`,
|
|
1286
|
+
* patches: ['split-tax-calculation'], // ← declare it
|
|
1287
|
+
* })
|
|
1288
|
+
*
|
|
1289
|
+
* // inside the body:
|
|
1290
|
+
* if (yield* patch('split-tax-calculation')) {
|
|
1291
|
+
* const net = yield* step({ name: 'net-total', execute })
|
|
1292
|
+
* const tax = yield* step({ name: 'tax', execute })
|
|
1293
|
+
* return net + tax
|
|
1294
|
+
* }
|
|
1295
|
+
* return yield* step({ name: 'total', execute }) // the pre-patch path
|
|
1296
|
+
* ```
|
|
1297
|
+
*
|
|
1298
|
+
* ── The answer is pinned to the RUN, not to the code ────────────────────────
|
|
1299
|
+
*
|
|
1300
|
+
* `patches` is stamped onto `_voltro_workflow_runs.workflowPatches` when the
|
|
1301
|
+
* run starts and read back from that row on every resume. So a run started
|
|
1302
|
+
* before you added the marker answers `false` for the rest of its life,
|
|
1303
|
+
* however many times it replays and whatever the deployed code says — which is
|
|
1304
|
+
* the property that makes the branch deterministic across a redeploy.
|
|
1305
|
+
*
|
|
1306
|
+
* Under `@effect/workflow` the naive implementation does the opposite: an
|
|
1307
|
+
* activity that is ABSENT from an old run's journal EXECUTES on replay (the
|
|
1308
|
+
* journal is keyed name/attempt), so a marker journaled by an activity would
|
|
1309
|
+
* answer `true` for exactly the old runs it must answer `false` for. Reading
|
|
1310
|
+
* the run row sidesteps that entirely.
|
|
1311
|
+
*
|
|
1312
|
+
* ── Retiring a patch ───────────────────────────────────────────────────────
|
|
1313
|
+
*
|
|
1314
|
+
* Once no run predating the marker can still be in flight, delete the old
|
|
1315
|
+
* branch and the entry from `patches`. Runs that stamped it keep the marker on
|
|
1316
|
+
* their row for the audit trail; `patch()` simply stops being called.
|
|
1317
|
+
*
|
|
1318
|
+
* Outside a recorded workflow body (a unit test, a bare `step()` call) this is
|
|
1319
|
+
* `false` — the pre-patch branch, which is the safe direction.
|
|
1320
|
+
*/
|
|
1321
|
+
export declare const patch: (id: string) => Effect.Effect<boolean>;
|
|
1322
|
+
|
|
1068
1323
|
export declare const PAUSES_TABLE = "_voltro_workflow_pauses";
|
|
1069
1324
|
|
|
1070
1325
|
export declare const pauseWorkflow: (store: AdmissionDataStore, workflowName: string, by: string | null, reason: string | null) => Promise<void>;
|
|
@@ -1074,13 +1329,27 @@ declare type PayloadSchemaOf<P> = P extends Schema.Struct.Fields ? Schema.Struct
|
|
|
1074
1329
|
|
|
1075
1330
|
export declare const PENDING_TABLE = "_voltro_workflow_pending";
|
|
1076
1331
|
|
|
1332
|
+
/** Every hold currently parked — the operator's view, and the poller's input. */
|
|
1333
|
+
export declare const pendingBudgetHolds: (store: DataStore, options?: {
|
|
1334
|
+
readonly budget?: string;
|
|
1335
|
+
}) => Promise<ReadonlyArray<{
|
|
1336
|
+
readonly id: string;
|
|
1337
|
+
readonly holdKey: string;
|
|
1338
|
+
readonly budget: string;
|
|
1339
|
+
readonly tenantId: string | null;
|
|
1340
|
+
readonly executionId: string;
|
|
1341
|
+
readonly workflowName: string;
|
|
1342
|
+
readonly stepName: string;
|
|
1343
|
+
readonly heldAt: string;
|
|
1344
|
+
}>>;
|
|
1345
|
+
|
|
1077
1346
|
/** A pending row in domain shape. */
|
|
1078
1347
|
export declare interface PendingIntent {
|
|
1079
1348
|
readonly id: string;
|
|
1080
1349
|
readonly tenantId: string | null;
|
|
1081
1350
|
readonly workflowName: string;
|
|
1082
1351
|
readonly controlKey: string;
|
|
1083
|
-
readonly mode: 'debounce' | 'batch' | 'throttle' | 'concurrency' | 'paused';
|
|
1352
|
+
readonly mode: 'debounce' | 'batch' | 'throttle' | 'concurrency' | 'paused' | 'delayed';
|
|
1084
1353
|
readonly payload: unknown;
|
|
1085
1354
|
readonly callerContext: unknown;
|
|
1086
1355
|
readonly priority: number;
|
|
@@ -1295,6 +1564,44 @@ export declare interface RecordedStep {
|
|
|
1295
1564
|
durationMs: number | null;
|
|
1296
1565
|
}
|
|
1297
1566
|
|
|
1567
|
+
/** The slice of a `_voltro_workflow_run_steps` row this needs. */
|
|
1568
|
+
export declare interface RecordedStepShape {
|
|
1569
|
+
readonly stepName: string;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Resume every run parked on `budget`.
|
|
1574
|
+
*
|
|
1575
|
+
* Requires the `WorkflowEngine` — run it through the same runner that executes
|
|
1576
|
+
* workflows (`workflowRuntime.runPromise`), exactly like `completeSuspendingSignal`.
|
|
1577
|
+
*
|
|
1578
|
+
* A release WAKES a run; it does not authorise a spend. The resumed body
|
|
1579
|
+
* re-reads the budget and parks again on a fresh generation if it is still over
|
|
1580
|
+
* — see `suspendForBudget`. So this is safe to call optimistically (on a
|
|
1581
|
+
* `recovered` signal, or from an operator's console) without knowing whether
|
|
1582
|
+
* every held run will actually proceed.
|
|
1583
|
+
*
|
|
1584
|
+
* The row is closed BEFORE the deferred is completed, deliberately: the resumed
|
|
1585
|
+
* body counts existing holds to derive its next generation, and a row still
|
|
1586
|
+
* reading `releasedAt: null` would make the re-park collide with the hold it was
|
|
1587
|
+
* just released from.
|
|
1588
|
+
*/
|
|
1589
|
+
export declare const releaseBudgetHolds: (input: ReleaseBudgetHoldsInput) => Effect.Effect<{
|
|
1590
|
+
readonly released: number;
|
|
1591
|
+
}, never, WorkflowEngine.WorkflowEngine>;
|
|
1592
|
+
|
|
1593
|
+
export declare interface ReleaseBudgetHoldsInput {
|
|
1594
|
+
readonly store: DataStore;
|
|
1595
|
+
/** Which budget regained headroom. */
|
|
1596
|
+
readonly budget: string;
|
|
1597
|
+
/** Restrict to one tenant. Omitted → every tenant held on this budget (a
|
|
1598
|
+
* window rollover is per-tenant in the accountant but the counter key rotates
|
|
1599
|
+
* for everyone, so both shapes are legitimate). */
|
|
1600
|
+
readonly tenantId?: string | null | undefined;
|
|
1601
|
+
/** `released` = the budget itself recovered; `lifted` = an operator decided. */
|
|
1602
|
+
readonly outcome?: 'released' | 'lifted';
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1298
1605
|
/**
|
|
1299
1606
|
* Free the slot an execution holds. Called when a run reaches a terminal state.
|
|
1300
1607
|
*
|
|
@@ -1304,6 +1611,37 @@ export declare interface RecordedStep {
|
|
|
1304
1611
|
*/
|
|
1305
1612
|
export declare const releaseLease: (store: AdmissionDataStore, executionId: string, now: number) => Promise<boolean>;
|
|
1306
1613
|
|
|
1614
|
+
export declare interface ReplayShapeTracker {
|
|
1615
|
+
/**
|
|
1616
|
+
* The body reached `stepName`. Returns a finding the FIRST time that reach
|
|
1617
|
+
* is anomalous, `undefined` otherwise — so a step in a hot loop reports once
|
|
1618
|
+
* rather than once per iteration.
|
|
1619
|
+
*/
|
|
1620
|
+
readonly reach: (stepName: string) => NondeterminismFinding | undefined;
|
|
1621
|
+
/**
|
|
1622
|
+
* The body ran to a COMPLETE outcome. Returns the set-membership findings —
|
|
1623
|
+
* recorded names the body never reached.
|
|
1624
|
+
*
|
|
1625
|
+
* Call this ONLY on success/failure, never on a suspend: a suspended body
|
|
1626
|
+
* stopped partway through on purpose, so everything after the suspension
|
|
1627
|
+
* point is legitimately unreached.
|
|
1628
|
+
*/
|
|
1629
|
+
readonly settle: () => ReadonlyArray<NondeterminismFinding>;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Emit findings from INSIDE the workflow body, where the run id and the
|
|
1634
|
+
* recorder are only reachable through the fiber context.
|
|
1635
|
+
*
|
|
1636
|
+
* Best-effort in the strongest sense: no recorder, no run id, or a rejecting
|
|
1637
|
+
* write all resolve to "nothing happened". A tripwire that can break the run it
|
|
1638
|
+
* is watching is not a tripwire.
|
|
1639
|
+
*/
|
|
1640
|
+
export declare const reportNondeterminism: (findings: ReadonlyArray<NondeterminismFinding>) => Effect.Effect<void>;
|
|
1641
|
+
|
|
1642
|
+
/** Test seam: forget which (workflow, signal) pairs have been hinted. */
|
|
1643
|
+
export declare const resetSuspendHintsForTest: () => void;
|
|
1644
|
+
|
|
1307
1645
|
/** Resolve every declared key against one payload. Pure; throws only
|
|
1308
1646
|
* {@link FlowControlKeyError}. */
|
|
1309
1647
|
export declare const resolveAdmissionKeys: (control: ResolvedFlowControl, payload: unknown) => AdmissionKeys;
|
|
@@ -1376,6 +1714,35 @@ export declare interface ResolvedFlowControl {
|
|
|
1376
1714
|
readonly cancelOn?: ReadonlyArray<ResolvedCancelOn>;
|
|
1377
1715
|
}
|
|
1378
1716
|
|
|
1717
|
+
/** The declaration with defaults filled in — what the CLI lowering reads. */
|
|
1718
|
+
export declare interface ResolvedWorkflowSchedule {
|
|
1719
|
+
readonly cron: string;
|
|
1720
|
+
readonly timezone: string;
|
|
1721
|
+
readonly payload?: unknown | ((fire: {
|
|
1722
|
+
readonly scheduledAt: Date;
|
|
1723
|
+
}) => unknown | Promise<unknown>);
|
|
1724
|
+
readonly onOverlap: WorkflowScheduleOverlap;
|
|
1725
|
+
readonly backfill: 'skip' | 'latest' | 'all';
|
|
1726
|
+
readonly maxRuntimeMs: number;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Read the run-guard knobs from the environment, for the boot paths to spread
|
|
1731
|
+
* into {@link wrapWorkflowExecuteWithRunRecording}. Mirrors
|
|
1732
|
+
* `resolveFailoverTuning` in `clusterLayer.ts` — same shape, same
|
|
1733
|
+
* malformed-value-is-ignored rule, so an operator learns one convention:
|
|
1734
|
+
*
|
|
1735
|
+
* - `VOLTRO_WORKFLOW_MAX_RECLAIMS` (→ `maxRunReclaims`, default 3)
|
|
1736
|
+
* - `VOLTRO_WORKFLOW_REPLAY_SHAPE_LIMIT` (→ `replayShapeLimit`, default 2000)
|
|
1737
|
+
*/
|
|
1738
|
+
export declare const resolveRunGuardTuning: () => {
|
|
1739
|
+
maxRunReclaims?: number;
|
|
1740
|
+
replayShapeLimit?: number;
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
/** Env (operator, no rebuild) → config (project) → default. */
|
|
1744
|
+
export declare const resolveSuspendHintMs: () => number;
|
|
1745
|
+
|
|
1379
1746
|
export declare const resolveWorkflowMessageRun: (store: WorkflowMessageStore, target: WorkflowMessageTarget) => Promise<WorkflowResolvedRun>;
|
|
1380
1747
|
|
|
1381
1748
|
export declare const resumeWorkflow: (store: AdmissionDataStore, workflowName: string) => Promise<boolean>;
|
|
@@ -1401,6 +1768,15 @@ export declare const sendWorkflowUpdate: (input: {
|
|
|
1401
1768
|
|
|
1402
1769
|
export declare const serialiseWorkflowRowForWire: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
1403
1770
|
|
|
1771
|
+
/**
|
|
1772
|
+
* Set the threshold from `app.config.ts` (`workflows.suspendSignalHintMs`).
|
|
1773
|
+
* Called by the framework boot (both paths, through `wireFlowControl`); the
|
|
1774
|
+
* env override below still wins, per the standing tunables rule.
|
|
1775
|
+
* Non-positive/non-finite values are ignored; `0` cannot silence the hint —
|
|
1776
|
+
* pass a very large threshold to effectively disable it.
|
|
1777
|
+
*/
|
|
1778
|
+
export declare const setSuspendSignalHintMs: (ms: number | undefined) => void;
|
|
1779
|
+
|
|
1404
1780
|
/**
|
|
1405
1781
|
* Durable sleep — wake time journaled in the cluster. Wrapper around
|
|
1406
1782
|
* `DurableClock.sleep` that emits `timer-set` / `timer-fired` events
|
|
@@ -1441,6 +1817,82 @@ export declare const sleepUntil: (options: {
|
|
|
1441
1817
|
readonly until: Date | number;
|
|
1442
1818
|
}) => Effect.Effect<void, never, never>;
|
|
1443
1819
|
|
|
1820
|
+
/** The narrow store slice this needs. Structural, like `CancelOnDataStore`. */
|
|
1821
|
+
export declare interface StalenessDataStore {
|
|
1822
|
+
query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row>>;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
export declare interface StalenessSweepDeps {
|
|
1826
|
+
readonly store: StalenessDataStore;
|
|
1827
|
+
/**
|
|
1828
|
+
* Record the `run-stalled` event. Wired to the run recorder's `recordEvent`.
|
|
1829
|
+
* Absent → detection without a durable trace (the handler still fires), which
|
|
1830
|
+
* is the right shape for a `voltro doctor` style one-shot check.
|
|
1831
|
+
*/
|
|
1832
|
+
readonly recordEvent?: (input: {
|
|
1833
|
+
readonly runId: string;
|
|
1834
|
+
readonly eventType: 'run-stalled';
|
|
1835
|
+
readonly payload: Record<string, unknown>;
|
|
1836
|
+
}) => Promise<unknown>;
|
|
1837
|
+
/** App-supplied reaction — page someone, open a ticket. Must not throw;
|
|
1838
|
+
* a rejection is collected as a failure and the sweep continues. */
|
|
1839
|
+
readonly onStalled?: (run: StalledRun) => Promise<void>;
|
|
1840
|
+
readonly now?: () => number;
|
|
1841
|
+
readonly log?: {
|
|
1842
|
+
readonly info: (message: string, fields?: Record<string, unknown>) => void;
|
|
1843
|
+
readonly warn: (message: string, fields?: Record<string, unknown>) => void;
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
export declare interface StalenessSweepOptions {
|
|
1848
|
+
readonly stallAfterMs?: number;
|
|
1849
|
+
readonly runPage?: number;
|
|
1850
|
+
readonly eventLookback?: number;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
export declare interface StalenessSweepResult {
|
|
1854
|
+
/** Live runs old enough to be worth examining. */
|
|
1855
|
+
readonly examined: number;
|
|
1856
|
+
/** Newly reported this tick. */
|
|
1857
|
+
readonly stalled: ReadonlyArray<StalledRun>;
|
|
1858
|
+
/** Stalled, but already reported since their last progress — reported
|
|
1859
|
+
* separately rather than folded into `stalled`, so a caller can tell "the
|
|
1860
|
+
* wedge persists" from "nothing is stuck". */
|
|
1861
|
+
readonly alreadyReported: number;
|
|
1862
|
+
/** Excluded because they are inside a durable timer that has not come due. */
|
|
1863
|
+
readonly waitingOnTimer: number;
|
|
1864
|
+
/** The candidate page filled up: more live old runs exist and the next tick
|
|
1865
|
+
* will reach them. Reported rather than inferred — the counts alone cannot
|
|
1866
|
+
* distinguish it from "nothing else was old". */
|
|
1867
|
+
readonly sawFullRunPage: boolean;
|
|
1868
|
+
readonly failures: ReadonlyArray<{
|
|
1869
|
+
readonly subject: string;
|
|
1870
|
+
readonly detail: string;
|
|
1871
|
+
}>;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
export declare interface StalledRun {
|
|
1875
|
+
readonly runId: string;
|
|
1876
|
+
readonly tag: string;
|
|
1877
|
+
readonly executionId: string;
|
|
1878
|
+
readonly status: string;
|
|
1879
|
+
readonly lastProgressAt: Date;
|
|
1880
|
+
readonly idleMs: number;
|
|
1881
|
+
readonly reason: StallReason;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/** Why the sweep believes this run is not moving. Diagnostic, not a taxonomy
|
|
1885
|
+
* the caller should branch on for correctness. */
|
|
1886
|
+
export declare type StallReason =
|
|
1887
|
+
/** Suspended with a `signal-awaited` outstanding — the classic wedge: the
|
|
1888
|
+
* sender never came. */
|
|
1889
|
+
'awaiting-signal'
|
|
1890
|
+
/** Suspended, but not on a signal and not on a timer. */
|
|
1891
|
+
| 'suspended'
|
|
1892
|
+
/** Status `running` with no step or event movement — a step that never
|
|
1893
|
+
* returns, or a runner that vanished without the reclaim path noticing. */
|
|
1894
|
+
| 'no-progress';
|
|
1895
|
+
|
|
1444
1896
|
export declare const step: <R, Success extends Schema.Schema.Any = typeof Schema.Void, Error extends Schema.Schema.All = typeof Schema.Never>(options: StepOptions<R, Success, Error>) => ReturnType<typeof stepModule.make<R, Success, Error>>;
|
|
1445
1897
|
|
|
1446
1898
|
/**
|
|
@@ -1543,6 +1995,72 @@ export declare interface StepRetryPolicy {
|
|
|
1543
1995
|
readonly note?: string;
|
|
1544
1996
|
}
|
|
1545
1997
|
|
|
1998
|
+
/**
|
|
1999
|
+
* Park the run until the budget has headroom.
|
|
2000
|
+
*
|
|
2001
|
+
* Returns (successfully) only when `hasHeadroom` answers true — so the caller
|
|
2002
|
+
* can spend immediately afterwards without a second check of its own. Fails with
|
|
2003
|
+
* `BudgetHoldExpired` once the total `timeoutMs` is used up, having spent
|
|
2004
|
+
* nothing.
|
|
2005
|
+
*
|
|
2006
|
+
* ── Why it is a LOOP, and why the loop is not a spin ─────────────────────────
|
|
2007
|
+
*
|
|
2008
|
+
* Two independent things can end one park: an external release, and the
|
|
2009
|
+
* recheck clock. Neither proves there is headroom (a window can roll over for a
|
|
2010
|
+
* tenant that immediately spends again; an operator can lift the wrong hold), so
|
|
2011
|
+
* each wake re-reads and — if still over — parks AGAIN, on a FRESH generation.
|
|
2012
|
+
*
|
|
2013
|
+
* It is not a spin because every iteration is a real `Workflow.suspend`: the
|
|
2014
|
+
* fiber is released, the wake is a `DurableClock`, and the minimum time between
|
|
2015
|
+
* two iterations is `recheckEveryMs`. The generation is what makes that true —
|
|
2016
|
+
* re-parking on the SAME key would await a deferred the previous release had
|
|
2017
|
+
* already completed, return instantly, and turn this loop into exactly the
|
|
2018
|
+
* busy-wait it looks like.
|
|
2019
|
+
*
|
|
2020
|
+
* The deadline is evaluated against the WALL CLOCK at each wake, from the FIRST
|
|
2021
|
+
* hold's durable `heldAt` rather than a captured `Date.now()`. A replay reading a
|
|
2022
|
+
* later clock therefore gives up sooner-or-equal, never later — which is the
|
|
2023
|
+
* safe direction for a control whose purpose is to stop spending.
|
|
2024
|
+
*/
|
|
2025
|
+
export declare const suspendForBudget: (options: SuspendForBudgetOptions) => Effect.Effect<void, BudgetHoldExpired>;
|
|
2026
|
+
|
|
2027
|
+
export declare interface SuspendForBudgetOptions {
|
|
2028
|
+
readonly store: DataStore;
|
|
2029
|
+
readonly budget: string;
|
|
2030
|
+
readonly tenantId: string | null;
|
|
2031
|
+
readonly executionId: string;
|
|
2032
|
+
readonly workflowName: string;
|
|
2033
|
+
readonly stepName: string;
|
|
2034
|
+
readonly spentMicroUsd?: number | undefined;
|
|
2035
|
+
readonly limitMicroUsd?: number | undefined;
|
|
2036
|
+
/** Total time the run may stay held across ALL parks. Default 7 days;
|
|
2037
|
+
* `Number.POSITIVE_INFINITY` waits forever (a legitimate choice for a
|
|
2038
|
+
* monthly cap). */
|
|
2039
|
+
readonly timeoutMs?: number | undefined;
|
|
2040
|
+
/** Self-recheck cadence. Default 15 minutes. */
|
|
2041
|
+
readonly recheckEveryMs?: number | undefined;
|
|
2042
|
+
/**
|
|
2043
|
+
* Does the budget have headroom NOW?
|
|
2044
|
+
*
|
|
2045
|
+
* Re-read on every wake, and it is the only thing that ends the hold. A
|
|
2046
|
+
* release — from an operator, or from the finops accountant's `recovered`
|
|
2047
|
+
* signal — WAKES the run; it does not authorise the spend. Those are different
|
|
2048
|
+
* facts, and conflating them is how a run resumes into a budget that is still
|
|
2049
|
+
* exceeded and spends anyway.
|
|
2050
|
+
*/
|
|
2051
|
+
readonly hasHeadroom: Effect.Effect<boolean>;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
export declare interface SuspendHintInput {
|
|
2055
|
+
/** Undefined outside an engine context — the hint then keys on the signal
|
|
2056
|
+
* name alone, which still fires once rather than never. */
|
|
2057
|
+
readonly workflowName: string | undefined;
|
|
2058
|
+
readonly signalName: string;
|
|
2059
|
+
/** The wait's DECLARED timeout, ms. */
|
|
2060
|
+
readonly timeoutMs: number;
|
|
2061
|
+
readonly warn?: (message: string) => void;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1546
2064
|
/**
|
|
1547
2065
|
* The stable `DurableDeferred` name for a suspending signal wait. It is a pure
|
|
1548
2066
|
* function of the workflow name + the signal name, so the awaiting body and an
|
|
@@ -1562,6 +2080,15 @@ export declare const suspendingSignalDeferredName: (workflowName: string, signal
|
|
|
1562
2080
|
*/
|
|
1563
2081
|
export declare const sweepCancelOn: (deps: CancelOnSweepDeps, options: CancelOnSweepOptions) => Promise<CancelOnSweepResult>;
|
|
1564
2082
|
|
|
2083
|
+
/**
|
|
2084
|
+
* One staleness tick. Never throws.
|
|
2085
|
+
*
|
|
2086
|
+
* Same contract as `sweepCancelOn`: one malformed row must not take the whole
|
|
2087
|
+
* scheduled tick down and stop detection for every workflow until somebody
|
|
2088
|
+
* notices. Failures are collected and reported.
|
|
2089
|
+
*/
|
|
2090
|
+
export declare const sweepStalledRuns: (deps: StalenessSweepDeps, options?: StalenessSweepOptions) => Promise<StalenessSweepResult>;
|
|
2091
|
+
|
|
1565
2092
|
export declare const truncateWorkflowValue: (value: unknown, bytes?: number) => unknown;
|
|
1566
2093
|
|
|
1567
2094
|
/**
|
|
@@ -1581,6 +2108,22 @@ export declare interface ValidateFlowControlInput {
|
|
|
1581
2108
|
readonly control: (WorkflowFlowControl<never> & WorkflowCancelOnDeclared) | undefined;
|
|
1582
2109
|
}
|
|
1583
2110
|
|
|
2111
|
+
/**
|
|
2112
|
+
* Validate at DEFINITION time — module load, i.e. boot — with a message naming
|
|
2113
|
+
* the workflow and the field. Same posture as `validateFlowControl` and
|
|
2114
|
+
* `defineSchedule`: a cron typo that surfaces as "never fired" three days in
|
|
2115
|
+
* is the failure class this repo keeps paying for.
|
|
2116
|
+
*/
|
|
2117
|
+
export declare const validateWorkflowSchedule: (workflowName: string, declaration: WorkflowScheduleDeclaration<never> | undefined) => ResolvedWorkflowSchedule | undefined;
|
|
2118
|
+
|
|
2119
|
+
/**
|
|
2120
|
+
* One parked run, waiting on budget headroom.
|
|
2121
|
+
*
|
|
2122
|
+
* Reactive so a dashboard can show "3 runs held on `ai-usd`" without polling —
|
|
2123
|
+
* which is the operator's entry point to lifting one.
|
|
2124
|
+
*/
|
|
2125
|
+
export declare const _voltroBudgetHoldsTable: TableLike;
|
|
2126
|
+
|
|
1584
2127
|
/**
|
|
1585
2128
|
* `_voltro_workflow_admissions` — append-only ledger, and the lease register.
|
|
1586
2129
|
*
|
|
@@ -1904,6 +2447,37 @@ export declare interface WorkflowExecuteRecordingOptions extends WorkflowRunReco
|
|
|
1904
2447
|
readonly runId?: string;
|
|
1905
2448
|
readonly status: 'succeeded' | 'failed';
|
|
1906
2449
|
}) => Promise<void>;
|
|
2450
|
+
/**
|
|
2451
|
+
* CRASH-LOOP BREAKER — how many CONSECUTIVE runner deaths this run may cost
|
|
2452
|
+
* the fleet before it is parked instead of reclaimed again.
|
|
2453
|
+
*
|
|
2454
|
+
* The failure it bounds has no error to catch: a step that kills the process
|
|
2455
|
+
* (OOM, a native crash, a `process.exit` in a dependency) leaves the shard
|
|
2456
|
+
* lease to age out, a survivor claims it, and executes the same payload —
|
|
2457
|
+
* forever, across every replica in turn. Poison handling exists at ADMISSION
|
|
2458
|
+
* (`admissionTables.ts`), which is the wrong side of the boundary: the run
|
|
2459
|
+
* was admitted long ago and dies on the way out.
|
|
2460
|
+
*
|
|
2461
|
+
* At the cap the run becomes `suspended` with a `run-crashlooped` event, the
|
|
2462
|
+
* body is NOT entered, and the fleet stops rotating. An operator resume
|
|
2463
|
+
* re-arms it — and any clean re-entry (a durable sleep waking, a redrive)
|
|
2464
|
+
* resets the counter to 0, so this measures a loop, not a lifetime.
|
|
2465
|
+
*
|
|
2466
|
+
* Default {@link DEFAULT_MAX_RUN_RECLAIMS}; `VOLTRO_WORKFLOW_MAX_RECLAIMS`
|
|
2467
|
+
* overrides it via {@link resolveRunGuardTuning}.
|
|
2468
|
+
*/
|
|
2469
|
+
readonly maxRunReclaims?: number;
|
|
2470
|
+
/**
|
|
2471
|
+
* Step rows read to build the replay-shape snapshot for the nondeterminism
|
|
2472
|
+
* tripwire. A run with more recorded steps than this SKIPS the tripwire
|
|
2473
|
+
* entirely rather than comparing against a truncated history — a partial
|
|
2474
|
+
* prior shape would report the steps it failed to load as `unreached-step`,
|
|
2475
|
+
* which is a false positive manufactured by the bound itself.
|
|
2476
|
+
*
|
|
2477
|
+
* Default {@link DEFAULT_REPLAY_SHAPE_LIMIT};
|
|
2478
|
+
* `VOLTRO_WORKFLOW_REPLAY_SHAPE_LIMIT` overrides it.
|
|
2479
|
+
*/
|
|
2480
|
+
readonly replayShapeLimit?: number;
|
|
1907
2481
|
}
|
|
1908
2482
|
|
|
1909
2483
|
/**
|
|
@@ -2051,6 +2625,12 @@ declare type WorkflowFn = <const Name extends string, Payload extends WorkflowPa
|
|
|
2051
2625
|
* type comes from its OWN `schema` — see {@link WorkflowCancelOnList} for
|
|
2052
2626
|
* why that needs a tuple type parameter rather than a plain array. */
|
|
2053
2627
|
readonly cancelOn?: WorkflowCancelOnList<Cancels, DecodedPayload<Payload>>;
|
|
2628
|
+
/** Run this workflow on a cron — the workflow-side spelling of a
|
|
2629
|
+
* `defineSchedule({ workflow })` target, with Temporal's overlap
|
|
2630
|
+
* vocabulary (`skip` / `buffer` / `cancelOther`). Lowered by the CLI into
|
|
2631
|
+
* a real schedule named `workflow:<name>`; see
|
|
2632
|
+
* {@link WorkflowScheduleDeclaration}. */
|
|
2633
|
+
readonly schedule?: WorkflowScheduleDeclaration<DecodedPayload<Payload>>;
|
|
2054
2634
|
} & Omit<WorkflowBaseOptions, 'name' | 'payload' | 'idempotencyKey' | 'success' | 'error'> & WorkflowVersionOptions & {
|
|
2055
2635
|
readonly messages?: Messages;
|
|
2056
2636
|
} & WorkflowFlowControl<DecodedPayload<Payload>>) => workflowModule.Workflow<Name, PayloadSchemaOf<Payload>, Success, Error> & WorkflowMessagesCarrier<NormaliseWorkflowMessages<Messages>>;
|
|
@@ -2066,16 +2646,24 @@ export declare interface WorkflowMessagesCarrier<M extends WorkflowMessagesMetad
|
|
|
2066
2646
|
readonly [WorkflowMessagesProperty]: M;
|
|
2067
2647
|
}
|
|
2068
2648
|
|
|
2649
|
+
/**
|
|
2650
|
+
* The message channels a workflow declares.
|
|
2651
|
+
*
|
|
2652
|
+
* TWO, not three. `queries` used to sit here as a third channel and there was
|
|
2653
|
+
* never a send path for it — no `sendWorkflowQuery`, no `awaitQuery`, nothing
|
|
2654
|
+
* to receive one. It was normalised into metadata, projected into the generated
|
|
2655
|
+
* rpcGroup and carried on the client's `WorkflowState`, so a user got a fully
|
|
2656
|
+
* typed record they could not invoke from anywhere. Signals (fire-and-forget)
|
|
2657
|
+
* and updates (synchronous, with a result) are the channels that exist.
|
|
2658
|
+
*/
|
|
2069
2659
|
export declare interface WorkflowMessageSchemas {
|
|
2070
2660
|
readonly signals?: Readonly<Record<string, Schema.Schema.Any>>;
|
|
2071
2661
|
readonly updates?: Readonly<Record<string, WorkflowMessagePairSchemas>>;
|
|
2072
|
-
readonly queries?: Readonly<Record<string, WorkflowMessagePairSchemas>>;
|
|
2073
2662
|
}
|
|
2074
2663
|
|
|
2075
2664
|
export declare interface WorkflowMessagesMetadata {
|
|
2076
2665
|
readonly signals: Readonly<Record<string, Schema.Schema.Any>>;
|
|
2077
2666
|
readonly updates: Readonly<Record<string, WorkflowMessagePairSchemas>>;
|
|
2078
|
-
readonly queries: Readonly<Record<string, WorkflowMessagePairSchemas>>;
|
|
2079
2667
|
}
|
|
2080
2668
|
|
|
2081
2669
|
export declare const WorkflowMessagesProperty = "__voltroWorkflowMessages";
|
|
@@ -2224,7 +2812,23 @@ export declare type WorkflowRunEventType = 'run-started' | 'run-suspended' | 'ru
|
|
|
2224
2812
|
/** Emitted by `awaitUpdate(...)` when schema validation or the
|
|
2225
2813
|
* update handler fails. `payload` carries `{ updateId, updateName,
|
|
2226
2814
|
* errorTag, errorMessage }`; the caller's update promise rejects. */
|
|
2227
|
-
| 'update-failed'
|
|
2815
|
+
| 'update-failed'
|
|
2816
|
+
/** The replay tripwire fired: this run is re-executing against a body whose
|
|
2817
|
+
* step shape no longer matches what the run journaled. `payload` carries
|
|
2818
|
+
* `{ kind, stepName, recorded, reached, detail }` — see
|
|
2819
|
+
* `nondeterminism.ts`. NEVER accompanied by a state change: the run keeps
|
|
2820
|
+
* going, because a false positive that killed a run would be worse than the
|
|
2821
|
+
* divergence it suspects. */
|
|
2822
|
+
| 'nondeterminism-suspected'
|
|
2823
|
+
/** The crash-loop breaker parked this run: its runner died mid-body
|
|
2824
|
+
* `maxRunReclaims` times in a row and a survivor reclaimed it each time.
|
|
2825
|
+
* `payload` carries `{ reclaimCount, maxRunReclaims }`. The run row is
|
|
2826
|
+
* `suspended` — an operator resume re-arms it with a fresh budget. */
|
|
2827
|
+
| 'run-crashlooped'
|
|
2828
|
+
/** The staleness sweep saw no progress on a live run for longer than the
|
|
2829
|
+
* configured threshold. `payload` carries `{ idleMs, stallAfterMs,
|
|
2830
|
+
* lastProgressAt, reason }`. Diagnostic only — the run is untouched. */
|
|
2831
|
+
| 'run-stalled';
|
|
2228
2832
|
|
|
2229
2833
|
/** A workflow run's TERMINAL outcome, handed to the metrics hook. Kept local to
|
|
2230
2834
|
* this package (structurally matching `@voltro/runtime`'s `WorkflowRunRecord`) so
|
|
@@ -2277,6 +2881,23 @@ export declare interface WorkflowRunRecorderOptions {
|
|
|
2277
2881
|
* rather than as garbage — see `inspectWorkflow`.
|
|
2278
2882
|
*/
|
|
2279
2883
|
readonly encryptStepPayload?: (value: unknown) => unknown;
|
|
2884
|
+
/**
|
|
2885
|
+
* How much this recorder writes per STEP — `'full'` (default) or `'coarse'`.
|
|
2886
|
+
*
|
|
2887
|
+
* `'coarse'` makes `startStep` answer "not recording this step" (the
|
|
2888
|
+
* contract callers already handle) and `endStep*` no-ops, removing the two
|
|
2889
|
+
* fire-and-forget store writes every step otherwise costs. Run rows and
|
|
2890
|
+
* `recordEvent` (signals, timers, cancels, stall reports) are DELIBERATELY
|
|
2891
|
+
* unaffected: they are per-run, not per-step, and they are what the
|
|
2892
|
+
* dead-letter view and the cancel/staleness sweeps read. The engine's own
|
|
2893
|
+
* durable journal is not touched either way — this is the introspection
|
|
2894
|
+
* copy, never replay state.
|
|
2895
|
+
*
|
|
2896
|
+
* App-facing spelling: `workflows.recording` in `app.config.ts`, env
|
|
2897
|
+
* override `VOLTRO_WORKFLOW_RECORDING` (resolved by the CLI, threaded to
|
|
2898
|
+
* BOTH boot paths through one resolver).
|
|
2899
|
+
*/
|
|
2900
|
+
readonly recording?: 'full' | 'coarse';
|
|
2280
2901
|
}
|
|
2281
2902
|
|
|
2282
2903
|
/** Per-step lifecycle hooks. Implementations should not throw —
|
|
@@ -2349,6 +2970,68 @@ export declare interface WorkflowRunRecorderService {
|
|
|
2349
2970
|
}>;
|
|
2350
2971
|
}
|
|
2351
2972
|
|
|
2973
|
+
export declare interface WorkflowScheduleDeclaration<Payload> {
|
|
2974
|
+
/** Standard cron expression — 5-field or 6-field (leading seconds). */
|
|
2975
|
+
readonly cron: string;
|
|
2976
|
+
/** IANA timezone the expression is interpreted in. REQUIRED, like
|
|
2977
|
+
* `defineSchedule` — server-local time in a container is a bug factory. */
|
|
2978
|
+
readonly timezone: string;
|
|
2979
|
+
/**
|
|
2980
|
+
* The payload each firing starts the workflow with. A value, or a function
|
|
2981
|
+
* of the firing (`({ scheduledAt }) => …`) for payloads that carry the slot
|
|
2982
|
+
* — a backfilled firing then computes against ITS instant, not "now".
|
|
2983
|
+
* Omitted ⇒ `{}`, which only typechecks for workflows whose payload has no
|
|
2984
|
+
* required fields; a mismatch fails the start with `WorkflowPayloadError`
|
|
2985
|
+
* naming the missing fields, same as any other start.
|
|
2986
|
+
*/
|
|
2987
|
+
readonly payload?: Payload | ((fire: {
|
|
2988
|
+
readonly scheduledAt: Date;
|
|
2989
|
+
}) => Payload | Promise<Payload>);
|
|
2990
|
+
/** Default `'skip'` — Temporal's default too, and the only answer that is
|
|
2991
|
+
* safe for every job shape. */
|
|
2992
|
+
readonly onOverlap?: WorkflowScheduleOverlap;
|
|
2993
|
+
/** Boot catch-up policy for firings missed during downtime — the scheduler's
|
|
2994
|
+
* own vocabulary, unchanged. Default `'skip'`. For a RANGE older than boot
|
|
2995
|
+
* backfill reaches, use `voltro schedule backfill`. */
|
|
2996
|
+
readonly backfill?: 'skip' | 'latest' | 'all';
|
|
2997
|
+
/**
|
|
2998
|
+
* Watchdog on ONE firing — which, for a workflow schedule, INCLUDES the
|
|
2999
|
+
* awaited run (that await is what makes `onOverlap` bind on the run's
|
|
3000
|
+
* duration). Default 24 hours rather than `defineSchedule`'s 30 minutes,
|
|
3001
|
+
* because a durable run legitimately outlives a handler; set it above your
|
|
3002
|
+
* slowest expected run. Past it the FIRING records `failed`/timeout and
|
|
3003
|
+
* releases the overlap guard — the workflow run itself is NOT cancelled
|
|
3004
|
+
* (declare flow-control `timeouts.finish` to bound the run).
|
|
3005
|
+
*/
|
|
3006
|
+
readonly maxRuntime?: FlowDuration;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/**
|
|
3010
|
+
* What happens when a firing arrives while the RUN from the previous firing is
|
|
3011
|
+
* still going — Temporal Schedules' overlap vocabulary, applied to the
|
|
3012
|
+
* workflow run (not merely to the handler invocation):
|
|
3013
|
+
*
|
|
3014
|
+
* `'skip'` — the new firing stands down; recorded as `skipped`.
|
|
3015
|
+
* `'buffer'` — the new firing waits and runs after the previous one
|
|
3016
|
+
* finishes; firings serialize, none is lost.
|
|
3017
|
+
* `'cancelOther'` — the new firing CANCELS the still-running previous run
|
|
3018
|
+
* and starts fresh — for "recompute the latest state"
|
|
3019
|
+
* jobs where the old run's partial work is worthless.
|
|
3020
|
+
*
|
|
3021
|
+
* The first two lower into the scheduler's own `onOverlap: 'skip' | 'queue'`;
|
|
3022
|
+
* what makes them bind on the RUN's duration is that the synthesised handler
|
|
3023
|
+
* AWAITS the workflow run to completion, so the schedule-run row is `running`
|
|
3024
|
+
* for exactly as long as the workflow is.
|
|
3025
|
+
*/
|
|
3026
|
+
export declare type WorkflowScheduleOverlap = 'skip' | 'buffer' | 'cancelOther';
|
|
3027
|
+
|
|
3028
|
+
/** Attached via `defineProperty` like the flow-control carrier — the value's
|
|
3029
|
+
* TYPE does not know about it, and the web bundle never reads it. A STRING
|
|
3030
|
+
* property rather than a symbol for the same reason as
|
|
3031
|
+
* `WorkflowFlowControlProperty`: the CLI's discovery reads it off a module
|
|
3032
|
+
* that crossed a bundler boundary, and symbols do not survive every one. */
|
|
3033
|
+
export declare const WorkflowScheduleProperty = "__voltroWorkflowSchedule";
|
|
3034
|
+
|
|
2352
3035
|
/**
|
|
2353
3036
|
* At most one run per key. The newcomer either stands down or evicts.
|
|
2354
3037
|
*
|
|
@@ -2461,6 +3144,17 @@ export declare interface WorkflowVersionMetadata {
|
|
|
2461
3144
|
export declare interface WorkflowVersionOptions {
|
|
2462
3145
|
readonly version?: string | number;
|
|
2463
3146
|
readonly compatibleWith?: ReadonlyArray<string | number>;
|
|
3147
|
+
/**
|
|
3148
|
+
* Named change markers this workflow's body may branch on, via
|
|
3149
|
+
* {@link patch}. Declaring one here is what makes `yield* patch('id')`
|
|
3150
|
+
* answer `true` for runs started from now on and `false` for runs that were
|
|
3151
|
+
* already in flight — see {@link patch} for the semantics and the worked
|
|
3152
|
+
* example.
|
|
3153
|
+
*
|
|
3154
|
+
* The declared set is stamped onto `_voltro_workflow_runs.workflowPatches`
|
|
3155
|
+
* at run start and read back from THERE on every resume, so the answer is
|
|
3156
|
+
* pinned to the run and cannot change under a redeploy.
|
|
3157
|
+
*/
|
|
2464
3158
|
readonly patches?: ReadonlyArray<string>;
|
|
2465
3159
|
/**
|
|
2466
3160
|
* When `true`, a workflow whose top-level body FAILS does not become a
|