@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.cjs CHANGED
@@ -60,6 +60,8 @@ __export(index_exports, {
60
60
  WorkflowBlocked: () => WorkflowBlocked,
61
61
  WorkflowEngine: () => WorkflowEngine,
62
62
  WorkflowSuspended: () => WorkflowSuspended,
63
+ aggregateAnnouncements: () => aggregateAnnouncements,
64
+ announcementsOf: () => announcementsOf,
63
65
  applyAttributeQuery: () => applyAttributeQuery,
64
66
  attributeColumnFor: () => attributeColumnFor,
65
67
  attributeOperand: () => attributeOperand,
@@ -68,12 +70,14 @@ __export(index_exports, {
68
70
  canRoute: () => canRoute,
69
71
  capableWorkers: () => capableWorkers,
70
72
  classifyWaiterToken: () => classifyWaiterToken,
73
+ clearDurableExecutionWrappers: () => clearDurableExecutionWrappers,
71
74
  collectMetrics: () => collectMetrics,
72
75
  controlPlaneDescriptor: () => controlPlaneDescriptor,
73
76
  createStepLogger: () => createStepLogger,
74
77
  currentStep: () => currentStep,
75
78
  currentWorkflowCtx: () => currentWorkflowCtx,
76
79
  debug: () => debug,
80
+ describeWorker: () => describeWorker,
77
81
  descriptorHash: () => descriptorHash,
78
82
  error: () => error,
79
83
  eventMatchOf: () => eventMatchOf,
@@ -81,6 +85,7 @@ __export(index_exports, {
81
85
  eventNameOf: () => eventNameOf,
82
86
  eventPrefix: () => eventPrefix,
83
87
  eventToken: () => eventToken,
88
+ hasDurableExecutionWrappers: () => hasDurableExecutionWrappers,
84
89
  heartbeatStatus: () => heartbeatStatus,
85
90
  indexWaitersByRun: () => indexWaitersByRun,
86
91
  info: () => info,
@@ -100,6 +105,7 @@ __export(index_exports, {
100
105
  requiredCapabilities: () => requiredCapabilities,
101
106
  resolveRouting: () => resolveRouting,
102
107
  resolveRunWaiting: () => resolveRunWaiting,
108
+ runDurableExecution: () => runDurableExecution,
103
109
  runInStepLogger: () => runInStepLogger,
104
110
  runInWorkflowCtx: () => runInWorkflowCtx,
105
111
  runSchedules: () => runSchedules,
@@ -114,6 +120,7 @@ __export(index_exports, {
114
120
  subEvent: () => subEvent,
115
121
  subProcess: () => subProcess,
116
122
  tenantGroup: () => tenantGroup,
123
+ useDurableExecution: () => useDurableExecution,
117
124
  warn: () => warn,
118
125
  workflowName: () => workflowName
119
126
  });
@@ -599,6 +606,57 @@ function backoffDelay(attempt, options) {
599
606
  }
600
607
  __name(backoffDelay, "backoffDelay");
601
608
 
609
+ // src/execution-hooks.ts
610
+ var wrappers = [];
611
+ function useDurableExecution(wrapper) {
612
+ wrappers.push(wrapper);
613
+ return () => {
614
+ const i = wrappers.indexOf(wrapper);
615
+ if (i >= 0) wrappers.splice(i, 1);
616
+ };
617
+ }
618
+ __name(useDurableExecution, "useDurableExecution");
619
+ function clearDurableExecutionWrappers() {
620
+ wrappers.length = 0;
621
+ }
622
+ __name(clearDurableExecutionWrappers, "clearDurableExecutionWrappers");
623
+ function hasDurableExecutionWrappers() {
624
+ return wrappers.length > 0;
625
+ }
626
+ __name(hasDurableExecutionWrappers, "hasDurableExecutionWrappers");
627
+ async function runDurableExecution(execution, body) {
628
+ if (wrappers.length === 0) return body();
629
+ let outcome;
630
+ const inner = /* @__PURE__ */ __name(async () => {
631
+ try {
632
+ const value = await body();
633
+ outcome = {
634
+ ok: true,
635
+ value
636
+ };
637
+ return value;
638
+ } catch (error2) {
639
+ outcome = {
640
+ ok: false,
641
+ error: error2
642
+ };
643
+ throw error2;
644
+ }
645
+ }, "inner");
646
+ const chain = wrappers.reduceRight((next, wrapper) => () => wrapper(execution, next), inner);
647
+ try {
648
+ await chain();
649
+ } catch (error2) {
650
+ if (outcome === void 0) throw error2;
651
+ }
652
+ if (outcome === void 0) {
653
+ throw new Error("a durable execution wrapper returned without calling next() \u2014 the unit of work never ran");
654
+ }
655
+ if (outcome.ok) return outcome.value;
656
+ throw outcome.error;
657
+ }
658
+ __name(runDurableExecution, "runDurableExecution");
659
+
602
660
  // src/step-logger.ts
603
661
  function withDuration(start, now, data) {
604
662
  if (data && typeof data === "object" && "durationMs" in data) {
@@ -740,7 +798,14 @@ async function runStepHandler(task, handler) {
740
798
  } : result, "withEvents");
741
799
  try {
742
800
  const logger = createStepLogger(events, Date.now);
743
- const output = await runInStepLogger(logger, () => handler(task.input, logger));
801
+ const output = await runDurableExecution({
802
+ unit: "step",
803
+ runId: task.runId,
804
+ workflow: void 0,
805
+ name: task.name,
806
+ seq: task.seq,
807
+ attempt: task.attempt
808
+ }, async () => runInStepLogger(logger, () => handler(task.input, logger)));
744
809
  return withEvents({
745
810
  ...base,
746
811
  status: "completed",
@@ -1041,6 +1106,9 @@ function normalizeDescriptor(raw) {
1041
1106
  ],
1042
1107
  workflows: raw.workflows ?? [],
1043
1108
  steps: raw.steps ?? [],
1109
+ ...raw.registrations !== void 0 ? {
1110
+ registrations: raw.registrations
1111
+ } : {},
1044
1112
  ...raw.partition !== void 0 ? {
1045
1113
  partition: raw.partition
1046
1114
  } : {},
@@ -1051,6 +1119,23 @@ function normalizeDescriptor(raw) {
1051
1119
  };
1052
1120
  }
1053
1121
  __name(normalizeDescriptor, "normalizeDescriptor");
1122
+ function canonicalizeRegistrations(regs) {
1123
+ const canonical = regs.map((r) => ({
1124
+ name: r.name,
1125
+ version: r.version ?? null,
1126
+ group: r.group ?? null,
1127
+ requires: r.requires ? [
1128
+ ...new Set(r.requires)
1129
+ ].sort() : null,
1130
+ origin: r.origin ?? null
1131
+ }));
1132
+ const byJson = /* @__PURE__ */ new Map();
1133
+ for (const entry of canonical) byJson.set(JSON.stringify(entry), entry);
1134
+ return [
1135
+ ...byJson.keys()
1136
+ ].sort().map((k) => byJson.get(k));
1137
+ }
1138
+ __name(canonicalizeRegistrations, "canonicalizeRegistrations");
1054
1139
  function canonicalizeForHash(d) {
1055
1140
  const set = /* @__PURE__ */ __name((xs) => [
1056
1141
  ...new Set(xs)
@@ -1072,6 +1157,9 @@ function canonicalizeForHash(d) {
1072
1157
  capabilities: set(d.capabilities),
1073
1158
  workflows: set(d.workflows),
1074
1159
  steps: set(d.steps),
1160
+ ...d.registrations !== void 0 ? {
1161
+ registrations: canonicalizeRegistrations(d.registrations)
1162
+ } : {},
1075
1163
  partition: d.partition ?? null,
1076
1164
  namespace: d.namespace ?? null,
1077
1165
  startedAt: d.startedAt
@@ -1104,6 +1192,144 @@ function heartbeatStatus(descriptor, opts = {}) {
1104
1192
  };
1105
1193
  }
1106
1194
  __name(heartbeatStatus, "heartbeatStatus");
1195
+ function describeWorker(opts) {
1196
+ const version = CURRENT_PROTOCOL_VERSION;
1197
+ const registrations = [
1198
+ ...opts.registrations ?? []
1199
+ ].sort((a, b) => `${a.name}@${a.version ?? ""}`.localeCompare(`${b.name}@${b.version ?? ""}`));
1200
+ return {
1201
+ instanceId: opts.instanceId,
1202
+ runtime: opts.runtime,
1203
+ sdk: {
1204
+ name: opts.sdk.name,
1205
+ version: opts.sdk.version
1206
+ },
1207
+ protocol: {
1208
+ version,
1209
+ range: [
1210
+ version,
1211
+ version
1212
+ ]
1213
+ },
1214
+ capabilities: [
1215
+ ...LEGACY_V1_CAPABILITIES
1216
+ ],
1217
+ workflows: [
1218
+ ...opts.workflows ?? []
1219
+ ].sort(),
1220
+ steps: [
1221
+ ...opts.steps
1222
+ ].sort(),
1223
+ ...registrations.length > 0 ? {
1224
+ registrations
1225
+ } : {},
1226
+ ...opts.partition ? {
1227
+ partition: opts.partition
1228
+ } : {},
1229
+ ...opts.namespace && opts.namespace !== "default" ? {
1230
+ namespace: opts.namespace
1231
+ } : {},
1232
+ startedAt: opts.startedAt
1233
+ };
1234
+ }
1235
+ __name(describeWorker, "describeWorker");
1236
+
1237
+ // src/handshake/announced.ts
1238
+ function announcementsOf(raw) {
1239
+ const d = normalizeDescriptor(raw);
1240
+ const identity = {
1241
+ instanceId: d.instanceId,
1242
+ runtime: d.runtime
1243
+ };
1244
+ const announcements = (d.registrations ?? []).map((reg) => ({
1245
+ ...reg,
1246
+ ...identity
1247
+ }));
1248
+ const covered = new Set(announcements.map((a) => a.name));
1249
+ for (const name of d.workflows) {
1250
+ if (!covered.has(name)) announcements.push({
1251
+ name,
1252
+ ...identity
1253
+ });
1254
+ }
1255
+ return announcements;
1256
+ }
1257
+ __name(announcementsOf, "announcementsOf");
1258
+ function announcementKey(a) {
1259
+ return a.version === void 0 ? a.name : `${a.name}@${a.version}`;
1260
+ }
1261
+ __name(announcementKey, "announcementKey");
1262
+ function stated(values) {
1263
+ return [
1264
+ ...new Set(values.filter((v) => v !== void 0))
1265
+ ].sort();
1266
+ }
1267
+ __name(stated, "stated");
1268
+ function aggregateAnnouncements(descriptors) {
1269
+ const byKey = /* @__PURE__ */ new Map();
1270
+ for (const descriptor of descriptors) {
1271
+ for (const announcement of announcementsOf(descriptor)) {
1272
+ const key = announcementKey(announcement);
1273
+ const bucket = byKey.get(key);
1274
+ if (bucket) bucket.push(announcement);
1275
+ else byKey.set(key, [
1276
+ announcement
1277
+ ]);
1278
+ }
1279
+ }
1280
+ const out = [];
1281
+ for (const [key, announcements] of byKey) {
1282
+ const groups = stated(announcements.map((a) => a.group));
1283
+ const origins = stated(announcements.map((a) => a.origin));
1284
+ const requiresByJson = /* @__PURE__ */ new Map();
1285
+ for (const a of announcements) {
1286
+ if (a.requires === void 0) continue;
1287
+ const canonical = [
1288
+ ...new Set(a.requires)
1289
+ ].sort();
1290
+ requiresByJson.set(JSON.stringify(canonical), canonical);
1291
+ }
1292
+ const requires = [
1293
+ ...requiresByJson.values()
1294
+ ].sort((x, y) => x.join(",").localeCompare(y.join(",")));
1295
+ const disagreements = [];
1296
+ if (groups.length > 1) disagreements.push({
1297
+ axis: "group",
1298
+ values: groups
1299
+ });
1300
+ if (origins.length > 1) disagreements.push({
1301
+ axis: "origin",
1302
+ values: origins
1303
+ });
1304
+ if (requires.length > 1) {
1305
+ disagreements.push({
1306
+ axis: "requires",
1307
+ values: requires.map((r) => r.join(","))
1308
+ });
1309
+ }
1310
+ const first = announcements[0];
1311
+ if (!first) continue;
1312
+ out.push({
1313
+ key,
1314
+ name: first.name,
1315
+ ...first.version !== void 0 ? {
1316
+ version: first.version
1317
+ } : {},
1318
+ groups,
1319
+ origins,
1320
+ requires,
1321
+ runtimes: [
1322
+ ...new Set(announcements.map((a) => a.runtime))
1323
+ ].sort(),
1324
+ instances: [
1325
+ ...new Set(announcements.map((a) => a.instanceId))
1326
+ ].sort(),
1327
+ disagreements
1328
+ });
1329
+ }
1330
+ return out.sort((a, b) => a.key.localeCompare(b.key));
1331
+ }
1332
+ __name(aggregateAnnouncements, "aggregateAnnouncements");
1107
1333
 
1108
1334
  // src/handshake/negotiate.ts
1109
1335
  var rangesOverlap = /* @__PURE__ */ __name((a, b) => Math.max(a[0], b[0]) <= Math.min(a[1], b[1]), "rangesOverlap");
@@ -1631,6 +1857,30 @@ var TransportPool = class {
1631
1857
  }
1632
1858
  return descriptors;
1633
1859
  }
1860
+ /**
1861
+ * Every live descriptor in the deployment, merged across every transport that can report them and
1862
+ * de-duplicated by `instanceId` — one worker publishes the same descriptor under each token it
1863
+ * consumes, and it must count once. Returns `[]` when no transport implements
1864
+ * {@link Transport.readAllWorkerDescriptors} (a pure in-process pool advertises nothing).
1865
+ *
1866
+ * First writer wins on a duplicate id: the copies are the same bytes by construction (one process,
1867
+ * one descriptor, published N times), so there is nothing to choose between. Two DIFFERENT
1868
+ * processes sharing an `instanceId` would collapse into one here — that is a misconfiguration the
1869
+ * fleet already suffers from (they also share heartbeat and descriptor keys, so they overwrite each
1870
+ * other's advertisement), not something this read can repair.
1871
+ */
1872
+ async readAllWorkerDescriptors() {
1873
+ const byInstance = /* @__PURE__ */ new Map();
1874
+ for (const { transport } of this.transports) {
1875
+ if (!transport.readAllWorkerDescriptors) continue;
1876
+ for (const descriptor of await transport.readAllWorkerDescriptors()) {
1877
+ if (!byInstance.has(descriptor.instanceId)) byInstance.set(descriptor.instanceId, descriptor);
1878
+ }
1879
+ }
1880
+ return [
1881
+ ...byInstance.values()
1882
+ ];
1883
+ }
1634
1884
  /** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
1635
1885
  async listWorkerGroups() {
1636
1886
  const groups = /* @__PURE__ */ new Set();
@@ -2031,7 +2281,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
2031
2281
  return unwrapCompletion(await waitForSignal(`task:${runId}:${name}`), `task "${name}"`);
2032
2282
  }, "task");
2033
2283
  const child = /* @__PURE__ */ __name(async (workflow2, input, options) => {
2034
- const { childId, priority } = normalizeChildOptions(options);
2284
+ const { childId, priority, version } = normalizeChildOptions(options);
2035
2285
  const current = pos.next();
2036
2286
  const id = childId ?? `${runId}.child.${current}`;
2037
2287
  const existing = await readCheckpoint(current);
@@ -2059,7 +2309,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
2059
2309
  }));
2060
2310
  return unwrapCompletion(buffered.payload, `child "${id}"`);
2061
2311
  }
2062
- if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
2312
+ if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
2063
2313
  if (!existing) {
2064
2314
  await writeCheckpoint(instantCheckpoint({
2065
2315
  runId,
@@ -2142,12 +2392,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
2142
2392
  return outputs;
2143
2393
  }, "all");
2144
2394
  const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
2145
- const { childId, priority } = normalizeChildOptions(options);
2395
+ const { childId, priority, version } = normalizeChildOptions(options);
2146
2396
  const current = pos.next();
2147
2397
  const id = childId ?? `${runId}.child.${current}`;
2148
2398
  const existing = await readCheckpoint(current);
2149
2399
  if (existing && existing.status === "completed") return existing.output;
2150
- if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
2400
+ if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
2151
2401
  await writeCheckpoint(instantCheckpoint({
2152
2402
  runId,
2153
2403
  seq: current,
@@ -2707,6 +2957,15 @@ var WorkflowEngine = class {
2707
2957
  const reg = this.workflows.get(versionKey(name, version));
2708
2958
  return reg?.hasBody ? reg.fn : void 0;
2709
2959
  }
2960
+ /** The registered versions of `name`, for the pinned-start error — "2 is not registered" is a
2961
+ * guess-the-typo error without them, and the answer (which versions this deploy actually carries)
2962
+ * is right here. `none` when the name is unknown entirely. */
2963
+ registeredVersions(name) {
2964
+ const versions = [
2965
+ ...this.workflows.values()
2966
+ ].filter((w) => w.name === name).map((w) => w.version);
2967
+ return versions.length ? versions.join(", ") : "none";
2968
+ }
2710
2969
  /**
2711
2970
  * Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
2712
2971
  * as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
@@ -2856,12 +3115,13 @@ var WorkflowEngine = class {
2856
3115
  }
2857
3116
  async start(workflow, input, runId, opts) {
2858
3117
  const name = workflowName(workflow);
2859
- let registered = this.latest.get(name);
2860
- if (!registered) {
3118
+ const pinned = opts?.version;
3119
+ let registered = pinned === void 0 ? this.latest.get(name) : this.workflows.get(versionKey(name, pinned));
3120
+ if (!registered && pinned === void 0) {
2861
3121
  const ancestor = await this.findRemoteAncestor(runId);
2862
3122
  if (ancestor) registered = this.synthesizeRemoteChild(name, ancestor.version, ancestor);
2863
3123
  }
2864
- if (!registered) {
3124
+ if (!registered && pinned === void 0) {
2865
3125
  const conventionRun = {
2866
3126
  id: runId,
2867
3127
  workflow: name,
@@ -2878,7 +3138,9 @@ var WorkflowEngine = class {
2878
3138
  };
2879
3139
  registered = await this.resolveRemoteByConvention(conventionRun);
2880
3140
  }
2881
- if (!registered) throw new Error(`workflow ${name} is not registered`);
3141
+ if (!registered) {
3142
+ 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)})`);
3143
+ }
2882
3144
  await registered.validateInput?.(input);
2883
3145
  const prior = await this.store.getRun(runId);
2884
3146
  if (prior) {
@@ -3085,7 +3347,32 @@ var WorkflowEngine = class {
3085
3347
  /**
3086
3348
  * Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
3087
3349
  * timer poller alongside {@link resumeDueTimers}. A timed-out run is moved to `cancelled` with an
3088
- * `execution_timeout` error (terminal, so a late step result can't resurrect it).
3350
+ * `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
3351
+ * are cascaded exactly as an explicit {@link cancel} cascades them.
3352
+ *
3353
+ * WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
3354
+ * the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
3355
+ * a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
3356
+ * is a SCAN — it walks every in-flight run of every timed workflow on every poller tick, so it must
3357
+ * not pay `cancel`'s per-run re-read and singleton/control-plane work on runs it isn't cancelling.
3358
+ * The cascade below is charged only to the runs that ACTUALLY timed out, which are rare by
3359
+ * construction; the scan itself is unchanged.
3360
+ *
3361
+ * The subtree, though, has no reason to differ, and before this it did: a timed-out parent's
3362
+ * children kept running with the parent gone and nothing pointing at them — a silent orphan you
3363
+ * could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
3364
+ * walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
3365
+ * `cancel` returns early on a run that is already `completed`/`cancelled`/`dead` (a child that beat
3366
+ * the sweep to the finish is never clobbered, and a re-cancel is a no-op), and it writes the
3367
+ * parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
3368
+ * second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
3369
+ * the same terminal state twice and cascade into already-cancelled children — idempotent, not
3370
+ * corrupting. The one window it does NOT close is the pre-existing check-then-write race an
3371
+ * explicit `cancel` has always had (a child that completes between `cancel`'s read and its write);
3372
+ * closing that needs a compare-and-set in the store contract, not a change here.
3373
+ *
3374
+ * Cascade failures are swallowed per subtree for the same reason `cancelChildren` swallows them per
3375
+ * child: one unreachable child must not abort the sweep and leave every LATER timed-out run alive.
3089
3376
  */
3090
3377
  async sweepTimeouts(now = this.clock()) {
3091
3378
  for (const reg of new Set(this.latest.values())) {
@@ -3121,6 +3408,7 @@ var WorkflowEngine = class {
3121
3408
  namespace: run.namespace,
3122
3409
  error: error2
3123
3410
  });
3411
+ await this.cancelChildren(run.id).catch(() => void 0);
3124
3412
  }
3125
3413
  }
3126
3414
  }
@@ -3849,6 +4137,10 @@ var WorkflowEngine = class {
3849
4137
  * `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
3850
4138
  * checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
3851
4139
  * at finished / already-cancelled runs (no loops, no re-cancel).
4140
+ *
4141
+ * Two callers, deliberately the same walk: an explicit {@link cancel}, and {@link sweepTimeouts}
4142
+ * when a parent hits its `executionTimeout` — a timed-out parent's subtree dies the same way an
4143
+ * explicitly cancelled one's does, or the children are orphans nobody is holding a reference to.
3852
4144
  */
3853
4145
  async cancelChildren(parentRunId, opts) {
3854
4146
  for (const id of await this.getRunChildren(parentRunId)) {
@@ -3928,6 +4220,29 @@ var WorkflowEngine = class {
3928
4220
  return out;
3929
4221
  }
3930
4222
  /**
4223
+ * What workflows the LIVE fleet announces it can execute — the deployment-wide registry a picker
4224
+ * ("call this existing workflow") needs, and the one question this engine's own registry cannot
4225
+ * answer. {@link workflowBody} answers only for this process, and a missing body is ambiguous: it
4226
+ * means "not registered here", but equally "registered via {@link registerRemote} against another
4227
+ * SDK" or "a group resolved by convention against a worker". An inferred answer would therefore
4228
+ * differ per replica. This one does not: every pod folds the same published announcements.
4229
+ *
4230
+ * Scoped like every other poll surface — an engine WITH a `namespace` sees only workers in it, and
4231
+ * an operator (namespace unset) sees the whole deployment ("ver tudo = ausência de namespace"). A
4232
+ * descriptor that states no namespace is `default`, matching how a single-tenant fleet publishes.
4233
+ *
4234
+ * Cost: one advertisement scan per CALL, on demand. Nothing is added to the poll loop and nothing
4235
+ * is retained — no pod holds a table of the fleet's registrations. Empty when no transport can
4236
+ * introspect the advertisement keyspace (a pure in-process transport advertises nothing).
4237
+ *
4238
+ * Workflows only, and deliberately: see the "steps are out of scope" note in `handshake/announced`.
4239
+ */
4240
+ async announcedWorkflows() {
4241
+ const descriptors = await this.pool.readAllWorkerDescriptors();
4242
+ const mine = this.namespace === void 0 ? descriptors : descriptors.filter((d) => (d.namespace ?? "default") === this.namespace);
4243
+ return aggregateAnnouncements(mine);
4244
+ }
4245
+ /**
3931
4246
  * The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
3932
4247
  * dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
3933
4248
  * that is NEITHER a registration NOR heartbeating (the exact shape of a run stuck on an offline
@@ -4642,8 +4957,31 @@ var WorkflowEngine = class {
4642
4957
  error: error2
4643
4958
  };
4644
4959
  }
4645
- /** The run body, lease held + renewed by {@link execute}. */
4646
- async runExecution(run, fn, registered) {
4960
+ /**
4961
+ * The run body, lease held + renewed by {@link execute}, wrapped in the registered
4962
+ * {@link runDurableExecution} wrappers so an observability scope covers ONE WHOLE TURN.
4963
+ *
4964
+ * The wrap goes here — around the entire turn — rather than tightly around the `fn(ctx, input)`
4965
+ * call, because the store writes that bracket the body (the `pending` → `running` transition, the
4966
+ * step checkpoints, the terminal `settleRun` update) are exactly the queries an operator wants
4967
+ * correlated to the turn that issued them. Narrowing the wrap to the body would leave them
4968
+ * uncorrelated for the sake of a purity that buys nothing.
4969
+ *
4970
+ * Only the TS path is wrapped. {@link runRemoteExecution} does not execute a body at all — it
4971
+ * dispatches a workflow task and awaits the worker's decision — so a scope opened there would
4972
+ * describe a dispatch, not an execution, and would mislead anyone reading the trace.
4973
+ */
4974
+ runExecution(run, fn, registered) {
4975
+ return runDurableExecution({
4976
+ unit: "workflow",
4977
+ runId: run.id,
4978
+ workflow: run.workflow,
4979
+ name: void 0,
4980
+ seq: void 0,
4981
+ attempt: void 0
4982
+ }, () => this.runExecutionTurn(run, fn, registered));
4983
+ }
4984
+ async runExecutionTurn(run, fn, registered) {
4647
4985
  if (run.status === "cancelling") this.cancelRequested.add(run.id);
4648
4986
  if (run.status === "pending") {
4649
4987
  await this.store.updateRun(run.id, {
@@ -4983,10 +5321,13 @@ var WorkflowEngine = class {
4983
5321
  // namespace) is the one executing the parent. Otherwise an operator recovery-resuming a
4984
5322
  // `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
4985
5323
  // tenant's worker pool onto the shared/dev workers.
4986
- startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
5324
+ startChild: /* @__PURE__ */ __name((workflow, input, id, priority, version) => {
4987
5325
  this.startChildDeferred(workflow, input, id, {
4988
5326
  priority,
4989
- namespace: parentNamespace
5327
+ namespace: parentNamespace,
5328
+ // Absent unless the body pinned one (`ctx.child(ref, input, { version })`) — an omitted
5329
+ // pin must stay omitted, so the child keeps resolving `latest` exactly as before.
5330
+ version
4990
5331
  });
4991
5332
  }, "startChild"),
4992
5333
  // Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
@@ -6395,6 +6736,8 @@ var InMemoryTransport = class {
6395
6736
  WorkflowBlocked,
6396
6737
  WorkflowEngine,
6397
6738
  WorkflowSuspended,
6739
+ aggregateAnnouncements,
6740
+ announcementsOf,
6398
6741
  applyAttributeQuery,
6399
6742
  attributeColumnFor,
6400
6743
  attributeOperand,
@@ -6403,12 +6746,14 @@ var InMemoryTransport = class {
6403
6746
  canRoute,
6404
6747
  capableWorkers,
6405
6748
  classifyWaiterToken,
6749
+ clearDurableExecutionWrappers,
6406
6750
  collectMetrics,
6407
6751
  controlPlaneDescriptor,
6408
6752
  createStepLogger,
6409
6753
  currentStep,
6410
6754
  currentWorkflowCtx,
6411
6755
  debug,
6756
+ describeWorker,
6412
6757
  descriptorHash,
6413
6758
  error,
6414
6759
  eventMatchOf,
@@ -6416,6 +6761,7 @@ var InMemoryTransport = class {
6416
6761
  eventNameOf,
6417
6762
  eventPrefix,
6418
6763
  eventToken,
6764
+ hasDurableExecutionWrappers,
6419
6765
  heartbeatStatus,
6420
6766
  indexWaitersByRun,
6421
6767
  info,
@@ -6435,6 +6781,7 @@ var InMemoryTransport = class {
6435
6781
  requiredCapabilities,
6436
6782
  resolveRouting,
6437
6783
  resolveRunWaiting,
6784
+ runDurableExecution,
6438
6785
  runInStepLogger,
6439
6786
  runInWorkflowCtx,
6440
6787
  runSchedules,
@@ -6449,6 +6796,7 @@ var InMemoryTransport = class {
6449
6796
  subEvent,
6450
6797
  subProcess,
6451
6798
  tenantGroup,
6799
+ useDurableExecution,
6452
6800
  warn,
6453
6801
  workflowName
6454
6802
  });