@dudousxd/nestjs-durable-core 0.78.0 → 0.79.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));
@@ -3761,6 +3937,8 @@ var WorkflowEngine = class {
3761
3937
  updateValidators = /* @__PURE__ */ new Map();
3762
3938
  /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
3763
3939
  cancelRequested = /* @__PURE__ */ new Set();
3940
+ /** {@link announcedRoutes} reads, by group + workflow + version. */
3941
+ announcedRoutesCache = /* @__PURE__ */ new Map();
3764
3942
  /**
3765
3943
  * Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
3766
3944
  * THIS instance — so the turn can re-check them once its lease is released (see
@@ -3782,6 +3960,8 @@ var WorkflowEngine = class {
3782
3960
  stepFansFiredHere = /* @__PURE__ */ new Map();
3783
3961
  /** The tail of each run's queue of results being handled on this instance — see {@link serializedPerRun}. */
3784
3962
  resultChains = /* @__PURE__ */ new Map();
3963
+ /** Each run's results waiting to be settled on this instance — see {@link completeRemoteResult}. */
3964
+ resultQueues = /* @__PURE__ */ new Map();
3785
3965
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3786
3966
  admission;
3787
3967
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -3988,6 +4168,7 @@ var WorkflowEngine = class {
3988
4168
  name,
3989
4169
  version,
3990
4170
  fn,
4171
+ onError: this.checkedRoutes(name, opts?.onError),
3991
4172
  tags: opts?.tags,
3992
4173
  singleton: opts?.singleton,
3993
4174
  concurrency: opts?.concurrency,
@@ -4057,6 +4238,7 @@ var WorkflowEngine = class {
4057
4238
  executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
4058
4239
  validateInput: opts.validateInput,
4059
4240
  requires: opts.requires,
4241
+ onError: this.checkedRoutes(name, opts.onError),
4060
4242
  remote: {
4061
4243
  group: opts.group,
4062
4244
  executor: opts.executor
@@ -4834,6 +5016,24 @@ var WorkflowEngine = class {
4834
5016
  if (resurrecting) {
4835
5017
  const isFailureCompletion = /* @__PURE__ */ __name((v) => typeof v === "object" && v !== null && v.ok === false, "isFailureCompletion");
4836
5018
  for (const cp of await this.store.listCheckpoints(runId)) {
5019
+ const handlerId = handoffHandlerId(runId, cp);
5020
+ if (handlerId !== void 0) {
5021
+ await this.store.takeSignalWaiter(`child:${handlerId}`);
5022
+ await this.store.saveCheckpoint({
5023
+ ...cp,
5024
+ name: retiredHandoffName(handlerId),
5025
+ status: "completed"
5026
+ });
5027
+ const tags = run.tags?.filter((t) => !t.startsWith("onError:"));
5028
+ if (tags && tags.length !== run.tags?.length) {
5029
+ await this.store.updateRun(runId, {
5030
+ tags,
5031
+ updatedAt: /* @__PURE__ */ new Date()
5032
+ });
5033
+ run.tags = tags;
5034
+ }
5035
+ continue;
5036
+ }
4837
5037
  if (cp.status === "failed") {
4838
5038
  await this.store.saveCheckpoint({
4839
5039
  ...cp,
@@ -4978,13 +5178,13 @@ var WorkflowEngine = class {
4978
5178
  * `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
4979
5179
  */
4980
5180
  waitForRun(runId, opts) {
4981
- const TERMINAL = /* @__PURE__ */ new Set([
5181
+ const TERMINAL2 = /* @__PURE__ */ new Set([
4982
5182
  "completed",
4983
5183
  "failed",
4984
5184
  "cancelled",
4985
5185
  "dead"
4986
5186
  ]);
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");
5187
+ 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
5188
  const toResult = /* @__PURE__ */ __name((run) => ({
4989
5189
  runId,
4990
5190
  status: run.status,
@@ -5239,10 +5439,7 @@ var WorkflowEngine = class {
5239
5439
  async redriveSettledStepFans(runId, fans) {
5240
5440
  for (const fan of fans) {
5241
5441
  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;
5442
+ if (await this.stepFanPending(runId, fan.from, fan.to) > 0) continue;
5246
5443
  const nowMs = this.clock();
5247
5444
  if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
5248
5445
  return;
@@ -5952,6 +6149,209 @@ var WorkflowEngine = class {
5952
6149
  * us back here (a remote step finished, a timer fired) goes through `resume` like any TS workflow.
5953
6150
  */
5954
6151
  /**
6152
+ * The registered onError routes whose handler workflow nothing can run: not registered on this
6153
+ * engine, and no live worker group serves it. Meant for a boot-time warning: a typo in a handler's
6154
+ * name would otherwise surface only when the first run fails.
6155
+ */
6156
+ async unservedOnErrorRoutes() {
6157
+ const unserved = [];
6158
+ let groups;
6159
+ for (const registered of this.workflows.values()) {
6160
+ for (const route of registered.onError ?? []) {
6161
+ if (this.latest.has(route.workflow)) continue;
6162
+ groups ??= await this.pool.listWorkerGroups().catch(() => []);
6163
+ const token = sanitizeQueueToken(route.workflow);
6164
+ if (groups.some((g) => g === token || g.startsWith(`${token}@`))) continue;
6165
+ unserved.push({
6166
+ workflow: registered.name,
6167
+ route
6168
+ });
6169
+ }
6170
+ }
6171
+ return unserved;
6172
+ }
6173
+ /** Validate a workflow's {@link OnErrorRoute}s at registration. */
6174
+ checkedRoutes(name, routes) {
6175
+ if (!routes?.length) return void 0;
6176
+ assertRoutes(name, routes);
6177
+ return routes;
6178
+ }
6179
+ /**
6180
+ * Route a failed run to its {@link OnErrorRoute} handler, read from the workflow's local registration
6181
+ * or, for one served by convention, from what its live workers announce. Starts the handler run and
6182
+ * waits for it the way a parent waits for a `ctx.child`: a `child:<handlerId>` waiter plus a
6183
+ * `running` placeholder checkpoint carrying the handler's name and input, so the handler's terminal
6184
+ * state wakes the run (buffered and reconciled like any child). From then on every execution of the
6185
+ * run only follows the handler ({@link followHandoff}); the body never runs again. Undefined when no
6186
+ * route matches: the run fails as usual.
6187
+ *
6188
+ * The placeholder sits past every seq the body used and is left out of {@link remoteHistory}.
6189
+ */
6190
+ async routeFailure(run, error2) {
6191
+ const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
6192
+ const routes = registered ? registered.onError : await this.announcedRoutes(run, error2);
6193
+ const route = routeFor(routes, error2);
6194
+ if (!route) return void 0;
6195
+ const checkpoints = await this.handoffCheckpoints(run.id);
6196
+ const live = liveHandoff(run.id, checkpoints);
6197
+ if (live) return this.followHandoff(run, live);
6198
+ const attempt = checkpoints.length;
6199
+ const seq = HANDOFF_SEQ_BASE + attempt;
6200
+ const handlerId = onErrorRunId(run.id, attempt);
6201
+ const handler = {
6202
+ workflow: route.workflow,
6203
+ input: {
6204
+ runId: run.id,
6205
+ workflow: run.workflow,
6206
+ input: run.input,
6207
+ error: error2
6208
+ }
6209
+ };
6210
+ await this.store.putSignalWaiter({
6211
+ token: `child:${handlerId}`,
6212
+ runId: run.id,
6213
+ seq
6214
+ });
6215
+ await this.store.saveCheckpoint({
6216
+ ...instantCheckpoint({
6217
+ runId: run.id,
6218
+ seq,
6219
+ name: handoffName(handlerId),
6220
+ kind: "signal",
6221
+ status: "running"
6222
+ }),
6223
+ input: handler
6224
+ });
6225
+ const tag = `onError:${route.code ?? "*"}`;
6226
+ if (!run.tags?.includes(tag)) {
6227
+ const tags = [
6228
+ ...run.tags ?? [],
6229
+ tag
6230
+ ];
6231
+ await this.store.updateRun(run.id, {
6232
+ tags,
6233
+ updatedAt: /* @__PURE__ */ new Date()
6234
+ });
6235
+ run.tags = tags;
6236
+ }
6237
+ this.emit({
6238
+ type: "run.routed",
6239
+ runId: run.id,
6240
+ workflow: run.workflow,
6241
+ namespace: run.namespace,
6242
+ name: route.workflow,
6243
+ error: error2
6244
+ });
6245
+ this.startChildDeferred(handler.workflow, handler.input, handlerId, {
6246
+ namespace: run.namespace
6247
+ });
6248
+ return {
6249
+ kind: "suspended"
6250
+ };
6251
+ }
6252
+ /** A run's handoff checkpoints, live and retired: the only ones at {@link HANDOFF_SEQ_BASE} and up. */
6253
+ async handoffCheckpoints(runId) {
6254
+ const handoffs = await this.store.listCheckpointsByNamePrefix?.(runId, [
6255
+ `signal:child:${runId}.onError.`,
6256
+ `onError:retried:${runId}.onError.`
6257
+ ]);
6258
+ return (handoffs ?? await this.store.listCheckpoints(runId)).filter((cp) => cp.seq >= HANDOFF_SEQ_BASE);
6259
+ }
6260
+ /**
6261
+ * Where a run that handed its failure off stands: the handler's outcome once it settled (from the
6262
+ * wake's checkpoint, or off the handler run when the wake never arrived), else still waiting. Starts
6263
+ * the handler if the turn that handed off crashed before its deferred start ran.
6264
+ */
6265
+ async followHandoff(run, handoff) {
6266
+ if (handoff.status === "completed") {
6267
+ const outcome = handoff.output;
6268
+ return outcome.ok ? {
6269
+ kind: "completed",
6270
+ output: outcome.value
6271
+ } : {
6272
+ kind: "failed",
6273
+ error: completionError(outcome)
6274
+ };
6275
+ }
6276
+ const handlerId = handoffHandlerId(run.id, handoff);
6277
+ const handlerRun = await this.store.getRun(handlerId);
6278
+ if (handlerRun?.status === "completed") return {
6279
+ kind: "completed",
6280
+ output: handlerRun.output
6281
+ };
6282
+ if (handlerRun && TERMINAL_RUN_STATUSES.includes(handlerRun.status)) {
6283
+ return {
6284
+ kind: "failed",
6285
+ error: handlerRun.error ?? {
6286
+ message: `onError handler ${handlerId} ${handlerRun.status}`
6287
+ }
6288
+ };
6289
+ }
6290
+ if (!handlerRun) {
6291
+ const handler = handoff.input;
6292
+ this.startChildDeferred(handler.workflow, handler.input, handlerId, {
6293
+ namespace: run.namespace
6294
+ });
6295
+ }
6296
+ return {
6297
+ kind: "suspended"
6298
+ };
6299
+ }
6300
+ /**
6301
+ * The onError routes the live workers announce for a workflow the engine serves by convention: the
6302
+ * one set they agree on, or none when they state none or cannot be read. Workers that announce
6303
+ * different sets (a rollout in progress) route nothing, and the run's failure is reported as a
6304
+ * `run.unrouted` event. Announcers of another version of the workflow are ignored. The read is
6305
+ * cached for {@link ANNOUNCED_ROUTES_TTL_MS}, so a burst of failures costs one descriptor read.
6306
+ */
6307
+ async announcedRoutes(run, error2) {
6308
+ const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
6309
+ const key = `${group}
6310
+ ${run.workflow}
6311
+ ${run.workflowVersion}`;
6312
+ const now = this.clock();
6313
+ let cached = this.announcedRoutesCache.get(key);
6314
+ if (!cached || now - cached.at > ANNOUNCED_ROUTES_TTL_MS) {
6315
+ cached = {
6316
+ at: now,
6317
+ sets: await this.readAnnouncedRoutes(run, group)
6318
+ };
6319
+ this.announcedRoutesCache.set(key, cached);
6320
+ }
6321
+ if (cached.sets.length > 1) {
6322
+ this.emit({
6323
+ type: "run.unrouted",
6324
+ runId: run.id,
6325
+ workflow: run.workflow,
6326
+ namespace: run.namespace,
6327
+ error: error2,
6328
+ reason: `live workers announce ${cached.sets.length} different onError route sets`
6329
+ });
6330
+ return void 0;
6331
+ }
6332
+ return cached.sets[0];
6333
+ }
6334
+ /** Every distinct onError route set the live workers of `group` announce for the run's workflow. */
6335
+ async readAnnouncedRoutes(run, group) {
6336
+ const transport = await this.pool.transportWithLiveGroup(group);
6337
+ if (!transport?.readWorkerDescriptors) return [];
6338
+ let descriptors;
6339
+ try {
6340
+ descriptors = await transport.readWorkerDescriptors(group);
6341
+ } catch {
6342
+ return [];
6343
+ }
6344
+ const sets = /* @__PURE__ */ new Map();
6345
+ for (const announced of aggregateAnnouncements(descriptors)) {
6346
+ if (announced.name !== run.workflow) continue;
6347
+ if (announced.version !== void 0 && announced.version !== run.workflowVersion) continue;
6348
+ for (const routes of announced.onError) sets.set(JSON.stringify(routes), routes);
6349
+ }
6350
+ return [
6351
+ ...sets.values()
6352
+ ];
6353
+ }
6354
+ /**
5955
6355
  * Apply a terminal/suspended transition once: persist the new run status, emit the matching
5956
6356
  * lifecycle event, and (on completion/failure) wake a waiting parent. Both the TS and remote
5957
6357
  * executors funnel their completed/failed/suspended outcomes through here so a status update can
@@ -5985,9 +6385,12 @@ var WorkflowEngine = class {
5985
6385
  };
5986
6386
  }
5987
6387
  if (outcome.kind === "failed") {
6388
+ const handed = await this.routeFailure(run, outcome.error);
6389
+ if (handed && handed.kind !== "failed") return this.settleRun(run, handed);
6390
+ const error2 = handed?.error ?? outcome.error;
5988
6391
  await this.store.updateRun(run.id, {
5989
6392
  status: "failed",
5990
- error: outcome.error,
6393
+ error: error2,
5991
6394
  updatedAt
5992
6395
  });
5993
6396
  this.emit({
@@ -5995,16 +6398,13 @@ var WorkflowEngine = class {
5995
6398
  runId: run.id,
5996
6399
  workflow: run.workflow,
5997
6400
  namespace: run.namespace,
5998
- error: outcome.error
5999
- });
6000
- void this.notifyParent(run.id, {
6001
- ok: false,
6002
- error: outcome.error.message
6401
+ error: error2
6003
6402
  });
6403
+ void this.notifyParent(run.id, failedCompletion(error2));
6004
6404
  return {
6005
6405
  runId: run.id,
6006
6406
  status: "failed",
6007
- error: outcome.error
6407
+ error: error2
6008
6408
  };
6009
6409
  }
6010
6410
  if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
@@ -6093,7 +6493,14 @@ var WorkflowEngine = class {
6093
6493
  }
6094
6494
  const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
6095
6495
  if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
6096
- const history = await this.remoteHistory(run.id);
6496
+ const checkpoints = await this.store.listCheckpoints(run.id);
6497
+ const handoff = liveHandoff(run.id, checkpoints);
6498
+ if (handoff) {
6499
+ const result = await this.settleRun(run, await this.followHandoff(run, handoff));
6500
+ await this.store.releaseRunLock(run.id);
6501
+ return result;
6502
+ }
6503
+ const history = await this.remoteHistory(run.id, checkpoints);
6097
6504
  if (remote.executor.dispatch) {
6098
6505
  const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
6099
6506
  const silenceMs = this.remoteAdvanceSilenceMs;
@@ -6417,8 +6824,8 @@ var WorkflowEngine = class {
6417
6824
  }
6418
6825
  }
6419
6826
  /** 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);
6827
+ async remoteHistory(runId, listed) {
6828
+ const checkpoints = listed ?? await this.store.listCheckpoints(runId);
6422
6829
  const kindOf = {
6423
6830
  remote: "call",
6424
6831
  local: "step",
@@ -6428,15 +6835,14 @@ var WorkflowEngine = class {
6428
6835
  const events = [];
6429
6836
  for (const cp of checkpoints) {
6430
6837
  if (cp.status === "completed" || cp.status === "failed") {
6838
+ if (cp.seq >= HANDOFF_SEQ_BASE) continue;
6431
6839
  if (cp.kind === "signal" && cp.name.startsWith("signal:child:")) {
6432
6840
  const completion = cp.output;
6433
6841
  events.push({
6434
6842
  seq: cp.seq,
6435
6843
  kind: "child",
6436
6844
  output: completion?.ok ? completion.value : void 0,
6437
- error: completion && completion.ok === false ? {
6438
- message: completion.error
6439
- } : void 0
6845
+ error: completion && completion.ok === false ? completionError(completion) : void 0
6440
6846
  });
6441
6847
  continue;
6442
6848
  }
@@ -6717,6 +7123,10 @@ var WorkflowEngine = class {
6717
7123
  this.awaitedStepFans.set(run.id, awaitedFans);
6718
7124
  const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
6719
7125
  try {
7126
+ const handoff = liveHandoff(run.id, snapshot);
7127
+ if (handoff && !this.cancelRequested.has(run.id)) {
7128
+ return await this.settleRun(run, await this.followHandoff(run, handoff));
7129
+ }
6720
7130
  const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
6721
7131
  if (this.cancelRequested.has(run.id)) {
6722
7132
  return await this.compensateAndCancel(run, compensations, replay);
@@ -6779,7 +7189,8 @@ var WorkflowEngine = class {
6779
7189
  }
6780
7190
  const error2 = {
6781
7191
  message: err instanceof Error ? err.message : String(err),
6782
- stack: err instanceof Error ? err.stack : void 0
7192
+ stack: err instanceof Error ? err.stack : void 0,
7193
+ ...codeAndDetails(err)
6783
7194
  };
6784
7195
  const pending = await this.unwindCompensations(run, compensations, replay);
6785
7196
  if (pending) return await this.settleRun(run, {
@@ -7415,7 +7826,8 @@ var WorkflowEngine = class {
7415
7826
  const failure = {
7416
7827
  index: i,
7417
7828
  id: stepId(runId, seq),
7418
- error: err.message
7829
+ error: err.message,
7830
+ ...codeAndDetails(err)
7419
7831
  };
7420
7832
  if (opts.mode === "failFast") throw new GatherError([
7421
7833
  failure
@@ -7437,6 +7849,24 @@ var WorkflowEngine = class {
7437
7849
  });
7438
7850
  throw new WorkflowSuspended(wakeAt);
7439
7851
  }
7852
+ if (opts.settle) {
7853
+ const failed = new Map(failures.map((f) => [
7854
+ f.index,
7855
+ f
7856
+ ]));
7857
+ return Array.from(outputs, (value, i) => {
7858
+ const f = failed.get(i);
7859
+ if (!f) return {
7860
+ status: "fulfilled",
7861
+ value
7862
+ };
7863
+ const { index: _index, ...rejected } = f;
7864
+ return {
7865
+ status: "rejected",
7866
+ ...rejected
7867
+ };
7868
+ });
7869
+ }
7440
7870
  if (failures.length > 0) throw new GatherError(failures, inputs.length);
7441
7871
  return outputs;
7442
7872
  }
@@ -7508,6 +7938,25 @@ var WorkflowEngine = class {
7508
7938
  await this.store.saveCheckpoint(checkpoint);
7509
7939
  return true;
7510
7940
  }
7941
+ /**
7942
+ * {@link savePendingCheckpoint} for checkpoints at distinct seqs of one run: one
7943
+ * {@link StateStore.saveCheckpointsIfPending} call when the store has it, else one write each.
7944
+ * Never rejects as a whole: a write that failed is reported as that entry's error, and the batch
7945
+ * call rejecting means it wrote nothing, so the entries are then written one by one.
7946
+ */
7947
+ async savePendingCheckpoints(checkpoints) {
7948
+ if (checkpoints.length > 1 && this.store.saveCheckpointsIfPending) {
7949
+ try {
7950
+ const wrote = await this.store.saveCheckpointsIfPending(checkpoints);
7951
+ return checkpoints.map((_, i) => wrote[i] === true);
7952
+ } catch {
7953
+ }
7954
+ }
7955
+ const settled = await Promise.allSettled(checkpoints.map((cp) => this.savePendingCheckpoint(cp)));
7956
+ return settled.map((s) => s.status === "fulfilled" ? s.value : {
7957
+ error: s.reason
7958
+ });
7959
+ }
7511
7960
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
7512
7961
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
7513
7962
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -7546,57 +7995,161 @@ var WorkflowEngine = class {
7546
7995
  * result (the dispatching one may be gone), so the run is crash/scale-safe. Resolves whether the
7547
7996
  * run should now be resumed: not for a duplicate or late delivery (the checkpoint isn't `pending`),
7548
7997
  * a run already finished, or a step fan whose barrier is still closed.
7998
+ *
7999
+ * Results of one run are settled in batches: the first starts a batch, and the ones that arrive
8000
+ * while it is being written form the next. Under load a wide fan's results are written by one
8001
+ * {@link StateStore.saveCheckpointsIfPending} call per batch, and the fan's barrier is asked once
8002
+ * per batch, not once per result. A result resolves only once its own write has committed, so the
8003
+ * transport acknowledges nothing that is not durable yet.
8004
+ */
8005
+ completeRemoteResult(result) {
8006
+ return new Promise((resolve, reject) => {
8007
+ const entry = {
8008
+ result,
8009
+ resolve,
8010
+ reject
8011
+ };
8012
+ const queued = this.resultQueues.get(result.runId);
8013
+ if (queued) {
8014
+ queued.push(entry);
8015
+ return;
8016
+ }
8017
+ const queue = [
8018
+ entry
8019
+ ];
8020
+ this.resultQueues.set(result.runId, queue);
8021
+ void this.drainResultQueue(result.runId, queue);
8022
+ });
8023
+ }
8024
+ /** Settle `queue` (a run's queued results) batch by batch until it is empty, then retire it. */
8025
+ async drainResultQueue(runId, queue) {
8026
+ await Promise.resolve();
8027
+ while (queue.length > 0) {
8028
+ const batch = queue.splice(0, RESULT_BATCH_MAX);
8029
+ let outcomes;
8030
+ try {
8031
+ outcomes = await this.completeRemoteResults(runId, batch.map((q) => q.result));
8032
+ } catch (err) {
8033
+ outcomes = batch.map(() => ({
8034
+ error: err
8035
+ }));
8036
+ }
8037
+ batch.forEach((q, i) => {
8038
+ const outcome = outcomes[i] ?? false;
8039
+ if (typeof outcome === "boolean") q.resolve(outcome);
8040
+ else q.reject(outcome.error);
8041
+ });
8042
+ }
8043
+ this.resultQueues.delete(runId);
8044
+ }
8045
+ /**
8046
+ * Settle a batch of one run's step results. At most one of them resolves `true` (resume the run):
8047
+ * that resume replays every write of the batch, so a second would only replay it again. A result
8048
+ * whose read or write failed resolves its error, and the transport redelivers just that one.
7549
8049
  */
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;
8050
+ async completeRemoteResults(runId, results) {
8051
+ const outcomes = results.map(() => false);
8052
+ const seen = /* @__PURE__ */ new Set();
8053
+ const live = results.flatMap((r, i) => {
8054
+ if (seen.has(r.seq)) return [];
8055
+ seen.add(r.seq);
8056
+ return [
8057
+ i
8058
+ ];
8059
+ });
8060
+ const reads = await Promise.allSettled(live.map((i) => this.store.getCheckpoint(runId, results[i].seq)));
8061
+ const pending = [];
8062
+ reads.forEach((read, k) => {
8063
+ const index = live[k];
8064
+ if (read.status === "rejected") outcomes[index] = {
8065
+ error: read.reason
8066
+ };
8067
+ else if (read.value?.status === "pending") pending.push({
8068
+ index,
8069
+ cp: read.value
8070
+ });
8071
+ });
8072
+ if (pending.length === 0) return outcomes;
8073
+ for (const { cp } of pending) void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
8074
+ const run = await this.store.getRun(runId);
8075
+ if (run && (run.status === "cancelled" || run.status === "completed")) return outcomes;
7556
8076
  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
8077
+ const writes = pending.map(({ index, cp }) => {
8078
+ const result = results[index];
8079
+ return {
8080
+ ...cp,
8081
+ status: result.status,
8082
+ output: result.status === "completed" ? result.output : cp.output,
8083
+ error: result.error,
8084
+ events: result.events ?? cp.events,
8085
+ startedAt: result.startedAt ? new Date(result.startedAt) : cp.startedAt,
8086
+ finishedAt
8087
+ };
7566
8088
  });
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()
8089
+ const claims = await this.savePendingCheckpoints(writes);
8090
+ const settled = [];
8091
+ claims.forEach((claim, k) => {
8092
+ const { index, cp } = pending[k];
8093
+ if (claim === true) settled.push({
8094
+ index,
8095
+ cp,
8096
+ result: results[index]
8097
+ });
8098
+ else if (claim !== false) outcomes[index] = claim;
7579
8099
  });
7580
- return !await this.stepFanStillRunning(cp, result);
8100
+ for (const { cp, result } of settled) {
8101
+ this.noteSettledHere(`${runId}:${result.seq}`);
8102
+ const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
8103
+ this.emit({
8104
+ type: result.status === "completed" ? "step.completed" : "step.failed",
8105
+ runId,
8106
+ seq: result.seq,
8107
+ name: cp.name,
8108
+ kind: cp.kind,
8109
+ output: result.output,
8110
+ error: result.error,
8111
+ queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
8112
+ durationMs: finishedAt.getTime() - startedAt.getTime()
8113
+ });
8114
+ }
8115
+ const barriers = /* @__PURE__ */ new Map();
8116
+ let carrier;
8117
+ for (const { index, cp, result } of settled) {
8118
+ if (!await this.stepFanStillRunning(cp, result, barriers)) carrier = index;
8119
+ }
8120
+ if (carrier !== void 0) outcomes[carrier] = true;
8121
+ return outcomes;
7581
8122
  }
7582
8123
  /**
7583
8124
  * The join barrier of a step fan: a result for one of its steps resumes the parent only once no
7584
8125
  * step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
7585
8126
  * 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.
8127
+ * which makes a fan of N steps cost O(N²) checkpoint reads. `barriers` holds the answer per fan
8128
+ * for the rest of a batch. A barrier that cannot be read counts as open: a spare resume replays and
8129
+ * re-suspends, a lost one strands the run.
7587
8130
  */
7588
- async stepFanStillRunning(cp, result) {
8131
+ async stepFanStillRunning(cp, result, barriers) {
7589
8132
  const fan = parseStepFanGroup(cp.parallelGroup);
7590
8133
  if (!fan) return false;
7591
8134
  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;
8135
+ let closed = barriers.get(fan.firstSeq);
8136
+ if (!closed) {
8137
+ closed = this.stepFanPending(result.runId, fan.firstSeq, fan.firstSeq + fan.size - 1).then((pending) => pending > 0, () => false);
8138
+ barriers.set(fan.firstSeq, closed);
8139
+ }
8140
+ if (await closed) return true;
7597
8141
  this.noteRecent(this.stepFansFiredHere, `${result.runId}:${fan.firstSeq}`);
7598
8142
  return false;
7599
8143
  }
8144
+ /** How many steps of a step fan's seq range are still `pending`. */
8145
+ async stepFanPending(runId, from, to) {
8146
+ if (this.store.countCheckpoints) {
8147
+ return this.store.countCheckpoints(runId, from, to, [
8148
+ "pending"
8149
+ ]);
8150
+ }
8151
+ return (await this.store.listCheckpoints(runId)).filter((c) => c.seq >= from && c.seq <= to && c.status === "pending").length;
8152
+ }
7600
8153
  /**
7601
8154
  * Apply a remote workflow-turn {@link WorkflowDecision} durably — runs on WHATEVER instance receives
7602
8155
  * it over the broker (the dispatching one may be gone, or a point-to-point broker handed it to a
@@ -8043,14 +8596,339 @@ function summarizeTimeline(timeline, over) {
8043
8596
  __name(summarizeTimeline, "summarizeTimeline");
8044
8597
  function pageFan(timeline, group, query) {
8045
8598
  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;
8599
+ const { offset, limit } = fanPageBounds(query);
8048
8600
  return {
8049
8601
  items: matching.slice(offset, offset + limit),
8050
8602
  total: matching.length
8051
8603
  };
8052
8604
  }
8053
8605
  __name(pageFan, "pageFan");
8606
+ function fanPageBounds(query) {
8607
+ const offset = query.offset !== void 0 && Number.isFinite(query.offset) ? Math.max(0, Math.floor(query.offset)) : 0;
8608
+ const limit = query.limit !== void 0 && Number.isFinite(query.limit) ? Math.min(FAN_PAGE_MAX, Math.max(1, Math.floor(query.limit))) : 100;
8609
+ return {
8610
+ offset,
8611
+ limit
8612
+ };
8613
+ }
8614
+ __name(fanPageBounds, "fanPageBounds");
8615
+
8616
+ // src/run-detail-store.ts
8617
+ function aggregatesCheckpoints(store) {
8618
+ return store.checkpointStats !== void 0 && store.queryCheckpoints !== void 0;
8619
+ }
8620
+ __name(aggregatesCheckpoints, "aggregatesCheckpoints");
8621
+ function checkpointStatsOf(checkpoints, opts) {
8622
+ const buckets = /* @__PURE__ */ new Map();
8623
+ for (const cp of checkpoints) {
8624
+ const group = cp.parallelGroup ?? null;
8625
+ if (opts?.group !== void 0 && group !== opts.group) continue;
8626
+ const nameKind = checkpointNameKind(cp.name);
8627
+ const key = JSON.stringify([
8628
+ group,
8629
+ cp.status,
8630
+ nameKind
8631
+ ]);
8632
+ const enqueuedAt = cp.enqueuedAt ?? cp.startedAt;
8633
+ const bucket = buckets.get(key);
8634
+ if (!bucket) {
8635
+ buckets.set(key, {
8636
+ group,
8637
+ status: cp.status,
8638
+ nameKind,
8639
+ count: 1,
8640
+ minSeq: cp.seq,
8641
+ maxSeq: cp.seq,
8642
+ minName: cp.name,
8643
+ maxName: cp.name,
8644
+ minEnqueuedAt: enqueuedAt,
8645
+ minStartedAt: cp.startedAt,
8646
+ maxStartedAt: cp.startedAt,
8647
+ maxFinishedAt: cp.finishedAt,
8648
+ sumAttempts: cp.attempts
8649
+ });
8650
+ continue;
8651
+ }
8652
+ bucket.count += 1;
8653
+ bucket.minSeq = Math.min(bucket.minSeq, cp.seq);
8654
+ bucket.maxSeq = Math.max(bucket.maxSeq, cp.seq);
8655
+ if (cp.name < bucket.minName) bucket.minName = cp.name;
8656
+ if (cp.name > bucket.maxName) bucket.maxName = cp.name;
8657
+ if (enqueuedAt < bucket.minEnqueuedAt) bucket.minEnqueuedAt = enqueuedAt;
8658
+ if (cp.startedAt < bucket.minStartedAt) bucket.minStartedAt = cp.startedAt;
8659
+ if (cp.startedAt > bucket.maxStartedAt) bucket.maxStartedAt = cp.startedAt;
8660
+ if (cp.finishedAt > bucket.maxFinishedAt) bucket.maxFinishedAt = cp.finishedAt;
8661
+ bucket.sumAttempts += cp.attempts;
8662
+ }
8663
+ return [
8664
+ ...buckets.values()
8665
+ ];
8666
+ }
8667
+ __name(checkpointStatsOf, "checkpointStatsOf");
8668
+ function queryCheckpointsOf(checkpoints, query) {
8669
+ const excluded = new Set(query.excludeGroups ?? []);
8670
+ 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);
8671
+ const offset = query.offset ?? 0;
8672
+ return matching.slice(offset, query.limit === void 0 ? void 0 : offset + query.limit);
8673
+ }
8674
+ __name(queryCheckpointsOf, "queryCheckpointsOf");
8675
+ var TERMINAL = /* @__PURE__ */ __name((status) => status !== "pending" && status !== "running", "TERMINAL");
8676
+ function isWide(group, rows, over) {
8677
+ return rows > over || (parseFanGroup(group)?.size ?? 0) > over;
8678
+ }
8679
+ __name(isWide, "isWide");
8680
+ function mergeBySeq(a, b) {
8681
+ return [
8682
+ ...a,
8683
+ ...b
8684
+ ].sort((x, y) => x.seq - y.seq);
8685
+ }
8686
+ __name(mergeBySeq, "mergeBySeq");
8687
+ async function failedChildren(store, runId, group, buckets) {
8688
+ if (!buckets.some((b) => b.nameKind === "child" && b.status === "completed")) {
8689
+ return {
8690
+ completed: [],
8691
+ failed: []
8692
+ };
8693
+ }
8694
+ const completed = await store.queryCheckpoints(runId, {
8695
+ group,
8696
+ statuses: [
8697
+ "completed"
8698
+ ],
8699
+ namePrefixes: [
8700
+ "signal:child:"
8701
+ ]
8702
+ });
8703
+ return {
8704
+ completed,
8705
+ failed: completed.filter(isFailedMember)
8706
+ };
8707
+ }
8708
+ __name(failedChildren, "failedChildren");
8709
+ async function summarizeFanFromStats(store, runId, group, buckets, folded) {
8710
+ const counts = {};
8711
+ const add = /* @__PURE__ */ __name((status, n) => {
8712
+ counts[status] = (counts[status] ?? 0) + n;
8713
+ }, "add");
8714
+ let size = 0;
8715
+ let startedAt;
8716
+ let finishedAt;
8717
+ let firstSeq = Number.POSITIVE_INFINITY;
8718
+ let lastSeq = Number.NEGATIVE_INFINITY;
8719
+ let minName;
8720
+ let maxName;
8721
+ let childRuns = false;
8722
+ for (const b of buckets) {
8723
+ firstSeq = Math.min(firstSeq, b.minSeq);
8724
+ lastSeq = Math.max(lastSeq, b.maxSeq);
8725
+ if (minName === void 0 || b.minName < minName) minName = b.minName;
8726
+ if (maxName === void 0 || b.maxName > maxName) maxName = b.maxName;
8727
+ if (b.nameKind === "child" || b.nameKind === "folded") childRuns = true;
8728
+ if (TERMINAL(b.status)) {
8729
+ if (!startedAt || b.minStartedAt < startedAt) startedAt = b.minStartedAt;
8730
+ if (!finishedAt || b.maxFinishedAt > finishedAt) finishedAt = b.maxFinishedAt;
8731
+ } else if (!startedAt || b.minEnqueuedAt < startedAt) {
8732
+ startedAt = b.minEnqueuedAt;
8733
+ }
8734
+ if (b.nameKind === "folded") continue;
8735
+ if (b.nameKind === "child" && b.status === "completed") continue;
8736
+ add(b.status, b.count);
8737
+ size += b.count;
8738
+ }
8739
+ for (const cp of folded) {
8740
+ const n = foldedCount(cp) ?? 1;
8741
+ add(isFailedMember(cp) ? "failed" : cp.status, n);
8742
+ size += n;
8743
+ }
8744
+ const children = await failedChildren(store, runId, group, buckets);
8745
+ const failedChildSet = new Set(children.failed);
8746
+ for (const cp of children.completed) add(failedChildSet.has(cp) ? "failed" : "completed", 1);
8747
+ size += children.completed.length;
8748
+ const failedStatusRows = buckets.some((b) => b.status === "failed") ? await store.queryCheckpoints(runId, {
8749
+ group,
8750
+ statuses: [
8751
+ "failed"
8752
+ ],
8753
+ limit: FAN_SUMMARY_FAILURES
8754
+ }) : [];
8755
+ const failures = mergeBySeq(failedStatusRows, children.failed).slice(0, FAN_SUMMARY_FAILURES);
8756
+ const parsed = parseFanGroup(group);
8757
+ const total = parsed?.size;
8758
+ return {
8759
+ group,
8760
+ label: childRuns ? "child runs" : minName === maxName ? minName ?? "" : "steps",
8761
+ firstSeq,
8762
+ lastSeq,
8763
+ size,
8764
+ total,
8765
+ notStarted: total === void 0 ? void 0 : Math.max(0, total - size),
8766
+ failFast: parsed?.failFast ?? false,
8767
+ counts,
8768
+ startedAt,
8769
+ finishedAt,
8770
+ failures
8771
+ };
8772
+ }
8773
+ __name(summarizeFanFromStats, "summarizeFanFromStats");
8774
+ async function firstChildIds(store, runId) {
8775
+ const ids = [];
8776
+ for (let offset = 0; ids.length < SUMMARY_CHILD_LIMIT; offset += SUMMARY_CHILD_LIMIT) {
8777
+ const page = await store.queryCheckpoints(runId, {
8778
+ namePrefixes: CHILD_EDGE_PREFIXES,
8779
+ offset,
8780
+ limit: SUMMARY_CHILD_LIMIT
8781
+ });
8782
+ ids.push(...childIdsOf(page));
8783
+ if (page.length < SUMMARY_CHILD_LIMIT) break;
8784
+ }
8785
+ return ids.slice(0, SUMMARY_CHILD_LIMIT);
8786
+ }
8787
+ __name(firstChildIds, "firstChildIds");
8788
+ async function summarizeRunFromStore(store, runId, over, stats) {
8789
+ const all = stats ?? await store.checkpointStats(runId);
8790
+ const byGroup = /* @__PURE__ */ new Map();
8791
+ let edgeRows = 0;
8792
+ let hasFolded = false;
8793
+ for (const b of all) {
8794
+ if (b.nameKind === "child" || b.nameKind === "spawn") edgeRows += b.count;
8795
+ if (b.nameKind === "folded") hasFolded = true;
8796
+ if (b.group === null) continue;
8797
+ const buckets = byGroup.get(b.group);
8798
+ if (buckets) buckets.push(b);
8799
+ else byGroup.set(b.group, [
8800
+ b
8801
+ ]);
8802
+ }
8803
+ const wide = [
8804
+ ...byGroup
8805
+ ].filter(([group, buckets]) => isWide(group, buckets.reduce((n, b) => n + b.count, 0), over));
8806
+ const [timeline, folded, children] = await Promise.all([
8807
+ store.queryCheckpoints(runId, {
8808
+ excludeGroups: wide.map(([group]) => group)
8809
+ }),
8810
+ hasFolded ? store.queryCheckpoints(runId, {
8811
+ namePrefixes: [
8812
+ FOLDED_PREFIX
8813
+ ]
8814
+ }) : [],
8815
+ edgeRows > 0 || hasFolded ? firstChildIds(store, runId) : []
8816
+ ]);
8817
+ const fans = await Promise.all(wide.map(([group, buckets]) => summarizeFanFromStats(store, runId, group, buckets, folded.filter((cp) => cp.parallelGroup === group))));
8818
+ const childCount = folded.reduce((n, cp) => n + childIdsOf([
8819
+ cp
8820
+ ]).length, edgeRows);
8821
+ return {
8822
+ timeline,
8823
+ fans: fans.sort((a, b) => a.firstSeq - b.firstSeq),
8824
+ children,
8825
+ childCount
8826
+ };
8827
+ }
8828
+ __name(summarizeRunFromStore, "summarizeRunFromStore");
8829
+ async function pageFanFromStore(store, runId, group, query, stats) {
8830
+ const buckets = stats ?? await store.checkpointStats(runId, {
8831
+ group
8832
+ });
8833
+ const { offset, limit } = fanPageBounds(query);
8834
+ if (!query.failedOnly) {
8835
+ const total2 = buckets.reduce((n, b) => n + b.count, 0);
8836
+ const items = total2 > offset ? await store.queryCheckpoints(runId, {
8837
+ group,
8838
+ offset,
8839
+ limit
8840
+ }) : [];
8841
+ return {
8842
+ items,
8843
+ total: total2
8844
+ };
8845
+ }
8846
+ const failedStatus = buckets.filter((b) => b.status === "failed").reduce((n, b) => n + b.count, 0);
8847
+ const children = await failedChildren(store, runId, group, buckets);
8848
+ const total = failedStatus + children.failed.length;
8849
+ if (total <= offset) return {
8850
+ items: [],
8851
+ total
8852
+ };
8853
+ if (children.failed.length === 0) {
8854
+ return {
8855
+ items: await store.queryCheckpoints(runId, {
8856
+ group,
8857
+ statuses: [
8858
+ "failed"
8859
+ ],
8860
+ offset,
8861
+ limit
8862
+ }),
8863
+ total
8864
+ };
8865
+ }
8866
+ const failedRows = await store.queryCheckpoints(runId, {
8867
+ group,
8868
+ statuses: [
8869
+ "failed"
8870
+ ],
8871
+ limit: offset + limit
8872
+ });
8873
+ return {
8874
+ items: mergeBySeq(failedRows, children.failed).slice(offset, offset + limit),
8875
+ total
8876
+ };
8877
+ }
8878
+ __name(pageFanFromStore, "pageFanFromStore");
8879
+ function hash(text) {
8880
+ let h1 = 3735928559;
8881
+ let h2 = 1103547991;
8882
+ for (let i = 0; i < text.length; i++) {
8883
+ const ch = text.charCodeAt(i);
8884
+ h1 = Math.imul(h1 ^ ch, 2654435761);
8885
+ h2 = Math.imul(h2 ^ ch, 1597334677);
8886
+ }
8887
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
8888
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
8889
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
8890
+ }
8891
+ __name(hash, "hash");
8892
+ function statsFingerprint(stats) {
8893
+ return stats.map((b) => [
8894
+ b.group ?? "",
8895
+ b.status,
8896
+ b.nameKind,
8897
+ b.count,
8898
+ b.minSeq,
8899
+ b.maxSeq,
8900
+ b.minName,
8901
+ b.maxName,
8902
+ b.minEnqueuedAt.getTime(),
8903
+ b.minStartedAt.getTime(),
8904
+ b.maxStartedAt.getTime(),
8905
+ b.maxFinishedAt.getTime(),
8906
+ b.sumAttempts
8907
+ ].join("")).sort().join("");
8908
+ }
8909
+ __name(statsFingerprint, "statsFingerprint");
8910
+ function runDetailEtag(run, stats, over) {
8911
+ return `"rd-${hash(`${over}${JSON.stringify(run)}${statsFingerprint(stats)}`)}"`;
8912
+ }
8913
+ __name(runDetailEtag, "runDetailEtag");
8914
+ function fanPageEtag(group, stats, query) {
8915
+ const { offset, limit } = fanPageBounds(query);
8916
+ const key = [
8917
+ group,
8918
+ offset,
8919
+ limit,
8920
+ query.failedOnly === true
8921
+ ].join("");
8922
+ return `"fp-${hash(`${key}${statsFingerprint(stats)}`)}"`;
8923
+ }
8924
+ __name(fanPageEtag, "fanPageEtag");
8925
+ function etagMatches(ifNoneMatch, etag) {
8926
+ if (!ifNoneMatch) return false;
8927
+ const bare = /* @__PURE__ */ __name((tag) => tag.trim().replace(/^W\//, ""), "bare");
8928
+ const target = bare(etag);
8929
+ return ifNoneMatch.split(",").some((tag) => tag.trim() === "*" || bare(tag) === target);
8930
+ }
8931
+ __name(etagMatches, "etagMatches");
8054
8932
 
8055
8933
  // src/codec-state-store.ts
8056
8934
  var CodecStateStore = class {
@@ -8080,11 +8958,19 @@ var CodecStateStore = class {
8080
8958
  /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
8081
8959
  * plain-save fallback still kicks in through the wrapper. */
8082
8960
  saveCheckpointIfPending;
8961
+ /** Encodes each checkpoint like {@link saveCheckpoint}. Bound only when the inner store has it. */
8962
+ saveCheckpointsIfPending;
8083
8963
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store has it. */
8084
8964
  countCheckpoints;
8085
8965
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
8086
8966
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
8087
8967
  pruneTerminalRuns;
8968
+ /** Forwarded verbatim — the buckets aggregate names, statuses and timestamps, never a payload.
8969
+ * Bound only when the inner store aggregates, so the run detail's fallback still applies. */
8970
+ checkpointStats;
8971
+ /** Filters on unencoded columns, then decodes the rows like {@link listCheckpoints}. Bound only
8972
+ * when the inner store has it. */
8973
+ queryCheckpoints;
8088
8974
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
8089
8975
  * engine's facet/listing fallback still applies through the wrapper. */
8090
8976
  countRuns;
@@ -8111,8 +8997,14 @@ var CodecStateStore = class {
8111
8997
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
8112
8998
  const saveIfPending = inner.saveCheckpointIfPending;
8113
8999
  if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
9000
+ const saveManyIfPending = inner.saveCheckpointsIfPending;
9001
+ if (saveManyIfPending) this.saveCheckpointsIfPending = (cps) => saveManyIfPending.call(inner, cps.map((cp) => this.encCp(cp)));
8114
9002
  const countCps = inner.countCheckpoints;
8115
9003
  if (countCps) this.countCheckpoints = (runId, from, to, statuses) => countCps.call(inner, runId, from, to, statuses);
9004
+ const stats = inner.checkpointStats;
9005
+ if (stats) this.checkpointStats = (runId, opts) => stats.call(inner, runId, opts);
9006
+ const queryCps = inner.queryCheckpoints;
9007
+ if (queryCps) this.queryCheckpoints = async (runId, query) => (await queryCps.call(inner, runId, query)).map((c) => this.decCp(c));
8116
9008
  const prune = inner.pruneTerminalRuns;
8117
9009
  if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
8118
9010
  const count = inner.countRuns;
@@ -8320,8 +9212,11 @@ function collectMetrics(engine) {
8320
9212
  started: 0,
8321
9213
  completed: 0,
8322
9214
  failed: 0,
8323
- suspended: 0
9215
+ suspended: 0,
9216
+ routed: 0,
9217
+ unrouted: 0
8324
9218
  },
9219
+ routedByCode: {},
8325
9220
  steps: {
8326
9221
  started: 0,
8327
9222
  completed: 0,
@@ -8363,6 +9258,15 @@ function collectMetrics(engine) {
8363
9258
  case "run.suspended":
8364
9259
  m.runs.suspended += 1;
8365
9260
  break;
9261
+ case "run.routed": {
9262
+ m.runs.routed += 1;
9263
+ const code = e.error?.code ?? "*";
9264
+ m.routedByCode[code] = (m.routedByCode[code] ?? 0) + 1;
9265
+ break;
9266
+ }
9267
+ case "run.unrouted":
9268
+ m.runs.unrouted += 1;
9269
+ break;
8366
9270
  case "step.started":
8367
9271
  m.steps.started += 1;
8368
9272
  break;
@@ -8389,6 +9293,8 @@ function collectMetrics(engine) {
8389
9293
  for (const [k, v] of Object.entries(m.steps)) lines.push(`durable_steps_total{event="${k}"} ${v}`);
8390
9294
  lines.push("# TYPE durable_runs_by_workflow_total counter");
8391
9295
  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}`);
9296
+ lines.push("# TYPE durable_runs_routed_total counter");
9297
+ for (const [code, n] of Object.entries(m.routedByCode)) lines.push(`durable_runs_routed_total{code="${code}"} ${n}`);
8392
9298
  lines.push("# TYPE durable_step_duration_ms_sum counter");
8393
9299
  lines.push(`durable_step_duration_ms_sum ${m.stepDuration.sumMs}`);
8394
9300
  lines.push("# TYPE durable_step_duration_count counter");
@@ -8559,6 +9465,11 @@ var InMemoryStateStore = class {
8559
9465
  * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
8560
9466
  */
8561
9467
  lastCheckpointRowsRead = 0;
9468
+ /**
9469
+ * Test/observability hook: checkpoint rows materialized since construction, across every
9470
+ * checkpoint read that returns rows — what a run-detail benchmark compares between paths.
9471
+ */
9472
+ checkpointRowsRead = 0;
8562
9473
  key(runId, seq) {
8563
9474
  return `${runId}:${seq}`;
8564
9475
  }
@@ -8684,6 +9595,16 @@ var InMemoryStateStore = class {
8684
9595
  });
8685
9596
  return true;
8686
9597
  }
9598
+ async saveCheckpointsIfPending(checkpoints) {
9599
+ return checkpoints.map((checkpoint) => {
9600
+ const key = this.key(checkpoint.runId, checkpoint.seq);
9601
+ if (this.checkpoints.get(key)?.status !== "pending") return false;
9602
+ this.checkpoints.set(key, {
9603
+ ...checkpoint
9604
+ });
9605
+ return true;
9606
+ });
9607
+ }
8687
9608
  async listIncompleteRuns(namespace) {
8688
9609
  return [
8689
9610
  ...this.runs.values()
@@ -8963,6 +9884,23 @@ var InMemoryStateStore = class {
8963
9884
  ...cp
8964
9885
  }));
8965
9886
  this.lastCheckpointRowsRead = rows.length;
9887
+ this.checkpointRowsRead += rows.length;
9888
+ return rows;
9889
+ }
9890
+ runCheckpoints(runId) {
9891
+ return [
9892
+ ...this.checkpoints.values()
9893
+ ].filter((cp) => cp.runId === runId);
9894
+ }
9895
+ async checkpointStats(runId, opts) {
9896
+ return checkpointStatsOf(this.runCheckpoints(runId), opts);
9897
+ }
9898
+ async queryCheckpoints(runId, query) {
9899
+ const rows = queryCheckpointsOf(this.runCheckpoints(runId), query).map((cp) => ({
9900
+ ...cp
9901
+ }));
9902
+ this.lastCheckpointRowsRead = rows.length;
9903
+ this.checkpointRowsRead += rows.length;
8966
9904
  return rows;
8967
9905
  }
8968
9906
  async getLatestCheckpointByName(runId, name) {
@@ -8990,6 +9928,7 @@ var InMemoryStateStore = class {
8990
9928
  ...cp
8991
9929
  }));
8992
9930
  this.lastCheckpointRowsRead = rows.length;
9931
+ this.checkpointRowsRead += rows.length;
8993
9932
  return rows;
8994
9933
  }
8995
9934
  };
@@ -9040,6 +9979,7 @@ var InMemoryTransport = class {
9040
9979
  }
9041
9980
  };
9042
9981
  export {
9982
+ CHILD_EDGE_PREFIXES,
9043
9983
  CONTROL_FLOW_SIGNAL,
9044
9984
  CURRENT_PROTOCOL_VERSION,
9045
9985
  CodecStateStore,
@@ -9060,6 +10000,7 @@ export {
9060
10000
  FAN_SUMMARY_FAILURES,
9061
10001
  FatalError,
9062
10002
  GatherError,
10003
+ HANDOFF_SEQ_BASE,
9063
10004
  InMemoryAdmissionBackend,
9064
10005
  InMemoryStateStore,
9065
10006
  InMemoryTransport,
@@ -9094,9 +10035,11 @@ export {
9094
10035
  WorkflowEngine,
9095
10036
  WorkflowSuspended,
9096
10037
  aggregateAnnouncements,
10038
+ aggregatesCheckpoints,
9097
10039
  announcementsOf,
9098
10040
  applyAttributeQuery,
9099
10041
  assertConcurrency,
10042
+ assertRoutes,
9100
10043
  attributeColumnFor,
9101
10044
  attributeOperand,
9102
10045
  attributePredicateOperands,
@@ -9106,10 +10049,14 @@ export {
9106
10049
  buildWorkflowDirectory,
9107
10050
  canRoute,
9108
10051
  capableWorkers,
10052
+ checkpointNameKind,
10053
+ checkpointStatsOf,
9109
10054
  childIdsOf,
9110
10055
  classifyWaiterToken,
9111
10056
  clearDurableExecutionWrappers,
9112
10057
  clearTerminalSingletonAdmission,
10058
+ codeAndDetails,
10059
+ codeOf,
9113
10060
  collectMetrics,
9114
10061
  concurrencyTag,
9115
10062
  controlPlaneDescriptor,
@@ -9122,13 +10069,20 @@ export {
9122
10069
  debug,
9123
10070
  describeWorker,
9124
10071
  descriptorHash,
10072
+ detailsOf,
9125
10073
  error,
10074
+ etagMatches,
9126
10075
  eventMatchOf,
9127
10076
  eventMatches,
9128
10077
  eventNameOf,
9129
10078
  eventPrefix,
9130
10079
  eventToken,
9131
10080
  facetOrigin,
10081
+ fanPageBounds,
10082
+ fanPageEtag,
10083
+ foldedCount,
10084
+ handoffHandlerId,
10085
+ handoffName,
9132
10086
  hasDurableExecutionWrappers,
9133
10087
  heartbeatStatus,
9134
10088
  indexWaitersByRun,
@@ -9140,6 +10094,7 @@ export {
9140
10094
  isNonEmptyRunScope,
9141
10095
  isWorkflowControlFlowSignal,
9142
10096
  listSignalWaitersOfRuns,
10097
+ liveHandoff,
9143
10098
  log,
9144
10099
  matchesAttributes,
9145
10100
  mergeRunFacetRows,
@@ -9150,17 +10105,23 @@ export {
9150
10105
  normalizeAttributeRows,
9151
10106
  normalizeDescriptor,
9152
10107
  observedAnnouncements,
10108
+ onErrorRunId,
9153
10109
  pageFan,
10110
+ pageFanFromStore,
9154
10111
  parentRunIdOf,
9155
10112
  parseDuration,
9156
10113
  partitionOfToken,
9157
10114
  planDispatch,
9158
10115
  prevCronFireMs,
10116
+ queryCheckpointsOf,
9159
10117
  readSearchAttributes,
9160
10118
  requiredCapabilities,
9161
10119
  resolveConcurrency,
9162
10120
  resolveRouting,
9163
10121
  resolveRunWaiting,
10122
+ retiredHandoffName,
10123
+ routeFor,
10124
+ runDetailEtag,
9164
10125
  runDurableExecution,
9165
10126
  runInStepLogger,
9166
10127
  runInStepScope,
@@ -9181,6 +10142,7 @@ export {
9181
10142
  subEvent,
9182
10143
  subProcess,
9183
10144
  summarizeFan,
10145
+ summarizeRunFromStore,
9184
10146
  summarizeTimeline,
9185
10147
  tenantGroup,
9186
10148
  useDurableExecution,