@dudousxd/nestjs-durable-core 0.63.0 → 0.65.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
@@ -481,6 +481,57 @@ function backoffDelay(attempt, options) {
481
481
  }
482
482
  __name(backoffDelay, "backoffDelay");
483
483
 
484
+ // src/execution-hooks.ts
485
+ var wrappers = [];
486
+ function useDurableExecution(wrapper) {
487
+ wrappers.push(wrapper);
488
+ return () => {
489
+ const i = wrappers.indexOf(wrapper);
490
+ if (i >= 0) wrappers.splice(i, 1);
491
+ };
492
+ }
493
+ __name(useDurableExecution, "useDurableExecution");
494
+ function clearDurableExecutionWrappers() {
495
+ wrappers.length = 0;
496
+ }
497
+ __name(clearDurableExecutionWrappers, "clearDurableExecutionWrappers");
498
+ function hasDurableExecutionWrappers() {
499
+ return wrappers.length > 0;
500
+ }
501
+ __name(hasDurableExecutionWrappers, "hasDurableExecutionWrappers");
502
+ async function runDurableExecution(execution, body) {
503
+ if (wrappers.length === 0) return body();
504
+ let outcome;
505
+ const inner = /* @__PURE__ */ __name(async () => {
506
+ try {
507
+ const value = await body();
508
+ outcome = {
509
+ ok: true,
510
+ value
511
+ };
512
+ return value;
513
+ } catch (error2) {
514
+ outcome = {
515
+ ok: false,
516
+ error: error2
517
+ };
518
+ throw error2;
519
+ }
520
+ }, "inner");
521
+ const chain = wrappers.reduceRight((next, wrapper) => () => wrapper(execution, next), inner);
522
+ try {
523
+ await chain();
524
+ } catch (error2) {
525
+ if (outcome === void 0) throw error2;
526
+ }
527
+ if (outcome === void 0) {
528
+ throw new Error("a durable execution wrapper returned without calling next() \u2014 the unit of work never ran");
529
+ }
530
+ if (outcome.ok) return outcome.value;
531
+ throw outcome.error;
532
+ }
533
+ __name(runDurableExecution, "runDurableExecution");
534
+
484
535
  // src/step-logger.ts
485
536
  function withDuration(start, now, data) {
486
537
  if (data && typeof data === "object" && "durationMs" in data) {
@@ -622,7 +673,14 @@ async function runStepHandler(task, handler) {
622
673
  } : result, "withEvents");
623
674
  try {
624
675
  const logger = createStepLogger(events, Date.now);
625
- const output = await runInStepLogger(logger, () => handler(task.input, logger));
676
+ const output = await runDurableExecution({
677
+ unit: "step",
678
+ runId: task.runId,
679
+ workflow: void 0,
680
+ name: task.name,
681
+ seq: task.seq,
682
+ attempt: task.attempt
683
+ }, async () => runInStepLogger(logger, () => handler(task.input, logger)));
626
684
  return withEvents({
627
685
  ...base,
628
686
  status: "completed",
@@ -923,6 +981,9 @@ function normalizeDescriptor(raw) {
923
981
  ],
924
982
  workflows: raw.workflows ?? [],
925
983
  steps: raw.steps ?? [],
984
+ ...raw.registrations !== void 0 ? {
985
+ registrations: raw.registrations
986
+ } : {},
926
987
  ...raw.partition !== void 0 ? {
927
988
  partition: raw.partition
928
989
  } : {},
@@ -933,6 +994,23 @@ function normalizeDescriptor(raw) {
933
994
  };
934
995
  }
935
996
  __name(normalizeDescriptor, "normalizeDescriptor");
997
+ function canonicalizeRegistrations(regs) {
998
+ const canonical = regs.map((r) => ({
999
+ name: r.name,
1000
+ version: r.version ?? null,
1001
+ group: r.group ?? null,
1002
+ requires: r.requires ? [
1003
+ ...new Set(r.requires)
1004
+ ].sort() : null,
1005
+ origin: r.origin ?? null
1006
+ }));
1007
+ const byJson = /* @__PURE__ */ new Map();
1008
+ for (const entry of canonical) byJson.set(JSON.stringify(entry), entry);
1009
+ return [
1010
+ ...byJson.keys()
1011
+ ].sort().map((k) => byJson.get(k));
1012
+ }
1013
+ __name(canonicalizeRegistrations, "canonicalizeRegistrations");
936
1014
  function canonicalizeForHash(d) {
937
1015
  const set = /* @__PURE__ */ __name((xs) => [
938
1016
  ...new Set(xs)
@@ -954,6 +1032,9 @@ function canonicalizeForHash(d) {
954
1032
  capabilities: set(d.capabilities),
955
1033
  workflows: set(d.workflows),
956
1034
  steps: set(d.steps),
1035
+ ...d.registrations !== void 0 ? {
1036
+ registrations: canonicalizeRegistrations(d.registrations)
1037
+ } : {},
957
1038
  partition: d.partition ?? null,
958
1039
  namespace: d.namespace ?? null,
959
1040
  startedAt: d.startedAt
@@ -986,6 +1067,144 @@ function heartbeatStatus(descriptor, opts = {}) {
986
1067
  };
987
1068
  }
988
1069
  __name(heartbeatStatus, "heartbeatStatus");
1070
+ function describeWorker(opts) {
1071
+ const version = CURRENT_PROTOCOL_VERSION;
1072
+ const registrations = [
1073
+ ...opts.registrations ?? []
1074
+ ].sort((a, b) => `${a.name}@${a.version ?? ""}`.localeCompare(`${b.name}@${b.version ?? ""}`));
1075
+ return {
1076
+ instanceId: opts.instanceId,
1077
+ runtime: opts.runtime,
1078
+ sdk: {
1079
+ name: opts.sdk.name,
1080
+ version: opts.sdk.version
1081
+ },
1082
+ protocol: {
1083
+ version,
1084
+ range: [
1085
+ version,
1086
+ version
1087
+ ]
1088
+ },
1089
+ capabilities: [
1090
+ ...LEGACY_V1_CAPABILITIES
1091
+ ],
1092
+ workflows: [
1093
+ ...opts.workflows ?? []
1094
+ ].sort(),
1095
+ steps: [
1096
+ ...opts.steps
1097
+ ].sort(),
1098
+ ...registrations.length > 0 ? {
1099
+ registrations
1100
+ } : {},
1101
+ ...opts.partition ? {
1102
+ partition: opts.partition
1103
+ } : {},
1104
+ ...opts.namespace && opts.namespace !== "default" ? {
1105
+ namespace: opts.namespace
1106
+ } : {},
1107
+ startedAt: opts.startedAt
1108
+ };
1109
+ }
1110
+ __name(describeWorker, "describeWorker");
1111
+
1112
+ // src/handshake/announced.ts
1113
+ function announcementsOf(raw) {
1114
+ const d = normalizeDescriptor(raw);
1115
+ const identity = {
1116
+ instanceId: d.instanceId,
1117
+ runtime: d.runtime
1118
+ };
1119
+ const announcements = (d.registrations ?? []).map((reg) => ({
1120
+ ...reg,
1121
+ ...identity
1122
+ }));
1123
+ const covered = new Set(announcements.map((a) => a.name));
1124
+ for (const name of d.workflows) {
1125
+ if (!covered.has(name)) announcements.push({
1126
+ name,
1127
+ ...identity
1128
+ });
1129
+ }
1130
+ return announcements;
1131
+ }
1132
+ __name(announcementsOf, "announcementsOf");
1133
+ function announcementKey(a) {
1134
+ return a.version === void 0 ? a.name : `${a.name}@${a.version}`;
1135
+ }
1136
+ __name(announcementKey, "announcementKey");
1137
+ function stated(values) {
1138
+ return [
1139
+ ...new Set(values.filter((v) => v !== void 0))
1140
+ ].sort();
1141
+ }
1142
+ __name(stated, "stated");
1143
+ function aggregateAnnouncements(descriptors) {
1144
+ const byKey = /* @__PURE__ */ new Map();
1145
+ for (const descriptor of descriptors) {
1146
+ for (const announcement of announcementsOf(descriptor)) {
1147
+ const key = announcementKey(announcement);
1148
+ const bucket = byKey.get(key);
1149
+ if (bucket) bucket.push(announcement);
1150
+ else byKey.set(key, [
1151
+ announcement
1152
+ ]);
1153
+ }
1154
+ }
1155
+ const out = [];
1156
+ for (const [key, announcements] of byKey) {
1157
+ const groups = stated(announcements.map((a) => a.group));
1158
+ const origins = stated(announcements.map((a) => a.origin));
1159
+ const requiresByJson = /* @__PURE__ */ new Map();
1160
+ for (const a of announcements) {
1161
+ if (a.requires === void 0) continue;
1162
+ const canonical = [
1163
+ ...new Set(a.requires)
1164
+ ].sort();
1165
+ requiresByJson.set(JSON.stringify(canonical), canonical);
1166
+ }
1167
+ const requires = [
1168
+ ...requiresByJson.values()
1169
+ ].sort((x, y) => x.join(",").localeCompare(y.join(",")));
1170
+ const disagreements = [];
1171
+ if (groups.length > 1) disagreements.push({
1172
+ axis: "group",
1173
+ values: groups
1174
+ });
1175
+ if (origins.length > 1) disagreements.push({
1176
+ axis: "origin",
1177
+ values: origins
1178
+ });
1179
+ if (requires.length > 1) {
1180
+ disagreements.push({
1181
+ axis: "requires",
1182
+ values: requires.map((r) => r.join(","))
1183
+ });
1184
+ }
1185
+ const first = announcements[0];
1186
+ if (!first) continue;
1187
+ out.push({
1188
+ key,
1189
+ name: first.name,
1190
+ ...first.version !== void 0 ? {
1191
+ version: first.version
1192
+ } : {},
1193
+ groups,
1194
+ origins,
1195
+ requires,
1196
+ runtimes: [
1197
+ ...new Set(announcements.map((a) => a.runtime))
1198
+ ].sort(),
1199
+ instances: [
1200
+ ...new Set(announcements.map((a) => a.instanceId))
1201
+ ].sort(),
1202
+ disagreements
1203
+ });
1204
+ }
1205
+ return out.sort((a, b) => a.key.localeCompare(b.key));
1206
+ }
1207
+ __name(aggregateAnnouncements, "aggregateAnnouncements");
989
1208
 
990
1209
  // src/handshake/negotiate.ts
991
1210
  var rangesOverlap = /* @__PURE__ */ __name((a, b) => Math.max(a[0], b[0]) <= Math.min(a[1], b[1]), "rangesOverlap");
@@ -1513,6 +1732,30 @@ var TransportPool = class {
1513
1732
  }
1514
1733
  return descriptors;
1515
1734
  }
1735
+ /**
1736
+ * Every live descriptor in the deployment, merged across every transport that can report them and
1737
+ * de-duplicated by `instanceId` — one worker publishes the same descriptor under each token it
1738
+ * consumes, and it must count once. Returns `[]` when no transport implements
1739
+ * {@link Transport.readAllWorkerDescriptors} (a pure in-process pool advertises nothing).
1740
+ *
1741
+ * First writer wins on a duplicate id: the copies are the same bytes by construction (one process,
1742
+ * one descriptor, published N times), so there is nothing to choose between. Two DIFFERENT
1743
+ * processes sharing an `instanceId` would collapse into one here — that is a misconfiguration the
1744
+ * fleet already suffers from (they also share heartbeat and descriptor keys, so they overwrite each
1745
+ * other's advertisement), not something this read can repair.
1746
+ */
1747
+ async readAllWorkerDescriptors() {
1748
+ const byInstance = /* @__PURE__ */ new Map();
1749
+ for (const { transport } of this.transports) {
1750
+ if (!transport.readAllWorkerDescriptors) continue;
1751
+ for (const descriptor of await transport.readAllWorkerDescriptors()) {
1752
+ if (!byInstance.has(descriptor.instanceId)) byInstance.set(descriptor.instanceId, descriptor);
1753
+ }
1754
+ }
1755
+ return [
1756
+ ...byInstance.values()
1757
+ ];
1758
+ }
1516
1759
  /** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
1517
1760
  async listWorkerGroups() {
1518
1761
  const groups = /* @__PURE__ */ new Set();
@@ -1913,7 +2156,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
1913
2156
  return unwrapCompletion(await waitForSignal(`task:${runId}:${name}`), `task "${name}"`);
1914
2157
  }, "task");
1915
2158
  const child = /* @__PURE__ */ __name(async (workflow2, input, options) => {
1916
- const { childId, priority } = normalizeChildOptions(options);
2159
+ const { childId, priority, version } = normalizeChildOptions(options);
1917
2160
  const current = pos.next();
1918
2161
  const id = childId ?? `${runId}.child.${current}`;
1919
2162
  const existing = await readCheckpoint(current);
@@ -1941,7 +2184,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
1941
2184
  }));
1942
2185
  return unwrapCompletion(buffered.payload, `child "${id}"`);
1943
2186
  }
1944
- if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
2187
+ if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
1945
2188
  if (!existing) {
1946
2189
  await writeCheckpoint(instantCheckpoint({
1947
2190
  runId,
@@ -2024,12 +2267,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
2024
2267
  return outputs;
2025
2268
  }, "all");
2026
2269
  const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
2027
- const { childId, priority } = normalizeChildOptions(options);
2270
+ const { childId, priority, version } = normalizeChildOptions(options);
2028
2271
  const current = pos.next();
2029
2272
  const id = childId ?? `${runId}.child.${current}`;
2030
2273
  const existing = await readCheckpoint(current);
2031
2274
  if (existing && existing.status === "completed") return existing.output;
2032
- if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
2275
+ if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
2033
2276
  await writeCheckpoint(instantCheckpoint({
2034
2277
  runId,
2035
2278
  seq: current,
@@ -2589,6 +2832,15 @@ var WorkflowEngine = class {
2589
2832
  const reg = this.workflows.get(versionKey(name, version));
2590
2833
  return reg?.hasBody ? reg.fn : void 0;
2591
2834
  }
2835
+ /** The registered versions of `name`, for the pinned-start error — "2 is not registered" is a
2836
+ * guess-the-typo error without them, and the answer (which versions this deploy actually carries)
2837
+ * is right here. `none` when the name is unknown entirely. */
2838
+ registeredVersions(name) {
2839
+ const versions = [
2840
+ ...this.workflows.values()
2841
+ ].filter((w) => w.name === name).map((w) => w.version);
2842
+ return versions.length ? versions.join(", ") : "none";
2843
+ }
2592
2844
  /**
2593
2845
  * Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
2594
2846
  * as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
@@ -2738,12 +2990,13 @@ var WorkflowEngine = class {
2738
2990
  }
2739
2991
  async start(workflow, input, runId, opts) {
2740
2992
  const name = workflowName(workflow);
2741
- let registered = this.latest.get(name);
2742
- if (!registered) {
2993
+ const pinned = opts?.version;
2994
+ let registered = pinned === void 0 ? this.latest.get(name) : this.workflows.get(versionKey(name, pinned));
2995
+ if (!registered && pinned === void 0) {
2743
2996
  const ancestor = await this.findRemoteAncestor(runId);
2744
2997
  if (ancestor) registered = this.synthesizeRemoteChild(name, ancestor.version, ancestor);
2745
2998
  }
2746
- if (!registered) {
2999
+ if (!registered && pinned === void 0) {
2747
3000
  const conventionRun = {
2748
3001
  id: runId,
2749
3002
  workflow: name,
@@ -2760,7 +3013,9 @@ var WorkflowEngine = class {
2760
3013
  };
2761
3014
  registered = await this.resolveRemoteByConvention(conventionRun);
2762
3015
  }
2763
- if (!registered) throw new Error(`workflow ${name} is not registered`);
3016
+ if (!registered) {
3017
+ throw new Error(pinned === void 0 ? `workflow ${name} is not registered` : `workflow ${versionKey(name, pinned)} is not registered \u2014 a pinned start never falls back to the newest version (registered: ${this.registeredVersions(name)})`);
3018
+ }
2764
3019
  await registered.validateInput?.(input);
2765
3020
  const prior = await this.store.getRun(runId);
2766
3021
  if (prior) {
@@ -2967,7 +3222,32 @@ var WorkflowEngine = class {
2967
3222
  /**
2968
3223
  * Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
2969
3224
  * timer poller alongside {@link resumeDueTimers}. A timed-out run is moved to `cancelled` with an
2970
- * `execution_timeout` error (terminal, so a late step result can't resurrect it).
3225
+ * `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
3226
+ * are cascaded exactly as an explicit {@link cancel} cascades them.
3227
+ *
3228
+ * WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
3229
+ * the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
3230
+ * a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
3231
+ * is a SCAN — it walks every in-flight run of every timed workflow on every poller tick, so it must
3232
+ * not pay `cancel`'s per-run re-read and singleton/control-plane work on runs it isn't cancelling.
3233
+ * The cascade below is charged only to the runs that ACTUALLY timed out, which are rare by
3234
+ * construction; the scan itself is unchanged.
3235
+ *
3236
+ * The subtree, though, has no reason to differ, and before this it did: a timed-out parent's
3237
+ * children kept running with the parent gone and nothing pointing at them — a silent orphan you
3238
+ * could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
3239
+ * walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
3240
+ * `cancel` returns early on a run that is already `completed`/`cancelled`/`dead` (a child that beat
3241
+ * the sweep to the finish is never clobbered, and a re-cancel is a no-op), and it writes the
3242
+ * parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
3243
+ * second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
3244
+ * the same terminal state twice and cascade into already-cancelled children — idempotent, not
3245
+ * corrupting. The one window it does NOT close is the pre-existing check-then-write race an
3246
+ * explicit `cancel` has always had (a child that completes between `cancel`'s read and its write);
3247
+ * closing that needs a compare-and-set in the store contract, not a change here.
3248
+ *
3249
+ * Cascade failures are swallowed per subtree for the same reason `cancelChildren` swallows them per
3250
+ * child: one unreachable child must not abort the sweep and leave every LATER timed-out run alive.
2971
3251
  */
2972
3252
  async sweepTimeouts(now = this.clock()) {
2973
3253
  for (const reg of new Set(this.latest.values())) {
@@ -3003,6 +3283,7 @@ var WorkflowEngine = class {
3003
3283
  namespace: run.namespace,
3004
3284
  error: error2
3005
3285
  });
3286
+ await this.cancelChildren(run.id).catch(() => void 0);
3006
3287
  }
3007
3288
  }
3008
3289
  }
@@ -3731,6 +4012,10 @@ var WorkflowEngine = class {
3731
4012
  * `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
3732
4013
  * checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
3733
4014
  * at finished / already-cancelled runs (no loops, no re-cancel).
4015
+ *
4016
+ * Two callers, deliberately the same walk: an explicit {@link cancel}, and {@link sweepTimeouts}
4017
+ * when a parent hits its `executionTimeout` — a timed-out parent's subtree dies the same way an
4018
+ * explicitly cancelled one's does, or the children are orphans nobody is holding a reference to.
3734
4019
  */
3735
4020
  async cancelChildren(parentRunId, opts) {
3736
4021
  for (const id of await this.getRunChildren(parentRunId)) {
@@ -3810,6 +4095,29 @@ var WorkflowEngine = class {
3810
4095
  return out;
3811
4096
  }
3812
4097
  /**
4098
+ * What workflows the LIVE fleet announces it can execute — the deployment-wide registry a picker
4099
+ * ("call this existing workflow") needs, and the one question this engine's own registry cannot
4100
+ * answer. {@link workflowBody} answers only for this process, and a missing body is ambiguous: it
4101
+ * means "not registered here", but equally "registered via {@link registerRemote} against another
4102
+ * SDK" or "a group resolved by convention against a worker". An inferred answer would therefore
4103
+ * differ per replica. This one does not: every pod folds the same published announcements.
4104
+ *
4105
+ * Scoped like every other poll surface — an engine WITH a `namespace` sees only workers in it, and
4106
+ * an operator (namespace unset) sees the whole deployment ("ver tudo = ausência de namespace"). A
4107
+ * descriptor that states no namespace is `default`, matching how a single-tenant fleet publishes.
4108
+ *
4109
+ * Cost: one advertisement scan per CALL, on demand. Nothing is added to the poll loop and nothing
4110
+ * is retained — no pod holds a table of the fleet's registrations. Empty when no transport can
4111
+ * introspect the advertisement keyspace (a pure in-process transport advertises nothing).
4112
+ *
4113
+ * Workflows only, and deliberately: see the "steps are out of scope" note in `handshake/announced`.
4114
+ */
4115
+ async announcedWorkflows() {
4116
+ const descriptors = await this.pool.readAllWorkerDescriptors();
4117
+ const mine = this.namespace === void 0 ? descriptors : descriptors.filter((d) => (d.namespace ?? "default") === this.namespace);
4118
+ return aggregateAnnouncements(mine);
4119
+ }
4120
+ /**
3813
4121
  * The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
3814
4122
  * dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
3815
4123
  * that is NEITHER a registration NOR heartbeating (the exact shape of a run stuck on an offline
@@ -4524,8 +4832,31 @@ var WorkflowEngine = class {
4524
4832
  error: error2
4525
4833
  };
4526
4834
  }
4527
- /** The run body, lease held + renewed by {@link execute}. */
4528
- async runExecution(run, fn, registered) {
4835
+ /**
4836
+ * The run body, lease held + renewed by {@link execute}, wrapped in the registered
4837
+ * {@link runDurableExecution} wrappers so an observability scope covers ONE WHOLE TURN.
4838
+ *
4839
+ * The wrap goes here — around the entire turn — rather than tightly around the `fn(ctx, input)`
4840
+ * call, because the store writes that bracket the body (the `pending` → `running` transition, the
4841
+ * step checkpoints, the terminal `settleRun` update) are exactly the queries an operator wants
4842
+ * correlated to the turn that issued them. Narrowing the wrap to the body would leave them
4843
+ * uncorrelated for the sake of a purity that buys nothing.
4844
+ *
4845
+ * Only the TS path is wrapped. {@link runRemoteExecution} does not execute a body at all — it
4846
+ * dispatches a workflow task and awaits the worker's decision — so a scope opened there would
4847
+ * describe a dispatch, not an execution, and would mislead anyone reading the trace.
4848
+ */
4849
+ runExecution(run, fn, registered) {
4850
+ return runDurableExecution({
4851
+ unit: "workflow",
4852
+ runId: run.id,
4853
+ workflow: run.workflow,
4854
+ name: void 0,
4855
+ seq: void 0,
4856
+ attempt: void 0
4857
+ }, () => this.runExecutionTurn(run, fn, registered));
4858
+ }
4859
+ async runExecutionTurn(run, fn, registered) {
4529
4860
  if (run.status === "cancelling") this.cancelRequested.add(run.id);
4530
4861
  if (run.status === "pending") {
4531
4862
  await this.store.updateRun(run.id, {
@@ -4865,10 +5196,13 @@ var WorkflowEngine = class {
4865
5196
  // namespace) is the one executing the parent. Otherwise an operator recovery-resuming a
4866
5197
  // `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
4867
5198
  // tenant's worker pool onto the shared/dev workers.
4868
- startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
5199
+ startChild: /* @__PURE__ */ __name((workflow, input, id, priority, version) => {
4869
5200
  this.startChildDeferred(workflow, input, id, {
4870
5201
  priority,
4871
- namespace: parentNamespace
5202
+ namespace: parentNamespace,
5203
+ // Absent unless the body pinned one (`ctx.child(ref, input, { version })`) — an omitted
5204
+ // pin must stay omitted, so the child keeps resolving `latest` exactly as before.
5205
+ version
4872
5206
  });
4873
5207
  }, "startChild"),
4874
5208
  // Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
@@ -6276,6 +6610,8 @@ export {
6276
6610
  WorkflowBlocked,
6277
6611
  WorkflowEngine,
6278
6612
  WorkflowSuspended,
6613
+ aggregateAnnouncements,
6614
+ announcementsOf,
6279
6615
  applyAttributeQuery,
6280
6616
  attributeColumnFor,
6281
6617
  attributeOperand,
@@ -6284,12 +6620,14 @@ export {
6284
6620
  canRoute,
6285
6621
  capableWorkers,
6286
6622
  classifyWaiterToken,
6623
+ clearDurableExecutionWrappers,
6287
6624
  collectMetrics,
6288
6625
  controlPlaneDescriptor,
6289
6626
  createStepLogger,
6290
6627
  currentStep,
6291
6628
  currentWorkflowCtx,
6292
6629
  debug,
6630
+ describeWorker,
6293
6631
  descriptorHash,
6294
6632
  error,
6295
6633
  eventMatchOf,
@@ -6297,6 +6635,7 @@ export {
6297
6635
  eventNameOf,
6298
6636
  eventPrefix,
6299
6637
  eventToken,
6638
+ hasDurableExecutionWrappers,
6300
6639
  heartbeatStatus,
6301
6640
  indexWaitersByRun,
6302
6641
  info,
@@ -6316,6 +6655,7 @@ export {
6316
6655
  requiredCapabilities,
6317
6656
  resolveRouting,
6318
6657
  resolveRunWaiting,
6658
+ runDurableExecution,
6319
6659
  runInStepLogger,
6320
6660
  runInWorkflowCtx,
6321
6661
  runSchedules,
@@ -6330,6 +6670,7 @@ export {
6330
6670
  subEvent,
6331
6671
  subProcess,
6332
6672
  tenantGroup,
6673
+ useDurableExecution,
6333
6674
  warn,
6334
6675
  workflowName
6335
6676
  };