@dudousxd/nestjs-durable-core 0.78.0 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -195,6 +195,16 @@ interface WorkflowRegistration {
195
195
  /** The package that DECLARED the workflow (see `WorkflowRun.origin`). Absent = unknown; nothing
196
196
  * ever substitutes a default, because a wrong origin is worse than no origin. */
197
197
  origin?: string;
198
+ /**
199
+ * Where the engine routes a failed run of this workflow, by error `code` (`code` absent = the
200
+ * catch-all): the handler workflow it starts instead of failing the run. The one announced field
201
+ * the engine ACTS on, for a workflow it serves by convention rather than by local registration.
202
+ * Absent = not stated, which routes nothing.
203
+ */
204
+ onError?: {
205
+ code?: string;
206
+ workflow: string;
207
+ }[];
198
208
  }
199
209
  /**
200
210
  * A worker's (or control-plane's) advertised identity, wire-protocol support, feature capabilities
@@ -509,16 +519,28 @@ interface FanPage {
509
519
  items: StepCheckpoint[];
510
520
  /** Members matching the query, across all pages. */
511
521
  total: number;
522
+ /** A validator for this page, when the gateway can compute one without reading the members. */
523
+ etag?: string | undefined;
524
+ /**
525
+ * Set when the query's `ifNoneMatch` matched {@link etag}: the page has not changed, so nothing
526
+ * was read. `items` is empty and `total` is 0 as placeholders, not the fan's; keep the page the
527
+ * caller already holds.
528
+ */
529
+ notModified?: true | undefined;
512
530
  }
513
531
  interface FanPageQuery {
514
532
  offset?: number | undefined;
515
533
  limit?: number | undefined;
516
534
  /** Only failed members (a failed step, or a child that failed). */
517
535
  failedOnly?: boolean | undefined;
536
+ /** An HTTP `If-None-Match` value: a page whose `etag` it names comes back `notModified`. */
537
+ ifNoneMatch?: string | undefined;
518
538
  }
519
539
  declare const FAN_SUMMARY_FAILURES = 20;
520
540
  /** The most children a summarized detail lists by id; `childCount` carries the rest. */
521
541
  declare const SUMMARY_CHILD_LIMIT = 200;
542
+ /** How many children a folded `fan:` checkpoint stands for; undefined for any other checkpoint. */
543
+ declare function foldedCount(cp: StepCheckpoint): number | undefined;
522
544
  /** A failed member: a failed step, or a child whose completion reports `ok: false`. */
523
545
  declare function isFailedMember(cp: StepCheckpoint): boolean;
524
546
  /** Summarize one fan's members (in seq order). */
@@ -534,6 +556,11 @@ declare function summarizeTimeline(timeline: StepCheckpoint[], over: number): {
534
556
  };
535
557
  /** One page of a fan's members out of a run's timeline. */
536
558
  declare function pageFan(timeline: StepCheckpoint[], group: string, query: FanPageQuery): FanPage;
559
+ /** A {@link FanPageQuery}'s offset and limit, clamped to sane bounds. */
560
+ declare function fanPageBounds(query: FanPageQuery): {
561
+ offset: number;
562
+ limit: number;
563
+ };
537
564
 
538
565
  /** A run as returned by the gateway's list — the durable run plus an optional {@link RunWaiting}
539
566
  * descriptor the control plane resolves (what a suspended run is parked on), so the dashboard can
@@ -566,6 +593,16 @@ interface RunDetail {
566
593
  fans?: FanSummary[] | undefined;
567
594
  /** How many children the run has, when `children` lists only the first of them. */
568
595
  childCount?: number | undefined;
596
+ /**
597
+ * A validator for this detail, when the gateway can compute one without reading the timeline:
598
+ * it changes whenever the run or any of its checkpoints does. Absent when it cannot.
599
+ */
600
+ etag?: string | undefined;
601
+ /**
602
+ * Set when {@link RunDetailOptions.ifNoneMatch} matched {@link etag}: nothing changed, so only
603
+ * `run` was read — `timeline` and `children` are empty, not the run's.
604
+ */
605
+ notModified?: true | undefined;
569
606
  }
570
607
  /** How to shape a {@link RunDetail}. */
571
608
  interface RunDetailOptions {
@@ -575,6 +612,11 @@ interface RunDetailOptions {
575
612
  * with a wide fan should set it: the full detail of a 40k-member fan is megabytes.
576
613
  */
577
614
  summarizeFansOver?: number | undefined;
615
+ /**
616
+ * An HTTP `If-None-Match` value. When it names the detail's current {@link RunDetail.etag}, the
617
+ * gateway answers `notModified` without reading the timeline. A gateway that cannot tell ignores it.
618
+ */
619
+ ifNoneMatch?: string | undefined;
578
620
  }
579
621
  /**
580
622
  * Maps every `Date` (and `Date | undefined`) field of `T` to its ISO-string wire form — the shape a
@@ -1101,6 +1143,9 @@ interface StepError {
1101
1143
  message: string;
1102
1144
  /** Optional machine-readable code, e.g. `declined`, `timeout`. */
1103
1145
  code?: string | undefined;
1146
+ /** Structured, JSON-serializable detail of the failure (e.g. validation issues), from
1147
+ * `FatalError`'s third argument or a `details` on what a step handler threw. */
1148
+ details?: unknown;
1104
1149
  /** Whether the engine should treat this as retryable. */
1105
1150
  retryable?: boolean | undefined;
1106
1151
  stack?: string | undefined;
@@ -1124,6 +1169,21 @@ interface StateStore {
1124
1169
  tryAdmitSingleton?(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
1125
1170
  createRun(run: WorkflowRun): Promise<void>;
1126
1171
  updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
1172
+ /**
1173
+ * {@link updateRun}, but only if the run's CURRENT status is one of `expectedStatuses`: a
1174
+ * compare-and-set on the status. Resolves whether the patch applied; never throws on a mismatch or
1175
+ * a missing run (both resolve `false`). The engine settles a turn's outcome (completed, failed,
1176
+ * suspended, blocked) through it, so a run that reached `cancelled`/`completed`/`dead` through
1177
+ * another path while the turn executed (an operator's `cancel()`, a timeout sweep, a dead-letter)
1178
+ * is never overwritten by the turn's stale outcome.
1179
+ *
1180
+ * For a SQL store the predicate must be part of the `UPDATE ... WHERE` itself; a read-then-write
1181
+ * in application code reintroduces the race this exists to close.
1182
+ *
1183
+ * Optional: a store that omits it gets the engine's read-then-write fallback, which narrows the
1184
+ * race to the gap between that read and the write but cannot close it.
1185
+ */
1186
+ updateRunIf?(runId: string, expectedStatuses: RunStatus[], patch: Partial<WorkflowRun>): Promise<boolean>;
1127
1187
  getRun(runId: string): Promise<WorkflowRun | null>;
1128
1188
  /**
1129
1189
  * Hard-delete a run and all of its rows — the run plus its checkpoints, signal waiters, and
@@ -1171,8 +1231,28 @@ interface StateStore {
1171
1231
  * Optional: a store that omits it falls back to {@link saveCheckpoint}, which keeps that race.
1172
1232
  */
1173
1233
  saveCheckpointIfPending?(checkpoint: StepCheckpoint): Promise<boolean>;
1234
+ /**
1235
+ * {@link saveCheckpointIfPending} for many checkpoints in one call: resolves, per entry and in
1236
+ * input order, whether it wrote. Same result as calling it on each entry in turn (a later entry for
1237
+ * the same `(runId, seq)` is compared against the earlier one's write), with each row's
1238
+ * compare-and-set still atomic. The call as a whole is atomic too: if it rejects, nothing was
1239
+ * written. The engine settles a batch of one run's step results through it, one call per batch.
1240
+ *
1241
+ * Optional: a store that omits it gets one {@link saveCheckpointIfPending} per entry.
1242
+ */
1243
+ saveCheckpointsIfPending?(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
1174
1244
  /** Used by recovery on boot to find runs to resume (crashed, left `running`). */
1175
1245
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
1246
+ /**
1247
+ * The ORPHANS a periodic recovery pass should reclaim: {@link listIncompleteRuns}'s runs
1248
+ * (`running` or `cancelling`) whose lease is free (`lockedUntil` unset or `<= nowMs`), oldest
1249
+ * (`createdAt`) first, at most `limit`. When `namespace` is given, restricted to that partition.
1250
+ *
1251
+ * Optional but recommended: without it the engine lists every incomplete run and filters the
1252
+ * leases in process, which on a busy fleet reads every healthy running run once per tick per
1253
+ * worker.
1254
+ */
1255
+ listOrphanedRuns?(nowMs: number, limit: number, namespace?: string): Promise<WorkflowRun[]>;
1176
1256
  /** The oldest `pending` runs awaiting dispatch (FIFO, by `createdAt`), capped at `limit`. */
1177
1257
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
1178
1258
  /**
@@ -1380,6 +1460,72 @@ interface StateStore {
1380
1460
  * and counts in JS, which loads the whole run on every result of a fan.
1381
1461
  */
1382
1462
  countCheckpoints?(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
1463
+ /**
1464
+ * The run's checkpoints aggregated into {@link CheckpointStat} buckets — one per
1465
+ * `(parallelGroup, status, nameKind)` present — with `opts.group` narrowing it to one group. A
1466
+ * `GROUP BY`, not a load: it is what lets a run detail draw a 40,000-member fan from a few rows.
1467
+ * Ungrouped checkpoints come back under `group: null`. Bucket order is unspecified.
1468
+ *
1469
+ * Optional: a store that omits it still works — the console's run detail then loads every
1470
+ * checkpoint and summarizes it in JS (`summarizeTimeline`), which is the same answer read the
1471
+ * expensive way. A store that implements it must implement {@link queryCheckpoints} too.
1472
+ */
1473
+ checkpointStats?(runId: string, opts?: {
1474
+ group?: string | undefined;
1475
+ }): Promise<CheckpointStat[]>;
1476
+ /**
1477
+ * The run's checkpoints that match `query`, in `seq` order (the order of {@link listCheckpoints}),
1478
+ * paged by `offset`/`limit`. Every filter is optional; an absent one does not narrow. An EMPTY
1479
+ * `statuses` or `namePrefixes` matches nothing; an empty `excludeGroups` excludes nothing.
1480
+ *
1481
+ * Optional, paired with {@link checkpointStats}: the run detail reads a fan's failures, its folded
1482
+ * checkpoints and one page of its members through it, instead of the run's whole history.
1483
+ */
1484
+ queryCheckpoints?(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
1485
+ }
1486
+ /**
1487
+ * How a checkpoint's name classifies it, for {@link CheckpointStat}: an awaited child's
1488
+ * `signal:child:` checkpoint, a folded `fan:` checkpoint standing for many children, a `spawn:`
1489
+ * edge to a fire-and-forget child, or any other step.
1490
+ */
1491
+ type CheckpointNameKind = 'child' | 'folded' | 'spawn' | 'step';
1492
+ /** The {@link CheckpointNameKind} of a checkpoint name — what a store's SQL `CASE` must agree with. */
1493
+ declare function checkpointNameKind(name: string): CheckpointNameKind;
1494
+ /**
1495
+ * One bucket of {@link StateStore.checkpointStats}: the checkpoints of one run sharing a
1496
+ * `parallelGroup`, a `status` and a {@link CheckpointNameKind}, reduced to aggregates.
1497
+ */
1498
+ interface CheckpointStat {
1499
+ /** The bucket's `parallelGroup`, or `null` for ungrouped checkpoints. */
1500
+ group: string | null;
1501
+ status: StepCheckpoint['status'];
1502
+ nameKind: CheckpointNameKind;
1503
+ /** Rows in the bucket (at least 1). */
1504
+ count: number;
1505
+ minSeq: number;
1506
+ maxSeq: number;
1507
+ /** Smallest and largest `name`, by the database's string order. Equal when every row shares one. */
1508
+ minName: string;
1509
+ maxName: string;
1510
+ /** Earliest `enqueuedAt`, read as {@link StateStore.listCheckpoints} reads it (falling back to `startedAt`). */
1511
+ minEnqueuedAt: Date;
1512
+ minStartedAt: Date;
1513
+ maxStartedAt: Date;
1514
+ maxFinishedAt: Date;
1515
+ /** Sum of `attempts` — moves when a step is retried in place, so a change marker sees it. */
1516
+ sumAttempts: number;
1517
+ }
1518
+ /** What {@link StateStore.queryCheckpoints} selects. */
1519
+ interface CheckpointQuery {
1520
+ /** Only this `parallelGroup`'s checkpoints. */
1521
+ group?: string | undefined;
1522
+ /** Leave out these groups' checkpoints (ungrouped checkpoints are kept). */
1523
+ excludeGroups?: string[] | undefined;
1524
+ statuses?: StepCheckpoint['status'][] | undefined;
1525
+ /** Only names starting with one of these. */
1526
+ namePrefixes?: string[] | undefined;
1527
+ offset?: number | undefined;
1528
+ limit?: number | undefined;
1383
1529
  }
1384
1530
  /**
1385
1531
  * A persisted schedule as the STORE sees it: a handful of queryable columns plus two JSON documents
@@ -1705,6 +1851,26 @@ interface AllStepsOptions extends Omit<StepDispatchOpts, 'queue' | 'timeoutMs'>
1705
1851
  */
1706
1852
  concurrency?: number;
1707
1853
  }
1854
+ /** Options for {@link WorkflowCtx.allSettled} over child workflows: {@link AllOptions} without `mode`. */
1855
+ type AllSettledOptions = Omit<AllOptions, 'mode'>;
1856
+ /** Options for {@link WorkflowCtx.allSettled} over a `@Step`: {@link AllStepsOptions} without `mode`. */
1857
+ type AllStepsSettledOptions = Omit<AllStepsOptions, 'mode'>;
1858
+ /**
1859
+ * One item of a {@link WorkflowCtx.allSettled} fan, in input order. `id` is the child run id (child
1860
+ * fan) or the step id (step fan); `error` is the failure message the child or step settled with.
1861
+ */
1862
+ type Settled<T> = {
1863
+ status: 'fulfilled';
1864
+ value: T;
1865
+ } | {
1866
+ status: 'rejected';
1867
+ id: string;
1868
+ error: string;
1869
+ /** The failure's `code` (e.g. a `FatalError`'s), so a parent can tell its kinds apart. */
1870
+ code?: string | undefined;
1871
+ /** The failure's structured `details`, when it carried any. */
1872
+ details?: unknown;
1873
+ };
1708
1874
  /** One child of a fan as {@link StateStore.registerChildWaits} writes it. */
1709
1875
  interface ChildWait {
1710
1876
  waiter: SignalWaiter;
@@ -2595,6 +2761,17 @@ interface WorkflowCtx<A extends SearchAttributes = SearchAttributes> {
2595
2761
  */
2596
2762
  all<TInput, TOutput>(step: StepRef<TInput, TOutput>, inputs: TInput[], opts?: AllStepsOptions): Promise<TOutput[]>;
2597
2763
  all<TOutput = unknown>(workflow: string, inputs: unknown[], opts?: AllOptions): Promise<TOutput[]>;
2764
+ /**
2765
+ * {@link all} that never throws for a failed item: waits for every child (or step) to settle, then
2766
+ * resolves with one {@link Settled} per input, in **input order** — the durable
2767
+ * `Promise.allSettled`. Same fan as `all` in `waitAll` mode (ids, `parallelGroup`, `concurrency`
2768
+ * waves, one parent turn per settled wave); only the ending differs. Use it when one item failing
2769
+ * is an expected outcome the parent handles (record it, dead-letter it) rather than a reason to
2770
+ * fail the whole run. Cancelling the parent still propagates.
2771
+ */
2772
+ allSettled<C extends WorkflowClass>(workflow: C, inputs: WorkflowInputOf<C>[], opts?: AllSettledOptions): Promise<Settled<WorkflowOutputOf<C>>[]>;
2773
+ allSettled<TInput, TOutput>(step: StepRef<TInput, TOutput>, inputs: TInput[], opts?: AllStepsSettledOptions): Promise<Settled<TOutput>[]>;
2774
+ allSettled<TOutput = unknown>(workflow: string, inputs: unknown[], opts?: AllSettledOptions): Promise<Settled<TOutput>[]>;
2598
2775
  /**
2599
2776
  * Pause the run at this point until a human resumes it from the dashboard (or
2600
2777
  * `engine.continue(runId)`). Records a visible `pending` checkpoint so the breakpoint shows up
@@ -2687,8 +2864,8 @@ interface WorkflowCtx<A extends SearchAttributes = SearchAttributes> {
2687
2864
  localStep<TOutput>(name: string, fn: (log: StepLogger) => Promise<TOutput>, options?: StepOptions): Promise<TOutput>;
2688
2865
  }
2689
2866
  /**
2690
- * {@link WorkflowCtx} with ONE signature per method: the string-addressed forms of the four overloaded
2691
- * methods (`step`, `child`, `startChild`, `all`), everything else unchanged.
2867
+ * {@link WorkflowCtx} with ONE signature per method: the string-addressed forms of the five overloaded
2868
+ * methods (`step`, `child`, `startChild`, `all`, `allSettled`), everything else unchanged.
2692
2869
  *
2693
2870
  * For code that drives the ctx DYNAMICALLY — an interpreter that runs a user-defined graph and only
2694
2871
  * ever knows workflow/step names as strings — and for test fakes. An overloaded method is awkward to
@@ -2707,7 +2884,7 @@ interface WorkflowCtx<A extends SearchAttributes = SearchAttributes> {
2707
2884
  * Every `WorkflowCtx<A>` is assignable to `DynamicWorkflowCtx<A>` (guarded by
2708
2885
  * `dynamic-workflow-ctx.type-test.ts`), and a `Pick` of it is a plain object literal in a test.
2709
2886
  */
2710
- interface DynamicWorkflowCtx<A extends SearchAttributes = SearchAttributes> extends Omit<WorkflowCtx<A>, 'step' | 'child' | 'startChild' | 'all'> {
2887
+ interface DynamicWorkflowCtx<A extends SearchAttributes = SearchAttributes> extends Omit<WorkflowCtx<A>, 'step' | 'child' | 'startChild' | 'all' | 'allSettled'> {
2711
2888
  /** {@link WorkflowCtx.step} by step name (a cross-runtime handler, or a `@Step` by its name). */
2712
2889
  step<TOutput = unknown, TInput = unknown>(name: string, input: TInput, opts?: StepDispatchOpts & {
2713
2890
  compensate?: string;
@@ -2718,6 +2895,8 @@ interface DynamicWorkflowCtx<A extends SearchAttributes = SearchAttributes> exte
2718
2895
  startChild(workflow: string, input: unknown, options?: string | ChildCallOptions): Promise<string>;
2719
2896
  /** {@link WorkflowCtx.all} by workflow name. */
2720
2897
  all<TOutput = unknown>(workflow: string, inputs: unknown[], opts?: AllOptions): Promise<TOutput[]>;
2898
+ /** {@link WorkflowCtx.allSettled} by workflow name. */
2899
+ allSettled<TOutput = unknown>(workflow: string, inputs: unknown[], opts?: AllSettledOptions): Promise<Settled<TOutput>[]>;
2721
2900
  }
2722
2901
  /** Result of executing or resuming a workflow run. */
2723
2902
  interface RunResult {
@@ -2740,7 +2919,7 @@ type UpdateResult = {
2740
2919
  accepted: true;
2741
2920
  run: RunResult | null;
2742
2921
  };
2743
- type EngineEventType = 'run.started' | 'run.completed' | 'run.failed' | 'run.suspended' | 'step.started' | 'step.completed' | 'step.failed' | 'step.progress' | 'run.blocked';
2922
+ type EngineEventType = 'run.started' | 'run.completed' | 'run.failed' | 'run.suspended' | 'step.started' | 'step.completed' | 'step.failed' | 'step.progress' | 'run.blocked' | 'run.routed' | 'run.unrouted';
2744
2923
  /**
2745
2924
  * A lifecycle event emitted by the engine. The observability surfaces (dashboard, OTel, the
2746
2925
  * Telescope integration) all subscribe to these rather than reaching into the store.
@@ -2780,6 +2959,8 @@ interface EngineEvent {
2780
2959
  * gap fired, how many live workers were considered, and both descriptors — enough for the health
2781
2960
  * panel/telescope to render the reason. Absent on all other event types. */
2782
2961
  diagnostics?: DispatchDiagnostics | undefined;
2962
+ /** Why a `run.unrouted` run's onError routes were not applied. */
2963
+ reason?: string | undefined;
2783
2964
  at: Date;
2784
2965
  }
2785
2966
  type EngineListener = (event: EngineEvent) => void;
@@ -3180,7 +3361,7 @@ interface WorkflowAnnouncement extends WorkflowRegistration {
3180
3361
  /** An axis on which the live announcers of one workflow do not agree. `values` holds every distinct
3181
3362
  * DECLARED value (sorted); an announcer that stated nothing on the axis contributes nothing. */
3182
3363
  interface Disagreement {
3183
- axis: 'group' | 'origin' | 'requires';
3364
+ axis: 'group' | 'origin' | 'requires' | 'onError';
3184
3365
  values: string[];
3185
3366
  }
3186
3367
  /**
@@ -3216,6 +3397,12 @@ interface AnnouncedWorkflow {
3216
3397
  /** Every distinct capability demand announced, each de-duplicated + sorted. Empty = nobody stated
3217
3398
  * one. Two entries here means two workers demand different capabilities for the same version. */
3218
3399
  requires: string[][];
3400
+ /** Every distinct set of `onError` routes announced, each in canonical order. Empty = nobody stated
3401
+ * one. Two entries here means the live workers route this workflow's failures differently. */
3402
+ onError: {
3403
+ code?: string;
3404
+ workflow: string;
3405
+ }[][];
3219
3406
  /** Runtimes of the live announcers (sorted, de-duplicated). EMPTY on an `'observed'` entry: a
3220
3407
  * heartbeat does not say what runtime wrote it, and the instance id's shape is a convention, not
3221
3408
  * a statement, so it is not read as one. */
@@ -3529,6 +3716,64 @@ type RoutingResolution = RoutableResolution | BlockedResolution;
3529
3716
  */
3530
3717
  declare function resolveRouting(handler: CapabilityRequirement, workers: readonly (RawWorkerDescriptor | WorkerDescriptor)[]): RoutingResolution;
3531
3718
 
3719
+ /**
3720
+ * Routes a failed run to a handler workflow instead of failing it: when a run of the workflow ends
3721
+ * `failed` with an error whose `code` matches (or with any error, for the route without a `code`), the
3722
+ * engine starts `workflow` with an {@link OnErrorInput} and the failed run waits for it. The run then
3723
+ * settles with the handler's outcome: its output on success, its error on failure.
3724
+ */
3725
+ interface OnErrorRoute {
3726
+ /** The error `code` to route. Omitted: routes any failure no other route's `code` matches. */
3727
+ code?: string | undefined;
3728
+ /** The registered handler workflow's name. */
3729
+ workflow: string;
3730
+ }
3731
+ /** The input an {@link OnErrorRoute} handler workflow is started with. */
3732
+ interface OnErrorInput<TInput = unknown> {
3733
+ /** The run that failed; it completes with this handler's output. */
3734
+ runId: string;
3735
+ /** The failed run's workflow. */
3736
+ workflow: string;
3737
+ /** The failed run's input. */
3738
+ input: TInput;
3739
+ /** The failure, as the run recorded it. */
3740
+ error: StepError;
3741
+ }
3742
+ /**
3743
+ * Handoff checkpoints live at seqs from here up, one per handoff, so a body that runs again after a
3744
+ * retry can never reach one. Fits a 32-bit `seq` column.
3745
+ */
3746
+ declare const HANDOFF_SEQ_BASE = 1000000000;
3747
+ /** The id of the handler run of a run's `attempt`-th handoff. */
3748
+ declare const onErrorRunId: (runId: string, attempt: number) => string;
3749
+ /** The waiter checkpoint a run waits on its handler under; renamed {@link retiredHandoffName} on retry. */
3750
+ declare const handoffName: (handlerId: string) => string;
3751
+ /** What a retry renames a handoff checkpoint to: kept in the timeline, never followed again. */
3752
+ declare const retiredHandoffName: (handlerId: string) => string;
3753
+ /** The handler run id a live handoff checkpoint of `runId` names, or undefined for any other one. */
3754
+ declare function handoffHandlerId(runId: string, cp: {
3755
+ seq: number;
3756
+ name: string;
3757
+ }): string | undefined;
3758
+ /** The run's live handoff (its latest, unretired one), if it handed a failure off. */
3759
+ declare function liveHandoff<C extends {
3760
+ seq: number;
3761
+ name: string;
3762
+ }>(runId: string, checkpoints: readonly C[]): C | undefined;
3763
+ /** The route a failure takes: the one for its exact code, else the catch-all, else none. */
3764
+ declare function routeFor(routes: OnErrorRoute[] | undefined, error: StepError): OnErrorRoute | undefined;
3765
+ /** Reject two routes for one code, or two catch-alls — ambiguous config, caught at registration. */
3766
+ declare function assertRoutes(workflow: string, routes: OnErrorRoute[]): void;
3767
+ /** The `code` carried by a thrown value: its own, a dispatched step's, or a thin-worker step's. */
3768
+ declare function codeOf(err: unknown): string | undefined;
3769
+ /** The `details` carried by a thrown value, from the same places as {@link codeOf}. */
3770
+ declare function detailsOf(err: unknown): unknown;
3771
+ /** `code` and `details` as optional keys, so an absent one is not written as `undefined`. */
3772
+ declare function codeAndDetails(err: unknown): {
3773
+ code?: string;
3774
+ details?: unknown;
3775
+ };
3776
+
3532
3777
  /**
3533
3778
  * A schedule definition — what `engine.schedules.create/upsert` take. Temporal-style: a persisted,
3534
3779
  * runtime-managed recurring start of `workflow` with `input`, fired by every driving engine's timer
@@ -3749,6 +3994,18 @@ interface StartOptions {
3749
3994
  * workflow's registered `concurrency`. See {@link ConcurrencyQuota}.
3750
3995
  */
3751
3996
  concurrency?: ConcurrencyQuota | undefined;
3997
+ /**
3998
+ * Delayed start: don't execute before this time (epoch ms, or a `Date`), compared against the
3999
+ * engine's clock. The run is created `suspended` with its `wakeAt` set to it, which is the same
4000
+ * durable timer a `ctx.sleep` parks on: nothing is held in memory, it survives restarts, and the
4001
+ * timer poller (`resumeDueTimers`) starts it once due, on any instance. A time that is not in the
4002
+ * future starts the run at once, as if it were omitted.
4003
+ *
4004
+ * A delayed run reads `suspended` until it fires (so `start` returns `suspended`, and `waitForRun`
4005
+ * settles on that right away), and skips the `run.started` event's `pending → running` hop. It
4006
+ * counts as in flight for singleton back-pressure and concurrency quotas while it waits.
4007
+ */
4008
+ startAt?: number | Date | undefined;
3752
4009
  }
3753
4010
  /**
3754
4011
  * Serialize runs of a workflow that share a key — a durable, FIFO mutex (e.g. one pipeline per base).
@@ -4042,6 +4299,8 @@ declare class WorkflowEngine {
4042
4299
  private readonly updateValidators;
4043
4300
  /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
4044
4301
  private readonly cancelRequested;
4302
+ /** {@link announcedRoutes} reads, by group + workflow + version. */
4303
+ private readonly announcedRoutesCache;
4045
4304
  /**
4046
4305
  * Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
4047
4306
  * THIS instance — so the turn can re-check them once its lease is released (see
@@ -4061,8 +4320,22 @@ declare class WorkflowEngine {
4061
4320
  private readonly awaitedStepFans;
4062
4321
  /** Step fans whose join barrier this instance opened, by `runId:firstSeq` — see {@link settledHere}. */
4063
4322
  private readonly stepFansFiredHere;
4323
+ /**
4324
+ * The in-process execution gate, by run id: the execution of the run currently in flight in THIS
4325
+ * process (`tail`), and at most one resume queued behind it (`queued`). See {@link resumeRun}.
4326
+ *
4327
+ * The run lease is keyed by engine instance, so it fences OTHER processes only: to the store, a
4328
+ * second execution in this same process holds the very same lease (and {@link execute} skips
4329
+ * `tryLockRun` when its snapshot says this instance holds it). Without this gate, a resume landing
4330
+ * while this process was still executing the run (a signal, a late step result, a child wake, an
4331
+ * explicit `resume()`) went straight past the lease and ran the body concurrently: a
4332
+ * not-yet-checkpointed step executed twice.
4333
+ */
4334
+ private readonly runGates;
4064
4335
  /** The tail of each run's queue of results being handled on this instance — see {@link serializedPerRun}. */
4065
4336
  private readonly resultChains;
4337
+ /** Each run's results waiting to be settled on this instance — see {@link completeRemoteResult}. */
4338
+ private readonly resultQueues;
4066
4339
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
4067
4340
  private readonly admission;
4068
4341
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -4137,6 +4410,8 @@ declare class WorkflowEngine {
4137
4410
  * when it cannot be derived with confidence: absent means UNKNOWN, and an absent origin is far
4138
4411
  * cheaper to live with than a wrong one. */
4139
4412
  origin?: string | undefined;
4413
+ /** Handler workflows a matching failure is routed to — see {@link OnErrorRoute}. */
4414
+ onError?: OnErrorRoute[] | undefined;
4140
4415
  }): void;
4141
4416
  /**
4142
4417
  * The retained in-process body for `name@version`, for an IN-APP worker that consumes this engine's
@@ -4166,6 +4441,8 @@ declare class WorkflowEngine {
4166
4441
  validateInput?: (input: unknown) => void | Promise<void>;
4167
4442
  /** Capabilities a live worker must advertise to run this remote workflow's turns (handshake §7.5). */
4168
4443
  requires?: string[];
4444
+ /** Handler workflows a matching failure is routed to — see {@link OnErrorRoute}. */
4445
+ onError?: OnErrorRoute[];
4169
4446
  }): void;
4170
4447
  /**
4171
4448
  * The convenience form of {@link registerRemote}: register a workflow whose body runs on a worker
@@ -4192,6 +4469,8 @@ declare class WorkflowEngine {
4192
4469
  validateInput?: (input: unknown) => void | Promise<void>;
4193
4470
  /** Capabilities a live worker must advertise to run this workflow's turns (handshake §7.5). */
4194
4471
  requires?: string[];
4472
+ /** Handler workflows a matching failure is routed to — see {@link OnErrorRoute}. */
4473
+ onError?: OnErrorRoute[];
4195
4474
  }): void;
4196
4475
  /**
4197
4476
  * Register a flow-control queue referenced by `ctx.step(step, input, { queue })`. Caps concurrent
@@ -4257,7 +4536,49 @@ declare class WorkflowEngine {
4257
4536
  * stale waiter or a late result left behind by a failed run silently re-executes its body.
4258
4537
  */
4259
4538
  private resumeImplicit;
4539
+ /**
4540
+ * One execution of a run at a time in this process. A resume that lands while the run executes
4541
+ * here neither joins that execution (the double-run) nor no-ops against its held lease (a lost
4542
+ * wake: its checkpoint may postdate the running turn's snapshot): it is QUEUED, and re-drives the
4543
+ * run once the running execution has settled and released its lease, re-reading the run then
4544
+ * (terminal: echoed, never re-executed). Resumes landing while one is already queued coalesce
4545
+ * into it: it has not started yet, so it will observe every write they made. Their callers get
4546
+ * the queued execution's result, i.e. the run's state after their own wake was applied.
4547
+ *
4548
+ * Across processes, exclusion stays the lease's job: {@link execute} still `tryLockRun`s when it
4549
+ * does not hold the lease.
4550
+ */
4260
4551
  private resumeRun;
4552
+ /** Drop `gate` once `tail` settles, unless another execution queued behind it meanwhile. */
4553
+ private closeGateAfter;
4554
+ /**
4555
+ * Release a run's lease AFTER its outcome is decided (persisted, or about to be returned). Best
4556
+ * effort, and that matters most on the failure path: the thing that failed the run is frequently
4557
+ * the very store this release talks to (a step that violated a DB constraint leaves Postgres
4558
+ * refusing every further statement on that connection), so the release throws too. A throwing
4559
+ * release in a `finally` REPLACES the run's real outcome with a lease write nobody asked about,
4560
+ * skips the post-turn re-drives after it, and, for a turn driven as a background resume, surfaces
4561
+ * as an unhandled rejection instead of a failed run.
4562
+ *
4563
+ * Letting it go is safe: the lease expires on its own and `recoverIncomplete` re-drives whatever
4564
+ * is still held. A lost release costs one lease window of latency; a masked error costs the
4565
+ * diagnosis.
4566
+ */
4567
+ private releaseLeaseBestEffort;
4568
+ /**
4569
+ * The run's persisted state as a {@link RunResult}, without executing anything: the answer to a
4570
+ * resume issued from inside the run's own execution, and to a settle whose compare-and-set lost
4571
+ * (the path that won already emitted and notified). A run that is gone reads as `cancelled`, the
4572
+ * safest terminal reading, never a fabricated success.
4573
+ */
4574
+ private echoCurrentStatus;
4575
+ /**
4576
+ * The engine's stand-in for {@link StateStore.updateRunIf} on a store without it, called with the
4577
+ * store as `this`: read-then-write, which narrows the race to the gap between the read and the
4578
+ * write (the pre-CAS behavior, now applied to every settle) but cannot close it.
4579
+ */
4580
+ private updateRunIfFallback;
4581
+ private resumeRunNow;
4261
4582
  /**
4262
4583
  * The registration for an UNREGISTERED run, INHERITED from its nearest REMOTE ancestor — or
4263
4584
  * `undefined` if it has none (the skew-protection error case in {@link resume}). Callers check the
@@ -4386,6 +4707,16 @@ declare class WorkflowEngine {
4386
4707
  * replay from their checkpoints, so only the work that had not finished runs again.
4387
4708
  */
4388
4709
  recoverIncomplete(nowMs?: number): Promise<RunResult[]>;
4710
+ /**
4711
+ * The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
4712
+ * expired, never the healthy ones a live turn is renewing. Every worker runs recovery on every
4713
+ * timer tick, so listing every running run and lock-probing each (one doomed UPDATE per healthy
4714
+ * run per worker per tick) was pure database load. The store answers it directly when it can
4715
+ * ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
4716
+ * filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
4717
+ * recovering at once spread over the batch instead of contending for the same leases.
4718
+ */
4719
+ private orphanedRuns;
4389
4720
  /**
4390
4721
  * The remote step this run dispatched and is still awaiting a result for, if any — the signature
4391
4722
  * that tells {@link recoverIncomplete} "this run's work is ON THE TRANSPORT, not in a crashed
@@ -4842,6 +5173,47 @@ declare class WorkflowEngine {
4842
5173
  * {@link runExecution}'s settle/suspend; the lease is held by {@link execute}. The result that lands
4843
5174
  * us back here (a remote step finished, a timer fired) goes through `resume` like any TS workflow.
4844
5175
  */
5176
+ /**
5177
+ * The registered onError routes whose handler workflow nothing can run: not registered on this
5178
+ * engine, and no live worker group serves it. Meant for a boot-time warning: a typo in a handler's
5179
+ * name would otherwise surface only when the first run fails.
5180
+ */
5181
+ unservedOnErrorRoutes(): Promise<{
5182
+ workflow: string;
5183
+ route: OnErrorRoute;
5184
+ }[]>;
5185
+ /** Validate a workflow's {@link OnErrorRoute}s at registration. */
5186
+ private checkedRoutes;
5187
+ /**
5188
+ * Route a failed run to its {@link OnErrorRoute} handler, read from the workflow's local registration
5189
+ * or, for one served by convention, from what its live workers announce. Starts the handler run and
5190
+ * waits for it the way a parent waits for a `ctx.child`: a `child:<handlerId>` waiter plus a
5191
+ * `running` placeholder checkpoint carrying the handler's name and input, so the handler's terminal
5192
+ * state wakes the run (buffered and reconciled like any child). From then on every execution of the
5193
+ * run only follows the handler ({@link followHandoff}); the body never runs again. Undefined when no
5194
+ * route matches: the run fails as usual.
5195
+ *
5196
+ * The placeholder sits past every seq the body used and is left out of {@link remoteHistory}.
5197
+ */
5198
+ private routeFailure;
5199
+ /** A run's handoff checkpoints, live and retired: the only ones at {@link HANDOFF_SEQ_BASE} and up. */
5200
+ private handoffCheckpoints;
5201
+ /**
5202
+ * Where a run that handed its failure off stands: the handler's outcome once it settled (from the
5203
+ * wake's checkpoint, or off the handler run when the wake never arrived), else still waiting. Starts
5204
+ * the handler if the turn that handed off crashed before its deferred start ran.
5205
+ */
5206
+ private followHandoff;
5207
+ /**
5208
+ * The onError routes the live workers announce for a workflow the engine serves by convention: the
5209
+ * one set they agree on, or none when they state none or cannot be read. Workers that announce
5210
+ * different sets (a rollout in progress) route nothing, and the run's failure is reported as a
5211
+ * `run.unrouted` event. Announcers of another version of the workflow are ignored. The read is
5212
+ * cached for {@link ANNOUNCED_ROUTES_TTL_MS}, so a burst of failures costs one descriptor read.
5213
+ */
5214
+ private announcedRoutes;
5215
+ /** Every distinct onError route set the live workers of `group` announce for the run's workflow. */
5216
+ private readAnnouncedRoutes;
4845
5217
  /**
4846
5218
  * Apply a terminal/suspended transition once: persist the new run status, emit the matching
4847
5219
  * lifecycle event, and (on completion/failure) wake a waiting parent. Both the TS and remote
@@ -5127,6 +5499,13 @@ declare class WorkflowEngine {
5127
5499
  private noteRecent;
5128
5500
  /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
5129
5501
  private savePendingCheckpoint;
5502
+ /**
5503
+ * {@link savePendingCheckpoint} for checkpoints at distinct seqs of one run: one
5504
+ * {@link StateStore.saveCheckpointsIfPending} call when the store has it, else one write each.
5505
+ * Never rejects as a whole: a write that failed is reported as that entry's error, and the batch
5506
+ * call rejecting means it wrote nothing, so the entries are then written one by one.
5507
+ */
5508
+ private savePendingCheckpoints;
5130
5509
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
5131
5510
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
5132
5511
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -5143,15 +5522,33 @@ declare class WorkflowEngine {
5143
5522
  * result (the dispatching one may be gone), so the run is crash/scale-safe. Resolves whether the
5144
5523
  * run should now be resumed: not for a duplicate or late delivery (the checkpoint isn't `pending`),
5145
5524
  * a run already finished, or a step fan whose barrier is still closed.
5525
+ *
5526
+ * Results of one run are settled in batches: the first starts a batch, and the ones that arrive
5527
+ * while it is being written form the next. Under load a wide fan's results are written by one
5528
+ * {@link StateStore.saveCheckpointsIfPending} call per batch, and the fan's barrier is asked once
5529
+ * per batch, not once per result. A result resolves only once its own write has committed, so the
5530
+ * transport acknowledges nothing that is not durable yet.
5146
5531
  */
5147
5532
  private completeRemoteResult;
5533
+ /** Settle `queue` (a run's queued results) batch by batch until it is empty, then retire it. */
5534
+ private drainResultQueue;
5535
+ /**
5536
+ * Settle a batch of one run's step results. At most one of them resolves `true` (resume the run):
5537
+ * that resume replays every write of the batch, so a second would only replay it again. A result
5538
+ * whose read or write failed resolves its error, and the transport redelivers just that one.
5539
+ */
5540
+ private completeRemoteResults;
5148
5541
  /**
5149
5542
  * The join barrier of a step fan: a result for one of its steps resumes the parent only once no
5150
5543
  * step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
5151
5544
  * retry needs the parent to schedule it). Without it every result would replay the whole parent,
5152
- * which makes a fan of N steps cost O(N²) checkpoint reads.
5545
+ * which makes a fan of N steps cost O(N²) checkpoint reads. `barriers` holds the answer per fan
5546
+ * for the rest of a batch. A barrier that cannot be read counts as open: a spare resume replays and
5547
+ * re-suspends, a lost one strands the run.
5153
5548
  */
5154
5549
  private stepFanStillRunning;
5550
+ /** How many steps of a step fan's seq range are still `pending`. */
5551
+ private stepFanPending;
5155
5552
  /**
5156
5553
  * Apply a remote workflow-turn {@link WorkflowDecision} durably — runs on WHATEVER instance receives
5157
5554
  * it over the broker (the dispatching one may be gone, or a point-to-point broker handed it to a
@@ -5205,6 +5602,8 @@ declare class RemoteStepError extends Error {
5205
5602
  readonly stepError?: StepError | undefined;
5206
5603
  constructor(stepError?: StepError);
5207
5604
  }
5605
+ /** Checkpoint names that record a parent→child edge: an awaited child, a folded group of them, a spawn. */
5606
+ declare const CHILD_EDGE_PREFIXES: string[];
5208
5607
  /** The child run ids a parent's {@link CHILD_EDGE_PREFIXES} checkpoints point at (others are skipped). */
5209
5608
  declare function childIdsOf(checkpoints: StepCheckpoint[]): string[];
5210
5609
 
@@ -5288,7 +5687,12 @@ declare function parseDuration(duration: string | number): number;
5288
5687
  */
5289
5688
  declare class FatalError extends Error {
5290
5689
  readonly code?: string | undefined;
5291
- constructor(message: string, code?: string);
5690
+ /** Structured, JSON-serializable detail (e.g. validation issues). It travels with the failure to
5691
+ * the run's error, a parent's `allSettled` item and an onError handler's input. */
5692
+ readonly details?: unknown;
5693
+ /** Read by the step result builders, so a dispatched step's retry policy honours it too. */
5694
+ readonly retryable = false;
5695
+ constructor(message: string, code?: string, details?: unknown);
5292
5696
  }
5293
5697
  /**
5294
5698
  * Thrown by `start` when a run would exceed its concurrency quota: `active` runs sharing the quota
@@ -5366,6 +5770,8 @@ declare class RemoteWorkflowTimeout extends Error {
5366
5770
  * breaking changes (old runs finish on the version they started on).
5367
5771
  */
5368
5772
  declare class NonDeterminismError extends Error {
5773
+ /** Marks the failure as a broken deploy, which an `onError` route never takes. */
5774
+ readonly code = "non_determinism";
5369
5775
  readonly runId: string;
5370
5776
  readonly seq: number;
5371
5777
  constructor(runId: string, seq: number, expected: string, recorded: string);
@@ -5396,17 +5802,17 @@ declare class NestedWorkflowCallError extends FatalError {
5396
5802
  * and the failing ids — the wait-all/fail-fast counterpart to a single child's FatalError. Mirrors
5397
5803
  * the Python SDK's `GatherFailed`.
5398
5804
  */
5805
+ /** One failed item of a fan: its input index, the child run or step id, and its failure. */
5806
+ interface GatherItemFailure {
5807
+ index: number;
5808
+ id: string;
5809
+ error: string;
5810
+ code?: string | undefined;
5811
+ details?: unknown;
5812
+ }
5399
5813
  declare class GatherError extends Error {
5400
- readonly failures: {
5401
- index: number;
5402
- id: string;
5403
- error: string;
5404
- }[];
5405
- constructor(failures: {
5406
- index: number;
5407
- id: string;
5408
- error: string;
5409
- }[], total?: number);
5814
+ readonly failures: GatherItemFailure[];
5815
+ constructor(failures: GatherItemFailure[], total?: number);
5410
5816
  }
5411
5817
  declare class WorkflowSuspended extends Error {
5412
5818
  /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
@@ -5637,6 +6043,49 @@ declare function mergeRunValueFacetRows(rows: readonly {
5637
6043
  */
5638
6044
  declare function runValueFacetsFromRuns(runs: readonly WorkflowRun[], axis: RunValueAxis, opts?: RunValueFacetOptions): RunValueFacetRow[];
5639
6045
 
6046
+ /** A store that aggregates checkpoints itself: both halves of the pair are present. */
6047
+ type CheckpointAggregatingStore = StateStore & Required<Pick<StateStore, 'checkpointStats' | 'queryCheckpoints'>>;
6048
+ declare function aggregatesCheckpoints(store: StateStore): store is CheckpointAggregatingStore;
6049
+ /**
6050
+ * {@link StateStore.checkpointStats} over checkpoints already in memory — the reference a store's
6051
+ * `GROUP BY` must agree with, and the in-memory store's implementation.
6052
+ */
6053
+ declare function checkpointStatsOf(checkpoints: StepCheckpoint[], opts?: {
6054
+ group?: string | undefined;
6055
+ }): CheckpointStat[];
6056
+ /** {@link StateStore.queryCheckpoints} over checkpoints already in memory, sorted by `seq` here. */
6057
+ declare function queryCheckpointsOf(checkpoints: StepCheckpoint[], query: CheckpointQuery): StepCheckpoint[];
6058
+ /**
6059
+ * What `summarizeTimeline` plus `childIdsOf` make of a run's whole checkpoint list, read through the
6060
+ * store's aggregates instead: the checkpoints outside wide fans, one {@link FanSummary} per wide fan,
6061
+ * and the run's children capped at {@link SUMMARY_CHILD_LIMIT} with their count.
6062
+ *
6063
+ * What it reads besides the aggregates: the rows outside wide fans, every folded `fan:` checkpoint
6064
+ * (one per settled wave), up to {@link FAN_SUMMARY_FAILURES} failed rows per wide fan, the first
6065
+ * child-edge rows, and — while a wide child fan is in flight — that fan's completed `signal:child:`
6066
+ * rows, whose `ok: false` payload is what marks a failed child.
6067
+ *
6068
+ * `childCount` counts each `spawn:` checkpoint as one child: the engine always writes the child's id
6069
+ * as its output, which is what `childIdsOf` reads.
6070
+ */
6071
+ declare function summarizeRunFromStore(store: CheckpointAggregatingStore, runId: string, over: number, stats?: CheckpointStat[]): Promise<{
6072
+ timeline: StepCheckpoint[];
6073
+ fans: FanSummary[];
6074
+ children: string[];
6075
+ childCount: number;
6076
+ }>;
6077
+ /** What `pageFan` makes of a run's whole checkpoint list, read one page at a time from the store. */
6078
+ declare function pageFanFromStore(store: CheckpointAggregatingStore, runId: string, group: string, query: FanPageQuery, stats?: CheckpointStat[]): Promise<FanPage>;
6079
+ /**
6080
+ * The ETag of a summarized run detail: the run row as read (so any run field that changes shows)
6081
+ * plus its checkpoint buckets, and the threshold it was summarized with.
6082
+ */
6083
+ declare function runDetailEtag(run: WorkflowRun, stats: CheckpointStat[], over: number): string;
6084
+ /** The ETag of one page of a fan: its group's buckets and the page asked for. */
6085
+ declare function fanPageEtag(group: string, stats: CheckpointStat[], query: FanPageQuery): string;
6086
+ /** Whether an `If-None-Match` header value names `etag` (weak comparison, as RFC 9110 asks for). */
6087
+ declare function etagMatches(ifNoneMatch: string | undefined, etag: string): boolean;
6088
+
5640
6089
  /**
5641
6090
  * Resolve WHAT a suspended run is parked on from the token of the {@link SignalWaiter} it registered.
5642
6091
  * The token prefix carries the kind — no extra store lookup and no new persisted field. Used by the
@@ -5668,9 +6117,10 @@ declare function indexWaitersByRun(waiters: readonly SignalWaiter[]): Map<string
5668
6117
 
5669
6118
  /**
5670
6119
  * The parent of a child run whose id the engine minted: a `ctx.all` child
5671
- * (`<parent>.all.<seq>.<i>`, or `.w<k>.<i>` under a `concurrency` limit) or a `ctx.child` /
5672
- * `ctx.startChild` child without an explicit id (`<parent>.child.<seq>`). Undefined for any other
5673
- * run, including a child started with a caller-chosen id.
6120
+ * (`<parent>.all.<seq>.<i>`, or `.w<k>.<i>` under a `concurrency` limit), a `ctx.child` /
6121
+ * `ctx.startChild` child without an explicit id (`<parent>.child.<seq>`), or the onError handler of
6122
+ * a failed run (`<parent>.onError.<attempt>`). Undefined for any other run, including a child started with a
6123
+ * caller-chosen id.
5674
6124
  */
5675
6125
  declare function parentRunIdOf(runId: string): string | undefined;
5676
6126
 
@@ -5714,14 +6164,30 @@ declare class CodecStateStore implements StateStore {
5714
6164
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
5715
6165
  * engine's per-run fallback still kicks in through the wrapper. */
5716
6166
  readonly deleteRuns?: (runIds: string[]) => Promise<void>;
6167
+ /** Decodes the runs like {@link listIncompleteRuns}. Bound only when the inner store has it, so
6168
+ * the engine's list-and-filter fallback still kicks in through the wrapper. */
6169
+ readonly listOrphanedRuns?: (nowMs: number, limit: number, namespace?: string) => Promise<WorkflowRun[]>;
6170
+ /** Encodes the patch like {@link updateRun}. Bound only when the inner store has it, so the
6171
+ * engine's read-then-write fallback still kicks in through the wrapper. */
6172
+ readonly updateRunIf?: (runId: string, expectedStatuses: RunStatus[], patch: Partial<WorkflowRun>) => Promise<boolean>;
5717
6173
  /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
5718
6174
  * plain-save fallback still kicks in through the wrapper. */
5719
6175
  readonly saveCheckpointIfPending?: (checkpoint: StepCheckpoint) => Promise<boolean>;
6176
+ /** Encodes each checkpoint like {@link saveCheckpoint}. Bound only when the inner store has it. */
6177
+ readonly saveCheckpointsIfPending?: (checkpoints: StepCheckpoint[]) => Promise<boolean[]>;
5720
6178
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store has it. */
5721
6179
  readonly countCheckpoints?: (runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]) => Promise<number>;
5722
6180
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
5723
6181
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
5724
6182
  readonly pruneTerminalRuns?: (policy: RetentionPolicy, nowMs: number, limit: number) => Promise<number>;
6183
+ /** Forwarded verbatim — the buckets aggregate names, statuses and timestamps, never a payload.
6184
+ * Bound only when the inner store aggregates, so the run detail's fallback still applies. */
6185
+ readonly checkpointStats?: (runId: string, opts?: {
6186
+ group?: string | undefined;
6187
+ }) => Promise<CheckpointStat[]>;
6188
+ /** Filters on unencoded columns, then decodes the rows like {@link listCheckpoints}. Bound only
6189
+ * when the inner store has it. */
6190
+ readonly queryCheckpoints?: (runId: string, query: CheckpointQuery) => Promise<StepCheckpoint[]>;
5725
6191
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
5726
6192
  * engine's facet/listing fallback still applies through the wrapper. */
5727
6193
  readonly countRuns?: (query: Omit<RunQuery, 'limit' | 'offset'>) => Promise<number>;
@@ -5753,6 +6219,7 @@ declare class CodecStateStore implements StateStore {
5753
6219
  ensureSchema(): Promise<void>;
5754
6220
  createRun(run: WorkflowRun): Promise<void>;
5755
6221
  updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
6222
+ private encRunPatch;
5756
6223
  getRun(runId: string): Promise<WorkflowRun | null>;
5757
6224
  deleteRun(runId: string): Promise<void>;
5758
6225
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
@@ -5821,12 +6288,17 @@ declare function eventMatchOf(token: string): Record<string, unknown>;
5821
6288
  declare function eventMatches(payload: unknown, match: Record<string, unknown>): boolean;
5822
6289
 
5823
6290
  interface MetricsSnapshot {
6291
+ /** `routed`: failures handed to an onError handler; `unrouted`: routes the engine could not apply. */
5824
6292
  runs: {
5825
6293
  started: number;
5826
6294
  completed: number;
5827
6295
  failed: number;
5828
6296
  suspended: number;
6297
+ routed: number;
6298
+ unrouted: number;
5829
6299
  };
6300
+ /** Routed failures by error code (`*` for a failure without one). */
6301
+ routedByCode: Record<string, number>;
5830
6302
  steps: {
5831
6303
  started: number;
5832
6304
  completed: number;
@@ -6176,12 +6648,19 @@ declare class InMemoryStateStore implements StateStore {
6176
6648
  * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
6177
6649
  */
6178
6650
  lastCheckpointRowsRead: number;
6651
+ /**
6652
+ * Test/observability hook: checkpoint rows materialized since construction, across every
6653
+ * checkpoint read that returns rows — what a run-detail benchmark compares between paths.
6654
+ */
6655
+ checkpointRowsRead: number;
6179
6656
  private key;
6180
6657
  /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
6181
6658
  private reindexAttributes;
6182
6659
  tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
6183
6660
  createRun(run: WorkflowRun): Promise<void>;
6184
6661
  updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
6662
+ updateRunIf(runId: string, expectedStatuses: RunStatus[], patch: Partial<WorkflowRun>): Promise<boolean>;
6663
+ private applyRunPatch;
6185
6664
  getRun(runId: string): Promise<WorkflowRun | null>;
6186
6665
  deleteRun(runId: string): Promise<void>;
6187
6666
  deleteRuns(runIds: string[]): Promise<void>;
@@ -6189,6 +6668,8 @@ declare class InMemoryStateStore implements StateStore {
6189
6668
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
6190
6669
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
6191
6670
  saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
6671
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
6672
+ listOrphanedRuns(nowMs: number, limit: number, namespace?: string): Promise<WorkflowRun[]>;
6192
6673
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
6193
6674
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
6194
6675
  listDueTimers(nowMs: number, namespace?: string, limit?: number): Promise<WorkflowRun[]>;
@@ -6250,6 +6731,11 @@ declare class InMemoryStateStore implements StateStore {
6250
6731
  deleteSchedule(id: string): Promise<boolean>;
6251
6732
  listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
6252
6733
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
6734
+ private runCheckpoints;
6735
+ checkpointStats(runId: string, opts?: {
6736
+ group?: string | undefined;
6737
+ }): Promise<CheckpointStat[]>;
6738
+ queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
6253
6739
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
6254
6740
  countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
6255
6741
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
@@ -6295,4 +6781,4 @@ declare function singletonAdmissionDenialPatch(runs: WorkflowRun[], runId: strin
6295
6781
  updatedAt: Date;
6296
6782
  } | undefined;
6297
6783
 
6298
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AllOptions, type AllStepsOptions, type AnnouncedWorkflow, type AnnouncementEvidence, type AttributeFilter, type AttributeOp, type AttributePredicateOperand, type AttributeValue, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type ChildCallOptions, type ChildWait, CodecStateStore, type ConcurrencyConfig, ConcurrencyLimitError, type ConcurrencyQuota, ContinueAsNew, type ControlMessage, type ControlPlane, DEFAULT_CONCURRENCY_STATUSES, DEFAULT_PARTITION, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type Disagreement, type DispatchDiagnostics, type DispatchPlan, type DurableExecution, type DurableExecutionWrapper, type DurableTopology, type DurableWebhook, DurableWorkflow, type DynamicWorkflowCtx, ENGINE_MINTED_TAG_PREFIXES, EVENT_BUFFER_SCAN_LIMIT, EVENT_TOKEN_PREFIX, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FAN_SUMMARY_FAILURES, type FanJoin, type FanPage, type FanPageQuery, type FanSummary, type FanWait, FatalError, GatherError, type GroupHealth, type Heartbeat, type HeartbeatSighting, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NestedWorkflowCallError, NonDeterminismError, type PartitionSighting, type PayloadCodec, type QueueConfig, QueueController, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDetailOptions, type RunDispatcher, type RunFacetQuery, type RunFacetRow, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunScope, type RunStatus, type RunValueAxis, type RunValueFacetOptions, type RunValueFacetOrder, type RunValueFacetRow, type RunWaiting, SINGLETON_ACTIVE_STATUSES, SINGLETON_ADMITTED_TAG, STATE_STORE, STATE_STORE_CANONICAL, SUMMARY_CHILD_LIMIT, type ScalarAttributeFilter, ScheduleAlreadyExistsError, ScheduleClient, type ScheduleClientOptions, type ScheduleDescription, type ScheduleHost, type ScheduleListQuery, ScheduleNotFoundError, type ScheduleOptions, type ScheduleQuery, type ScheduleRecord, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepScope, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, VERSION_UNDECLARED_TAG, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, type WorkflowAnnouncement, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, type WorkflowDirectory, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRegistration, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, aggregateAnnouncements, announcementsOf, applyAttributeQuery, assertConcurrency, attributeColumnFor, attributeOperand, attributePredicateOperands, axisIsRunColumn, bindWorkflowClass, breakpointToken, buildWorkflowDirectory, canRoute, capableWorkers, childIdsOf, classifyWaiterToken, clearDurableExecutionWrappers, clearTerminalSingletonAdmission, collectMetrics, concurrencyTag, controlPlaneDescriptor, countRuns, coveredTokens, createStepLogger, currentStep, currentStepScope, currentWorkflowCtx, debug, describeWorker, descriptorHash, error, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, facetOrigin, hasDurableExecutionWrappers, heartbeatStatus, indexWaitersByRun, info, isEngineMintedTag, isEventToken, isFailedMember, isLegacyDescriptor, isNonEmptyRunScope, isWorkflowControlFlowSignal, listSignalWaitersOfRuns, log, matchesAttributes, mergeRunFacetRows, mergeRunValueFacetRows, missingCapabilities, negotiate, nextCronFireMs, normalizeAttributeRows, normalizeDescriptor, observedAnnouncements, pageFan, parentRunIdOf, parseDuration, partitionOfToken, planDispatch, prevCronFireMs, readSearchAttributes, requiredCapabilities, resolveConcurrency, resolveRouting, resolveRunWaiting, runDurableExecution, runInStepLogger, runInStepScope, runInWorkflowCtx, runOutsideWorkflowCtx, runSchedules, runStepHandler, runValueFacetsFromRuns, sanitizeQueueToken, scheduledRunId, singletonAdmissionDenialPatch, singletonAdmissionTags, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, summarizeFan, summarizeTimeline, tenantGroup, useDurableExecution, warn, workflowName };
6784
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AllOptions, type AllSettledOptions, type AllStepsOptions, type AllStepsSettledOptions, type AnnouncedWorkflow, type AnnouncementEvidence, type AttributeFilter, type AttributeOp, type AttributePredicateOperand, type AttributeValue, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CHILD_EDGE_PREFIXES, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type CheckpointAggregatingStore, type CheckpointNameKind, type CheckpointQuery, type CheckpointStat, type ChildCallOptions, type ChildWait, CodecStateStore, type ConcurrencyConfig, ConcurrencyLimitError, type ConcurrencyQuota, ContinueAsNew, type ControlMessage, type ControlPlane, DEFAULT_CONCURRENCY_STATUSES, DEFAULT_PARTITION, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type Disagreement, type DispatchDiagnostics, type DispatchPlan, type DurableExecution, type DurableExecutionWrapper, type DurableTopology, type DurableWebhook, DurableWorkflow, type DynamicWorkflowCtx, ENGINE_MINTED_TAG_PREFIXES, EVENT_BUFFER_SCAN_LIMIT, EVENT_TOKEN_PREFIX, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FAN_SUMMARY_FAILURES, type FanJoin, type FanPage, type FanPageQuery, type FanSummary, type FanWait, FatalError, GatherError, type GatherItemFailure, type GroupHealth, HANDOFF_SEQ_BASE, type Heartbeat, type HeartbeatSighting, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NestedWorkflowCallError, NonDeterminismError, type OnErrorInput, type OnErrorRoute, type PartitionSighting, type PayloadCodec, type QueueConfig, QueueController, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDetailOptions, type RunDispatcher, type RunFacetQuery, type RunFacetRow, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunScope, type RunStatus, type RunValueAxis, type RunValueFacetOptions, type RunValueFacetOrder, type RunValueFacetRow, type RunWaiting, SINGLETON_ACTIVE_STATUSES, SINGLETON_ADMITTED_TAG, STATE_STORE, STATE_STORE_CANONICAL, SUMMARY_CHILD_LIMIT, type ScalarAttributeFilter, ScheduleAlreadyExistsError, ScheduleClient, type ScheduleClientOptions, type ScheduleDescription, type ScheduleHost, type ScheduleListQuery, ScheduleNotFoundError, type ScheduleOptions, type ScheduleQuery, type ScheduleRecord, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, type Settled, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepScope, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, VERSION_UNDECLARED_TAG, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, type WorkflowAnnouncement, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, type WorkflowDirectory, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRegistration, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, aggregateAnnouncements, aggregatesCheckpoints, announcementsOf, applyAttributeQuery, assertConcurrency, assertRoutes, attributeColumnFor, attributeOperand, attributePredicateOperands, axisIsRunColumn, bindWorkflowClass, breakpointToken, buildWorkflowDirectory, canRoute, capableWorkers, checkpointNameKind, checkpointStatsOf, childIdsOf, classifyWaiterToken, clearDurableExecutionWrappers, clearTerminalSingletonAdmission, codeAndDetails, codeOf, collectMetrics, concurrencyTag, controlPlaneDescriptor, countRuns, coveredTokens, createStepLogger, currentStep, currentStepScope, currentWorkflowCtx, debug, describeWorker, descriptorHash, detailsOf, error, etagMatches, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, facetOrigin, fanPageBounds, fanPageEtag, foldedCount, handoffHandlerId, handoffName, hasDurableExecutionWrappers, heartbeatStatus, indexWaitersByRun, info, isEngineMintedTag, isEventToken, isFailedMember, isLegacyDescriptor, isNonEmptyRunScope, isWorkflowControlFlowSignal, listSignalWaitersOfRuns, liveHandoff, log, matchesAttributes, mergeRunFacetRows, mergeRunValueFacetRows, missingCapabilities, negotiate, nextCronFireMs, normalizeAttributeRows, normalizeDescriptor, observedAnnouncements, onErrorRunId, pageFan, pageFanFromStore, parentRunIdOf, parseDuration, partitionOfToken, planDispatch, prevCronFireMs, queryCheckpointsOf, readSearchAttributes, requiredCapabilities, resolveConcurrency, resolveRouting, resolveRunWaiting, retiredHandoffName, routeFor, runDetailEtag, runDurableExecution, runInStepLogger, runInStepScope, runInWorkflowCtx, runOutsideWorkflowCtx, runSchedules, runStepHandler, runValueFacetsFromRuns, sanitizeQueueToken, scheduledRunId, singletonAdmissionDenialPatch, singletonAdmissionTags, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, summarizeFan, summarizeRunFromStore, summarizeTimeline, tenantGroup, useDurableExecution, warn, workflowName };