@dudousxd/nestjs-durable-core 0.58.0 → 0.60.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();
@@ -4789,6 +5269,13 @@ var TERMINAL_RUN_STATUSES = [
4789
5269
  "dead"
4790
5270
  ];
4791
5271
 
5272
+ // src/run-gateway.ts
5273
+ var RunGateway = class {
5274
+ static {
5275
+ __name(this, "RunGateway");
5276
+ }
5277
+ };
5278
+
4792
5279
  // src/codec-state-store.ts
4793
5280
  var CodecStateStore = class {
4794
5281
  static {
@@ -5569,6 +6056,7 @@ var InMemoryTransport = class {
5569
6056
  };
5570
6057
  export {
5571
6058
  CONTROL_FLOW_SIGNAL,
6059
+ CURRENT_PROTOCOL_VERSION,
5572
6060
  CodecStateStore,
5573
6061
  ContinueAsNew,
5574
6062
  DURABLE_OPTIONS,
@@ -5585,12 +6073,15 @@ export {
5585
6073
  InMemoryAdmissionBackend,
5586
6074
  InMemoryStateStore,
5587
6075
  InMemoryTransport,
6076
+ LEGACY_V1_CAPABILITIES,
6077
+ LEGACY_V1_PROTOCOL,
5588
6078
  NonDeterminismError,
5589
6079
  QueueController,
5590
6080
  RemoteStepError,
5591
6081
  RemoteStepTimeout,
5592
6082
  RemoteWorkflowExecutor,
5593
6083
  RemoteWorkflowTimeout,
6084
+ RunGateway,
5594
6085
  STATE_STORE,
5595
6086
  STATE_STORE_CANONICAL,
5596
6087
  SignalTimeoutError,
@@ -5599,6 +6090,7 @@ export {
5599
6090
  TRANSPORT,
5600
6091
  TRANSPORT_CANONICAL,
5601
6092
  WORKFLOW_NAME_KEY,
6093
+ WorkflowBlocked,
5602
6094
  WorkflowEngine,
5603
6095
  WorkflowSuspended,
5604
6096
  applyAttributeQuery,
@@ -5606,23 +6098,35 @@ export {
5606
6098
  attributeOperand,
5607
6099
  bindWorkflowClass,
5608
6100
  breakpointToken,
6101
+ canRoute,
6102
+ capableWorkers,
5609
6103
  classifyWaiterToken,
5610
6104
  collectMetrics,
6105
+ controlPlaneDescriptor,
5611
6106
  createStepLogger,
5612
6107
  currentWorkflowCtx,
6108
+ descriptorHash,
5613
6109
  eventMatchOf,
5614
6110
  eventMatches,
5615
6111
  eventNameOf,
5616
6112
  eventPrefix,
5617
6113
  eventToken,
6114
+ heartbeatStatus,
5618
6115
  indexWaitersByRun,
5619
6116
  isEventToken,
6117
+ isLegacyDescriptor,
5620
6118
  isWorkflowControlFlowSignal,
5621
6119
  matchesAttributes,
6120
+ missingCapabilities,
6121
+ negotiate,
5622
6122
  normalizeAttributeRows,
6123
+ normalizeDescriptor,
5623
6124
  parseDuration,
6125
+ planDispatch,
5624
6126
  prevCronFireMs,
5625
6127
  readSearchAttributes,
6128
+ requiredCapabilities,
6129
+ resolveRouting,
5626
6130
  resolveRunWaiting,
5627
6131
  runInWorkflowCtx,
5628
6132
  runSchedules,