@dudousxd/nestjs-durable-core 0.81.0 → 0.82.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
@@ -2816,6 +2816,66 @@ var SingletonGate = class {
2816
2816
  }
2817
2817
  };
2818
2818
 
2819
+ // src/stalled-runs.ts
2820
+ var DEFAULT_STALLED_AFTER_MS = 15 * 6e4;
2821
+ async function findStalledRuns(store, options = {}) {
2822
+ const now = options.now ?? Date.now();
2823
+ const thresholdMs = options.thresholdMs ?? DEFAULT_STALLED_AFTER_MS;
2824
+ const scan = Math.max(1, options.scan ?? 1e3);
2825
+ const pageSize = Math.min(200, scan);
2826
+ const stalled = [];
2827
+ for (let offset = 0; offset < scan; offset += pageSize) {
2828
+ const limit = Math.min(pageSize, scan - offset);
2829
+ const page = await store.listRuns({
2830
+ statuses: [
2831
+ "running",
2832
+ "suspended"
2833
+ ],
2834
+ ...options.namespace !== void 0 ? {
2835
+ namespace: options.namespace
2836
+ } : {},
2837
+ limit,
2838
+ offset
2839
+ });
2840
+ for (const run of page) {
2841
+ const ageMs = now - run.updatedAt.getTime();
2842
+ if (ageMs < thresholdMs) continue;
2843
+ const info2 = await stalledInfo(store, run, now, thresholdMs, ageMs);
2844
+ if (info2) stalled.push(info2);
2845
+ }
2846
+ if (page.length < limit) break;
2847
+ }
2848
+ return stalled;
2849
+ }
2850
+ __name(findStalledRuns, "findStalledRuns");
2851
+ async function stalledInfo(store, run, now, thresholdMs, ageMs) {
2852
+ let stalePending = null;
2853
+ for (const cp of await store.listCheckpoints(run.id)) {
2854
+ if (cp.kind !== "remote" || cp.status !== "pending") continue;
2855
+ const stepAgeMs = now - cp.enqueuedAt.getTime();
2856
+ if (stepAgeMs < thresholdMs) continue;
2857
+ if (cp.wakeAt != null && cp.wakeAt > now) continue;
2858
+ if (!stalePending || stepAgeMs > stalePending.ageMs) {
2859
+ stalePending = {
2860
+ seq: cp.seq,
2861
+ name: cp.name,
2862
+ attempts: cp.attempts,
2863
+ ageMs: stepAgeMs,
2864
+ leaseUntil: cp.wakeAt ?? null
2865
+ };
2866
+ }
2867
+ }
2868
+ const wakeForever = run.status === "suspended" && run.wakeAt == null;
2869
+ if (!wakeForever && !stalePending) return null;
2870
+ return {
2871
+ run,
2872
+ ageMs,
2873
+ wakeForever,
2874
+ stalePending
2875
+ };
2876
+ }
2877
+ __name(stalledInfo, "stalledInfo");
2878
+
2819
2879
  // src/transport-pool.ts
2820
2880
  var TransportPool = class {
2821
2881
  static {
@@ -4173,6 +4233,18 @@ var WorkflowEngine = class {
4173
4233
  stepQueue = /* @__PURE__ */ new Map();
4174
4234
  /** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
4175
4235
  inflight = /* @__PURE__ */ new Set();
4236
+ /**
4237
+ * The run lease of each execution in flight on THIS instance, by run id — renewed while it runs
4238
+ * and checked by every settle (the zombie fence, see {@link ExecutionLease}). Also what `drain()`
4239
+ * releases when its budget runs out.
4240
+ */
4241
+ executionLeases = /* @__PURE__ */ new Map();
4242
+ /** Pager hooks for stranded runs (see {@link onStalled}). */
4243
+ stalledListeners = /* @__PURE__ */ new Set();
4244
+ /** Runs already paged about, by id → the `updatedAt` of the paged episode (see {@link sweepStalled}). */
4245
+ notifiedStalled = /* @__PURE__ */ new Map();
4246
+ lastStalledSweepAt = Number.NEGATIVE_INFINITY;
4247
+ stalledAfterMs;
4176
4248
  draining = false;
4177
4249
  /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
4178
4250
  drainSpentMs = 0;
@@ -4197,6 +4269,8 @@ var WorkflowEngine = class {
4197
4269
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
4198
4270
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
4199
4271
  this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
4272
+ const stalledAfter = deps.stalledAfterMs;
4273
+ this.stalledAfterMs = stalledAfter !== void 0 && Number.isFinite(stalledAfter) ? Math.max(6e4, stalledAfter) : DEFAULT_STALLED_AFTER_MS;
4200
4274
  const batch = deps.timerBatchSize;
4201
4275
  this.timerBatchSize = batch !== void 0 && Number.isFinite(batch) && batch >= 1 ? Math.floor(batch) : 500;
4202
4276
  this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
@@ -4540,6 +4614,64 @@ var WorkflowEngine = class {
4540
4614
  this.deadListeners.add(listener);
4541
4615
  return () => this.deadListeners.delete(listener);
4542
4616
  }
4617
+ /**
4618
+ * Register a pager hook for STRANDED runs: an in-flight run untouched past `stalledAfterMs` that is
4619
+ * either suspended with NO wake timer (wake-forever: nothing will ever re-drive it) or parked on an
4620
+ * old pending remote step with no live lease (dispatched and, most likely, lost). The timer poller
4621
+ * sweeps for these ({@link sweepStalled}, throttled to one pass a minute) and notifies each episode
4622
+ * ONCE per instance: a run is paged again only after it makes progress and strands again. Wire your
4623
+ * alerting here like {@link onDead}. A listener that throws never breaks the sweep. Returns an
4624
+ * unsubscribe function.
4625
+ */
4626
+ onStalled(listener) {
4627
+ this.stalledListeners.add(listener);
4628
+ return () => this.stalledListeners.delete(listener);
4629
+ }
4630
+ /**
4631
+ * Detect stranded in-flight runs and notify the {@link onStalled} listeners. Self-throttled to one
4632
+ * pass a minute, skipped while draining, and a no-op without listeners — call it every tick (the
4633
+ * NestJS timer poller does). Scoped to this engine's `namespace` (an operator engine with no
4634
+ * namespace sweeps every namespace). Returns the runs notified THIS pass.
4635
+ */
4636
+ async sweepStalled(now = this.clock()) {
4637
+ if (this.stalledListeners.size === 0 || this.draining) return [];
4638
+ if (now - this.lastStalledSweepAt < 6e4) return [];
4639
+ this.lastStalledSweepAt = now;
4640
+ const notified = [];
4641
+ for (const info2 of await this.listStalledRuns({
4642
+ now
4643
+ })) {
4644
+ const episode = info2.run.updatedAt.getTime();
4645
+ if (this.notifiedStalled.get(info2.run.id) === episode) continue;
4646
+ this.notifiedStalled.set(info2.run.id, episode);
4647
+ if (this.notifiedStalled.size > 1e3) this.notifiedStalled.clear();
4648
+ for (const listener of this.stalledListeners) {
4649
+ try {
4650
+ listener(info2);
4651
+ } catch {
4652
+ }
4653
+ }
4654
+ notified.push(info2);
4655
+ }
4656
+ return notified;
4657
+ }
4658
+ /**
4659
+ * The stranded runs right now, without notifying anyone: the query behind {@link sweepStalled} and
4660
+ * the CLI's `durable inspect --stale`. A `running`/`suspended` run qualifies when it has been
4661
+ * untouched for `thresholdMs` (default `stalledAfterMs`) AND either it is suspended with no wake
4662
+ * timer, or its oldest pending remote step is older than the threshold with no lease in the future
4663
+ * (a step with a `timeoutMs`, a heartbeat or `remoteRedispatchMs` keeps a lease; a long
4664
+ * `ctx.sleep` has a future `wakeAt`; neither is stranded). Scans up to `scan` candidates (default
4665
+ * 1000), newest first, in this engine's namespace.
4666
+ */
4667
+ async listStalledRuns(options = {}) {
4668
+ return findStalledRuns(this.store, {
4669
+ now: options.now ?? this.clock(),
4670
+ thresholdMs: options.thresholdMs ?? this.stalledAfterMs,
4671
+ scan: options.scan,
4672
+ namespace: this.namespace
4673
+ });
4674
+ }
4543
4675
  notifyDead(run) {
4544
4676
  for (const fn of this.deadListeners) {
4545
4677
  try {
@@ -4698,6 +4830,11 @@ var WorkflowEngine = class {
4698
4830
  getRun(runId) {
4699
4831
  return this.store.getRun(runId);
4700
4832
  }
4833
+ /** A run's full checkpoint timeline, ordered by seq. A thin pass-through to the store (it is what
4834
+ * the testing kit's `captureHistory` reads, next to {@link getRun}). */
4835
+ listCheckpoints(runId) {
4836
+ return this.store.listCheckpoints(runId);
4837
+ }
4701
4838
  /**
4702
4839
  * Re-execute a run's body (replaying its checkpoints). This is the EXPLICIT entry point: besides
4703
4840
  * driving a live run, it re-drives a `failed` run — the manual retry (prefer {@link requeue}, which
@@ -4783,6 +4920,8 @@ var WorkflowEngine = class {
4783
4920
  * diagnosis.
4784
4921
  */
4785
4922
  async releaseLeaseBestEffort(runId) {
4923
+ if (this.isFenced(runId)) return;
4924
+ this.handOffLease(runId);
4786
4925
  try {
4787
4926
  await this.store.releaseRunLock(runId);
4788
4927
  } catch (err) {
@@ -5032,17 +5171,47 @@ var WorkflowEngine = class {
5032
5171
  rearm();
5033
5172
  });
5034
5173
  this.drainRearms.add(rearm);
5174
+ let settled = false;
5035
5175
  try {
5036
5176
  await Promise.race([
5037
5177
  Promise.allSettled([
5038
5178
  ...this.inflight
5039
- ]),
5179
+ ]).then(() => {
5180
+ settled = true;
5181
+ }),
5040
5182
  timer
5041
5183
  ]);
5042
5184
  } finally {
5043
5185
  this.drainRearms.delete(rearm);
5044
5186
  clearTimeout(timeout);
5045
5187
  }
5188
+ if (!settled) await this.releaseInflightLeases();
5189
+ }
5190
+ /**
5191
+ * The drain budget ran out with executions still in flight: release their recovery leases so
5192
+ * another instance re-drives those runs on its next recovery pass, instead of waiting out
5193
+ * `leaseMs` after this process exits. Each still-running turn is FENCED first, so whatever it
5194
+ * computes after this point is never written over the new owner's work (see settleRun). Its
5195
+ * completed steps are checkpointed, so the new owner replays them; a step still executing here
5196
+ * may run again there, exactly as after a crash. The release is owner-scoped where the store
5197
+ * supports it (only a lease this instance still holds is cleared). Best effort: never throws.
5198
+ */
5199
+ async releaseInflightLeases() {
5200
+ const released = [];
5201
+ for (const [runId, lease] of [
5202
+ ...this.executionLeases
5203
+ ]) {
5204
+ if (lease.lost || lease.handedOff || lease.done) continue;
5205
+ lease.lost = true;
5206
+ try {
5207
+ await this.store.releaseRunLock(runId, this.instanceId);
5208
+ released.push(runId);
5209
+ } catch {
5210
+ }
5211
+ }
5212
+ if (released.length > 0) {
5213
+ console.warn(`[nestjs-durable] drain timed out with ${released.length} run(s) still executing; released their leases so another instance recovers them now: ${released.join(", ")}`);
5214
+ }
5046
5215
  }
5047
5216
  /**
5048
5217
  * Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
@@ -6532,7 +6701,10 @@ var WorkflowEngine = class {
6532
6701
  }
6533
6702
  async singletonWaitResult(run) {
6534
6703
  const current = await this.store.getRun(run.id);
6535
- await this.store.releaseRunLock(run.id);
6704
+ if (!this.isFenced(run.id)) {
6705
+ this.handOffLease(run.id);
6706
+ await this.store.releaseRunLock(run.id);
6707
+ }
6536
6708
  if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
6537
6709
  if (current.status === "suspended") {
6538
6710
  this.emit({
@@ -6560,10 +6732,7 @@ var WorkflowEngine = class {
6560
6732
  };
6561
6733
  }
6562
6734
  }
6563
- const renew = setInterval(() => {
6564
- void this.store.renewRunLock(run.id, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
6565
- }, Math.max(50, Math.floor(this.leaseMs / 2)));
6566
- renew.unref?.();
6735
+ const held = this.holdExecutionLease(run.id);
6567
6736
  try {
6568
6737
  const result = registered?.remote ? await this.runRemoteExecution(run, registered) : await this.runExecution(run, fn, registered);
6569
6738
  if (registered?.singleton && (result.status === "completed" || result.status === "failed" || result.status === "cancelled" || result.status === "dead")) {
@@ -6571,10 +6740,59 @@ var WorkflowEngine = class {
6571
6740
  }
6572
6741
  return result;
6573
6742
  } finally {
6574
- clearInterval(renew);
6743
+ held.stop();
6575
6744
  }
6576
6745
  }
6577
6746
  /**
6747
+ * Renew `runId`'s lease for as long as this execution holds it, and register it in
6748
+ * {@link executionLeases} so settles can check the zombie fence and `drain()` can release it.
6749
+ *
6750
+ * A renewal that returns `false` means the lease is gone: this turn stalled past `leaseMs` and
6751
+ * another instance reclaimed the run, so this executor is now a zombie. It is FENCED (`lost`): its
6752
+ * settles degrade to an echo of the store's state instead of clobbering the new owner's writes, and
6753
+ * its own lease release is skipped (it would free the new owner's lease). A renewal that THROWS (a
6754
+ * transient store error) is not evidence of a takeover, so it stays swallowed and the next
6755
+ * interval retries. A lease this execution released on purpose (`handedOff`) is not renewed, and a
6756
+ * renewal resolving after the execution ended (`done`) fences nothing.
6757
+ */
6758
+ holdExecutionLease(runId) {
6759
+ const lease = {
6760
+ lost: false,
6761
+ handedOff: false,
6762
+ done: false
6763
+ };
6764
+ this.executionLeases.set(runId, lease);
6765
+ const renew = setInterval(() => {
6766
+ if (lease.handedOff || lease.lost || lease.done) return;
6767
+ void this.store.renewRunLock(runId, this.instanceId, this.clock() + this.leaseMs).then((renewed) => {
6768
+ if (renewed || lease.handedOff || lease.lost || lease.done) return;
6769
+ lease.lost = true;
6770
+ console.warn(`[nestjs-durable] lost the lease of run ${runId} while executing it (another instance took it over after it expired); this instance will not write the run's outcome`);
6771
+ }).catch(() => void 0);
6772
+ }, Math.max(50, Math.floor(this.leaseMs / 2)));
6773
+ renew.unref?.();
6774
+ return {
6775
+ lease,
6776
+ stop: /* @__PURE__ */ __name(() => {
6777
+ clearInterval(renew);
6778
+ lease.done = true;
6779
+ if (this.executionLeases.get(runId) === lease) this.executionLeases.delete(runId);
6780
+ }, "stop")
6781
+ };
6782
+ }
6783
+ /** Whether `runId`'s execution on this instance lost its lease (see {@link holdExecutionLease}). */
6784
+ isFenced(runId) {
6785
+ return this.executionLeases.get(runId)?.lost === true;
6786
+ }
6787
+ /**
6788
+ * Mark `runId`'s lease as given away on purpose by its own execution (released mid-turn), so a
6789
+ * renewal that now fails is not read as a takeover.
6790
+ */
6791
+ handOffLease(runId) {
6792
+ const lease = this.executionLeases.get(runId);
6793
+ if (lease) lease.handedOff = true;
6794
+ }
6795
+ /**
6578
6796
  * Advance a remote (cross-SDK) workflow one turn: hand its history to the executor (which dispatches
6579
6797
  * a workflow task to the worker and awaits its replay) and apply the decision. Mirrors
6580
6798
  * {@link runExecution}'s settle/suspend; the lease is held by {@link execute}. The result that lands
@@ -6791,6 +7009,7 @@ ${run.workflowVersion}`;
6791
7009
  * keeps its own lease handling (the TS executor's `finally`, the remote path's caller).
6792
7010
  */
6793
7011
  async settleRun(run, outcome) {
7012
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6794
7013
  const updatedAt = /* @__PURE__ */ new Date();
6795
7014
  if (outcome.kind === "completed") {
6796
7015
  const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
@@ -6871,6 +7090,7 @@ ${run.workflowVersion}`;
6871
7090
  }
6872
7091
  async runRemoteExecution(run, registered) {
6873
7092
  if (run.status === "cancelling") {
7093
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6874
7094
  const error2 = {
6875
7095
  message: "cancelled"
6876
7096
  };
@@ -6879,6 +7099,7 @@ ${run.workflowVersion}`;
6879
7099
  error: error2,
6880
7100
  updatedAt: /* @__PURE__ */ new Date()
6881
7101
  });
7102
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6882
7103
  this.emit({
6883
7104
  type: "run.failed",
6884
7105
  runId: run.id,
@@ -6925,6 +7146,7 @@ ${run.workflowVersion}`;
6925
7146
  }
6926
7147
  const history = await this.remoteHistory(run.id, checkpoints);
6927
7148
  if (remote.executor.dispatch) {
7149
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6928
7150
  const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
6929
7151
  const silenceMs = this.remoteAdvanceSilenceMs;
6930
7152
  const wakeAt = silenceMs != null ? this.clock() + silenceMs : void 0;
@@ -6940,6 +7162,7 @@ ${run.workflowVersion}`;
6940
7162
  workflow: run.workflow,
6941
7163
  namespace: run.namespace
6942
7164
  });
7165
+ this.handOffLease(run.id);
6943
7166
  await this.store.releaseRunLock(run.id);
6944
7167
  await remote.executor.dispatch(run, history, taskId);
6945
7168
  return {
@@ -6968,6 +7191,7 @@ ${run.workflowVersion}`;
6968
7191
  message: err instanceof Error ? err.message : String(err),
6969
7192
  stack: err instanceof Error ? err.stack : void 0
6970
7193
  };
7194
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6971
7195
  await this.store.updateRun(run.id, {
6972
7196
  status: "failed",
6973
7197
  error: error2,
@@ -6996,6 +7220,7 @@ ${run.workflowVersion}`;
6996
7220
  * — the caller has already ensured this is the decision for the currently-awaited turn.
6997
7221
  */
6998
7222
  async applyDecision(run, decision) {
7223
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6999
7224
  if (decision.status === "completed") {
7000
7225
  await this.applyCommands(run, decision.commands);
7001
7226
  return this.settleRun(run, {
@@ -7465,6 +7690,7 @@ ${run.workflowVersion}`;
7465
7690
  * cancel flag stays too, re-derived from the status on any later re-drive regardless.
7466
7691
  */
7467
7692
  async compensateAndCancel(run, compensations, replay) {
7693
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
7468
7694
  const pending = await this.unwindCompensations(run, compensations, replay);
7469
7695
  if (pending) return {
7470
7696
  runId: run.id,
@@ -7556,6 +7782,7 @@ ${run.workflowVersion}`;
7556
7782
  });
7557
7783
  } catch (err) {
7558
7784
  if (err instanceof ContinueAsNew) {
7785
+ if (this.isFenced(run.id)) return await this.echoCurrentStatus(run.id);
7559
7786
  if (this.cancelRequested.has(run.id)) {
7560
7787
  return await this.compensateAndCancel(run, compensations, replay);
7561
7788
  }
@@ -8007,6 +8234,7 @@ ${run.workflowVersion}`;
8007
8234
  * own `finally` also releases (the run-execution path) is safe.
8008
8235
  */
8009
8236
  async parkBlocked(run, blocked) {
8237
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
8010
8238
  const error2 = {
8011
8239
  message: blocked.reason,
8012
8240
  code: blocked.code,
@@ -8593,10 +8821,7 @@ ${run.workflowVersion}`;
8593
8821
  if (!await this.store.tryLockRun(decision.runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
8594
8822
  return;
8595
8823
  }
8596
- const renew = setInterval(() => {
8597
- void this.store.renewRunLock(decision.runId, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
8598
- }, Math.max(50, Math.floor(this.leaseMs / 2)));
8599
- renew.unref?.();
8824
+ const held = this.holdExecutionLease(decision.runId);
8600
8825
  try {
8601
8826
  const fresh = await this.store.getRun(decision.runId);
8602
8827
  if (!fresh || fresh.awaitingDecisionTaskId !== decision.taskId) return;
@@ -8611,8 +8836,9 @@ ${run.workflowVersion}`;
8611
8836
  fresh.awaitingDecisionTaskId = void 0;
8612
8837
  await this.applyDecision(fresh, decision);
8613
8838
  } finally {
8614
- clearInterval(renew);
8615
- await this.store.releaseRunLock(decision.runId);
8839
+ const fenced = held.lease.lost;
8840
+ held.stop();
8841
+ if (!fenced) await this.store.releaseRunLock(decision.runId);
8616
8842
  }
8617
8843
  }
8618
8844
  /** Earliest clock time a suspended turn's deadline is worth LOOKING at again (`runId` → clock ms) —
@@ -8808,6 +9034,9 @@ function childIdsOf(checkpoints) {
8808
9034
  }
8809
9035
  __name(childIdsOf, "childIdsOf");
8810
9036
 
9037
+ // src/run-history.ts
9038
+ var RUN_HISTORY_FIXTURE_VERSION = 1;
9039
+
8811
9040
  // src/run-facets.ts
8812
9041
  function facetOrigin(origin) {
8813
9042
  return origin?.trim() ? origin : null;
@@ -9361,13 +9590,14 @@ var CodecStateStore = class {
9361
9590
  }
9362
9591
  inner;
9363
9592
  codec;
9593
+ extended;
9364
9594
  /** Atomic claims touch only searchable metadata and bypass payload encoding. */
9365
9595
  tryAdmitSingleton;
9366
9596
  /**
9367
9597
  * Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
9368
- * (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
9369
- * actually aggregates, so `store.runFacets?.()` still reports absence through the wrapper instead
9370
- * of resolving to a silently wrong empty result.
9598
+ * (it encodes payloads: input/output, plus events/error under `extended` coverage). Bound as a
9599
+ * PROPERTY, and only when the inner store actually aggregates, so `store.runFacets?.()` still
9600
+ * reports absence through the wrapper instead of resolving to a silently wrong empty result.
9371
9601
  */
9372
9602
  runFacets;
9373
9603
  /**
@@ -9419,9 +9649,10 @@ var CodecStateStore = class {
9419
9649
  updateSchedule;
9420
9650
  deleteSchedule;
9421
9651
  listSchedules;
9422
- constructor(inner, codec) {
9652
+ constructor(inner, codec, options = {}) {
9423
9653
  this.inner = inner;
9424
9654
  this.codec = codec;
9655
+ this.extended = options.coverage === "extended";
9425
9656
  if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
9426
9657
  const deleteRuns = inner.deleteRuns;
9427
9658
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
@@ -9479,32 +9710,52 @@ var CodecStateStore = class {
9479
9710
  return v === void 0 ? void 0 : this.codec.decode(v);
9480
9711
  }
9481
9712
  encRun(run) {
9482
- return {
9713
+ const next = {
9483
9714
  ...run,
9484
9715
  input: this.enc(run.input),
9485
9716
  output: this.enc(run.output)
9486
9717
  };
9718
+ if (this.extended && run.error !== void 0) {
9719
+ next.error = this.enc(run.error);
9720
+ }
9721
+ return next;
9487
9722
  }
9488
9723
  decRun(run) {
9489
- return run && {
9724
+ if (!run) return run;
9725
+ const next = {
9490
9726
  ...run,
9491
9727
  input: this.dec(run.input),
9492
9728
  output: this.dec(run.output)
9493
9729
  };
9730
+ if (this.extended && run.error !== void 0) {
9731
+ next.error = this.dec(run.error);
9732
+ }
9733
+ return next;
9494
9734
  }
9495
9735
  encCp(cp) {
9496
- return {
9736
+ const next = {
9497
9737
  ...cp,
9498
9738
  input: this.enc(cp.input),
9499
9739
  output: this.enc(cp.output)
9500
9740
  };
9741
+ if (this.extended) {
9742
+ if (cp.events !== void 0) next.events = this.enc(cp.events);
9743
+ if (cp.error !== void 0) next.error = this.enc(cp.error);
9744
+ }
9745
+ return next;
9501
9746
  }
9502
9747
  decCp(cp) {
9503
- return cp && {
9748
+ if (!cp) return cp;
9749
+ const next = {
9504
9750
  ...cp,
9505
9751
  input: this.dec(cp.input),
9506
9752
  output: this.dec(cp.output)
9507
9753
  };
9754
+ if (this.extended) {
9755
+ if (cp.events !== void 0) next.events = this.dec(cp.events);
9756
+ if (cp.error !== void 0) next.error = this.dec(cp.error);
9757
+ }
9758
+ return next;
9508
9759
  }
9509
9760
  encSpec(spec) {
9510
9761
  return "input" in spec ? {
@@ -9543,6 +9794,9 @@ var CodecStateStore = class {
9543
9794
  };
9544
9795
  if ("input" in patch) next.input = this.enc(patch.input);
9545
9796
  if ("output" in patch) next.output = this.enc(patch.output);
9797
+ if (this.extended && patch.error !== void 0) {
9798
+ next.error = this.enc(patch.error);
9799
+ }
9546
9800
  return next;
9547
9801
  }
9548
9802
  async getRun(runId) {
@@ -9578,8 +9832,8 @@ var CodecStateStore = class {
9578
9832
  tryLockRun(runId, owner, leaseUntilMs, nowMs) {
9579
9833
  return this.inner.tryLockRun(runId, owner, leaseUntilMs, nowMs);
9580
9834
  }
9581
- releaseRunLock(runId) {
9582
- return this.inner.releaseRunLock(runId);
9835
+ releaseRunLock(runId, owner) {
9836
+ return owner === void 0 ? this.inner.releaseRunLock(runId) : this.inner.releaseRunLock(runId, owner);
9583
9837
  }
9584
9838
  renewRunLock(runId, owner, leaseUntilMs) {
9585
9839
  return this.inner.renewRunLock(runId, owner, leaseUntilMs);
@@ -10087,9 +10341,9 @@ var InMemoryStateStore = class {
10087
10341
  run.lockedUntil = leaseUntilMs;
10088
10342
  return true;
10089
10343
  }
10090
- async releaseRunLock(runId) {
10344
+ async releaseRunLock(runId, owner) {
10091
10345
  const run = this.runs.get(runId);
10092
- if (run) {
10346
+ if (run && (owner === void 0 || run.lockedBy === owner)) {
10093
10347
  run.lockedBy = void 0;
10094
10348
  run.lockedUntil = void 0;
10095
10349
  }
@@ -10439,6 +10693,7 @@ export {
10439
10693
  ContinueAsNew,
10440
10694
  DEFAULT_CONCURRENCY_STATUSES,
10441
10695
  DEFAULT_PARTITION,
10696
+ DEFAULT_STALLED_AFTER_MS,
10442
10697
  DURABLE_OPTIONS,
10443
10698
  DURABLE_OPTIONS_CANONICAL,
10444
10699
  DURABLE_STEP_CONFIG,
@@ -10462,6 +10717,7 @@ export {
10462
10717
  NestedWorkflowCallError,
10463
10718
  NonDeterminismError,
10464
10719
  QueueController,
10720
+ RUN_HISTORY_FIXTURE_VERSION,
10465
10721
  RUN_VALUE_FACET_LIMIT,
10466
10722
  RUN_VALUE_FACET_SCAN,
10467
10723
  RemoteStepError,
@@ -10535,6 +10791,7 @@ export {
10535
10791
  facetOrigin,
10536
10792
  fanPageBounds,
10537
10793
  fanPageEtag,
10794
+ findStalledRuns,
10538
10795
  foldedCount,
10539
10796
  handoffHandlerId,
10540
10797
  handoffName,