@dudousxd/nestjs-durable-core 0.45.0 → 0.46.1

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.cjs CHANGED
@@ -26,6 +26,8 @@ __export(index_exports, {
26
26
  ContinueAsNew: () => ContinueAsNew,
27
27
  DURABLE_OPTIONS: () => DURABLE_OPTIONS,
28
28
  DURABLE_OPTIONS_CANONICAL: () => DURABLE_OPTIONS_CANONICAL,
29
+ DURABLE_STEP_CONFIG: () => DURABLE_STEP_CONFIG,
30
+ DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
29
31
  Entities: () => Entities,
30
32
  EventAccumulators: () => EventAccumulators,
31
33
  FatalError: () => FatalError,
@@ -63,12 +65,15 @@ __export(index_exports, {
63
65
  normalizeAttributeRows: () => normalizeAttributeRows,
64
66
  parseDuration: () => parseDuration,
65
67
  prevCronFireMs: () => prevCronFireMs,
66
- remoteStep: () => remoteStep,
67
68
  runSchedules: () => runSchedules,
68
69
  runStepHandler: () => runStepHandler,
70
+ sanitizeQueueToken: () => sanitizeQueueToken,
69
71
  scheduledRunId: () => scheduledRunId,
70
72
  sqlComparator: () => sqlComparator,
73
+ stepConfigOf: () => stepConfigOf,
71
74
  stepId: () => stepId,
75
+ stepNameOf: () => stepNameOf,
76
+ tenantGroup: () => tenantGroup,
72
77
  workflowName: () => workflowName
73
78
  });
74
79
  module.exports = __toCommonJS(index_exports);
@@ -646,7 +651,7 @@ var Entities = class {
646
651
  let state = config.initialState();
647
652
  for (let i = 0; ; i += 1) {
648
653
  const msg = await ctx.waitForSignal(token);
649
- const out = await ctx.step(`op:${i}`, async () => {
654
+ const out = await ctx.localStep(`op:${i}`, async () => {
650
655
  const handler = config.handlers[msg.op];
651
656
  if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${msg.op}"`);
652
657
  const result = await handler(state, msg.arg);
@@ -658,7 +663,7 @@ var Entities = class {
658
663
  state = out.state;
659
664
  await ctx.setEvent("state", state);
660
665
  if (msg.reply) {
661
- await ctx.step(`reply:${i}`, async () => {
666
+ await ctx.localStep(`reply:${i}`, async () => {
662
667
  await this.host.signal(msg.reply, out.result);
663
668
  });
664
669
  }
@@ -772,16 +777,28 @@ function eventMatches(payload, match) {
772
777
  }
773
778
  __name(eventMatches, "eventMatches");
774
779
 
780
+ // src/tenant-group.ts
781
+ function tenantGroup(baseGroup, tenant) {
782
+ return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
783
+ }
784
+ __name(tenantGroup, "tenantGroup");
785
+ function sanitizeQueueToken(name) {
786
+ return name.replace(/:/g, "-");
787
+ }
788
+ __name(sanitizeQueueToken, "sanitizeQueueToken");
789
+
775
790
  // src/remote-workflow-executor.ts
776
791
  var RemoteWorkflowExecutor = class {
777
792
  static {
778
793
  __name(this, "RemoteWorkflowExecutor");
779
794
  }
780
795
  transport;
781
- group;
782
- constructor(transport, group, _opts = {}) {
796
+ name;
797
+ partition;
798
+ constructor(transport, name, partition, _opts = {}) {
783
799
  this.transport = transport;
784
- this.group = group;
800
+ this.name = name;
801
+ this.partition = partition;
785
802
  }
786
803
  /**
787
804
  * Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
@@ -801,7 +818,7 @@ var RemoteWorkflowExecutor = class {
801
818
  ...pendingSignals ? {
802
819
  pendingSignals
803
820
  } : {},
804
- group: this.group,
821
+ group: tenantGroup(sanitizeQueueToken(this.name), this.partition),
805
822
  priority: run.priority,
806
823
  attempt: 1
807
824
  };
@@ -973,6 +990,16 @@ var TransportPool = class {
973
990
  liveWorkers: reports.flatMap((r) => r.liveWorkers)
974
991
  };
975
992
  }
993
+ /**
994
+ * Register a control-plane `start-run` consumer on every transport that carries the channel. Only
995
+ * control-plane-side transports implement {@link Transport.onStartRun}; a transport without it is
996
+ * skipped, so a plain worker/in-process pool is a no-op. Mirrors {@link bind}'s per-transport wiring.
997
+ */
998
+ onStartRun(handler) {
999
+ for (const { transport } of this.transports) {
1000
+ transport.onStartRun?.(handler);
1001
+ }
1002
+ }
976
1003
  /** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
977
1004
  async listWorkerGroups() {
978
1005
  const groups = /* @__PURE__ */ new Set();
@@ -1018,6 +1045,26 @@ function unwrapCompletion(payload, label) {
1018
1045
  }
1019
1046
  __name(unwrapCompletion, "unwrapCompletion");
1020
1047
 
1048
+ // src/step-name-symbol.ts
1049
+ var DURABLE_STEP_NAME = Symbol.for("durable.step.name");
1050
+ var DURABLE_STEP_CONFIG = Symbol.for("durable.step.config");
1051
+ function isStepConfig(value) {
1052
+ return typeof value === "object" && value !== null;
1053
+ }
1054
+ __name(isStepConfig, "isStepConfig");
1055
+ function stepNameOf(ref) {
1056
+ if (typeof ref !== "function") return void 0;
1057
+ const stamped = ref[DURABLE_STEP_NAME];
1058
+ return typeof stamped === "string" ? stamped : void 0;
1059
+ }
1060
+ __name(stepNameOf, "stepNameOf");
1061
+ function stepConfigOf(ref) {
1062
+ if (typeof ref !== "function") return void 0;
1063
+ const stamped = ref[DURABLE_STEP_CONFIG];
1064
+ return isStepConfig(stamped) ? stamped : void 0;
1065
+ }
1066
+ __name(stepConfigOf, "stepConfigOf");
1067
+
1021
1068
  // src/workflow-ref.ts
1022
1069
  var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
1023
1070
  function workflowName(ref) {
@@ -1037,6 +1084,23 @@ function normalizeChildOptions(options) {
1037
1084
  } : options ?? {};
1038
1085
  }
1039
1086
  __name(normalizeChildOptions, "normalizeChildOptions");
1087
+ function resolveStepPolicy(config, opts) {
1088
+ const policy = {};
1089
+ const retries = opts?.retries ?? config?.retries;
1090
+ if (retries !== void 0) policy.retries = retries;
1091
+ const backoff = opts?.backoff ?? config?.backoff;
1092
+ if (backoff !== void 0) policy.backoff = backoff;
1093
+ const backoffMs = opts?.backoffMs ?? config?.backoffMs;
1094
+ if (backoffMs !== void 0) policy.backoffMs = backoffMs;
1095
+ const backoffMaxMs = opts?.backoffMaxMs ?? config?.backoffMaxMs;
1096
+ if (backoffMaxMs !== void 0) policy.backoffMaxMs = backoffMaxMs;
1097
+ const jitter = opts?.jitter ?? config?.jitter;
1098
+ if (jitter !== void 0) policy.jitter = jitter;
1099
+ const timeoutMs = opts?.timeoutMs ?? config?.timeoutMs;
1100
+ if (timeoutMs !== void 0) policy.timeoutMs = timeoutMs;
1101
+ return policy;
1102
+ }
1103
+ __name(resolveStepPolicy, "resolveStepPolicy");
1040
1104
  var Position = class Position2 {
1041
1105
  static {
1042
1106
  __name(this, "Position");
@@ -1062,7 +1126,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1062
1126
  await store.saveCheckpoint(cp);
1063
1127
  replay?.set(cp.seq, cp);
1064
1128
  }, "writeCheckpoint");
1065
- const step = /* @__PURE__ */ __name(async (name, fn, options) => {
1129
+ const localStep = /* @__PURE__ */ __name(async (name, fn, options) => {
1066
1130
  const current = pos.next();
1067
1131
  const existing = await readCheckpoint(current);
1068
1132
  if (existing && existing.name !== name) {
@@ -1136,7 +1200,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1136
1200
  if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
1137
1201
  }
1138
1202
  }
1139
- }, "step");
1203
+ }, "localStep");
1140
1204
  const transaction = /* @__PURE__ */ __name(async (name, fn) => {
1141
1205
  if (!store.transaction) {
1142
1206
  throw new Error("ctx.transaction needs a store that supports transactions (the SQL adapters do). Use ctx.step for non-transactional work.");
@@ -1283,7 +1347,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1283
1347
  throw new WorkflowSuspended(deadline);
1284
1348
  }, "waitForEvent");
1285
1349
  const task = /* @__PURE__ */ __name(async (name, dispatch, options) => {
1286
- await step(`task:dispatch:${name}`, dispatch, options);
1350
+ await localStep(`task:dispatch:${name}`, dispatch, options);
1287
1351
  return unwrapCompletion(await waitForSignal(`task:${runId}:${name}`), `task "${name}"`);
1288
1352
  }, "task");
1289
1353
  const child = /* @__PURE__ */ __name(async (workflow2, input, options) => {
@@ -1496,9 +1560,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1496
1560
  wait
1497
1561
  };
1498
1562
  }, "webhook");
1499
- const now = /* @__PURE__ */ __name(() => step("now", async () => host.clock()), "now");
1500
- const random = /* @__PURE__ */ __name(() => step("random", async () => Math.random()), "random");
1501
- const uuid = /* @__PURE__ */ __name(() => step("uuid", async () => globalThis.crypto.randomUUID()), "uuid");
1563
+ const sideEffect = /* @__PURE__ */ __name((fn) => localStep("sideEffect", async () => fn()), "sideEffect");
1564
+ const now = /* @__PURE__ */ __name(() => localStep("now", async () => host.clock()), "now");
1502
1565
  const upsertSearchAttributes = /* @__PURE__ */ __name(async (attrs) => {
1503
1566
  const current = pos.next();
1504
1567
  const existing = await readCheckpoint(current);
@@ -1517,9 +1580,26 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1517
1580
  output: attrs
1518
1581
  }));
1519
1582
  }, "upsertSearchAttributes");
1583
+ function step(handlerOrName, input, opts) {
1584
+ const name = typeof handlerOrName === "string" ? handlerOrName : stepNameOf(handlerOrName);
1585
+ if (name === void 0) {
1586
+ throw new Error("ctx.step: handler is not a @Step reference (no stamped step name) \u2014 pass a @Step-decorated method or a step name string");
1587
+ }
1588
+ const config = typeof handlerOrName === "string" ? void 0 : stepConfigOf(handlerOrName);
1589
+ const def = {
1590
+ name,
1591
+ ...resolveStepPolicy(config, opts)
1592
+ };
1593
+ return host.callRemote(runId, pos.next(), def, input, opts?.queue, opts?.transport, {
1594
+ priority: opts?.priority,
1595
+ fairnessKey: opts?.fairnessKey
1596
+ });
1597
+ }
1598
+ __name(step, "step");
1520
1599
  return {
1521
1600
  runId,
1522
1601
  step,
1602
+ localStep,
1523
1603
  upsertSearchAttributes,
1524
1604
  transaction,
1525
1605
  callEntity,
@@ -1539,12 +1619,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1539
1619
  onUpdate,
1540
1620
  patched,
1541
1621
  now,
1542
- random,
1543
- uuid,
1544
- call: /* @__PURE__ */ __name((remote, input, opts) => host.callRemote(runId, pos.next(), remote, input, opts?.queue, opts?.transport, {
1545
- priority: opts?.priority,
1546
- fairnessKey: opts?.fairnessKey
1547
- }), "call")
1622
+ sideEffect
1548
1623
  };
1549
1624
  }
1550
1625
  __name(createWorkflowCtx, "createWorkflowCtx");
@@ -1662,8 +1737,10 @@ var WorkflowEngine = class {
1662
1737
  this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
1663
1738
  this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
1664
1739
  this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
1665
- this.namespace = deps.namespace ?? "default";
1666
- this.pool.useNamespace(this.namespace);
1740
+ this.namespace = deps.namespace;
1741
+ if (this.namespace !== void 0) {
1742
+ this.pool.useNamespace(this.namespace);
1743
+ }
1667
1744
  this.leaseMs = deps.leaseMs ?? 3e4;
1668
1745
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1669
1746
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
@@ -1728,6 +1805,13 @@ var WorkflowEngine = class {
1728
1805
  await this.completeRemoteDecision(decision);
1729
1806
  }
1730
1807
  );
1808
+ this.pool.onStartRun(async (message) => {
1809
+ await this.start(message.workflow, message.input, message.runId ?? globalThis.crypto.randomUUID(), {
1810
+ namespace: message.tenant,
1811
+ tags: message.tags,
1812
+ searchAttributes: message.searchAttributes
1813
+ });
1814
+ });
1731
1815
  this.controlPlane?.onControl((msg) => {
1732
1816
  if (msg.from === this.instanceId) return;
1733
1817
  if (msg.kind === "event") {
@@ -1892,7 +1976,7 @@ var WorkflowEngine = class {
1892
1976
  });
1893
1977
  }
1894
1978
  /**
1895
- * Register a flow-control queue referenced by `ctx.call(step, input, { queue })`. Caps concurrent
1979
+ * Register a flow-control queue referenced by `ctx.step(step, input, { queue })`. Caps concurrent
1896
1980
  * in-flight steps and/or the admission rate; blocked calls re-suspend and retry, so the limit is
1897
1981
  * durable. Per engine instance (see {@link QueueConfig}). Registering the same name replaces it.
1898
1982
  */
@@ -1995,6 +2079,23 @@ var WorkflowEngine = class {
1995
2079
  const ancestor = await this.findRemoteAncestor(runId);
1996
2080
  if (ancestor) registered = this.synthesizeRemoteChild(name, ancestor.version, ancestor);
1997
2081
  }
2082
+ if (!registered) {
2083
+ const conventionRun = {
2084
+ id: runId,
2085
+ workflow: name,
2086
+ workflowVersion: "1",
2087
+ status: "pending",
2088
+ // Stamp the namespace the REAL run will carry (below) so convention routing resolves the same
2089
+ // tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
2090
+ // `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
2091
+ // not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
2092
+ namespace: opts?.namespace ?? this.namespace,
2093
+ input,
2094
+ createdAt: /* @__PURE__ */ new Date(),
2095
+ updatedAt: /* @__PURE__ */ new Date()
2096
+ };
2097
+ registered = await this.resolveRemoteByConvention(conventionRun);
2098
+ }
1998
2099
  if (!registered) throw new Error(`workflow ${name} is not registered`);
1999
2100
  await registered.validateInput?.(input);
2000
2101
  const prior = await this.store.getRun(runId);
@@ -2019,7 +2120,7 @@ var WorkflowEngine = class {
2019
2120
  workflow: name,
2020
2121
  workflowVersion: registered.version,
2021
2122
  status: "pending",
2022
- namespace: this.namespace,
2123
+ namespace: opts?.namespace ?? this.namespace,
2023
2124
  input,
2024
2125
  tags,
2025
2126
  searchAttributes: opts?.searchAttributes,
@@ -2056,11 +2157,11 @@ var WorkflowEngine = class {
2056
2157
  error: run.error
2057
2158
  };
2058
2159
  }
2059
- if (run.namespace !== void 0 && run.namespace !== this.namespace) {
2160
+ if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
2060
2161
  await this.store.releaseRunLock(runId);
2061
2162
  throw new NamespaceMismatch();
2062
2163
  }
2063
- const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
2164
+ const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
2064
2165
  if (!registered) {
2065
2166
  throw new Error(`workflow ${run.workflow}@${run.workflowVersion} is not registered \u2014 keep the prior version deployed so in-flight runs can drain (skew protection)`);
2066
2167
  }
@@ -2106,6 +2207,34 @@ var WorkflowEngine = class {
2106
2207
  };
2107
2208
  }
2108
2209
  /**
2210
+ * Attempt to route an unregistered workflow to the live worker group of the same name: if the
2211
+ * transport reports a live heartbeat for group `run.workflow`, synthesize a throwaway
2212
+ * {@link RegisteredWorkflow} that dispatches to that group over the primary transport. Returns
2213
+ * `undefined` when the group is not live. Always attempted — there is no opt-in flag; an explicit
2214
+ * `register`/`registerRemote` still takes precedence (this only runs when nothing is registered).
2215
+ *
2216
+ * Recomputed per call (never memoized into `this.workflows`) so it stays correct if group
2217
+ * liveness changes between calls, exactly as {@link synthesizeRemoteChild} does.
2218
+ */
2219
+ async resolveRemoteByConvention(run) {
2220
+ const liveGroups = await this.pool.listWorkerGroups();
2221
+ const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
2222
+ if (!liveGroups.includes(group)) return void 0;
2223
+ const executor = new RemoteWorkflowExecutor(this.pool.primary, run.workflow, run.namespace);
2224
+ return {
2225
+ name: run.workflow,
2226
+ version: run.workflowVersion,
2227
+ fn: /* @__PURE__ */ __name(() => {
2228
+ throw new Error(`remote workflow ${run.workflow} has no local body`);
2229
+ }, "fn"),
2230
+ hasBody: false,
2231
+ remote: {
2232
+ group,
2233
+ executor
2234
+ }
2235
+ };
2236
+ }
2237
+ /**
2109
2238
  * Walk the parent chain of `childRunId` to the nearest ancestor registered as REMOTE, returning its
2110
2239
  * registration (or `undefined` if there is none). The parent of an awaited child is found via its
2111
2240
  * live `child:<childId>` signal waiter — present for as long as the parent is suspended awaiting the
@@ -2202,6 +2331,7 @@ var WorkflowEngine = class {
2202
2331
  type: "run.failed",
2203
2332
  runId: run.id,
2204
2333
  workflow: run.workflow,
2334
+ namespace: run.namespace,
2205
2335
  error
2206
2336
  });
2207
2337
  }
@@ -2259,6 +2389,7 @@ var WorkflowEngine = class {
2259
2389
  type: "run.failed",
2260
2390
  runId: run.id,
2261
2391
  workflow: run.workflow,
2392
+ namespace: run.namespace,
2262
2393
  error
2263
2394
  });
2264
2395
  this.notifyDead({
@@ -2333,7 +2464,8 @@ var WorkflowEngine = class {
2333
2464
  if (!run) return null;
2334
2465
  const id = newRunId ?? `${runId}~retry~${globalThis.crypto.randomUUID().slice(0, 8)}`;
2335
2466
  await this.start(run.workflow, input, id, {
2336
- tags: run.tags
2467
+ tags: run.tags,
2468
+ namespace: run.namespace
2337
2469
  });
2338
2470
  return {
2339
2471
  runId: id
@@ -2567,6 +2699,7 @@ var WorkflowEngine = class {
2567
2699
  type: "run.failed",
2568
2700
  runId,
2569
2701
  workflow: run.workflow,
2702
+ namespace: run.namespace,
2570
2703
  error
2571
2704
  });
2572
2705
  await this.cancelChildren(runId, opts);
@@ -2843,7 +2976,7 @@ var WorkflowEngine = class {
2843
2976
  });
2844
2977
  }
2845
2978
  async execute(run, fn) {
2846
- const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
2979
+ const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
2847
2980
  const lockNow = this.clock();
2848
2981
  if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
2849
2982
  if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
@@ -2893,6 +3026,7 @@ var WorkflowEngine = class {
2893
3026
  type: "run.completed",
2894
3027
  runId: run.id,
2895
3028
  workflow: run.workflow,
3029
+ namespace: run.namespace,
2896
3030
  output: outcome.output
2897
3031
  });
2898
3032
  void this.notifyParent(run.id, {
@@ -2915,6 +3049,7 @@ var WorkflowEngine = class {
2915
3049
  type: "run.failed",
2916
3050
  runId: run.id,
2917
3051
  workflow: run.workflow,
3052
+ namespace: run.namespace,
2918
3053
  error: outcome.error
2919
3054
  });
2920
3055
  void this.notifyParent(run.id, {
@@ -2935,7 +3070,8 @@ var WorkflowEngine = class {
2935
3070
  this.emit({
2936
3071
  type: "run.suspended",
2937
3072
  runId: run.id,
2938
- workflow: run.workflow
3073
+ workflow: run.workflow,
3074
+ namespace: run.namespace
2939
3075
  });
2940
3076
  return {
2941
3077
  runId: run.id,
@@ -2956,6 +3092,7 @@ var WorkflowEngine = class {
2956
3092
  type: "run.failed",
2957
3093
  runId: run.id,
2958
3094
  workflow: run.workflow,
3095
+ namespace: run.namespace,
2959
3096
  error
2960
3097
  });
2961
3098
  this.notifyCancelled(run.id);
@@ -2983,7 +3120,8 @@ var WorkflowEngine = class {
2983
3120
  this.emit({
2984
3121
  type: "run.started",
2985
3122
  runId: run.id,
2986
- workflow: run.workflow
3123
+ workflow: run.workflow,
3124
+ namespace: run.namespace
2987
3125
  });
2988
3126
  }
2989
3127
  if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
@@ -2996,7 +3134,8 @@ var WorkflowEngine = class {
2996
3134
  this.emit({
2997
3135
  type: "run.suspended",
2998
3136
  runId: run.id,
2999
- workflow: run.workflow
3137
+ workflow: run.workflow,
3138
+ namespace: run.namespace
3000
3139
  });
3001
3140
  await this.store.releaseRunLock(run.id);
3002
3141
  return {
@@ -3018,7 +3157,8 @@ var WorkflowEngine = class {
3018
3157
  this.emit({
3019
3158
  type: "run.suspended",
3020
3159
  runId: run.id,
3021
- workflow: run.workflow
3160
+ workflow: run.workflow,
3161
+ namespace: run.namespace
3022
3162
  });
3023
3163
  await this.store.releaseRunLock(run.id);
3024
3164
  await remote.executor.dispatch(run, history, taskId);
@@ -3057,6 +3197,7 @@ var WorkflowEngine = class {
3057
3197
  type: "run.failed",
3058
3198
  runId: run.id,
3059
3199
  workflow: run.workflow,
3200
+ namespace: run.namespace,
3060
3201
  error
3061
3202
  });
3062
3203
  return {
@@ -3207,13 +3348,16 @@ var WorkflowEngine = class {
3207
3348
  status: "pending",
3208
3349
  input: cmd.input,
3209
3350
  attempts: 1,
3210
- workerGroup: cmd.group,
3351
+ // `cmd.group` is the routing declaration the worker's replay emitted (cross-SDK, or a
3352
+ // group-served TS body) — treated as an isolation partition on top of the step's own
3353
+ // name, mirroring the StepDef formula below (identical expression everywhere).
3354
+ workerGroup: tenantGroup(sanitizeQueueToken(cmd.name), cmd.group),
3211
3355
  enqueuedAt: at,
3212
3356
  startedAt: at,
3213
3357
  finishedAt: at,
3214
3358
  // A `ctx.gather_calls([...])` fan-out stamps every dispatched `call` in the fan with the
3215
- // same group, so the dashboard renders the remote steps as one parallel fan (parity with
3216
- // the gathered `recordStep`/`startChild` tags). Undefined for a lone sequential `ctx.call`.
3359
+ // same group, so the dashboard renders the dispatched steps as one parallel fan (parity
3360
+ // with the gathered `recordStep`/`startChild` tags). Undefined for a lone sequential `ctx.step`.
3217
3361
  parallelGroup: cmd.parallelGroup
3218
3362
  }));
3219
3363
  await this.pool.dispatch({
@@ -3221,7 +3365,7 @@ var WorkflowEngine = class {
3221
3365
  seq: cmd.seq,
3222
3366
  name: cmd.name,
3223
3367
  stepId: id,
3224
- group: cmd.group,
3368
+ group: tenantGroup(sanitizeQueueToken(cmd.name), cmd.group),
3225
3369
  input: cmd.input,
3226
3370
  traceparent: this.traceparent?.(),
3227
3371
  context: this.context?.(),
@@ -3280,7 +3424,9 @@ var WorkflowEngine = class {
3280
3424
  parallelGroup: cmd.parallelGroup
3281
3425
  });
3282
3426
  if (!await this.store.getRun(childId)) {
3283
- queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId).catch(() => void 0));
3427
+ queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId, {
3428
+ namespace: run.namespace
3429
+ }).catch(() => void 0));
3284
3430
  }
3285
3431
  } else {
3286
3432
  throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
@@ -3312,6 +3458,7 @@ var WorkflowEngine = class {
3312
3458
  type: "run.failed",
3313
3459
  runId: run.id,
3314
3460
  workflow: run.workflow,
3461
+ namespace: run.namespace,
3315
3462
  error
3316
3463
  });
3317
3464
  return {
@@ -3332,7 +3479,8 @@ var WorkflowEngine = class {
3332
3479
  this.emit({
3333
3480
  type: "run.started",
3334
3481
  runId: run.id,
3335
- workflow: run.workflow
3482
+ workflow: run.workflow,
3483
+ namespace: run.namespace
3336
3484
  });
3337
3485
  }
3338
3486
  if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
@@ -3345,7 +3493,8 @@ var WorkflowEngine = class {
3345
3493
  this.emit({
3346
3494
  type: "run.suspended",
3347
3495
  runId: run.id,
3348
- workflow: run.workflow
3496
+ workflow: run.workflow,
3497
+ namespace: run.namespace
3349
3498
  });
3350
3499
  await this.store.releaseRunLock(run.id);
3351
3500
  return {
@@ -3357,7 +3506,7 @@ var WorkflowEngine = class {
3357
3506
  const snapshot = await this.store.listCheckpoints(run.id);
3358
3507
  const replay = /* @__PURE__ */ new Map();
3359
3508
  for (const cp of snapshot) replay.set(cp.seq, cp);
3360
- const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
3509
+ const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
3361
3510
  try {
3362
3511
  const output = await fn(ctx, run.input);
3363
3512
  if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
@@ -3377,7 +3526,8 @@ var WorkflowEngine = class {
3377
3526
  this.emit({
3378
3527
  type: "run.completed",
3379
3528
  runId: run.id,
3380
- workflow: run.workflow
3529
+ workflow: run.workflow,
3530
+ namespace: run.namespace
3381
3531
  });
3382
3532
  void this.notifyParent(run.id, {
3383
3533
  ok: true,
@@ -3455,7 +3605,7 @@ var WorkflowEngine = class {
3455
3605
  /** The seam handed to {@link createWorkflowCtx}: the authoring API reaches durability + lifecycle
3456
3606
  * (checkpointing, dispatch, child start) through this, so the ctx primitives live in their own
3457
3607
  * module and the engine stays the orchestrator. */
3458
- ctxHostFor(replay) {
3608
+ ctxHostFor(replay, parentNamespace) {
3459
3609
  return {
3460
3610
  store: this.store,
3461
3611
  replay,
@@ -3465,10 +3615,16 @@ var WorkflowEngine = class {
3465
3615
  completeStep: /* @__PURE__ */ __name((s) => this.completeStep(s), "completeStep"),
3466
3616
  failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
3467
3617
  callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
3468
- // Defer so a fast child can't reentrantly resume a still-running parent.
3618
+ // Defer so a fast child can't reentrantly resume a still-running parent. Stamp the child with
3619
+ // the PARENT run's namespace (not this engine's) so a child belongs to the tenant/partition of
3620
+ // the run it was spawned from — even when an operator (namespace: undefined, drives every
3621
+ // namespace) is the one executing the parent. Otherwise an operator recovery-resuming a
3622
+ // `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
3623
+ // tenant's worker pool onto the shared/dev workers.
3469
3624
  startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
3470
3625
  queueMicrotask(() => void this.start(workflow, input, id, {
3471
- priority
3626
+ priority,
3627
+ namespace: parentNamespace
3472
3628
  }).catch(() => void 0));
3473
3629
  }, "startChild"),
3474
3630
  // Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
@@ -3596,7 +3752,6 @@ var WorkflowEngine = class {
3596
3752
  this.queueWaiters.get(queue)?.delete(runId);
3597
3753
  this.stepQueue.set(id, queue);
3598
3754
  }
3599
- const validInput = step.input.parse(input);
3600
3755
  const enqueuedAt = /* @__PURE__ */ new Date();
3601
3756
  await this.store.saveCheckpoint({
3602
3757
  runId,
@@ -3605,9 +3760,9 @@ var WorkflowEngine = class {
3605
3760
  kind: "remote",
3606
3761
  stepId: id,
3607
3762
  status: "pending",
3608
- input: validInput,
3763
+ input,
3609
3764
  attempts: attempt,
3610
- workerGroup: step.group,
3765
+ workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3611
3766
  enqueuedAt,
3612
3767
  startedAt: enqueuedAt,
3613
3768
  finishedAt: enqueuedAt
@@ -3617,8 +3772,8 @@ var WorkflowEngine = class {
3617
3772
  seq,
3618
3773
  name: step.name,
3619
3774
  stepId: id,
3620
- group: step.group,
3621
- input: validInput,
3775
+ group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3776
+ input,
3622
3777
  traceparent: this.traceparent?.(),
3623
3778
  context: this.context?.(),
3624
3779
  priority: admission?.priority,
@@ -3683,7 +3838,9 @@ var WorkflowEngine = class {
3683
3838
  const run = await this.store.getRun(decision.runId);
3684
3839
  if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
3685
3840
  if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
3686
- if (run.namespace !== void 0 && run.namespace !== this.namespace) return;
3841
+ if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
3842
+ return;
3843
+ }
3687
3844
  const nowMs = this.clock();
3688
3845
  if (!await this.store.tryLockRun(decision.runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
3689
3846
  return;
@@ -3749,7 +3906,7 @@ var WorkflowEngine = class {
3749
3906
  /** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
3750
3907
  async callRemoteInMemory(runId, seq, step, input, transport) {
3751
3908
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
3752
- const validInput = step.input.parse(input);
3909
+ const validInput = step.input ? step.input.parse(input) : input;
3753
3910
  const id = stepId(runId, seq);
3754
3911
  const enqueuedAt = /* @__PURE__ */ new Date();
3755
3912
  this.emit({
@@ -3772,7 +3929,7 @@ var WorkflowEngine = class {
3772
3929
  seq,
3773
3930
  name: step.name,
3774
3931
  stepId: id,
3775
- group: step.group,
3932
+ group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3776
3933
  input: validInput,
3777
3934
  traceparent: this.traceparent?.(),
3778
3935
  context: this.context?.(),
@@ -3780,7 +3937,7 @@ var WorkflowEngine = class {
3780
3937
  }, transport);
3781
3938
  try {
3782
3939
  const resolution = step.timeoutMs ? await this.awaitWithHeartbeat(id, resultPromise, step.timeoutMs) : await resultPromise;
3783
- const output = step.output.parse(resolution.output);
3940
+ const output = step.output ? step.output.parse(resolution.output) : resolution.output;
3784
3941
  const startedAt = resolution.startedAt ? new Date(resolution.startedAt) : enqueuedAt;
3785
3942
  await this.completeStep({
3786
3943
  runId,
@@ -3791,7 +3948,7 @@ var WorkflowEngine = class {
3791
3948
  output,
3792
3949
  events: resolution.events,
3793
3950
  attempts: attempt,
3794
- workerGroup: step.group,
3951
+ workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3795
3952
  enqueuedAt,
3796
3953
  startedAt
3797
3954
  });
@@ -3867,16 +4024,6 @@ var TERMINAL_RUN_STATUSES = [
3867
4024
  "dead"
3868
4025
  ];
3869
4026
 
3870
- // src/remote-step-factory.ts
3871
- function remoteStep(config) {
3872
- return {
3873
- ...config,
3874
- group: config.group ?? config.name.split(".")[0] ?? config.name,
3875
- __remote: true
3876
- };
3877
- }
3878
- __name(remoteStep, "remoteStep");
3879
-
3880
4027
  // src/codec-state-store.ts
3881
4028
  var CodecStateStore = class {
3882
4029
  static {
@@ -4601,6 +4748,8 @@ var InMemoryTransport = class {
4601
4748
  ContinueAsNew,
4602
4749
  DURABLE_OPTIONS,
4603
4750
  DURABLE_OPTIONS_CANONICAL,
4751
+ DURABLE_STEP_CONFIG,
4752
+ DURABLE_STEP_NAME,
4604
4753
  Entities,
4605
4754
  EventAccumulators,
4606
4755
  FatalError,
@@ -4638,12 +4787,15 @@ var InMemoryTransport = class {
4638
4787
  normalizeAttributeRows,
4639
4788
  parseDuration,
4640
4789
  prevCronFireMs,
4641
- remoteStep,
4642
4790
  runSchedules,
4643
4791
  runStepHandler,
4792
+ sanitizeQueueToken,
4644
4793
  scheduledRunId,
4645
4794
  sqlComparator,
4795
+ stepConfigOf,
4646
4796
  stepId,
4797
+ stepNameOf,
4798
+ tenantGroup,
4647
4799
  workflowName
4648
4800
  });
4649
4801
  //# sourceMappingURL=index.cjs.map