@dudousxd/nestjs-durable-core 0.58.0 → 0.59.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
@@ -822,6 +822,358 @@ function eventMatches(payload, match) {
822
822
  }
823
823
  __name(eventMatches, "eventMatches");
824
824
 
825
+ // src/handshake/descriptor.ts
826
+ var CURRENT_PROTOCOL_VERSION = 1;
827
+ var LEGACY_V1_PROTOCOL = {
828
+ version: 1,
829
+ range: [
830
+ 1,
831
+ 1
832
+ ]
833
+ };
834
+ var LEGACY_V1_CAPABILITIES = Object.freeze([
835
+ "saga",
836
+ "signals",
837
+ "search-attributes",
838
+ "priority",
839
+ "entities",
840
+ "child-workflows",
841
+ "singleton",
842
+ "schedules",
843
+ "continue-as-new",
844
+ "queries",
845
+ "cancellation"
846
+ ]);
847
+ function isLegacyDescriptor(raw) {
848
+ return raw.protocol === void 0;
849
+ }
850
+ __name(isLegacyDescriptor, "isLegacyDescriptor");
851
+ function normalizeDescriptor(raw) {
852
+ return {
853
+ instanceId: raw.instanceId,
854
+ runtime: raw.runtime,
855
+ sdk: raw.sdk ?? {
856
+ name: "unknown",
857
+ version: "0"
858
+ },
859
+ protocol: raw.protocol ?? {
860
+ ...LEGACY_V1_PROTOCOL
861
+ },
862
+ capabilities: raw.capabilities ?? [
863
+ ...LEGACY_V1_CAPABILITIES
864
+ ],
865
+ workflows: raw.workflows ?? [],
866
+ steps: raw.steps ?? [],
867
+ ...raw.partition !== void 0 ? {
868
+ partition: raw.partition
869
+ } : {},
870
+ ...raw.namespace !== void 0 ? {
871
+ namespace: raw.namespace
872
+ } : {},
873
+ startedAt: raw.startedAt ?? 0
874
+ };
875
+ }
876
+ __name(normalizeDescriptor, "normalizeDescriptor");
877
+ function canonicalizeForHash(d) {
878
+ const set = /* @__PURE__ */ __name((xs) => [
879
+ ...new Set(xs)
880
+ ].sort(), "set");
881
+ return {
882
+ instanceId: d.instanceId,
883
+ runtime: d.runtime,
884
+ sdk: {
885
+ name: d.sdk.name,
886
+ version: d.sdk.version
887
+ },
888
+ protocol: {
889
+ version: d.protocol.version,
890
+ range: [
891
+ d.protocol.range[0],
892
+ d.protocol.range[1]
893
+ ]
894
+ },
895
+ capabilities: set(d.capabilities),
896
+ workflows: set(d.workflows),
897
+ steps: set(d.steps),
898
+ partition: d.partition ?? null,
899
+ namespace: d.namespace ?? null,
900
+ startedAt: d.startedAt
901
+ };
902
+ }
903
+ __name(canonicalizeForHash, "canonicalizeForHash");
904
+ function fnv1a64Hex(input) {
905
+ const OFFSET = 0xcbf29ce484222325n;
906
+ const PRIME = 0x100000001b3n;
907
+ const MASK = 0xffffffffffffffffn;
908
+ let hash = OFFSET;
909
+ for (let i = 0; i < input.length; i += 1) {
910
+ hash ^= BigInt(input.charCodeAt(i) & 255);
911
+ hash ^= BigInt(input.charCodeAt(i) >> 8 & 255);
912
+ hash = hash * PRIME & MASK;
913
+ }
914
+ return hash.toString(16).padStart(16, "0");
915
+ }
916
+ __name(fnv1a64Hex, "fnv1a64Hex");
917
+ function descriptorHash(descriptor) {
918
+ const canonical = canonicalizeForHash(normalizeDescriptor(descriptor));
919
+ return fnv1a64Hex(JSON.stringify(canonical));
920
+ }
921
+ __name(descriptorHash, "descriptorHash");
922
+ function heartbeatStatus(descriptor, opts = {}) {
923
+ return {
924
+ ts: opts.ts ?? Date.now(),
925
+ status: opts.status ?? "up",
926
+ descriptorHash: descriptorHash(descriptor)
927
+ };
928
+ }
929
+ __name(heartbeatStatus, "heartbeatStatus");
930
+
931
+ // src/handshake/negotiate.ts
932
+ var rangesOverlap = /* @__PURE__ */ __name((a, b) => Math.max(a[0], b[0]) <= Math.min(a[1], b[1]), "rangesOverlap");
933
+ var difference = /* @__PURE__ */ __name((a, b) => {
934
+ const bset = new Set(b);
935
+ return [
936
+ ...new Set(a)
937
+ ].filter((x) => !bset.has(x)).sort();
938
+ }, "difference");
939
+ var intersection = /* @__PURE__ */ __name((a, b) => {
940
+ const bset = new Set(b);
941
+ return [
942
+ ...new Set(a)
943
+ ].filter((x) => bset.has(x)).sort();
944
+ }, "intersection");
945
+ function negotiate(localRaw, remoteRaw, opts = {}) {
946
+ const local = normalizeDescriptor(localRaw);
947
+ const remote = normalizeDescriptor(remoteRaw);
948
+ const localRange = local.protocol.range;
949
+ const remoteRange = remote.protocol.range;
950
+ const shared = intersection(local.capabilities, remote.capabilities);
951
+ const missingOnRemote = difference(local.capabilities, remote.capabilities);
952
+ const missingOnLocal = difference(remote.capabilities, local.capabilities);
953
+ const capabilities = {
954
+ shared,
955
+ missingOnRemote,
956
+ missingOnLocal
957
+ };
958
+ if (!rangesOverlap(localRange, remoteRange)) {
959
+ return {
960
+ outcome: "incompatible",
961
+ negotiatedProtocol: null,
962
+ protocol: {
963
+ localRange,
964
+ remoteRange
965
+ },
966
+ capabilities,
967
+ reason: {
968
+ code: "protocol.incompatible",
969
+ message: `no common protocol major: local speaks [${localRange[0]}, ${localRange[1]}], remote speaks [${remoteRange[0]}, ${remoteRange[1]}]`,
970
+ detail: {
971
+ localRange,
972
+ remoteRange
973
+ }
974
+ },
975
+ local,
976
+ remote
977
+ };
978
+ }
979
+ const negotiatedProtocol = Math.min(localRange[1], remoteRange[1]);
980
+ const required = opts.required ?? [];
981
+ const missingRequired = difference(required, remote.capabilities);
982
+ if (missingRequired.length > 0) {
983
+ return {
984
+ outcome: "degraded",
985
+ negotiatedProtocol,
986
+ protocol: {
987
+ localRange,
988
+ remoteRange
989
+ },
990
+ capabilities,
991
+ reason: {
992
+ code: "capability.unavailable",
993
+ message: `remote is missing required capability(ies): ${missingRequired.join(", ")}`,
994
+ detail: {
995
+ missingRequired,
996
+ missingOnRemote,
997
+ missingOnLocal
998
+ }
999
+ },
1000
+ local,
1001
+ remote
1002
+ };
1003
+ }
1004
+ if (missingOnRemote.length > 0 || missingOnLocal.length > 0) {
1005
+ const parts = [];
1006
+ if (missingOnRemote.length > 0) parts.push(`remote lacks ${missingOnRemote.join(", ")}`);
1007
+ if (missingOnLocal.length > 0) parts.push(`local lacks ${missingOnLocal.join(", ")}`);
1008
+ return {
1009
+ outcome: "degraded",
1010
+ negotiatedProtocol,
1011
+ protocol: {
1012
+ localRange,
1013
+ remoteRange
1014
+ },
1015
+ capabilities,
1016
+ reason: {
1017
+ code: "capability.unavailable",
1018
+ message: `capability mismatch: ${parts.join("; ")}`,
1019
+ detail: {
1020
+ missingOnRemote,
1021
+ missingOnLocal
1022
+ }
1023
+ },
1024
+ local,
1025
+ remote
1026
+ };
1027
+ }
1028
+ return {
1029
+ outcome: "compatible",
1030
+ negotiatedProtocol,
1031
+ protocol: {
1032
+ localRange,
1033
+ remoteRange
1034
+ },
1035
+ capabilities,
1036
+ local,
1037
+ remote
1038
+ };
1039
+ }
1040
+ __name(negotiate, "negotiate");
1041
+
1042
+ // src/handshake/routing.ts
1043
+ function requiredCapabilities(handler) {
1044
+ const list = Array.isArray(handler) ? handler : handler.requires ?? [];
1045
+ return [
1046
+ ...new Set(list)
1047
+ ];
1048
+ }
1049
+ __name(requiredCapabilities, "requiredCapabilities");
1050
+ function canRoute(handler, worker) {
1051
+ const required = requiredCapabilities(handler);
1052
+ if (required.length === 0) return true;
1053
+ const advertised = new Set(normalizeDescriptor(worker).capabilities);
1054
+ return required.every((cap) => advertised.has(cap));
1055
+ }
1056
+ __name(canRoute, "canRoute");
1057
+ function missingCapabilities(handler, worker) {
1058
+ const advertised = new Set(normalizeDescriptor(worker).capabilities);
1059
+ return requiredCapabilities(handler).filter((cap) => !advertised.has(cap));
1060
+ }
1061
+ __name(missingCapabilities, "missingCapabilities");
1062
+ function capableWorkers(handler, workers) {
1063
+ return workers.filter((w) => canRoute(handler, w));
1064
+ }
1065
+ __name(capableWorkers, "capableWorkers");
1066
+ function resolveRouting(handler, workers) {
1067
+ const requires = requiredCapabilities(handler);
1068
+ const capable = capableWorkers(handler, workers).map(normalizeDescriptor);
1069
+ if (capable.length > 0) {
1070
+ return {
1071
+ status: "routable",
1072
+ workers: capable,
1073
+ requires
1074
+ };
1075
+ }
1076
+ const suffix = requires.length > 0 ? ` (requires ${requires.join(", ")})` : "";
1077
+ return {
1078
+ status: "blocked",
1079
+ reason: `blocked: no compatible worker${suffix}`,
1080
+ requires
1081
+ };
1082
+ }
1083
+ __name(resolveRouting, "resolveRouting");
1084
+
1085
+ // src/handshake/dispatch-routing.ts
1086
+ function controlPlaneDescriptor(opts) {
1087
+ const version = CURRENT_PROTOCOL_VERSION;
1088
+ return {
1089
+ instanceId: opts.instanceId,
1090
+ runtime: "node",
1091
+ sdk: opts.sdk ?? {
1092
+ name: "@dudousxd/nestjs-durable-core",
1093
+ version: String(version)
1094
+ },
1095
+ protocol: opts.protocol ?? {
1096
+ version,
1097
+ range: [
1098
+ version,
1099
+ version
1100
+ ]
1101
+ },
1102
+ capabilities: opts.capabilities ?? [],
1103
+ workflows: [],
1104
+ steps: [],
1105
+ ...opts.partition !== void 0 ? {
1106
+ partition: opts.partition
1107
+ } : {},
1108
+ ...opts.namespace !== void 0 ? {
1109
+ namespace: opts.namespace
1110
+ } : {},
1111
+ startedAt: opts.startedAt ?? 0
1112
+ };
1113
+ }
1114
+ __name(controlPlaneDescriptor, "controlPlaneDescriptor");
1115
+ var WorkflowBlocked = class extends Error {
1116
+ static {
1117
+ __name(this, "WorkflowBlocked");
1118
+ }
1119
+ plan;
1120
+ constructor(plan) {
1121
+ super(plan.reason), this.plan = plan;
1122
+ this.name = "WorkflowBlocked";
1123
+ }
1124
+ };
1125
+ function planDispatch(requires, liveDescriptors, cp, token) {
1126
+ const required = requiredCapabilities({
1127
+ requires
1128
+ });
1129
+ const workers = liveDescriptors.map(normalizeDescriptor);
1130
+ const capable = workers.filter((w) => canRoute({
1131
+ requires
1132
+ }, w));
1133
+ const compatible = capable.filter((w) => negotiate(cp, w).outcome !== "incompatible");
1134
+ if (compatible.length > 0) return {
1135
+ status: "routable",
1136
+ workers: compatible
1137
+ };
1138
+ if (capable.length > 0) {
1139
+ const workerRanges = capable.map((w) => w.protocol.range);
1140
+ return {
1141
+ status: "blocked",
1142
+ code: "protocol.incompatible",
1143
+ reason: `blocked: no protocol-compatible worker${required.length > 0 ? ` (requires ${required.join(", ")})` : ""}`,
1144
+ requires: required,
1145
+ diagnostics: {
1146
+ code: "protocol.incompatible",
1147
+ token,
1148
+ requires: required,
1149
+ liveWorkers: workers.length,
1150
+ controlPlaneRange: cp.protocol.range,
1151
+ workerRanges,
1152
+ controlPlane: cp,
1153
+ workers
1154
+ }
1155
+ };
1156
+ }
1157
+ const advertised = new Set(workers.flatMap((w) => w.capabilities));
1158
+ const missingCapabilities2 = required.filter((cap) => !advertised.has(cap));
1159
+ return {
1160
+ status: "blocked",
1161
+ code: "capability.unavailable",
1162
+ reason: `blocked: no compatible worker${required.length > 0 ? ` (requires ${required.join(", ")})` : ""}`,
1163
+ requires: required,
1164
+ diagnostics: {
1165
+ code: "capability.unavailable",
1166
+ token,
1167
+ requires: required,
1168
+ liveWorkers: workers.length,
1169
+ missingCapabilities: missingCapabilities2,
1170
+ controlPlane: cp,
1171
+ workers
1172
+ }
1173
+ };
1174
+ }
1175
+ __name(planDispatch, "planDispatch");
1176
+
825
1177
  // src/tenant-group.ts
826
1178
  function tenantGroup(baseGroup, tenant) {
827
1179
  return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
@@ -1086,6 +1438,22 @@ var TransportPool = class {
1086
1438
  transport.onStartRun?.(handler);
1087
1439
  }
1088
1440
  }
1441
+ /**
1442
+ * The live handshake descriptors advertised for `group` (design §7.2), merged across every transport
1443
+ * that can report them. Returns `[]` when NO transport implements
1444
+ * {@link Transport.readWorkerDescriptors} (a pure in-process pool) OR when descriptors are supported
1445
+ * but none is published — both are the "guard disengaged / legacy assume-compatible" case, so the
1446
+ * engine's routing guard treats an empty result identically (design §7.7).
1447
+ */
1448
+ async readWorkerDescriptors(group) {
1449
+ const descriptors = [];
1450
+ for (const { transport } of this.transports) {
1451
+ if (transport.readWorkerDescriptors) {
1452
+ descriptors.push(...await transport.readWorkerDescriptors(group));
1453
+ }
1454
+ }
1455
+ return descriptors;
1456
+ }
1089
1457
  /** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
1090
1458
  async listWorkerGroups() {
1091
1459
  const groups = /* @__PURE__ */ new Set();
@@ -1181,6 +1549,8 @@ function resolveStepPolicy(config, opts) {
1181
1549
  if (jitter !== void 0) policy.jitter = jitter;
1182
1550
  const timeoutMs = opts?.timeoutMs ?? config?.timeoutMs;
1183
1551
  if (timeoutMs !== void 0) policy.timeoutMs = timeoutMs;
1552
+ const requires = opts?.requires ?? config?.requires;
1553
+ if (requires !== void 0) policy.requires = requires;
1184
1554
  return policy;
1185
1555
  }
1186
1556
  __name(resolveStepPolicy, "resolveStepPolicy");
@@ -1886,6 +2256,10 @@ var WorkflowEngine = class {
1886
2256
  eventBufferTtlMs;
1887
2257
  remoteRedispatchMs;
1888
2258
  remoteRedispatchMax;
2259
+ /** How long a `blocked` run waits before the recovery poll re-checks the live fleet (design §7.5). */
2260
+ blockedPollMs;
2261
+ /** This engine's control-plane handshake descriptor — negotiated against each worker (design §7.3). */
2262
+ cpDescriptor;
1889
2263
  webhookUrl;
1890
2264
  traceparent;
1891
2265
  context;
@@ -1957,6 +2331,13 @@ var WorkflowEngine = class {
1957
2331
  this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
1958
2332
  this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
1959
2333
  this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
2334
+ this.blockedPollMs = deps.blockedPollMs ?? 5e3;
2335
+ this.cpDescriptor = controlPlaneDescriptor({
2336
+ instanceId: `cp-${this.instanceId}`,
2337
+ ...this.namespace !== void 0 ? {
2338
+ namespace: this.namespace
2339
+ } : {}
2340
+ });
1960
2341
  this.webhookUrl = deps.webhookUrl;
1961
2342
  this.traceparent = deps.traceparent;
1962
2343
  this.context = deps.context;
@@ -2110,6 +2491,7 @@ var WorkflowEngine = class {
2110
2491
  searchAttributesSchema: opts?.searchAttributesSchema,
2111
2492
  onEvent: opts?.onEvent,
2112
2493
  eventBatch: opts?.eventBatch,
2494
+ requires: opts?.requires,
2113
2495
  // A real, retained in-process body — so `workflowBody` can hand it to an in-app worker, and so
2114
2496
  // it is NOT confused with the throwing stub `registerRemote` installs for a body-less remote.
2115
2497
  hasBody: true
@@ -2158,6 +2540,7 @@ var WorkflowEngine = class {
2158
2540
  singleton: opts.singleton,
2159
2541
  executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
2160
2542
  validateInput: opts.validateInput,
2543
+ requires: opts.requires,
2161
2544
  remote: {
2162
2545
  group: opts.group,
2163
2546
  executor: opts.executor
@@ -2641,9 +3024,13 @@ var WorkflowEngine = class {
2641
3024
  */
2642
3025
  async resumeDueTimers(nowMs = this.clock()) {
2643
3026
  const due = await this.store.listDueTimers(nowMs, this.namespace);
2644
- if (due.length === 0) return [];
3027
+ const blocked = await this.dueBlockedRuns(nowMs);
3028
+ if (due.length === 0 && blocked.length === 0) return [];
2645
3029
  const waiterByRun = indexWaitersByRun(await this.store.listSignalWaiters(""));
2646
- return this.resumeLeased(due, nowMs, (run) => this.reconcileSignalWaiter(run, waiterByRun.get(run.id)));
3030
+ return this.resumeLeased([
3031
+ ...due,
3032
+ ...blocked
3033
+ ], nowMs, (run) => this.reconcileSignalWaiter(run, waiterByRun.get(run.id)));
2647
3034
  }
2648
3035
  /**
2649
3036
  * The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
@@ -3552,6 +3939,7 @@ var WorkflowEngine = class {
3552
3939
  error: outcome.error
3553
3940
  };
3554
3941
  }
3942
+ if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
3555
3943
  const latest = await this.store.getRun(run.id);
3556
3944
  if (latest?.status === "cancelled") {
3557
3945
  return {
@@ -3651,6 +4039,8 @@ var WorkflowEngine = class {
3651
4039
  status: "suspended"
3652
4040
  };
3653
4041
  }
4042
+ const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
4043
+ if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
3654
4044
  const history = await this.remoteHistory(run.id);
3655
4045
  if (remote.executor.dispatch) {
3656
4046
  const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
@@ -4074,6 +4464,15 @@ var WorkflowEngine = class {
4074
4464
  status: "completed"
4075
4465
  };
4076
4466
  }
4467
+ if (err instanceof WorkflowBlocked) {
4468
+ if (this.cancelRequested.has(run.id)) {
4469
+ return this.compensateAndCancel(run, compensations, replay);
4470
+ }
4471
+ return this.settleRun(run, {
4472
+ kind: "blocked",
4473
+ blocked: err.plan
4474
+ });
4475
+ }
4077
4476
  if (err instanceof WorkflowSuspended) {
4078
4477
  if (this.cancelRequested.has(run.id)) {
4079
4478
  return this.compensateAndCancel(run, compensations, replay);
@@ -4420,6 +4819,85 @@ var WorkflowEngine = class {
4420
4819
  run: result
4421
4820
  };
4422
4821
  }
4822
+ /**
4823
+ * Plan a dispatch to `token` for a handler requiring `requires` (handshake design §7.5). Returns:
4824
+ * - `undefined` — NO live descriptors published on this token: the guard does not engage, so a
4825
+ * pre-handshake fleet (or an in-process transport that advertises none) keeps flowing exactly as
4826
+ * before (design §7.7);
4827
+ * - a `routable` plan — ≥1 live worker is capability-capable AND protocol-compatible (dispatch);
4828
+ * - a `blocked` plan — descriptors exist but none can run it (park the run, never dispatch into it).
4829
+ */
4830
+ async planRoute(token, requires) {
4831
+ const descriptors = await this.pool.readWorkerDescriptors(token);
4832
+ if (descriptors.length === 0) return void 0;
4833
+ return planDispatch(requires ?? [], descriptors, this.cpDescriptor, token);
4834
+ }
4835
+ /** Throw {@link WorkflowBlocked} when no live worker can run a dispatch to `token` — so the caller's
4836
+ * run-execution catch parks the run `blocked` (with the plan) instead of persisting a `pending`
4837
+ * checkpoint + dispatching into a queue nobody consumes (design §7.5). A no-op when the guard does
4838
+ * not engage (no descriptors) or a capable+compatible worker exists. */
4839
+ async ensureRoutable(token, requires) {
4840
+ const plan = await this.planRoute(token, requires);
4841
+ if (plan?.status === "blocked") throw new WorkflowBlocked(plan);
4842
+ }
4843
+ /**
4844
+ * Park a run `blocked` (handshake design §7.5/§7.6): no live worker on its next-dispatch group is
4845
+ * capability-capable + protocol-compatible. Persists the `blocked` status with a human reason + the
4846
+ * machine `code` + a `wakeAt` so the blocked-recovery poll re-drives it, emits the LOUD structured
4847
+ * `capability.unavailable`/`protocol.incompatible` diagnostics (both descriptors + the precise delta),
4848
+ * and releases the run lease so any instance can re-drive it. Re-checks the persisted status first so
4849
+ * a concurrent cancel/complete is never clobbered. `releaseRunLock` is idempotent, so a caller whose
4850
+ * own `finally` also releases (the run-execution path) is safe.
4851
+ */
4852
+ async parkBlocked(run, blocked) {
4853
+ const current = await this.store.getRun(run.id);
4854
+ if (current && (current.status === "cancelled" || current.status === "completed")) {
4855
+ await this.store.releaseRunLock(run.id);
4856
+ return {
4857
+ runId: run.id,
4858
+ status: current.status,
4859
+ output: current.output,
4860
+ error: current.error
4861
+ };
4862
+ }
4863
+ const error = {
4864
+ message: blocked.reason,
4865
+ code: blocked.code,
4866
+ retryable: true
4867
+ };
4868
+ await this.store.updateRun(run.id, {
4869
+ status: "blocked",
4870
+ error,
4871
+ wakeAt: this.clock() + this.blockedPollMs,
4872
+ updatedAt: /* @__PURE__ */ new Date()
4873
+ });
4874
+ this.emit({
4875
+ type: "run.blocked",
4876
+ runId: run.id,
4877
+ workflow: run.workflow,
4878
+ namespace: run.namespace,
4879
+ error,
4880
+ requires: blocked.requires,
4881
+ diagnostics: blocked.diagnostics
4882
+ });
4883
+ await this.store.releaseRunLock(run.id);
4884
+ return {
4885
+ runId: run.id,
4886
+ status: "blocked",
4887
+ error
4888
+ };
4889
+ }
4890
+ /** Runs parked `blocked` whose recovery poll is due (`wakeAt <= nowMs`), namespace-scoped like
4891
+ * {@link resumeDueTimers}. Re-driving one re-checks the live fleet: it dispatches if a
4892
+ * capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
4893
+ async dueBlockedRuns(nowMs) {
4894
+ const blocked = await this.store.listRuns({
4895
+ statuses: [
4896
+ "blocked"
4897
+ ]
4898
+ });
4899
+ return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
4900
+ }
4423
4901
  async callRemote(runId, seq, step, input, queue, transport, replay, admission) {
4424
4902
  const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
4425
4903
  if (existing && existing.name !== step.name) {
@@ -4485,6 +4963,7 @@ var WorkflowEngine = class {
4485
4963
  if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
4486
4964
  attempt = existing.attempts + 1;
4487
4965
  }
4966
+ await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
4488
4967
  const id = stepId(runId, seq);
4489
4968
  if (queue && this.admission.handles(queue)) {
4490
4969
  const decision = await this.admission.tryAdmit(queue, {
@@ -4671,6 +5150,7 @@ var WorkflowEngine = class {
4671
5150
  /** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
4672
5151
  async callRemoteInMemory(runId, seq, step, input, transport) {
4673
5152
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
5153
+ await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
4674
5154
  const validInput = step.input ? step.input.parse(input) : input;
4675
5155
  const id = stepId(runId, seq);
4676
5156
  const enqueuedAt = /* @__PURE__ */ new Date();
@@ -5569,6 +6049,7 @@ var InMemoryTransport = class {
5569
6049
  };
5570
6050
  export {
5571
6051
  CONTROL_FLOW_SIGNAL,
6052
+ CURRENT_PROTOCOL_VERSION,
5572
6053
  CodecStateStore,
5573
6054
  ContinueAsNew,
5574
6055
  DURABLE_OPTIONS,
@@ -5585,6 +6066,8 @@ export {
5585
6066
  InMemoryAdmissionBackend,
5586
6067
  InMemoryStateStore,
5587
6068
  InMemoryTransport,
6069
+ LEGACY_V1_CAPABILITIES,
6070
+ LEGACY_V1_PROTOCOL,
5588
6071
  NonDeterminismError,
5589
6072
  QueueController,
5590
6073
  RemoteStepError,
@@ -5599,6 +6082,7 @@ export {
5599
6082
  TRANSPORT,
5600
6083
  TRANSPORT_CANONICAL,
5601
6084
  WORKFLOW_NAME_KEY,
6085
+ WorkflowBlocked,
5602
6086
  WorkflowEngine,
5603
6087
  WorkflowSuspended,
5604
6088
  applyAttributeQuery,
@@ -5606,23 +6090,35 @@ export {
5606
6090
  attributeOperand,
5607
6091
  bindWorkflowClass,
5608
6092
  breakpointToken,
6093
+ canRoute,
6094
+ capableWorkers,
5609
6095
  classifyWaiterToken,
5610
6096
  collectMetrics,
6097
+ controlPlaneDescriptor,
5611
6098
  createStepLogger,
5612
6099
  currentWorkflowCtx,
6100
+ descriptorHash,
5613
6101
  eventMatchOf,
5614
6102
  eventMatches,
5615
6103
  eventNameOf,
5616
6104
  eventPrefix,
5617
6105
  eventToken,
6106
+ heartbeatStatus,
5618
6107
  indexWaitersByRun,
5619
6108
  isEventToken,
6109
+ isLegacyDescriptor,
5620
6110
  isWorkflowControlFlowSignal,
5621
6111
  matchesAttributes,
6112
+ missingCapabilities,
6113
+ negotiate,
5622
6114
  normalizeAttributeRows,
6115
+ normalizeDescriptor,
5623
6116
  parseDuration,
6117
+ planDispatch,
5624
6118
  prevCronFireMs,
5625
6119
  readSearchAttributes,
6120
+ requiredCapabilities,
6121
+ resolveRouting,
5626
6122
  resolveRunWaiting,
5627
6123
  runInWorkflowCtx,
5628
6124
  runSchedules,