@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.js CHANGED
@@ -265,10 +265,16 @@ var FatalError = class extends Error {
265
265
  __name(this, "FatalError");
266
266
  }
267
267
  code;
268
- constructor(message, code) {
268
+ /** Structured, JSON-serializable detail (e.g. validation issues). It travels with the failure to
269
+ * the run's error, a parent's `allSettled` item and an onError handler's input. */
270
+ details;
271
+ /** Read by the step result builders, so a dispatched step's retry policy honours it too. */
272
+ retryable = false;
273
+ constructor(message, code, details) {
269
274
  super(message);
270
275
  this.name = "FatalError";
271
276
  this.code = code;
277
+ this.details = details;
272
278
  }
273
279
  };
274
280
  var ConcurrencyLimitError = class extends Error {
@@ -346,6 +352,8 @@ var NonDeterminismError = class extends Error {
346
352
  static {
347
353
  __name(this, "NonDeterminismError");
348
354
  }
355
+ /** Marks the failure as a broken deploy, which an `onError` route never takes. */
356
+ code = "non_determinism";
349
357
  runId;
350
358
  seq;
351
359
  constructor(runId, seq, expected, recorded) {
@@ -757,6 +765,9 @@ async function runStepHandler(task, handler) {
757
765
  ...typeof e?.code === "string" ? {
758
766
  code: e.code
759
767
  } : {},
768
+ ...e?.details !== void 0 ? {
769
+ details: e.details
770
+ } : {},
760
771
  ...typeof e?.retryable === "boolean" ? {
761
772
  retryable: e.retryable
762
773
  } : {}
@@ -833,11 +844,36 @@ __name(isFailedCompletion, "isFailedCompletion");
833
844
  function unwrapCompletion(payload, label) {
834
845
  const c = payload;
835
846
  if (c && typeof c === "object" && "ok" in c && c.ok === false) {
836
- throw new FatalError(`${label} failed: ${c.error}`);
847
+ throw new FatalError(`${label} failed: ${c.error}`, c.code, c.details);
837
848
  }
838
849
  return c?.value;
839
850
  }
840
851
  __name(unwrapCompletion, "unwrapCompletion");
852
+ function failedCompletion(error2) {
853
+ return {
854
+ ok: false,
855
+ error: error2.message,
856
+ ...error2.code !== void 0 ? {
857
+ code: error2.code
858
+ } : {},
859
+ ...error2.details !== void 0 ? {
860
+ details: error2.details
861
+ } : {}
862
+ };
863
+ }
864
+ __name(failedCompletion, "failedCompletion");
865
+ function completionError(c) {
866
+ return {
867
+ message: c.error,
868
+ ...c.code !== void 0 ? {
869
+ code: c.code
870
+ } : {},
871
+ ...c.details !== void 0 ? {
872
+ details: c.details
873
+ } : {}
874
+ };
875
+ }
876
+ __name(completionError, "completionError");
841
877
 
842
878
  // src/concurrency.ts
843
879
  var DEFAULT_CONCURRENCY_STATUSES = [
@@ -1158,7 +1194,7 @@ function allWaves(size, concurrency) {
1158
1194
  };
1159
1195
  }
1160
1196
  __name(allWaves, "allWaves");
1161
- var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+)$/;
1197
+ var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+|onError\.\d+)$/;
1162
1198
  function parentRunIdOf(runId) {
1163
1199
  return ENGINE_CHILD_ID.exec(runId)?.[1];
1164
1200
  }
@@ -1243,7 +1279,11 @@ function canonicalizeRegistrations(regs) {
1243
1279
  requires: r.requires ? [
1244
1280
  ...new Set(r.requires)
1245
1281
  ].sort() : null,
1246
- origin: r.origin ?? null
1282
+ origin: r.origin ?? null,
1283
+ // Conditional, so a registration that states no routes keeps the hash it had before routes existed.
1284
+ ...r.onError !== void 0 ? {
1285
+ onError: canonicalRoutes(r.onError)
1286
+ } : {}
1247
1287
  }));
1248
1288
  const byJson = /* @__PURE__ */ new Map();
1249
1289
  for (const entry of canonical) byJson.set(JSON.stringify(entry), entry);
@@ -1252,6 +1292,20 @@ function canonicalizeRegistrations(regs) {
1252
1292
  ].sort().map((k) => byJson.get(k));
1253
1293
  }
1254
1294
  __name(canonicalizeRegistrations, "canonicalizeRegistrations");
1295
+ function canonicalRoutes(routes) {
1296
+ const byJson = /* @__PURE__ */ new Map();
1297
+ for (const r of routes) {
1298
+ const entry = {
1299
+ code: r.code ?? null,
1300
+ workflow: r.workflow
1301
+ };
1302
+ byJson.set(JSON.stringify(entry), entry);
1303
+ }
1304
+ return [
1305
+ ...byJson.keys()
1306
+ ].sort().map((k) => byJson.get(k));
1307
+ }
1308
+ __name(canonicalRoutes, "canonicalRoutes");
1255
1309
  function canonicalizeForHash(d) {
1256
1310
  const set = /* @__PURE__ */ __name((xs) => [
1257
1311
  ...new Set(xs)
@@ -1286,13 +1340,13 @@ function fnv1a64Hex(input) {
1286
1340
  const OFFSET = 0xcbf29ce484222325n;
1287
1341
  const PRIME = 0x100000001b3n;
1288
1342
  const MASK = 0xffffffffffffffffn;
1289
- let hash = OFFSET;
1343
+ let hash2 = OFFSET;
1290
1344
  for (let i = 0; i < input.length; i += 1) {
1291
- hash ^= BigInt(input.charCodeAt(i) & 255);
1292
- hash ^= BigInt(input.charCodeAt(i) >> 8 & 255);
1293
- hash = hash * PRIME & MASK;
1345
+ hash2 ^= BigInt(input.charCodeAt(i) & 255);
1346
+ hash2 ^= BigInt(input.charCodeAt(i) >> 8 & 255);
1347
+ hash2 = hash2 * PRIME & MASK;
1294
1348
  }
1295
- return hash.toString(16).padStart(16, "0");
1349
+ return hash2.toString(16).padStart(16, "0");
1296
1350
  }
1297
1351
  __name(fnv1a64Hex, "fnv1a64Hex");
1298
1352
  function descriptorHash(descriptor) {
@@ -1408,6 +1462,20 @@ function aggregateAnnouncements(descriptors) {
1408
1462
  const requires = [
1409
1463
  ...requiresByJson.values()
1410
1464
  ].sort((x, y) => x.join(",").localeCompare(y.join(",")));
1465
+ const routesByJson = /* @__PURE__ */ new Map();
1466
+ for (const a of announcements) {
1467
+ if (a.onError === void 0) continue;
1468
+ const canonical = canonicalRoutes(a.onError).map((r) => r.code === null ? {
1469
+ workflow: r.workflow
1470
+ } : {
1471
+ code: r.code,
1472
+ workflow: r.workflow
1473
+ });
1474
+ routesByJson.set(JSON.stringify(canonical), canonical);
1475
+ }
1476
+ const onError = [
1477
+ ...routesByJson.keys()
1478
+ ].sort().map((k) => routesByJson.get(k) ?? []);
1411
1479
  const disagreements = [];
1412
1480
  if (groups.length > 1) disagreements.push({
1413
1481
  axis: "group",
@@ -1423,6 +1491,14 @@ function aggregateAnnouncements(descriptors) {
1423
1491
  values: requires.map((r) => r.join(","))
1424
1492
  });
1425
1493
  }
1494
+ if (onError.length > 1) {
1495
+ disagreements.push({
1496
+ axis: "onError",
1497
+ values: [
1498
+ ...routesByJson.keys()
1499
+ ].sort()
1500
+ });
1501
+ }
1426
1502
  const first = announcements[0];
1427
1503
  if (!first) continue;
1428
1504
  out.push({
@@ -1435,6 +1511,7 @@ function aggregateAnnouncements(descriptors) {
1435
1511
  groups,
1436
1512
  origins,
1437
1513
  requires,
1514
+ onError,
1438
1515
  runtimes: [
1439
1516
  ...new Set(announcements.map((a) => a.runtime))
1440
1517
  ].sort(),
@@ -1468,6 +1545,7 @@ function observedAnnouncements(heartbeats, covered) {
1468
1545
  ],
1469
1546
  origins: [],
1470
1547
  requires: [],
1548
+ onError: [],
1471
1549
  runtimes: [],
1472
1550
  instances: [
1473
1551
  ...instances
@@ -1826,6 +1904,13 @@ function planDispatch(requires, liveDescriptors, cp, token) {
1826
1904
  __name(planDispatch, "planDispatch");
1827
1905
 
1828
1906
  // src/interfaces.ts
1907
+ function checkpointNameKind(name) {
1908
+ if (name.startsWith("signal:child:")) return "child";
1909
+ if (name.startsWith("fan:")) return "folded";
1910
+ if (name.startsWith("spawn:")) return "spawn";
1911
+ return "step";
1912
+ }
1913
+ __name(checkpointNameKind, "checkpointNameKind");
1829
1914
  var TERMINAL_RUN_STATUSES = [
1830
1915
  "completed",
1831
1916
  "failed",
@@ -1838,6 +1923,71 @@ function isNonEmptyRunScope(scope) {
1838
1923
  }
1839
1924
  __name(isNonEmptyRunScope, "isNonEmptyRunScope");
1840
1925
 
1926
+ // src/on-error.ts
1927
+ var UNROUTABLE_CODES = /* @__PURE__ */ new Set([
1928
+ "non_determinism",
1929
+ "nested_workflow_call"
1930
+ ]);
1931
+ var HANDOFF_SEQ_BASE = 1e9;
1932
+ var onErrorRunId = /* @__PURE__ */ __name((runId, attempt) => `${runId}.onError.${attempt}`, "onErrorRunId");
1933
+ var handoffName = /* @__PURE__ */ __name((handlerId) => `signal:child:${handlerId}`, "handoffName");
1934
+ var retiredHandoffName = /* @__PURE__ */ __name((handlerId) => `onError:retried:${handlerId}`, "retiredHandoffName");
1935
+ function handoffHandlerId(runId, cp) {
1936
+ const prefix = `signal:child:${runId}.onError.`;
1937
+ return cp.seq >= HANDOFF_SEQ_BASE && cp.name.startsWith(prefix) ? cp.name.slice("signal:child:".length) : void 0;
1938
+ }
1939
+ __name(handoffHandlerId, "handoffHandlerId");
1940
+ function liveHandoff(runId, checkpoints) {
1941
+ let live;
1942
+ for (const cp of checkpoints) {
1943
+ if (handoffHandlerId(runId, cp) !== void 0 && (!live || cp.seq > live.seq)) live = cp;
1944
+ }
1945
+ return live;
1946
+ }
1947
+ __name(liveHandoff, "liveHandoff");
1948
+ function routeFor(routes, error2) {
1949
+ if (!routes?.length || error2.code && UNROUTABLE_CODES.has(error2.code)) return void 0;
1950
+ return (error2.code ? routes.find((r) => r.code === error2.code) : void 0) ?? routes.find((r) => r.code === void 0);
1951
+ }
1952
+ __name(routeFor, "routeFor");
1953
+ function assertRoutes(workflow, routes) {
1954
+ const seen = /* @__PURE__ */ new Set();
1955
+ for (const r of routes) {
1956
+ if (seen.has(r.code)) {
1957
+ throw new Error(r.code === void 0 ? `workflow ${workflow}: more than one onError route without a code` : `workflow ${workflow}: more than one onError route for code "${r.code}"`);
1958
+ }
1959
+ seen.add(r.code);
1960
+ }
1961
+ }
1962
+ __name(assertRoutes, "assertRoutes");
1963
+ function codeOf(err) {
1964
+ const e = err;
1965
+ return [
1966
+ e?.code,
1967
+ e?.stepError?.code,
1968
+ e?.error?.code
1969
+ ].find((c) => typeof c === "string");
1970
+ }
1971
+ __name(codeOf, "codeOf");
1972
+ function detailsOf(err) {
1973
+ const e = err;
1974
+ return e?.details ?? e?.stepError?.details ?? e?.error?.details;
1975
+ }
1976
+ __name(detailsOf, "detailsOf");
1977
+ function codeAndDetails(err) {
1978
+ const code = codeOf(err);
1979
+ const details = detailsOf(err);
1980
+ return {
1981
+ ...code !== void 0 ? {
1982
+ code
1983
+ } : {},
1984
+ ...details !== void 0 ? {
1985
+ details
1986
+ } : {}
1987
+ };
1988
+ }
1989
+ __name(codeAndDetails, "codeAndDetails");
1990
+
1841
1991
  // src/tenant-group.ts
1842
1992
  function tenantGroup(baseGroup, tenant) {
1843
1993
  return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
@@ -2897,7 +3047,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
2897
3047
  kind: "local",
2898
3048
  error: {
2899
3049
  message: err instanceof Error ? err.message : String(err),
2900
- stack: err instanceof Error ? err.stack : void 0
3050
+ stack: err instanceof Error ? err.stack : void 0,
3051
+ ...codeAndDetails(err)
2901
3052
  },
2902
3053
  events,
2903
3054
  attempts: attempt,
@@ -3153,12 +3304,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3153
3304
  }
3154
3305
  ]));
3155
3306
  }, "child");
3156
- const all = /* @__PURE__ */ __name(async (workflow2, inputs, opts) => {
3307
+ const fan = /* @__PURE__ */ __name(async (workflow2, inputs, opts, settle) => {
3157
3308
  if (inputs.length === 0) return [];
3158
3309
  if (stepNameOf(workflow2) !== void 0) {
3159
- return allSteps(workflow2, inputs, opts);
3310
+ return allSteps(workflow2, inputs, opts, settle);
3160
3311
  }
3161
- const mode = opts?.mode ?? "waitAll";
3312
+ const mode = settle ? "waitAll" : opts?.mode ?? "waitAll";
3162
3313
  const name = workflowName(workflow2);
3163
3314
  const waves = allWaves(inputs.length, opts?.concurrency);
3164
3315
  const positions = inputs.map(() => pos.next());
@@ -3280,24 +3431,45 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3280
3431
  throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
3281
3432
  }
3282
3433
  const outputs = [];
3434
+ const results = [];
3283
3435
  const failures = [];
3284
3436
  for (let i = 0; i < inputs.length; i += 1) {
3285
3437
  const c = existing[i]?.output;
3286
3438
  if (c && typeof c === "object" && "ok" in c && c.ok === false) {
3439
+ const { message, ...rest } = completionError({
3440
+ ...c,
3441
+ error: c.error ?? "unknown"
3442
+ });
3443
+ const failure = {
3444
+ id: id(i),
3445
+ error: message,
3446
+ ...rest
3447
+ };
3287
3448
  failures.push({
3288
3449
  index: i,
3289
- id: id(i),
3290
- error: c.error ?? "unknown"
3450
+ ...failure
3451
+ });
3452
+ results.push({
3453
+ status: "rejected",
3454
+ ...failure
3291
3455
  });
3292
3456
  outputs.push(void 0);
3293
3457
  } else {
3294
- outputs.push(c?.value);
3458
+ const value = c?.value;
3459
+ results.push({
3460
+ status: "fulfilled",
3461
+ value
3462
+ });
3463
+ outputs.push(value);
3295
3464
  }
3296
3465
  }
3466
+ if (settle) return results;
3297
3467
  if (failures.length > 0) throw new GatherError(failures, inputs.length);
3298
3468
  return outputs;
3299
- }, "all");
3300
- const allSteps = /* @__PURE__ */ __name(async (ref, inputs, opts) => {
3469
+ }, "fan");
3470
+ const all = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, false), "all");
3471
+ const allSettled = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, true), "allSettled");
3472
+ const allSteps = /* @__PURE__ */ __name(async (ref, inputs, opts, settle) => {
3301
3473
  if (opts?.queue !== void 0) {
3302
3474
  throw new Error("ctx.all: a flow-control queue is not supported on a step fan");
3303
3475
  }
@@ -3308,7 +3480,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3308
3480
  };
3309
3481
  const positions = inputs.map(() => pos.next());
3310
3482
  return host.callRemoteAll(runId, positions, def, inputs, {
3311
- mode: opts?.mode ?? "waitAll",
3483
+ mode: settle ? "waitAll" : opts?.mode ?? "waitAll",
3484
+ settle,
3312
3485
  concurrency: opts?.concurrency,
3313
3486
  transport: opts?.transport,
3314
3487
  admission: {
@@ -3626,6 +3799,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3626
3799
  task: guard("task", task),
3627
3800
  child: guard("child", child),
3628
3801
  all: guard("all", all),
3802
+ allSettled: guard("allSettled", allSettled),
3629
3803
  startChild: guard("startChild", startChild),
3630
3804
  breakpoint: guard("breakpoint", breakpoint),
3631
3805
  webhook: guardedWebhook,
@@ -3651,9 +3825,11 @@ function mergeTags(staticTags, runTags) {
3651
3825
  __name(mergeTags, "mergeTags");
3652
3826
  var BREAKPOINT = "breakpoint";
3653
3827
  var isBreakpoint = /* @__PURE__ */ __name((cp) => cp.status === "pending" && cp.name.startsWith(BREAKPOINT), "isBreakpoint");
3828
+ var ANNOUNCED_ROUTES_TTL_MS = 5e3;
3654
3829
  var VERSION_UNDECLARED_TAG = "version:undeclared";
3655
3830
  var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
3656
3831
  var STEP_FAN_CHUNK = 200;
3832
+ var RESULT_BATCH_MAX = 256;
3657
3833
  function shuffle(items) {
3658
3834
  for (let i = items.length - 1; i > 0; i -= 1) {
3659
3835
  const j = Math.floor(Math.random() * (i + 1));
@@ -3683,6 +3859,16 @@ function isNewerVersion(a, b) {
3683
3859
  }) > 0;
3684
3860
  }
3685
3861
  __name(isNewerVersion, "isNewerVersion");
3862
+ var SETTLE_ELIGIBLE_STATUSES = [
3863
+ "pending",
3864
+ "running",
3865
+ "suspended",
3866
+ "blocked",
3867
+ "failed",
3868
+ "cancelling"
3869
+ ];
3870
+ var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
3871
+ var RECOVERY_BATCH = 500;
3686
3872
  var NamespaceMismatch = class NamespaceMismatch2 extends Error {
3687
3873
  static {
3688
3874
  __name(this, "NamespaceMismatch");
@@ -3761,6 +3947,8 @@ var WorkflowEngine = class {
3761
3947
  updateValidators = /* @__PURE__ */ new Map();
3762
3948
  /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
3763
3949
  cancelRequested = /* @__PURE__ */ new Set();
3950
+ /** {@link announcedRoutes} reads, by group + workflow + version. */
3951
+ announcedRoutesCache = /* @__PURE__ */ new Map();
3764
3952
  /**
3765
3953
  * Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
3766
3954
  * THIS instance — so the turn can re-check them once its lease is released (see
@@ -3780,8 +3968,22 @@ var WorkflowEngine = class {
3780
3968
  awaitedStepFans = /* @__PURE__ */ new Map();
3781
3969
  /** Step fans whose join barrier this instance opened, by `runId:firstSeq` — see {@link settledHere}. */
3782
3970
  stepFansFiredHere = /* @__PURE__ */ new Map();
3971
+ /**
3972
+ * The in-process execution gate, by run id: the execution of the run currently in flight in THIS
3973
+ * process (`tail`), and at most one resume queued behind it (`queued`). See {@link resumeRun}.
3974
+ *
3975
+ * The run lease is keyed by engine instance, so it fences OTHER processes only: to the store, a
3976
+ * second execution in this same process holds the very same lease (and {@link execute} skips
3977
+ * `tryLockRun` when its snapshot says this instance holds it). Without this gate, a resume landing
3978
+ * while this process was still executing the run (a signal, a late step result, a child wake, an
3979
+ * explicit `resume()`) went straight past the lease and ran the body concurrently: a
3980
+ * not-yet-checkpointed step executed twice.
3981
+ */
3982
+ runGates = /* @__PURE__ */ new Map();
3783
3983
  /** The tail of each run's queue of results being handled on this instance — see {@link serializedPerRun}. */
3784
3984
  resultChains = /* @__PURE__ */ new Map();
3985
+ /** Each run's results waiting to be settled on this instance — see {@link completeRemoteResult}. */
3986
+ resultQueues = /* @__PURE__ */ new Map();
3785
3987
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3786
3988
  admission;
3787
3989
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -3988,6 +4190,7 @@ var WorkflowEngine = class {
3988
4190
  name,
3989
4191
  version,
3990
4192
  fn,
4193
+ onError: this.checkedRoutes(name, opts?.onError),
3991
4194
  tags: opts?.tags,
3992
4195
  singleton: opts?.singleton,
3993
4196
  concurrency: opts?.concurrency,
@@ -4057,6 +4260,7 @@ var WorkflowEngine = class {
4057
4260
  executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
4058
4261
  validateInput: opts.validateInput,
4059
4262
  requires: opts.requires,
4263
+ onError: this.checkedRoutes(name, opts.onError),
4060
4264
  remote: {
4061
4265
  group: opts.group,
4062
4266
  executor: opts.executor
@@ -4244,11 +4448,16 @@ var WorkflowEngine = class {
4244
4448
  if (registered.singleton) {
4245
4449
  await this.singletons.assertCapacity(name, registered.singleton, input);
4246
4450
  }
4451
+ const startAtMs = opts?.startAt instanceof Date ? opts.startAt.getTime() : opts?.startAt;
4452
+ const delayed = startAtMs !== void 0 && Number.isFinite(startAtMs) && startAtMs > this.clock();
4247
4453
  const run = {
4248
4454
  id: runId,
4249
4455
  workflow: name,
4250
4456
  workflowVersion: registered.version,
4251
- status: "pending",
4457
+ status: delayed ? "suspended" : "pending",
4458
+ ...delayed ? {
4459
+ wakeAt: startAtMs
4460
+ } : {},
4252
4461
  namespace: opts?.namespace ?? this.namespace,
4253
4462
  // DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
4254
4463
  // deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
@@ -4274,6 +4483,18 @@ var WorkflowEngine = class {
4274
4483
  error: winner.error
4275
4484
  };
4276
4485
  }
4486
+ if (delayed) {
4487
+ this.emit({
4488
+ type: "run.suspended",
4489
+ runId,
4490
+ workflow: name,
4491
+ namespace: run.namespace
4492
+ });
4493
+ return {
4494
+ runId,
4495
+ status: "suspended"
4496
+ };
4497
+ }
4277
4498
  await this.runDispatcher.dispatch(runId);
4278
4499
  if (this.controlPlane) {
4279
4500
  void this.controlPlane.publishControl({
@@ -4311,7 +4532,104 @@ var WorkflowEngine = class {
4311
4532
  resumeImplicit(runId) {
4312
4533
  return this.resumeRun(runId, false);
4313
4534
  }
4314
- async resumeRun(runId, allowFailed) {
4535
+ /**
4536
+ * One execution of a run at a time in this process. A resume that lands while the run executes
4537
+ * here neither joins that execution (the double-run) nor no-ops against its held lease (a lost
4538
+ * wake: its checkpoint may postdate the running turn's snapshot): it is QUEUED, and re-drives the
4539
+ * run once the running execution has settled and released its lease, re-reading the run then
4540
+ * (terminal: echoed, never re-executed). Resumes landing while one is already queued coalesce
4541
+ * into it: it has not started yet, so it will observe every write they made. Their callers get
4542
+ * the queued execution's result, i.e. the run's state after their own wake was applied.
4543
+ *
4544
+ * Across processes, exclusion stays the lease's job: {@link execute} still `tryLockRun`s when it
4545
+ * does not hold the lease.
4546
+ */
4547
+ resumeRun(runId, allowFailed) {
4548
+ const gate = this.runGates.get(runId);
4549
+ if (!gate) {
4550
+ const tail = this.resumeRunNow(runId, allowFailed);
4551
+ const fresh = {
4552
+ tail
4553
+ };
4554
+ this.runGates.set(runId, fresh);
4555
+ this.closeGateAfter(runId, fresh, tail);
4556
+ return tail;
4557
+ }
4558
+ let queued = gate.queued;
4559
+ if (queued) {
4560
+ queued.allowFailed ||= allowFailed;
4561
+ } else {
4562
+ const entry = {
4563
+ allowFailed
4564
+ };
4565
+ const result = gate.tail.catch(() => void 0).then(() => {
4566
+ if (gate.queued === entry) gate.queued = void 0;
4567
+ return this.resumeRunNow(runId, entry.allowFailed);
4568
+ });
4569
+ entry.result = result;
4570
+ queued = entry;
4571
+ gate.queued = queued;
4572
+ gate.tail = result;
4573
+ this.closeGateAfter(runId, gate, result);
4574
+ void this.track(result).catch(() => void 0);
4575
+ }
4576
+ if (currentWorkflowCtx()?.runId === runId) return this.echoCurrentStatus(runId);
4577
+ return queued.result;
4578
+ }
4579
+ /** Drop `gate` once `tail` settles, unless another execution queued behind it meanwhile. */
4580
+ closeGateAfter(runId, gate, tail) {
4581
+ const close = /* @__PURE__ */ __name(() => {
4582
+ if (this.runGates.get(runId) === gate && gate.tail === tail) this.runGates.delete(runId);
4583
+ }, "close");
4584
+ void tail.then(close, close);
4585
+ }
4586
+ /**
4587
+ * Release a run's lease AFTER its outcome is decided (persisted, or about to be returned). Best
4588
+ * effort, and that matters most on the failure path: the thing that failed the run is frequently
4589
+ * the very store this release talks to (a step that violated a DB constraint leaves Postgres
4590
+ * refusing every further statement on that connection), so the release throws too. A throwing
4591
+ * release in a `finally` REPLACES the run's real outcome with a lease write nobody asked about,
4592
+ * skips the post-turn re-drives after it, and, for a turn driven as a background resume, surfaces
4593
+ * as an unhandled rejection instead of a failed run.
4594
+ *
4595
+ * Letting it go is safe: the lease expires on its own and `recoverIncomplete` re-drives whatever
4596
+ * is still held. A lost release costs one lease window of latency; a masked error costs the
4597
+ * diagnosis.
4598
+ */
4599
+ async releaseLeaseBestEffort(runId) {
4600
+ try {
4601
+ await this.store.releaseRunLock(runId);
4602
+ } catch (err) {
4603
+ console.warn(`[nestjs-durable] releasing the lease of run ${runId} failed (it expires on its own and recovery re-drives): ${err instanceof Error ? err.message : String(err)}`);
4604
+ }
4605
+ }
4606
+ /**
4607
+ * The run's persisted state as a {@link RunResult}, without executing anything: the answer to a
4608
+ * resume issued from inside the run's own execution, and to a settle whose compare-and-set lost
4609
+ * (the path that won already emitted and notified). A run that is gone reads as `cancelled`, the
4610
+ * safest terminal reading, never a fabricated success.
4611
+ */
4612
+ async echoCurrentStatus(runId) {
4613
+ const run = await this.store.getRun(runId);
4614
+ return {
4615
+ runId,
4616
+ status: run?.status ?? "cancelled",
4617
+ output: run?.output,
4618
+ error: run?.error
4619
+ };
4620
+ }
4621
+ /**
4622
+ * The engine's stand-in for {@link StateStore.updateRunIf} on a store without it, called with the
4623
+ * store as `this`: read-then-write, which narrows the race to the gap between the read and the
4624
+ * write (the pre-CAS behavior, now applied to every settle) but cannot close it.
4625
+ */
4626
+ async updateRunIfFallback(runId, expected, patch) {
4627
+ const current = await this.getRun(runId);
4628
+ if (!current || !expected.includes(current.status)) return false;
4629
+ await this.updateRun(runId, patch);
4630
+ return true;
4631
+ }
4632
+ async resumeRunNow(runId, allowFailed) {
4315
4633
  const run = await this.store.getRun(runId);
4316
4634
  if (!run) throw new Error(`run ${runId} not found`);
4317
4635
  if (run.status === "cancelled" || run.status === "completed" || run.status === "dead" || run.status === "failed" && !allowFailed) {
@@ -4617,7 +4935,7 @@ var WorkflowEngine = class {
4617
4935
  async recoverIncomplete(nowMs = this.clock()) {
4618
4936
  if (this.draining) return [];
4619
4937
  const results = [];
4620
- for (const listed of await this.store.listIncompleteRuns(this.namespace)) {
4938
+ for (const listed of await this.orphanedRuns(nowMs)) {
4621
4939
  if (this.draining) break;
4622
4940
  const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4623
4941
  if (!acquired) continue;
@@ -4663,6 +4981,21 @@ var WorkflowEngine = class {
4663
4981
  return results;
4664
4982
  }
4665
4983
  /**
4984
+ * The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
4985
+ * expired, never the healthy ones a live turn is renewing. Every worker runs recovery on every
4986
+ * timer tick, so listing every running run and lock-probing each (one doomed UPDATE per healthy
4987
+ * run per worker per tick) was pure database load. The store answers it directly when it can
4988
+ * ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
4989
+ * filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
4990
+ * recovering at once spread over the batch instead of contending for the same leases.
4991
+ */
4992
+ async orphanedRuns(nowMs) {
4993
+ if (this.store.listOrphanedRuns) {
4994
+ return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
4995
+ }
4996
+ return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs));
4997
+ }
4998
+ /**
4666
4999
  * The remote step this run dispatched and is still awaiting a result for, if any — the signature
4667
5000
  * that tells {@link recoverIncomplete} "this run's work is ON THE TRANSPORT, not in a crashed
4668
5001
  * process". A `pending` remote checkpoint is written BEFORE the dispatch and settled by the
@@ -4834,6 +5167,24 @@ var WorkflowEngine = class {
4834
5167
  if (resurrecting) {
4835
5168
  const isFailureCompletion = /* @__PURE__ */ __name((v) => typeof v === "object" && v !== null && v.ok === false, "isFailureCompletion");
4836
5169
  for (const cp of await this.store.listCheckpoints(runId)) {
5170
+ const handlerId = handoffHandlerId(runId, cp);
5171
+ if (handlerId !== void 0) {
5172
+ await this.store.takeSignalWaiter(`child:${handlerId}`);
5173
+ await this.store.saveCheckpoint({
5174
+ ...cp,
5175
+ name: retiredHandoffName(handlerId),
5176
+ status: "completed"
5177
+ });
5178
+ const tags = run.tags?.filter((t) => !t.startsWith("onError:"));
5179
+ if (tags && tags.length !== run.tags?.length) {
5180
+ await this.store.updateRun(runId, {
5181
+ tags,
5182
+ updatedAt: /* @__PURE__ */ new Date()
5183
+ });
5184
+ run.tags = tags;
5185
+ }
5186
+ continue;
5187
+ }
4837
5188
  if (cp.status === "failed") {
4838
5189
  await this.store.saveCheckpoint({
4839
5190
  ...cp,
@@ -4978,13 +5329,13 @@ var WorkflowEngine = class {
4978
5329
  * `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
4979
5330
  */
4980
5331
  waitForRun(runId, opts) {
4981
- const TERMINAL = /* @__PURE__ */ new Set([
5332
+ const TERMINAL2 = /* @__PURE__ */ new Set([
4982
5333
  "completed",
4983
5334
  "failed",
4984
5335
  "cancelled",
4985
5336
  "dead"
4986
5337
  ]);
4987
- const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ? TERMINAL.has(run.status) : run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
5338
+ const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ? TERMINAL2.has(run.status) : run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
4988
5339
  const toResult = /* @__PURE__ */ __name((run) => ({
4989
5340
  runId,
4990
5341
  status: run.status,
@@ -5239,10 +5590,7 @@ var WorkflowEngine = class {
5239
5590
  async redriveSettledStepFans(runId, fans) {
5240
5591
  for (const fan of fans) {
5241
5592
  if (this.stepFansFiredHere.has(`${runId}:${fan.from}`)) continue;
5242
- const pending = this.store.countCheckpoints ? await this.store.countCheckpoints(runId, fan.from, fan.to, [
5243
- "pending"
5244
- ]) : (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= fan.from && c.seq <= fan.to && c.status === "pending").length;
5245
- if (pending > 0) continue;
5593
+ if (await this.stepFanPending(runId, fan.from, fan.to) > 0) continue;
5246
5594
  const nowMs = this.clock();
5247
5595
  if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
5248
5596
  return;
@@ -5952,6 +6300,209 @@ var WorkflowEngine = class {
5952
6300
  * us back here (a remote step finished, a timer fired) goes through `resume` like any TS workflow.
5953
6301
  */
5954
6302
  /**
6303
+ * The registered onError routes whose handler workflow nothing can run: not registered on this
6304
+ * engine, and no live worker group serves it. Meant for a boot-time warning: a typo in a handler's
6305
+ * name would otherwise surface only when the first run fails.
6306
+ */
6307
+ async unservedOnErrorRoutes() {
6308
+ const unserved = [];
6309
+ let groups;
6310
+ for (const registered of this.workflows.values()) {
6311
+ for (const route of registered.onError ?? []) {
6312
+ if (this.latest.has(route.workflow)) continue;
6313
+ groups ??= await this.pool.listWorkerGroups().catch(() => []);
6314
+ const token = sanitizeQueueToken(route.workflow);
6315
+ if (groups.some((g) => g === token || g.startsWith(`${token}@`))) continue;
6316
+ unserved.push({
6317
+ workflow: registered.name,
6318
+ route
6319
+ });
6320
+ }
6321
+ }
6322
+ return unserved;
6323
+ }
6324
+ /** Validate a workflow's {@link OnErrorRoute}s at registration. */
6325
+ checkedRoutes(name, routes) {
6326
+ if (!routes?.length) return void 0;
6327
+ assertRoutes(name, routes);
6328
+ return routes;
6329
+ }
6330
+ /**
6331
+ * Route a failed run to its {@link OnErrorRoute} handler, read from the workflow's local registration
6332
+ * or, for one served by convention, from what its live workers announce. Starts the handler run and
6333
+ * waits for it the way a parent waits for a `ctx.child`: a `child:<handlerId>` waiter plus a
6334
+ * `running` placeholder checkpoint carrying the handler's name and input, so the handler's terminal
6335
+ * state wakes the run (buffered and reconciled like any child). From then on every execution of the
6336
+ * run only follows the handler ({@link followHandoff}); the body never runs again. Undefined when no
6337
+ * route matches: the run fails as usual.
6338
+ *
6339
+ * The placeholder sits past every seq the body used and is left out of {@link remoteHistory}.
6340
+ */
6341
+ async routeFailure(run, error2) {
6342
+ const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
6343
+ const routes = registered ? registered.onError : await this.announcedRoutes(run, error2);
6344
+ const route = routeFor(routes, error2);
6345
+ if (!route) return void 0;
6346
+ const checkpoints = await this.handoffCheckpoints(run.id);
6347
+ const live = liveHandoff(run.id, checkpoints);
6348
+ if (live) return this.followHandoff(run, live);
6349
+ const attempt = checkpoints.length;
6350
+ const seq = HANDOFF_SEQ_BASE + attempt;
6351
+ const handlerId = onErrorRunId(run.id, attempt);
6352
+ const handler = {
6353
+ workflow: route.workflow,
6354
+ input: {
6355
+ runId: run.id,
6356
+ workflow: run.workflow,
6357
+ input: run.input,
6358
+ error: error2
6359
+ }
6360
+ };
6361
+ await this.store.putSignalWaiter({
6362
+ token: `child:${handlerId}`,
6363
+ runId: run.id,
6364
+ seq
6365
+ });
6366
+ await this.store.saveCheckpoint({
6367
+ ...instantCheckpoint({
6368
+ runId: run.id,
6369
+ seq,
6370
+ name: handoffName(handlerId),
6371
+ kind: "signal",
6372
+ status: "running"
6373
+ }),
6374
+ input: handler
6375
+ });
6376
+ const tag = `onError:${route.code ?? "*"}`;
6377
+ if (!run.tags?.includes(tag)) {
6378
+ const tags = [
6379
+ ...run.tags ?? [],
6380
+ tag
6381
+ ];
6382
+ await this.store.updateRun(run.id, {
6383
+ tags,
6384
+ updatedAt: /* @__PURE__ */ new Date()
6385
+ });
6386
+ run.tags = tags;
6387
+ }
6388
+ this.emit({
6389
+ type: "run.routed",
6390
+ runId: run.id,
6391
+ workflow: run.workflow,
6392
+ namespace: run.namespace,
6393
+ name: route.workflow,
6394
+ error: error2
6395
+ });
6396
+ this.startChildDeferred(handler.workflow, handler.input, handlerId, {
6397
+ namespace: run.namespace
6398
+ });
6399
+ return {
6400
+ kind: "suspended"
6401
+ };
6402
+ }
6403
+ /** A run's handoff checkpoints, live and retired: the only ones at {@link HANDOFF_SEQ_BASE} and up. */
6404
+ async handoffCheckpoints(runId) {
6405
+ const handoffs = await this.store.listCheckpointsByNamePrefix?.(runId, [
6406
+ `signal:child:${runId}.onError.`,
6407
+ `onError:retried:${runId}.onError.`
6408
+ ]);
6409
+ return (handoffs ?? await this.store.listCheckpoints(runId)).filter((cp) => cp.seq >= HANDOFF_SEQ_BASE);
6410
+ }
6411
+ /**
6412
+ * Where a run that handed its failure off stands: the handler's outcome once it settled (from the
6413
+ * wake's checkpoint, or off the handler run when the wake never arrived), else still waiting. Starts
6414
+ * the handler if the turn that handed off crashed before its deferred start ran.
6415
+ */
6416
+ async followHandoff(run, handoff) {
6417
+ if (handoff.status === "completed") {
6418
+ const outcome = handoff.output;
6419
+ return outcome.ok ? {
6420
+ kind: "completed",
6421
+ output: outcome.value
6422
+ } : {
6423
+ kind: "failed",
6424
+ error: completionError(outcome)
6425
+ };
6426
+ }
6427
+ const handlerId = handoffHandlerId(run.id, handoff);
6428
+ const handlerRun = await this.store.getRun(handlerId);
6429
+ if (handlerRun?.status === "completed") return {
6430
+ kind: "completed",
6431
+ output: handlerRun.output
6432
+ };
6433
+ if (handlerRun && TERMINAL_RUN_STATUSES.includes(handlerRun.status)) {
6434
+ return {
6435
+ kind: "failed",
6436
+ error: handlerRun.error ?? {
6437
+ message: `onError handler ${handlerId} ${handlerRun.status}`
6438
+ }
6439
+ };
6440
+ }
6441
+ if (!handlerRun) {
6442
+ const handler = handoff.input;
6443
+ this.startChildDeferred(handler.workflow, handler.input, handlerId, {
6444
+ namespace: run.namespace
6445
+ });
6446
+ }
6447
+ return {
6448
+ kind: "suspended"
6449
+ };
6450
+ }
6451
+ /**
6452
+ * The onError routes the live workers announce for a workflow the engine serves by convention: the
6453
+ * one set they agree on, or none when they state none or cannot be read. Workers that announce
6454
+ * different sets (a rollout in progress) route nothing, and the run's failure is reported as a
6455
+ * `run.unrouted` event. Announcers of another version of the workflow are ignored. The read is
6456
+ * cached for {@link ANNOUNCED_ROUTES_TTL_MS}, so a burst of failures costs one descriptor read.
6457
+ */
6458
+ async announcedRoutes(run, error2) {
6459
+ const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
6460
+ const key = `${group}
6461
+ ${run.workflow}
6462
+ ${run.workflowVersion}`;
6463
+ const now = this.clock();
6464
+ let cached = this.announcedRoutesCache.get(key);
6465
+ if (!cached || now - cached.at > ANNOUNCED_ROUTES_TTL_MS) {
6466
+ cached = {
6467
+ at: now,
6468
+ sets: await this.readAnnouncedRoutes(run, group)
6469
+ };
6470
+ this.announcedRoutesCache.set(key, cached);
6471
+ }
6472
+ if (cached.sets.length > 1) {
6473
+ this.emit({
6474
+ type: "run.unrouted",
6475
+ runId: run.id,
6476
+ workflow: run.workflow,
6477
+ namespace: run.namespace,
6478
+ error: error2,
6479
+ reason: `live workers announce ${cached.sets.length} different onError route sets`
6480
+ });
6481
+ return void 0;
6482
+ }
6483
+ return cached.sets[0];
6484
+ }
6485
+ /** Every distinct onError route set the live workers of `group` announce for the run's workflow. */
6486
+ async readAnnouncedRoutes(run, group) {
6487
+ const transport = await this.pool.transportWithLiveGroup(group);
6488
+ if (!transport?.readWorkerDescriptors) return [];
6489
+ let descriptors;
6490
+ try {
6491
+ descriptors = await transport.readWorkerDescriptors(group);
6492
+ } catch {
6493
+ return [];
6494
+ }
6495
+ const sets = /* @__PURE__ */ new Map();
6496
+ for (const announced of aggregateAnnouncements(descriptors)) {
6497
+ if (announced.name !== run.workflow) continue;
6498
+ if (announced.version !== void 0 && announced.version !== run.workflowVersion) continue;
6499
+ for (const routes of announced.onError) sets.set(JSON.stringify(routes), routes);
6500
+ }
6501
+ return [
6502
+ ...sets.values()
6503
+ ];
6504
+ }
6505
+ /**
5955
6506
  * Apply a terminal/suspended transition once: persist the new run status, emit the matching
5956
6507
  * lifecycle event, and (on completion/failure) wake a waiting parent. Both the TS and remote
5957
6508
  * executors funnel their completed/failed/suspended outcomes through here so a status update can
@@ -5961,12 +6512,13 @@ var WorkflowEngine = class {
5961
6512
  async settleRun(run, outcome) {
5962
6513
  const updatedAt = /* @__PURE__ */ new Date();
5963
6514
  if (outcome.kind === "completed") {
5964
- await this.store.updateRun(run.id, {
6515
+ const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
5965
6516
  status: "completed",
5966
6517
  output: outcome.output,
5967
6518
  error: void 0,
5968
6519
  updatedAt
5969
6520
  });
6521
+ if (!applied2) return this.echoCurrentStatus(run.id);
5970
6522
  this.emit({
5971
6523
  type: "run.completed",
5972
6524
  runId: run.id,
@@ -5985,42 +6537,36 @@ var WorkflowEngine = class {
5985
6537
  };
5986
6538
  }
5987
6539
  if (outcome.kind === "failed") {
5988
- await this.store.updateRun(run.id, {
6540
+ const handed = await this.routeFailure(run, outcome.error);
6541
+ if (handed && handed.kind !== "failed") return this.settleRun(run, handed);
6542
+ const error2 = handed?.error ?? outcome.error;
6543
+ const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
5989
6544
  status: "failed",
5990
- error: outcome.error,
6545
+ error: error2,
5991
6546
  updatedAt
5992
6547
  });
6548
+ if (!applied2) return this.echoCurrentStatus(run.id);
5993
6549
  this.emit({
5994
6550
  type: "run.failed",
5995
6551
  runId: run.id,
5996
6552
  workflow: run.workflow,
5997
6553
  namespace: run.namespace,
5998
- error: outcome.error
5999
- });
6000
- void this.notifyParent(run.id, {
6001
- ok: false,
6002
- error: outcome.error.message
6554
+ error: error2
6003
6555
  });
6556
+ void this.notifyParent(run.id, failedCompletion(error2));
6004
6557
  return {
6005
6558
  runId: run.id,
6006
6559
  status: "failed",
6007
- error: outcome.error
6560
+ error: error2
6008
6561
  };
6009
6562
  }
6010
6563
  if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
6011
- const latest = await this.store.getRun(run.id);
6012
- if (latest?.status === "cancelled") {
6013
- return {
6014
- runId: run.id,
6015
- status: "cancelled",
6016
- error: latest.error
6017
- };
6018
- }
6019
- await this.store.updateRun(run.id, {
6564
+ const applied = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SUSPEND_ELIGIBLE_STATUSES, {
6020
6565
  status: "suspended",
6021
6566
  wakeAt: this.reconcileWakeAt(outcome.wakeAt),
6022
6567
  updatedAt
6023
6568
  });
6569
+ if (!applied) return this.echoCurrentStatus(run.id);
6024
6570
  this.emit({
6025
6571
  type: "run.suspended",
6026
6572
  runId: run.id,
@@ -6067,7 +6613,7 @@ var WorkflowEngine = class {
6067
6613
  from: this.instanceId
6068
6614
  }).catch(() => void 0);
6069
6615
  }
6070
- await this.store.releaseRunLock(run.id);
6616
+ await this.releaseLeaseBestEffort(run.id);
6071
6617
  return {
6072
6618
  runId: run.id,
6073
6619
  status: "cancelled",
@@ -6093,7 +6639,14 @@ var WorkflowEngine = class {
6093
6639
  }
6094
6640
  const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
6095
6641
  if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
6096
- const history = await this.remoteHistory(run.id);
6642
+ const checkpoints = await this.store.listCheckpoints(run.id);
6643
+ const handoff = liveHandoff(run.id, checkpoints);
6644
+ if (handoff) {
6645
+ const result = await this.settleRun(run, await this.followHandoff(run, handoff));
6646
+ await this.releaseLeaseBestEffort(run.id);
6647
+ return result;
6648
+ }
6649
+ const history = await this.remoteHistory(run.id, checkpoints);
6097
6650
  if (remote.executor.dispatch) {
6098
6651
  const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
6099
6652
  const silenceMs = this.remoteAdvanceSilenceMs;
@@ -6128,7 +6681,7 @@ var WorkflowEngine = class {
6128
6681
  decision = silenceMs != null ? await this.awaitWithLivenessDeadline(run.id, advance, silenceMs, () => new RemoteWorkflowTimeout(run.id, silenceMs)) : await advance;
6129
6682
  } catch (err) {
6130
6683
  if (err instanceof RemoteWorkflowTimeout) {
6131
- await this.store.releaseRunLock(run.id);
6684
+ await this.releaseLeaseBestEffort(run.id);
6132
6685
  return {
6133
6686
  runId: run.id,
6134
6687
  status: "running"
@@ -6417,8 +6970,8 @@ var WorkflowEngine = class {
6417
6970
  }
6418
6971
  }
6419
6972
  /** The run's resolved durable ops as replay inputs: completed/failed steps + elapsed timers. */
6420
- async remoteHistory(runId) {
6421
- const checkpoints = await this.store.listCheckpoints(runId);
6973
+ async remoteHistory(runId, listed) {
6974
+ const checkpoints = listed ?? await this.store.listCheckpoints(runId);
6422
6975
  const kindOf = {
6423
6976
  remote: "call",
6424
6977
  local: "step",
@@ -6428,15 +6981,14 @@ var WorkflowEngine = class {
6428
6981
  const events = [];
6429
6982
  for (const cp of checkpoints) {
6430
6983
  if (cp.status === "completed" || cp.status === "failed") {
6984
+ if (cp.seq >= HANDOFF_SEQ_BASE) continue;
6431
6985
  if (cp.kind === "signal" && cp.name.startsWith("signal:child:")) {
6432
6986
  const completion = cp.output;
6433
6987
  events.push({
6434
6988
  seq: cp.seq,
6435
6989
  kind: "child",
6436
6990
  output: completion?.ok ? completion.value : void 0,
6437
- error: completion && completion.ok === false ? {
6438
- message: completion.error
6439
- } : void 0
6991
+ error: completion && completion.ok === false ? completionError(completion) : void 0
6440
6992
  });
6441
6993
  continue;
6442
6994
  }
@@ -6717,6 +7269,10 @@ var WorkflowEngine = class {
6717
7269
  this.awaitedStepFans.set(run.id, awaitedFans);
6718
7270
  const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
6719
7271
  try {
7272
+ const handoff = liveHandoff(run.id, snapshot);
7273
+ if (handoff && !this.cancelRequested.has(run.id)) {
7274
+ return await this.settleRun(run, await this.followHandoff(run, handoff));
7275
+ }
6720
7276
  const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
6721
7277
  if (this.cancelRequested.has(run.id)) {
6722
7278
  return await this.compensateAndCancel(run, compensations, replay);
@@ -6779,7 +7335,8 @@ var WorkflowEngine = class {
6779
7335
  }
6780
7336
  const error2 = {
6781
7337
  message: err instanceof Error ? err.message : String(err),
6782
- stack: err instanceof Error ? err.stack : void 0
7338
+ stack: err instanceof Error ? err.stack : void 0,
7339
+ ...codeAndDetails(err)
6783
7340
  };
6784
7341
  const pending = await this.unwindCompensations(run, compensations, replay);
6785
7342
  if (pending) return await this.settleRun(run, {
@@ -6795,7 +7352,7 @@ var WorkflowEngine = class {
6795
7352
  this.awaitedRemoteSteps.delete(run.id);
6796
7353
  }
6797
7354
  if (this.awaitedStepFans.get(run.id) === awaitedFans) this.awaitedStepFans.delete(run.id);
6798
- await this.store.releaseRunLock(run.id);
7355
+ await this.releaseLeaseBestEffort(run.id);
6799
7356
  if (suspendedOnFan) {
6800
7357
  void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
6801
7358
  }
@@ -7166,27 +7723,21 @@ var WorkflowEngine = class {
7166
7723
  * own `finally` also releases (the run-execution path) is safe.
7167
7724
  */
7168
7725
  async parkBlocked(run, blocked) {
7169
- const current = await this.store.getRun(run.id);
7170
- if (current && (current.status === "cancelled" || current.status === "completed")) {
7171
- await this.store.releaseRunLock(run.id);
7172
- return {
7173
- runId: run.id,
7174
- status: current.status,
7175
- output: current.output,
7176
- error: current.error
7177
- };
7178
- }
7179
7726
  const error2 = {
7180
7727
  message: blocked.reason,
7181
7728
  code: blocked.code,
7182
7729
  retryable: true
7183
7730
  };
7184
- await this.store.updateRun(run.id, {
7731
+ const applied = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SUSPEND_ELIGIBLE_STATUSES, {
7185
7732
  status: "blocked",
7186
7733
  error: error2,
7187
7734
  wakeAt: this.clock() + this.blockedPollMs,
7188
7735
  updatedAt: /* @__PURE__ */ new Date()
7189
7736
  });
7737
+ if (!applied) {
7738
+ await this.store.releaseRunLock(run.id);
7739
+ return this.echoCurrentStatus(run.id);
7740
+ }
7190
7741
  this.emit({
7191
7742
  type: "run.blocked",
7192
7743
  runId: run.id,
@@ -7415,7 +7966,8 @@ var WorkflowEngine = class {
7415
7966
  const failure = {
7416
7967
  index: i,
7417
7968
  id: stepId(runId, seq),
7418
- error: err.message
7969
+ error: err.message,
7970
+ ...codeAndDetails(err)
7419
7971
  };
7420
7972
  if (opts.mode === "failFast") throw new GatherError([
7421
7973
  failure
@@ -7437,6 +7989,24 @@ var WorkflowEngine = class {
7437
7989
  });
7438
7990
  throw new WorkflowSuspended(wakeAt);
7439
7991
  }
7992
+ if (opts.settle) {
7993
+ const failed = new Map(failures.map((f) => [
7994
+ f.index,
7995
+ f
7996
+ ]));
7997
+ return Array.from(outputs, (value, i) => {
7998
+ const f = failed.get(i);
7999
+ if (!f) return {
8000
+ status: "fulfilled",
8001
+ value
8002
+ };
8003
+ const { index: _index, ...rejected } = f;
8004
+ return {
8005
+ status: "rejected",
8006
+ ...rejected
8007
+ };
8008
+ });
8009
+ }
7440
8010
  if (failures.length > 0) throw new GatherError(failures, inputs.length);
7441
8011
  return outputs;
7442
8012
  }
@@ -7508,6 +8078,25 @@ var WorkflowEngine = class {
7508
8078
  await this.store.saveCheckpoint(checkpoint);
7509
8079
  return true;
7510
8080
  }
8081
+ /**
8082
+ * {@link savePendingCheckpoint} for checkpoints at distinct seqs of one run: one
8083
+ * {@link StateStore.saveCheckpointsIfPending} call when the store has it, else one write each.
8084
+ * Never rejects as a whole: a write that failed is reported as that entry's error, and the batch
8085
+ * call rejecting means it wrote nothing, so the entries are then written one by one.
8086
+ */
8087
+ async savePendingCheckpoints(checkpoints) {
8088
+ if (checkpoints.length > 1 && this.store.saveCheckpointsIfPending) {
8089
+ try {
8090
+ const wrote = await this.store.saveCheckpointsIfPending(checkpoints);
8091
+ return checkpoints.map((_, i) => wrote[i] === true);
8092
+ } catch {
8093
+ }
8094
+ }
8095
+ const settled = await Promise.allSettled(checkpoints.map((cp) => this.savePendingCheckpoint(cp)));
8096
+ return settled.map((s) => s.status === "fulfilled" ? s.value : {
8097
+ error: s.reason
8098
+ });
8099
+ }
7511
8100
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
7512
8101
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
7513
8102
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -7546,57 +8135,161 @@ var WorkflowEngine = class {
7546
8135
  * result (the dispatching one may be gone), so the run is crash/scale-safe. Resolves whether the
7547
8136
  * run should now be resumed: not for a duplicate or late delivery (the checkpoint isn't `pending`),
7548
8137
  * a run already finished, or a step fan whose barrier is still closed.
8138
+ *
8139
+ * Results of one run are settled in batches: the first starts a batch, and the ones that arrive
8140
+ * while it is being written form the next. Under load a wide fan's results are written by one
8141
+ * {@link StateStore.saveCheckpointsIfPending} call per batch, and the fan's barrier is asked once
8142
+ * per batch, not once per result. A result resolves only once its own write has committed, so the
8143
+ * transport acknowledges nothing that is not durable yet.
8144
+ */
8145
+ completeRemoteResult(result) {
8146
+ return new Promise((resolve, reject) => {
8147
+ const entry = {
8148
+ result,
8149
+ resolve,
8150
+ reject
8151
+ };
8152
+ const queued = this.resultQueues.get(result.runId);
8153
+ if (queued) {
8154
+ queued.push(entry);
8155
+ return;
8156
+ }
8157
+ const queue = [
8158
+ entry
8159
+ ];
8160
+ this.resultQueues.set(result.runId, queue);
8161
+ void this.drainResultQueue(result.runId, queue);
8162
+ });
8163
+ }
8164
+ /** Settle `queue` (a run's queued results) batch by batch until it is empty, then retire it. */
8165
+ async drainResultQueue(runId, queue) {
8166
+ await Promise.resolve();
8167
+ while (queue.length > 0) {
8168
+ const batch = queue.splice(0, RESULT_BATCH_MAX);
8169
+ let outcomes;
8170
+ try {
8171
+ outcomes = await this.completeRemoteResults(runId, batch.map((q) => q.result));
8172
+ } catch (err) {
8173
+ outcomes = batch.map(() => ({
8174
+ error: err
8175
+ }));
8176
+ }
8177
+ batch.forEach((q, i) => {
8178
+ const outcome = outcomes[i] ?? false;
8179
+ if (typeof outcome === "boolean") q.resolve(outcome);
8180
+ else q.reject(outcome.error);
8181
+ });
8182
+ }
8183
+ this.resultQueues.delete(runId);
8184
+ }
8185
+ /**
8186
+ * Settle a batch of one run's step results. At most one of them resolves `true` (resume the run):
8187
+ * that resume replays every write of the batch, so a second would only replay it again. A result
8188
+ * whose read or write failed resolves its error, and the transport redelivers just that one.
7549
8189
  */
7550
- async completeRemoteResult(result) {
7551
- const cp = await this.store.getCheckpoint(result.runId, result.seq);
7552
- if (!cp || cp.status !== "pending") return false;
7553
- void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
7554
- const run = await this.store.getRun(result.runId);
7555
- if (run && (run.status === "cancelled" || run.status === "completed")) return false;
8190
+ async completeRemoteResults(runId, results) {
8191
+ const outcomes = results.map(() => false);
8192
+ const seen = /* @__PURE__ */ new Set();
8193
+ const live = results.flatMap((r, i) => {
8194
+ if (seen.has(r.seq)) return [];
8195
+ seen.add(r.seq);
8196
+ return [
8197
+ i
8198
+ ];
8199
+ });
8200
+ const reads = await Promise.allSettled(live.map((i) => this.store.getCheckpoint(runId, results[i].seq)));
8201
+ const pending = [];
8202
+ reads.forEach((read, k) => {
8203
+ const index = live[k];
8204
+ if (read.status === "rejected") outcomes[index] = {
8205
+ error: read.reason
8206
+ };
8207
+ else if (read.value?.status === "pending") pending.push({
8208
+ index,
8209
+ cp: read.value
8210
+ });
8211
+ });
8212
+ if (pending.length === 0) return outcomes;
8213
+ for (const { cp } of pending) void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
8214
+ const run = await this.store.getRun(runId);
8215
+ if (run && (run.status === "cancelled" || run.status === "completed")) return outcomes;
7556
8216
  const finishedAt = /* @__PURE__ */ new Date();
7557
- const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
7558
- const claimed = await this.savePendingCheckpoint({
7559
- ...cp,
7560
- status: result.status,
7561
- output: result.status === "completed" ? result.output : cp.output,
7562
- error: result.error,
7563
- events: result.events ?? cp.events,
7564
- startedAt,
7565
- finishedAt
8217
+ const writes = pending.map(({ index, cp }) => {
8218
+ const result = results[index];
8219
+ return {
8220
+ ...cp,
8221
+ status: result.status,
8222
+ output: result.status === "completed" ? result.output : cp.output,
8223
+ error: result.error,
8224
+ events: result.events ?? cp.events,
8225
+ startedAt: result.startedAt ? new Date(result.startedAt) : cp.startedAt,
8226
+ finishedAt
8227
+ };
7566
8228
  });
7567
- if (!claimed) return false;
7568
- this.noteSettledHere(`${result.runId}:${result.seq}`);
7569
- this.emit({
7570
- type: result.status === "completed" ? "step.completed" : "step.failed",
7571
- runId: result.runId,
7572
- seq: result.seq,
7573
- name: cp.name,
7574
- kind: cp.kind,
7575
- output: result.output,
7576
- error: result.error,
7577
- queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
7578
- durationMs: finishedAt.getTime() - startedAt.getTime()
8229
+ const claims = await this.savePendingCheckpoints(writes);
8230
+ const settled = [];
8231
+ claims.forEach((claim, k) => {
8232
+ const { index, cp } = pending[k];
8233
+ if (claim === true) settled.push({
8234
+ index,
8235
+ cp,
8236
+ result: results[index]
8237
+ });
8238
+ else if (claim !== false) outcomes[index] = claim;
7579
8239
  });
7580
- return !await this.stepFanStillRunning(cp, result);
8240
+ for (const { cp, result } of settled) {
8241
+ this.noteSettledHere(`${runId}:${result.seq}`);
8242
+ const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
8243
+ this.emit({
8244
+ type: result.status === "completed" ? "step.completed" : "step.failed",
8245
+ runId,
8246
+ seq: result.seq,
8247
+ name: cp.name,
8248
+ kind: cp.kind,
8249
+ output: result.output,
8250
+ error: result.error,
8251
+ queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
8252
+ durationMs: finishedAt.getTime() - startedAt.getTime()
8253
+ });
8254
+ }
8255
+ const barriers = /* @__PURE__ */ new Map();
8256
+ let carrier;
8257
+ for (const { index, cp, result } of settled) {
8258
+ if (!await this.stepFanStillRunning(cp, result, barriers)) carrier = index;
8259
+ }
8260
+ if (carrier !== void 0) outcomes[carrier] = true;
8261
+ return outcomes;
7581
8262
  }
7582
8263
  /**
7583
8264
  * The join barrier of a step fan: a result for one of its steps resumes the parent only once no
7584
8265
  * step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
7585
8266
  * retry needs the parent to schedule it). Without it every result would replay the whole parent,
7586
- * which makes a fan of N steps cost O(N²) checkpoint reads.
8267
+ * which makes a fan of N steps cost O(N²) checkpoint reads. `barriers` holds the answer per fan
8268
+ * for the rest of a batch. A barrier that cannot be read counts as open: a spare resume replays and
8269
+ * re-suspends, a lost one strands the run.
7587
8270
  */
7588
- async stepFanStillRunning(cp, result) {
8271
+ async stepFanStillRunning(cp, result, barriers) {
7589
8272
  const fan = parseStepFanGroup(cp.parallelGroup);
7590
8273
  if (!fan) return false;
7591
8274
  if (result.status === "failed") return false;
7592
- const to = fan.firstSeq + fan.size - 1;
7593
- const pending = this.store.countCheckpoints ? await this.store.countCheckpoints(result.runId, fan.firstSeq, to, [
7594
- "pending"
7595
- ]) : (await this.store.listCheckpoints(result.runId)).filter((c) => c.seq >= fan.firstSeq && c.seq <= to && c.status === "pending").length;
7596
- if (pending > 0) return true;
8275
+ let closed = barriers.get(fan.firstSeq);
8276
+ if (!closed) {
8277
+ closed = this.stepFanPending(result.runId, fan.firstSeq, fan.firstSeq + fan.size - 1).then((pending) => pending > 0, () => false);
8278
+ barriers.set(fan.firstSeq, closed);
8279
+ }
8280
+ if (await closed) return true;
7597
8281
  this.noteRecent(this.stepFansFiredHere, `${result.runId}:${fan.firstSeq}`);
7598
8282
  return false;
7599
8283
  }
8284
+ /** How many steps of a step fan's seq range are still `pending`. */
8285
+ async stepFanPending(runId, from, to) {
8286
+ if (this.store.countCheckpoints) {
8287
+ return this.store.countCheckpoints(runId, from, to, [
8288
+ "pending"
8289
+ ]);
8290
+ }
8291
+ return (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= from && c.seq <= to && c.status === "pending").length;
8292
+ }
7600
8293
  /**
7601
8294
  * Apply a remote workflow-turn {@link WorkflowDecision} durably — runs on WHATEVER instance receives
7602
8295
  * it over the broker (the dispatching one may be gone, or a point-to-point broker handed it to a
@@ -8043,14 +8736,339 @@ function summarizeTimeline(timeline, over) {
8043
8736
  __name(summarizeTimeline, "summarizeTimeline");
8044
8737
  function pageFan(timeline, group, query) {
8045
8738
  const matching = timeline.filter((cp) => cp.parallelGroup === group && (!query.failedOnly || isFailedMember(cp)));
8046
- const offset = query.offset !== void 0 && Number.isFinite(query.offset) ? Math.max(0, Math.floor(query.offset)) : 0;
8047
- const limit = query.limit !== void 0 && Number.isFinite(query.limit) ? Math.min(FAN_PAGE_MAX, Math.max(1, Math.floor(query.limit))) : 100;
8739
+ const { offset, limit } = fanPageBounds(query);
8048
8740
  return {
8049
8741
  items: matching.slice(offset, offset + limit),
8050
8742
  total: matching.length
8051
8743
  };
8052
8744
  }
8053
8745
  __name(pageFan, "pageFan");
8746
+ function fanPageBounds(query) {
8747
+ const offset = query.offset !== void 0 && Number.isFinite(query.offset) ? Math.max(0, Math.floor(query.offset)) : 0;
8748
+ const limit = query.limit !== void 0 && Number.isFinite(query.limit) ? Math.min(FAN_PAGE_MAX, Math.max(1, Math.floor(query.limit))) : 100;
8749
+ return {
8750
+ offset,
8751
+ limit
8752
+ };
8753
+ }
8754
+ __name(fanPageBounds, "fanPageBounds");
8755
+
8756
+ // src/run-detail-store.ts
8757
+ function aggregatesCheckpoints(store) {
8758
+ return store.checkpointStats !== void 0 && store.queryCheckpoints !== void 0;
8759
+ }
8760
+ __name(aggregatesCheckpoints, "aggregatesCheckpoints");
8761
+ function checkpointStatsOf(checkpoints, opts) {
8762
+ const buckets = /* @__PURE__ */ new Map();
8763
+ for (const cp of checkpoints) {
8764
+ const group = cp.parallelGroup ?? null;
8765
+ if (opts?.group !== void 0 && group !== opts.group) continue;
8766
+ const nameKind = checkpointNameKind(cp.name);
8767
+ const key = JSON.stringify([
8768
+ group,
8769
+ cp.status,
8770
+ nameKind
8771
+ ]);
8772
+ const enqueuedAt = cp.enqueuedAt ?? cp.startedAt;
8773
+ const bucket = buckets.get(key);
8774
+ if (!bucket) {
8775
+ buckets.set(key, {
8776
+ group,
8777
+ status: cp.status,
8778
+ nameKind,
8779
+ count: 1,
8780
+ minSeq: cp.seq,
8781
+ maxSeq: cp.seq,
8782
+ minName: cp.name,
8783
+ maxName: cp.name,
8784
+ minEnqueuedAt: enqueuedAt,
8785
+ minStartedAt: cp.startedAt,
8786
+ maxStartedAt: cp.startedAt,
8787
+ maxFinishedAt: cp.finishedAt,
8788
+ sumAttempts: cp.attempts
8789
+ });
8790
+ continue;
8791
+ }
8792
+ bucket.count += 1;
8793
+ bucket.minSeq = Math.min(bucket.minSeq, cp.seq);
8794
+ bucket.maxSeq = Math.max(bucket.maxSeq, cp.seq);
8795
+ if (cp.name < bucket.minName) bucket.minName = cp.name;
8796
+ if (cp.name > bucket.maxName) bucket.maxName = cp.name;
8797
+ if (enqueuedAt < bucket.minEnqueuedAt) bucket.minEnqueuedAt = enqueuedAt;
8798
+ if (cp.startedAt < bucket.minStartedAt) bucket.minStartedAt = cp.startedAt;
8799
+ if (cp.startedAt > bucket.maxStartedAt) bucket.maxStartedAt = cp.startedAt;
8800
+ if (cp.finishedAt > bucket.maxFinishedAt) bucket.maxFinishedAt = cp.finishedAt;
8801
+ bucket.sumAttempts += cp.attempts;
8802
+ }
8803
+ return [
8804
+ ...buckets.values()
8805
+ ];
8806
+ }
8807
+ __name(checkpointStatsOf, "checkpointStatsOf");
8808
+ function queryCheckpointsOf(checkpoints, query) {
8809
+ const excluded = new Set(query.excludeGroups ?? []);
8810
+ const matching = checkpoints.filter((cp) => (query.group === void 0 || cp.parallelGroup === query.group) && (cp.parallelGroup === void 0 || !excluded.has(cp.parallelGroup)) && (query.statuses === void 0 || query.statuses.includes(cp.status)) && (query.namePrefixes === void 0 || query.namePrefixes.some((prefix) => cp.name.startsWith(prefix)))).sort((a, b) => a.seq - b.seq);
8811
+ const offset = query.offset ?? 0;
8812
+ return matching.slice(offset, query.limit === void 0 ? void 0 : offset + query.limit);
8813
+ }
8814
+ __name(queryCheckpointsOf, "queryCheckpointsOf");
8815
+ var TERMINAL = /* @__PURE__ */ __name((status) => status !== "pending" && status !== "running", "TERMINAL");
8816
+ function isWide(group, rows, over) {
8817
+ return rows > over || (parseFanGroup(group)?.size ?? 0) > over;
8818
+ }
8819
+ __name(isWide, "isWide");
8820
+ function mergeBySeq(a, b) {
8821
+ return [
8822
+ ...a,
8823
+ ...b
8824
+ ].sort((x, y) => x.seq - y.seq);
8825
+ }
8826
+ __name(mergeBySeq, "mergeBySeq");
8827
+ async function failedChildren(store, runId, group, buckets) {
8828
+ if (!buckets.some((b) => b.nameKind === "child" && b.status === "completed")) {
8829
+ return {
8830
+ completed: [],
8831
+ failed: []
8832
+ };
8833
+ }
8834
+ const completed = await store.queryCheckpoints(runId, {
8835
+ group,
8836
+ statuses: [
8837
+ "completed"
8838
+ ],
8839
+ namePrefixes: [
8840
+ "signal:child:"
8841
+ ]
8842
+ });
8843
+ return {
8844
+ completed,
8845
+ failed: completed.filter(isFailedMember)
8846
+ };
8847
+ }
8848
+ __name(failedChildren, "failedChildren");
8849
+ async function summarizeFanFromStats(store, runId, group, buckets, folded) {
8850
+ const counts = {};
8851
+ const add = /* @__PURE__ */ __name((status, n) => {
8852
+ counts[status] = (counts[status] ?? 0) + n;
8853
+ }, "add");
8854
+ let size = 0;
8855
+ let startedAt;
8856
+ let finishedAt;
8857
+ let firstSeq = Number.POSITIVE_INFINITY;
8858
+ let lastSeq = Number.NEGATIVE_INFINITY;
8859
+ let minName;
8860
+ let maxName;
8861
+ let childRuns = false;
8862
+ for (const b of buckets) {
8863
+ firstSeq = Math.min(firstSeq, b.minSeq);
8864
+ lastSeq = Math.max(lastSeq, b.maxSeq);
8865
+ if (minName === void 0 || b.minName < minName) minName = b.minName;
8866
+ if (maxName === void 0 || b.maxName > maxName) maxName = b.maxName;
8867
+ if (b.nameKind === "child" || b.nameKind === "folded") childRuns = true;
8868
+ if (TERMINAL(b.status)) {
8869
+ if (!startedAt || b.minStartedAt < startedAt) startedAt = b.minStartedAt;
8870
+ if (!finishedAt || b.maxFinishedAt > finishedAt) finishedAt = b.maxFinishedAt;
8871
+ } else if (!startedAt || b.minEnqueuedAt < startedAt) {
8872
+ startedAt = b.minEnqueuedAt;
8873
+ }
8874
+ if (b.nameKind === "folded") continue;
8875
+ if (b.nameKind === "child" && b.status === "completed") continue;
8876
+ add(b.status, b.count);
8877
+ size += b.count;
8878
+ }
8879
+ for (const cp of folded) {
8880
+ const n = foldedCount(cp) ?? 1;
8881
+ add(isFailedMember(cp) ? "failed" : cp.status, n);
8882
+ size += n;
8883
+ }
8884
+ const children = await failedChildren(store, runId, group, buckets);
8885
+ const failedChildSet = new Set(children.failed);
8886
+ for (const cp of children.completed) add(failedChildSet.has(cp) ? "failed" : "completed", 1);
8887
+ size += children.completed.length;
8888
+ const failedStatusRows = buckets.some((b) => b.status === "failed") ? await store.queryCheckpoints(runId, {
8889
+ group,
8890
+ statuses: [
8891
+ "failed"
8892
+ ],
8893
+ limit: FAN_SUMMARY_FAILURES
8894
+ }) : [];
8895
+ const failures = mergeBySeq(failedStatusRows, children.failed).slice(0, FAN_SUMMARY_FAILURES);
8896
+ const parsed = parseFanGroup(group);
8897
+ const total = parsed?.size;
8898
+ return {
8899
+ group,
8900
+ label: childRuns ? "child runs" : minName === maxName ? minName ?? "" : "steps",
8901
+ firstSeq,
8902
+ lastSeq,
8903
+ size,
8904
+ total,
8905
+ notStarted: total === void 0 ? void 0 : Math.max(0, total - size),
8906
+ failFast: parsed?.failFast ?? false,
8907
+ counts,
8908
+ startedAt,
8909
+ finishedAt,
8910
+ failures
8911
+ };
8912
+ }
8913
+ __name(summarizeFanFromStats, "summarizeFanFromStats");
8914
+ async function firstChildIds(store, runId) {
8915
+ const ids = [];
8916
+ for (let offset = 0; ids.length < SUMMARY_CHILD_LIMIT; offset += SUMMARY_CHILD_LIMIT) {
8917
+ const page = await store.queryCheckpoints(runId, {
8918
+ namePrefixes: CHILD_EDGE_PREFIXES,
8919
+ offset,
8920
+ limit: SUMMARY_CHILD_LIMIT
8921
+ });
8922
+ ids.push(...childIdsOf(page));
8923
+ if (page.length < SUMMARY_CHILD_LIMIT) break;
8924
+ }
8925
+ return ids.slice(0, SUMMARY_CHILD_LIMIT);
8926
+ }
8927
+ __name(firstChildIds, "firstChildIds");
8928
+ async function summarizeRunFromStore(store, runId, over, stats) {
8929
+ const all = stats ?? await store.checkpointStats(runId);
8930
+ const byGroup = /* @__PURE__ */ new Map();
8931
+ let edgeRows = 0;
8932
+ let hasFolded = false;
8933
+ for (const b of all) {
8934
+ if (b.nameKind === "child" || b.nameKind === "spawn") edgeRows += b.count;
8935
+ if (b.nameKind === "folded") hasFolded = true;
8936
+ if (b.group === null) continue;
8937
+ const buckets = byGroup.get(b.group);
8938
+ if (buckets) buckets.push(b);
8939
+ else byGroup.set(b.group, [
8940
+ b
8941
+ ]);
8942
+ }
8943
+ const wide = [
8944
+ ...byGroup
8945
+ ].filter(([group, buckets]) => isWide(group, buckets.reduce((n, b) => n + b.count, 0), over));
8946
+ const [timeline, folded, children] = await Promise.all([
8947
+ store.queryCheckpoints(runId, {
8948
+ excludeGroups: wide.map(([group]) => group)
8949
+ }),
8950
+ hasFolded ? store.queryCheckpoints(runId, {
8951
+ namePrefixes: [
8952
+ FOLDED_PREFIX
8953
+ ]
8954
+ }) : [],
8955
+ edgeRows > 0 || hasFolded ? firstChildIds(store, runId) : []
8956
+ ]);
8957
+ const fans = await Promise.all(wide.map(([group, buckets]) => summarizeFanFromStats(store, runId, group, buckets, folded.filter((cp) => cp.parallelGroup === group))));
8958
+ const childCount = folded.reduce((n, cp) => n + childIdsOf([
8959
+ cp
8960
+ ]).length, edgeRows);
8961
+ return {
8962
+ timeline,
8963
+ fans: fans.sort((a, b) => a.firstSeq - b.firstSeq),
8964
+ children,
8965
+ childCount
8966
+ };
8967
+ }
8968
+ __name(summarizeRunFromStore, "summarizeRunFromStore");
8969
+ async function pageFanFromStore(store, runId, group, query, stats) {
8970
+ const buckets = stats ?? await store.checkpointStats(runId, {
8971
+ group
8972
+ });
8973
+ const { offset, limit } = fanPageBounds(query);
8974
+ if (!query.failedOnly) {
8975
+ const total2 = buckets.reduce((n, b) => n + b.count, 0);
8976
+ const items = total2 > offset ? await store.queryCheckpoints(runId, {
8977
+ group,
8978
+ offset,
8979
+ limit
8980
+ }) : [];
8981
+ return {
8982
+ items,
8983
+ total: total2
8984
+ };
8985
+ }
8986
+ const failedStatus = buckets.filter((b) => b.status === "failed").reduce((n, b) => n + b.count, 0);
8987
+ const children = await failedChildren(store, runId, group, buckets);
8988
+ const total = failedStatus + children.failed.length;
8989
+ if (total <= offset) return {
8990
+ items: [],
8991
+ total
8992
+ };
8993
+ if (children.failed.length === 0) {
8994
+ return {
8995
+ items: await store.queryCheckpoints(runId, {
8996
+ group,
8997
+ statuses: [
8998
+ "failed"
8999
+ ],
9000
+ offset,
9001
+ limit
9002
+ }),
9003
+ total
9004
+ };
9005
+ }
9006
+ const failedRows = await store.queryCheckpoints(runId, {
9007
+ group,
9008
+ statuses: [
9009
+ "failed"
9010
+ ],
9011
+ limit: offset + limit
9012
+ });
9013
+ return {
9014
+ items: mergeBySeq(failedRows, children.failed).slice(offset, offset + limit),
9015
+ total
9016
+ };
9017
+ }
9018
+ __name(pageFanFromStore, "pageFanFromStore");
9019
+ function hash(text) {
9020
+ let h1 = 3735928559;
9021
+ let h2 = 1103547991;
9022
+ for (let i = 0; i < text.length; i++) {
9023
+ const ch = text.charCodeAt(i);
9024
+ h1 = Math.imul(h1 ^ ch, 2654435761);
9025
+ h2 = Math.imul(h2 ^ ch, 1597334677);
9026
+ }
9027
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
9028
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
9029
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
9030
+ }
9031
+ __name(hash, "hash");
9032
+ function statsFingerprint(stats) {
9033
+ return stats.map((b) => [
9034
+ b.group ?? "",
9035
+ b.status,
9036
+ b.nameKind,
9037
+ b.count,
9038
+ b.minSeq,
9039
+ b.maxSeq,
9040
+ b.minName,
9041
+ b.maxName,
9042
+ b.minEnqueuedAt.getTime(),
9043
+ b.minStartedAt.getTime(),
9044
+ b.maxStartedAt.getTime(),
9045
+ b.maxFinishedAt.getTime(),
9046
+ b.sumAttempts
9047
+ ].join("")).sort().join("");
9048
+ }
9049
+ __name(statsFingerprint, "statsFingerprint");
9050
+ function runDetailEtag(run, stats, over) {
9051
+ return `"rd-${hash(`${over}${JSON.stringify(run)}${statsFingerprint(stats)}`)}"`;
9052
+ }
9053
+ __name(runDetailEtag, "runDetailEtag");
9054
+ function fanPageEtag(group, stats, query) {
9055
+ const { offset, limit } = fanPageBounds(query);
9056
+ const key = [
9057
+ group,
9058
+ offset,
9059
+ limit,
9060
+ query.failedOnly === true
9061
+ ].join("");
9062
+ return `"fp-${hash(`${key}${statsFingerprint(stats)}`)}"`;
9063
+ }
9064
+ __name(fanPageEtag, "fanPageEtag");
9065
+ function etagMatches(ifNoneMatch, etag) {
9066
+ if (!ifNoneMatch) return false;
9067
+ const bare = /* @__PURE__ */ __name((tag) => tag.trim().replace(/^W\//, ""), "bare");
9068
+ const target = bare(etag);
9069
+ return ifNoneMatch.split(",").some((tag) => tag.trim() === "*" || bare(tag) === target);
9070
+ }
9071
+ __name(etagMatches, "etagMatches");
8054
9072
 
8055
9073
  // src/codec-state-store.ts
8056
9074
  var CodecStateStore = class {
@@ -8077,14 +9095,28 @@ var CodecStateStore = class {
8077
9095
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
8078
9096
  * engine's per-run fallback still kicks in through the wrapper. */
8079
9097
  deleteRuns;
9098
+ /** Decodes the runs like {@link listIncompleteRuns}. Bound only when the inner store has it, so
9099
+ * the engine's list-and-filter fallback still kicks in through the wrapper. */
9100
+ listOrphanedRuns;
9101
+ /** Encodes the patch like {@link updateRun}. Bound only when the inner store has it, so the
9102
+ * engine's read-then-write fallback still kicks in through the wrapper. */
9103
+ updateRunIf;
8080
9104
  /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
8081
9105
  * plain-save fallback still kicks in through the wrapper. */
8082
9106
  saveCheckpointIfPending;
9107
+ /** Encodes each checkpoint like {@link saveCheckpoint}. Bound only when the inner store has it. */
9108
+ saveCheckpointsIfPending;
8083
9109
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store has it. */
8084
9110
  countCheckpoints;
8085
9111
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
8086
9112
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
8087
9113
  pruneTerminalRuns;
9114
+ /** Forwarded verbatim — the buckets aggregate names, statuses and timestamps, never a payload.
9115
+ * Bound only when the inner store aggregates, so the run detail's fallback still applies. */
9116
+ checkpointStats;
9117
+ /** Filters on unencoded columns, then decodes the rows like {@link listCheckpoints}. Bound only
9118
+ * when the inner store has it. */
9119
+ queryCheckpoints;
8088
9120
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
8089
9121
  * engine's facet/listing fallback still applies through the wrapper. */
8090
9122
  countRuns;
@@ -8109,10 +9141,20 @@ var CodecStateStore = class {
8109
9141
  if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
8110
9142
  const deleteRuns = inner.deleteRuns;
8111
9143
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
9144
+ const orphans = inner.listOrphanedRuns;
9145
+ if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace) => (await orphans.call(inner, nowMs, limit, namespace)).map((r) => this.decRun(r));
9146
+ const updateIf = inner.updateRunIf;
9147
+ if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
8112
9148
  const saveIfPending = inner.saveCheckpointIfPending;
8113
9149
  if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
9150
+ const saveManyIfPending = inner.saveCheckpointsIfPending;
9151
+ if (saveManyIfPending) this.saveCheckpointsIfPending = (cps) => saveManyIfPending.call(inner, cps.map((cp) => this.encCp(cp)));
8114
9152
  const countCps = inner.countCheckpoints;
8115
9153
  if (countCps) this.countCheckpoints = (runId, from, to, statuses) => countCps.call(inner, runId, from, to, statuses);
9154
+ const stats = inner.checkpointStats;
9155
+ if (stats) this.checkpointStats = (runId, opts) => stats.call(inner, runId, opts);
9156
+ const queryCps = inner.queryCheckpoints;
9157
+ if (queryCps) this.queryCheckpoints = async (runId, query) => (await queryCps.call(inner, runId, query)).map((c) => this.decCp(c));
8116
9158
  const prune = inner.pruneTerminalRuns;
8117
9159
  if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
8118
9160
  const count = inner.countRuns;
@@ -8209,12 +9251,15 @@ var CodecStateStore = class {
8209
9251
  return this.inner.createRun(this.encRun(run));
8210
9252
  }
8211
9253
  updateRun(runId, patch) {
9254
+ return this.inner.updateRun(runId, this.encRunPatch(patch));
9255
+ }
9256
+ encRunPatch(patch) {
8212
9257
  const next = {
8213
9258
  ...patch
8214
9259
  };
8215
9260
  if ("input" in patch) next.input = this.enc(patch.input);
8216
9261
  if ("output" in patch) next.output = this.enc(patch.output);
8217
- return this.inner.updateRun(runId, next);
9262
+ return next;
8218
9263
  }
8219
9264
  async getRun(runId) {
8220
9265
  return this.decRun(await this.inner.getRun(runId));
@@ -8320,8 +9365,11 @@ function collectMetrics(engine) {
8320
9365
  started: 0,
8321
9366
  completed: 0,
8322
9367
  failed: 0,
8323
- suspended: 0
9368
+ suspended: 0,
9369
+ routed: 0,
9370
+ unrouted: 0
8324
9371
  },
9372
+ routedByCode: {},
8325
9373
  steps: {
8326
9374
  started: 0,
8327
9375
  completed: 0,
@@ -8363,6 +9411,15 @@ function collectMetrics(engine) {
8363
9411
  case "run.suspended":
8364
9412
  m.runs.suspended += 1;
8365
9413
  break;
9414
+ case "run.routed": {
9415
+ m.runs.routed += 1;
9416
+ const code = e.error?.code ?? "*";
9417
+ m.routedByCode[code] = (m.routedByCode[code] ?? 0) + 1;
9418
+ break;
9419
+ }
9420
+ case "run.unrouted":
9421
+ m.runs.unrouted += 1;
9422
+ break;
8366
9423
  case "step.started":
8367
9424
  m.steps.started += 1;
8368
9425
  break;
@@ -8389,6 +9446,8 @@ function collectMetrics(engine) {
8389
9446
  for (const [k, v] of Object.entries(m.steps)) lines.push(`durable_steps_total{event="${k}"} ${v}`);
8390
9447
  lines.push("# TYPE durable_runs_by_workflow_total counter");
8391
9448
  for (const [name, v] of Object.entries(m.byWorkflow)) for (const [event, n] of Object.entries(v)) lines.push(`durable_runs_by_workflow_total{workflow="${name}",event="${event}"} ${n}`);
9449
+ lines.push("# TYPE durable_runs_routed_total counter");
9450
+ for (const [code, n] of Object.entries(m.routedByCode)) lines.push(`durable_runs_routed_total{code="${code}"} ${n}`);
8392
9451
  lines.push("# TYPE durable_step_duration_ms_sum counter");
8393
9452
  lines.push(`durable_step_duration_ms_sum ${m.stepDuration.sumMs}`);
8394
9453
  lines.push("# TYPE durable_step_duration_count counter");
@@ -8559,6 +9618,11 @@ var InMemoryStateStore = class {
8559
9618
  * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
8560
9619
  */
8561
9620
  lastCheckpointRowsRead = 0;
9621
+ /**
9622
+ * Test/observability hook: checkpoint rows materialized since construction, across every
9623
+ * checkpoint read that returns rows — what a run-detail benchmark compares between paths.
9624
+ */
9625
+ checkpointRowsRead = 0;
8562
9626
  key(runId, seq) {
8563
9627
  return `${runId}:${seq}`;
8564
9628
  }
@@ -8604,6 +9668,14 @@ var InMemoryStateStore = class {
8604
9668
  this.reindexAttributes(run.id, run.searchAttributes);
8605
9669
  }
8606
9670
  async updateRun(runId, inputPatch) {
9671
+ this.applyRunPatch(runId, inputPatch);
9672
+ }
9673
+ updateRunIf(runId, expectedStatuses, patch) {
9674
+ const existing = this.runs.get(runId);
9675
+ if (!existing || !expectedStatuses.includes(existing.status)) return Promise.resolve(false);
9676
+ return this.updateRun(runId, patch).then(() => true);
9677
+ }
9678
+ applyRunPatch(runId, inputPatch) {
8607
9679
  const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
8608
9680
  ...inputPatch,
8609
9681
  tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
@@ -8684,6 +9756,23 @@ var InMemoryStateStore = class {
8684
9756
  });
8685
9757
  return true;
8686
9758
  }
9759
+ async saveCheckpointsIfPending(checkpoints) {
9760
+ return checkpoints.map((checkpoint) => {
9761
+ const key = this.key(checkpoint.runId, checkpoint.seq);
9762
+ if (this.checkpoints.get(key)?.status !== "pending") return false;
9763
+ this.checkpoints.set(key, {
9764
+ ...checkpoint
9765
+ });
9766
+ return true;
9767
+ });
9768
+ }
9769
+ async listOrphanedRuns(nowMs, limit, namespace) {
9770
+ return [
9771
+ ...this.runs.values()
9772
+ ].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
9773
+ ...r
9774
+ }));
9775
+ }
8687
9776
  async listIncompleteRuns(namespace) {
8688
9777
  return [
8689
9778
  ...this.runs.values()
@@ -8963,6 +10052,23 @@ var InMemoryStateStore = class {
8963
10052
  ...cp
8964
10053
  }));
8965
10054
  this.lastCheckpointRowsRead = rows.length;
10055
+ this.checkpointRowsRead += rows.length;
10056
+ return rows;
10057
+ }
10058
+ runCheckpoints(runId) {
10059
+ return [
10060
+ ...this.checkpoints.values()
10061
+ ].filter((cp) => cp.runId === runId);
10062
+ }
10063
+ async checkpointStats(runId, opts) {
10064
+ return checkpointStatsOf(this.runCheckpoints(runId), opts);
10065
+ }
10066
+ async queryCheckpoints(runId, query) {
10067
+ const rows = queryCheckpointsOf(this.runCheckpoints(runId), query).map((cp) => ({
10068
+ ...cp
10069
+ }));
10070
+ this.lastCheckpointRowsRead = rows.length;
10071
+ this.checkpointRowsRead += rows.length;
8966
10072
  return rows;
8967
10073
  }
8968
10074
  async getLatestCheckpointByName(runId, name) {
@@ -8990,6 +10096,7 @@ var InMemoryStateStore = class {
8990
10096
  ...cp
8991
10097
  }));
8992
10098
  this.lastCheckpointRowsRead = rows.length;
10099
+ this.checkpointRowsRead += rows.length;
8993
10100
  return rows;
8994
10101
  }
8995
10102
  };
@@ -9040,6 +10147,7 @@ var InMemoryTransport = class {
9040
10147
  }
9041
10148
  };
9042
10149
  export {
10150
+ CHILD_EDGE_PREFIXES,
9043
10151
  CONTROL_FLOW_SIGNAL,
9044
10152
  CURRENT_PROTOCOL_VERSION,
9045
10153
  CodecStateStore,
@@ -9060,6 +10168,7 @@ export {
9060
10168
  FAN_SUMMARY_FAILURES,
9061
10169
  FatalError,
9062
10170
  GatherError,
10171
+ HANDOFF_SEQ_BASE,
9063
10172
  InMemoryAdmissionBackend,
9064
10173
  InMemoryStateStore,
9065
10174
  InMemoryTransport,
@@ -9094,9 +10203,11 @@ export {
9094
10203
  WorkflowEngine,
9095
10204
  WorkflowSuspended,
9096
10205
  aggregateAnnouncements,
10206
+ aggregatesCheckpoints,
9097
10207
  announcementsOf,
9098
10208
  applyAttributeQuery,
9099
10209
  assertConcurrency,
10210
+ assertRoutes,
9100
10211
  attributeColumnFor,
9101
10212
  attributeOperand,
9102
10213
  attributePredicateOperands,
@@ -9106,10 +10217,14 @@ export {
9106
10217
  buildWorkflowDirectory,
9107
10218
  canRoute,
9108
10219
  capableWorkers,
10220
+ checkpointNameKind,
10221
+ checkpointStatsOf,
9109
10222
  childIdsOf,
9110
10223
  classifyWaiterToken,
9111
10224
  clearDurableExecutionWrappers,
9112
10225
  clearTerminalSingletonAdmission,
10226
+ codeAndDetails,
10227
+ codeOf,
9113
10228
  collectMetrics,
9114
10229
  concurrencyTag,
9115
10230
  controlPlaneDescriptor,
@@ -9122,13 +10237,20 @@ export {
9122
10237
  debug,
9123
10238
  describeWorker,
9124
10239
  descriptorHash,
10240
+ detailsOf,
9125
10241
  error,
10242
+ etagMatches,
9126
10243
  eventMatchOf,
9127
10244
  eventMatches,
9128
10245
  eventNameOf,
9129
10246
  eventPrefix,
9130
10247
  eventToken,
9131
10248
  facetOrigin,
10249
+ fanPageBounds,
10250
+ fanPageEtag,
10251
+ foldedCount,
10252
+ handoffHandlerId,
10253
+ handoffName,
9132
10254
  hasDurableExecutionWrappers,
9133
10255
  heartbeatStatus,
9134
10256
  indexWaitersByRun,
@@ -9140,6 +10262,7 @@ export {
9140
10262
  isNonEmptyRunScope,
9141
10263
  isWorkflowControlFlowSignal,
9142
10264
  listSignalWaitersOfRuns,
10265
+ liveHandoff,
9143
10266
  log,
9144
10267
  matchesAttributes,
9145
10268
  mergeRunFacetRows,
@@ -9150,17 +10273,23 @@ export {
9150
10273
  normalizeAttributeRows,
9151
10274
  normalizeDescriptor,
9152
10275
  observedAnnouncements,
10276
+ onErrorRunId,
9153
10277
  pageFan,
10278
+ pageFanFromStore,
9154
10279
  parentRunIdOf,
9155
10280
  parseDuration,
9156
10281
  partitionOfToken,
9157
10282
  planDispatch,
9158
10283
  prevCronFireMs,
10284
+ queryCheckpointsOf,
9159
10285
  readSearchAttributes,
9160
10286
  requiredCapabilities,
9161
10287
  resolveConcurrency,
9162
10288
  resolveRouting,
9163
10289
  resolveRunWaiting,
10290
+ retiredHandoffName,
10291
+ routeFor,
10292
+ runDetailEtag,
9164
10293
  runDurableExecution,
9165
10294
  runInStepLogger,
9166
10295
  runInStepScope,
@@ -9181,6 +10310,7 @@ export {
9181
10310
  subEvent,
9182
10311
  subProcess,
9183
10312
  summarizeFan,
10313
+ summarizeRunFromStore,
9184
10314
  summarizeTimeline,
9185
10315
  tenantGroup,
9186
10316
  useDurableExecution,