@dudousxd/nestjs-durable-core 0.80.1 → 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
@@ -928,6 +928,9 @@ async function assertConcurrency(store, workflow, quota) {
928
928
  __name(assertConcurrency, "assertConcurrency");
929
929
 
930
930
  // src/entities.ts
931
+ var NUDGE = {
932
+ nudge: true
933
+ };
931
934
  var ENTITY_WORKFLOW = "__entity";
932
935
  var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
933
936
  var Entities = class {
@@ -935,9 +938,11 @@ var Entities = class {
935
938
  __name(this, "Entities");
936
939
  }
937
940
  host;
941
+ workflowHost;
938
942
  configs = /* @__PURE__ */ new Map();
939
- constructor(host) {
943
+ constructor(host, workflowHost) {
940
944
  this.host = host;
945
+ this.workflowHost = workflowHost;
941
946
  host.register(ENTITY_WORKFLOW, "1", (ctx, input) => this.run(ctx, input));
942
947
  }
943
948
  register(name, config) {
@@ -959,6 +964,52 @@ var Entities = class {
959
964
  }
960
965
  });
961
966
  }
967
+ /**
968
+ * The durable half of a send from INSIDE a workflow (`ctx.callEntity` / `ctx.signalEntity`): write
969
+ * the op to the entity's signal buffer and return once it is stored, so the caller can checkpoint
970
+ * after it. The op carries `id`, the caller's stable `<runId>:<seq>`, so if the caller dies between
971
+ * this write and its checkpoint and sends again on replay, the entity skips the second copy. The
972
+ * entity's run is also ensured here, awaited (`start` only enqueues, it never runs the body
973
+ * inline), so an op sent on first contact can't be stranded in a buffer no run will ever read if
974
+ * the process dies right after. The wake-up ({@link kick}) is separate and may be lost: a busy
975
+ * entity reads the buffer at its next wait, and a parked one is paired with it by the engine's
976
+ * `reconcileMs` sweep. Returns `false` on a host without a workflow host.
977
+ */
978
+ async enqueue(name, key, op, arg, id, reply) {
979
+ if (!this.workflowHost) return false;
980
+ const message = {
981
+ op,
982
+ arg,
983
+ id,
984
+ ...reply ? {
985
+ reply
986
+ } : {}
987
+ };
988
+ const runId = entityRunId(name, key);
989
+ await this.workflowHost.bufferSignal(runId, message);
990
+ await this.workflowHost.ensureStarted(ENTITY_WORKFLOW, {
991
+ name,
992
+ key
993
+ }, runId);
994
+ return true;
995
+ }
996
+ /**
997
+ * Make the entity act on what {@link enqueue} buffered: wake its run if it is parked on its wait
998
+ * (the run itself was ensured by `enqueue`; ensured again here, idempotently, for a caller replay
999
+ * that only re-kicks). A run that is busy needs nothing: its next wait reads the buffer before
1000
+ * suspending. The wake carries a no-op `nudge`, never the op itself, so the op stays in
1001
+ * the buffer in arrival order and a lost or repeated kick costs at most one empty iteration.
1002
+ */
1003
+ async kick(name, key) {
1004
+ const host = this.workflowHost;
1005
+ if (!host) return;
1006
+ const runId = entityRunId(name, key);
1007
+ await host.ensureStarted(ENTITY_WORKFLOW, {
1008
+ name,
1009
+ key
1010
+ }, runId);
1011
+ await host.wakeWaiter(runId, NUDGE);
1012
+ }
962
1013
  async signal(name, key, op, arg) {
963
1014
  await this.dispatch(name, key, op, arg);
964
1015
  }
@@ -972,11 +1023,18 @@ var Entities = class {
972
1023
  if (!config) throw new FatalError(`entity "${name}" is not registered`);
973
1024
  const token = entityRunId(name, key);
974
1025
  let state = config.initialState();
1026
+ const seen = /* @__PURE__ */ new Set();
975
1027
  for (let i = 0; ; i += 1) {
976
1028
  const msg = await ctx.waitForSignal(token);
1029
+ if (msg.nudge) continue;
1030
+ if (msg.id !== void 0) {
1031
+ if (seen.has(msg.id)) continue;
1032
+ seen.add(msg.id);
1033
+ }
1034
+ const op = msg.op;
977
1035
  const out = await ctx.localStep(`op:${i}`, async () => {
978
- const handler = config.handlers[msg.op];
979
- if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${msg.op}"`);
1036
+ const handler = config.handlers[op];
1037
+ if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${op}"`);
980
1038
  const result = await handler(state, msg.arg);
981
1039
  return {
982
1040
  result,
@@ -986,8 +1044,9 @@ var Entities = class {
986
1044
  state = out.state;
987
1045
  await ctx.setEvent("state", state);
988
1046
  if (msg.reply) {
1047
+ const reply = msg.reply;
989
1048
  await ctx.localStep(`reply:${i}`, async () => {
990
- await this.host.signal(msg.reply, out.result);
1049
+ await this.host.signal(reply, out.result);
991
1050
  });
992
1051
  }
993
1052
  }
@@ -1911,6 +1970,10 @@ function checkpointNameKind(name) {
1911
1970
  return "step";
1912
1971
  }
1913
1972
  __name(checkpointNameKind, "checkpointNameKind");
1973
+ function poolNamespace(namespace) {
1974
+ return namespace === "" ? "default" : namespace;
1975
+ }
1976
+ __name(poolNamespace, "poolNamespace");
1914
1977
  var TERMINAL_RUN_STATUSES = [
1915
1978
  "completed",
1916
1979
  "failed",
@@ -2042,6 +2105,19 @@ var WEBHOOK_PREFIX = "wh:";
2042
2105
  var CHILD_PREFIX = "child:";
2043
2106
  var BREAKPOINT_PREFIX = "bp:";
2044
2107
  var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
2108
+ function parseWebhookToken(token) {
2109
+ if (!token.startsWith(WEBHOOK_PREFIX)) return null;
2110
+ const rest = token.slice(WEBHOOK_PREFIX.length);
2111
+ const at = rest.lastIndexOf(":");
2112
+ if (at <= 0) return null;
2113
+ const seqText = rest.slice(at + 1);
2114
+ if (!/^\d+$/.test(seqText)) return null;
2115
+ return {
2116
+ runId: rest.slice(0, at),
2117
+ seq: Number(seqText)
2118
+ };
2119
+ }
2120
+ __name(parseWebhookToken, "parseWebhookToken");
2045
2121
  function classifyWaiterToken(token) {
2046
2122
  if (token.startsWith(WEBHOOK_PREFIX)) return {
2047
2123
  on: "webhook",
@@ -2579,6 +2655,11 @@ __name(errorMessage, "errorMessage");
2579
2655
 
2580
2656
  // src/singleton-admission.ts
2581
2657
  var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
2658
+ var SINGLETON_TRACKED_TAG = "durable:singleton:tracked";
2659
+ var SINGLETON_RESERVED_TAGS = [
2660
+ SINGLETON_ADMITTED_TAG,
2661
+ SINGLETON_TRACKED_TAG
2662
+ ];
2582
2663
  var SINGLETON_ACTIVE_STATUSES = [
2583
2664
  "pending",
2584
2665
  "running",
@@ -2586,6 +2667,33 @@ var SINGLETON_ACTIVE_STATUSES = [
2586
2667
  "blocked",
2587
2668
  "cancelling"
2588
2669
  ];
2670
+ var LEGACY_SCAN_STATUSES = [
2671
+ "running",
2672
+ "suspended",
2673
+ "cancelling"
2674
+ ];
2675
+ var byAdmissionOrder = /* @__PURE__ */ __name((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id), "byAdmissionOrder");
2676
+ function withoutSingletonMarkers(tags) {
2677
+ return tags?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
2678
+ }
2679
+ __name(withoutSingletonMarkers, "withoutSingletonMarkers");
2680
+ function withSingletonTracked(tags) {
2681
+ const current = tags ?? [];
2682
+ if (current.includes(SINGLETON_ADMITTED_TAG) || current.includes(SINGLETON_TRACKED_TAG)) {
2683
+ return current;
2684
+ }
2685
+ return [
2686
+ ...current,
2687
+ SINGLETON_TRACKED_TAG
2688
+ ];
2689
+ }
2690
+ __name(withSingletonTracked, "withSingletonTracked");
2691
+ function legacySingletonHolders(active, runId, limit) {
2692
+ const scanned = active.filter((run) => LEGACY_SCAN_STATUSES.includes(run.status)).sort(byAdmissionOrder);
2693
+ const window = new Set(scanned.slice(0, limit).map((run) => run.id));
2694
+ return scanned.filter((run) => run.id !== runId && !run.tags?.includes(SINGLETON_ADMITTED_TAG) && (!run.tags?.includes(SINGLETON_TRACKED_TAG) || window.has(run.id)));
2695
+ }
2696
+ __name(legacySingletonHolders, "legacySingletonHolders");
2589
2697
  function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2590
2698
  if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
2591
2699
  const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
@@ -2594,12 +2702,13 @@ function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2594
2702
  if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
2595
2703
  if (candidate.status === "cancelling") return null;
2596
2704
  const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
2597
- const remaining = limit - holders.length;
2705
+ const legacy = new Set(legacySingletonHolders(active, runId, limit).map((run) => run.id));
2706
+ const remaining = limit - holders.length - legacy.size;
2598
2707
  if (remaining <= 0) return null;
2599
- const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
2708
+ const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG) && !legacy.has(run.id)).sort(byAdmissionOrder);
2600
2709
  if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
2601
2710
  return [
2602
- ...candidate.tags ?? [],
2711
+ ...(candidate.tags ?? []).filter((t) => t !== SINGLETON_TRACKED_TAG),
2603
2712
  SINGLETON_ADMITTED_TAG
2604
2713
  ];
2605
2714
  }
@@ -2609,7 +2718,7 @@ async function clearTerminalSingletonAdmission(runId, patch, getRun) {
2609
2718
  const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
2610
2719
  return {
2611
2720
  ...patch,
2612
- tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
2721
+ tags: withoutSingletonMarkers(tags)
2613
2722
  };
2614
2723
  }
2615
2724
  __name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
@@ -2620,6 +2729,7 @@ function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt)
2620
2729
  return {
2621
2730
  status: "suspended",
2622
2731
  wakeAt: retryWakeAt,
2732
+ tags: withSingletonTracked(candidate.tags),
2623
2733
  updatedAt: /* @__PURE__ */ new Date()
2624
2734
  };
2625
2735
  }
@@ -2672,7 +2782,7 @@ var SingletonGate = class {
2672
2782
  const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
2673
2783
  if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
2674
2784
  run.tags = [
2675
- ...run.tags ?? [],
2785
+ ...(run.tags ?? []).filter((tag) => tag !== SINGLETON_TRACKED_TAG),
2676
2786
  SINGLETON_ADMITTED_TAG
2677
2787
  ];
2678
2788
  }
@@ -2706,6 +2816,66 @@ var SingletonGate = class {
2706
2816
  }
2707
2817
  };
2708
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
+
2709
2879
  // src/transport-pool.ts
2710
2880
  var TransportPool = class {
2711
2881
  static {
@@ -3567,30 +3737,82 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3567
3737
  }));
3568
3738
  return id;
3569
3739
  }, "startChild");
3740
+ const sendEntityOp = /* @__PURE__ */ __name(async (name, key, op, arg, seq, reply) => await host.enqueueEntityOp?.(name, key, op, arg, `${runId}:${seq}`, reply) ?? false, "sendEntityOp");
3570
3741
  const callEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
3571
3742
  const current = pos.next();
3572
- const existing = await readCheckpoint(current);
3743
+ let existing = await readCheckpoint(current);
3573
3744
  if (existing && existing.status === "completed") return existing.output;
3574
3745
  const reply = `entityreply:${runId}:${current}`;
3746
+ const intentName = `signal:${reply}`;
3747
+ if (existing && existing.name !== intentName) {
3748
+ throw new NonDeterminismError(runId, current, intentName, existing.name);
3749
+ }
3750
+ if (existing && replay?.has(current)) {
3751
+ existing = await store.getCheckpoint(runId, current) ?? existing;
3752
+ if (existing.status === "completed") return existing.output;
3753
+ }
3754
+ const markSent = /* @__PURE__ */ __name(() => store.saveCheckpointIfPending?.(instantCheckpoint({
3755
+ runId,
3756
+ seq: current,
3757
+ name: intentName,
3758
+ kind: "signal",
3759
+ status: "pending",
3760
+ output: {
3761
+ sent: true
3762
+ }
3763
+ })) ?? Promise.resolve(false), "markSent");
3764
+ if (existing) {
3765
+ const buffered = await consumeBuffered(reply, current);
3766
+ if (buffered) {
3767
+ await store.removeSignalWaiter({
3768
+ token: reply,
3769
+ runId,
3770
+ seq: current
3771
+ });
3772
+ return buffered.value;
3773
+ }
3774
+ if (existing.output?.sent !== true) {
3775
+ if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
3776
+ }
3777
+ host.kickEntity?.(name, key);
3778
+ throw new WorkflowSuspended();
3779
+ }
3575
3780
  await store.putSignalWaiter({
3576
3781
  token: reply,
3577
3782
  runId,
3578
3783
  seq: current
3579
3784
  });
3580
- host.signalEntity?.(name, key, op, arg, reply);
3785
+ if (!host.enqueueEntityOp) {
3786
+ host.signalEntity?.(name, key, op, arg, reply);
3787
+ throw new WorkflowSuspended();
3788
+ }
3789
+ await writeCheckpoint(instantCheckpoint({
3790
+ runId,
3791
+ seq: current,
3792
+ name: intentName,
3793
+ kind: "signal",
3794
+ status: "pending",
3795
+ output: {
3796
+ sent: false
3797
+ }
3798
+ }));
3799
+ if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
3800
+ host.kickEntity?.(name, key);
3581
3801
  throw new WorkflowSuspended();
3582
3802
  }, "callEntity");
3583
3803
  const signalEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
3584
3804
  const current = pos.next();
3585
3805
  const existing = await readCheckpoint(current);
3586
3806
  if (existing && existing.status === "completed") return;
3587
- host.signalEntity?.(name, key, op, arg);
3807
+ const durable = await sendEntityOp(name, key, op, arg, current);
3808
+ if (!durable) host.signalEntity?.(name, key, op, arg);
3588
3809
  await writeCheckpoint(instantCheckpoint({
3589
3810
  runId,
3590
3811
  seq: current,
3591
3812
  name: `entitysig:${name}:${key}`,
3592
3813
  kind: "local"
3593
3814
  }));
3815
+ if (durable) host.kickEntity?.(name, key);
3594
3816
  }, "signalEntity");
3595
3817
  const breakpoint = /* @__PURE__ */ __name(async (label) => {
3596
3818
  const current = pos.next();
@@ -3658,17 +3880,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3658
3880
  const deadlineSeq = timeoutMs == null ? -1 : pos.next();
3659
3881
  const existing = await readCheckpoint(current);
3660
3882
  if (existing && existing.status === "completed") return existing.output;
3661
- if (timeoutMs == null) {
3662
- await store.putSignalWaiter({
3883
+ const buffered = await consumeBuffered(token, current);
3884
+ if (buffered) return buffered.value;
3885
+ const deadline = timeoutMs == null ? void 0 : await stampDeadline(deadlineSeq, token, timeoutMs);
3886
+ if (deadline !== void 0 && host.clock() >= deadline) {
3887
+ await store.removeSignalWaiter({
3663
3888
  token,
3664
3889
  runId,
3665
3890
  seq: current
3666
- });
3667
- throw new WorkflowSuspended();
3668
- }
3669
- const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
3670
- if (host.clock() >= deadline) {
3671
- await store.takeSignalWaiter(token).catch(() => void 0);
3891
+ }).catch(() => void 0);
3672
3892
  throw new SignalTimeoutError(token, timeoutMs);
3673
3893
  }
3674
3894
  await store.putSignalWaiter({
@@ -3676,6 +3896,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3676
3896
  runId,
3677
3897
  seq: current
3678
3898
  });
3899
+ const lateBuffered = await consumeBuffered(token, current);
3900
+ if (lateBuffered) {
3901
+ await store.removeSignalWaiter({
3902
+ token,
3903
+ runId,
3904
+ seq: current
3905
+ });
3906
+ return lateBuffered.value;
3907
+ }
3679
3908
  throw new WorkflowSuspended(deadline);
3680
3909
  }, "wait");
3681
3910
  return {
@@ -3867,6 +4096,12 @@ var SETTLE_ELIGIBLE_STATUSES = [
3867
4096
  "failed",
3868
4097
  "cancelling"
3869
4098
  ];
4099
+ var TIMEOUT_SWEPT_STATUSES = [
4100
+ "pending",
4101
+ "running",
4102
+ "suspended",
4103
+ "blocked"
4104
+ ];
3870
4105
  var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
3871
4106
  var RECOVERY_BATCH = 500;
3872
4107
  var NamespaceMismatch = class NamespaceMismatch2 extends Error {
@@ -3889,6 +4124,12 @@ var WorkflowEngine = class {
3889
4124
  instanceId;
3890
4125
  namespace;
3891
4126
  leaseMs;
4127
+ /**
4128
+ * Leases held by this `instanceId` that expire strictly before this epoch ms were taken by a
4129
+ * PREVIOUS process with the same id (every lease this one takes or renews expires at
4130
+ * `clock() + leaseMs`, so at or after it). See {@link orphanedRuns}.
4131
+ */
4132
+ ownLeaseCutoffMs;
3892
4133
  maxRecoveryAttempts;
3893
4134
  remoteAdvanceSilenceMs;
3894
4135
  reconcileMs;
@@ -3992,6 +4233,18 @@ var WorkflowEngine = class {
3992
4233
  stepQueue = /* @__PURE__ */ new Map();
3993
4234
  /** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
3994
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;
3995
4248
  draining = false;
3996
4249
  /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
3997
4250
  drainSpentMs = 0;
@@ -4010,11 +4263,14 @@ var WorkflowEngine = class {
4010
4263
  this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
4011
4264
  this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
4012
4265
  this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
4013
- this.namespace = deps.namespace;
4266
+ this.namespace = poolNamespace(deps.namespace);
4014
4267
  this.leaseMs = deps.leaseMs ?? 3e4;
4268
+ this.ownLeaseCutoffMs = this.clock() + this.leaseMs;
4015
4269
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
4016
4270
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
4017
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;
4018
4274
  const batch = deps.timerBatchSize;
4019
4275
  this.timerBatchSize = batch !== void 0 && Number.isFinite(batch) && batch >= 1 ? Math.floor(batch) : 500;
4020
4276
  this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
@@ -4131,7 +4387,11 @@ var WorkflowEngine = class {
4131
4387
  }
4132
4388
  });
4133
4389
  this.accumulators = new EventAccumulators(this);
4134
- this.entities = new Entities(this);
4390
+ this.entities = new Entities(this, {
4391
+ bufferSignal: /* @__PURE__ */ __name((token, payload) => this.store.bufferSignal(token, payload), "bufferSignal"),
4392
+ wakeWaiter: /* @__PURE__ */ __name((token, payload) => this.wakeSignalWaiter(token, payload), "wakeWaiter"),
4393
+ ensureStarted: /* @__PURE__ */ __name((workflow, input, runId) => this.start(workflow, input, runId), "ensureStarted")
4394
+ });
4135
4395
  }
4136
4396
  /**
4137
4397
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4354,6 +4614,64 @@ var WorkflowEngine = class {
4354
4614
  this.deadListeners.add(listener);
4355
4615
  return () => this.deadListeners.delete(listener);
4356
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
+ }
4357
4675
  notifyDead(run) {
4358
4676
  for (const fn of this.deadListeners) {
4359
4677
  try {
@@ -4407,7 +4725,7 @@ var WorkflowEngine = class {
4407
4725
  // tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
4408
4726
  // `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
4409
4727
  // not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
4410
- namespace: opts?.namespace ?? this.namespace,
4728
+ namespace: poolNamespace(opts?.namespace) ?? this.namespace,
4411
4729
  input,
4412
4730
  createdAt: /* @__PURE__ */ new Date(),
4413
4731
  updatedAt: /* @__PURE__ */ new Date()
@@ -4444,7 +4762,7 @@ var WorkflowEngine = class {
4444
4762
  VERSION_UNDECLARED_TAG
4445
4763
  ] : []
4446
4764
  ];
4447
- const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
4765
+ const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
4448
4766
  if (registered.singleton) {
4449
4767
  await this.singletons.assertCapacity(name, registered.singleton, input);
4450
4768
  }
@@ -4458,7 +4776,7 @@ var WorkflowEngine = class {
4458
4776
  ...delayed ? {
4459
4777
  wakeAt: startAtMs
4460
4778
  } : {},
4461
- namespace: opts?.namespace ?? this.namespace,
4779
+ namespace: poolNamespace(opts?.namespace) ?? this.namespace,
4462
4780
  // DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
4463
4781
  // deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
4464
4782
  // `undefined` when the registration carried none (remote/convention/synthesized child, or a
@@ -4512,6 +4830,11 @@ var WorkflowEngine = class {
4512
4830
  getRun(runId) {
4513
4831
  return this.store.getRun(runId);
4514
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
+ }
4515
4838
  /**
4516
4839
  * Re-execute a run's body (replaying its checkpoints). This is the EXPLICIT entry point: besides
4517
4840
  * driving a live run, it re-drives a `failed` run — the manual retry (prefer {@link requeue}, which
@@ -4597,6 +4920,8 @@ var WorkflowEngine = class {
4597
4920
  * diagnosis.
4598
4921
  */
4599
4922
  async releaseLeaseBestEffort(runId) {
4923
+ if (this.isFenced(runId)) return;
4924
+ this.handOffLease(runId);
4600
4925
  try {
4601
4926
  await this.store.releaseRunLock(runId);
4602
4927
  } catch (err) {
@@ -4640,12 +4965,12 @@ var WorkflowEngine = class {
4640
4965
  error: run.error
4641
4966
  };
4642
4967
  }
4643
- if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
4968
+ if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
4644
4969
  await this.store.releaseRunLock(runId);
4645
4970
  throw new NamespaceMismatch();
4646
4971
  }
4647
- if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
4648
- run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
4972
+ if (run.status === "failed" && (run.tags?.includes(SINGLETON_ADMITTED_TAG) || run.tags?.includes(SINGLETON_TRACKED_TAG))) {
4973
+ run.tags = withoutSingletonMarkers(run.tags);
4649
4974
  await this.store.updateRun(run.id, {
4650
4975
  tags: run.tags
4651
4976
  });
@@ -4846,17 +5171,47 @@ var WorkflowEngine = class {
4846
5171
  rearm();
4847
5172
  });
4848
5173
  this.drainRearms.add(rearm);
5174
+ let settled = false;
4849
5175
  try {
4850
5176
  await Promise.race([
4851
5177
  Promise.allSettled([
4852
5178
  ...this.inflight
4853
- ]),
5179
+ ]).then(() => {
5180
+ settled = true;
5181
+ }),
4854
5182
  timer
4855
5183
  ]);
4856
5184
  } finally {
4857
5185
  this.drainRearms.delete(rearm);
4858
5186
  clearTimeout(timeout);
4859
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
+ }
4860
5215
  }
4861
5216
  /**
4862
5217
  * Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
@@ -4864,6 +5219,15 @@ var WorkflowEngine = class {
4864
5219
  * `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
4865
5220
  * are cascaded exactly as an explicit {@link cancel} cascades them.
4866
5221
  *
5222
+ * WHICH RUNS. Every run not yet finished, counted from `createdAt`: `pending` and `blocked` as well
5223
+ * as `running` and `suspended`. A run nobody picks up (no worker on its queue) or nobody can run (no
5224
+ * worker with its capabilities) is as stuck as one that hangs mid-body, and the cap is the backstop
5225
+ * for exactly that — leaving them out meant a run that never started lived forever while one that
5226
+ * started and hung was cut off. Cancelling one a worker is picking up at that moment is safe: the
5227
+ * worker's settle only writes over a live status ({@link SETTLE_ELIGIBLE_STATUSES}), so it can't
5228
+ * bring the run back. `cancelling` is left alone on purpose — a compensating cancel's saga undo is
5229
+ * running, it ends `cancelled` by itself, and cutting it short would skip the undo.
5230
+ *
4867
5231
  * WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
4868
5232
  * the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
4869
5233
  * a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
@@ -4876,8 +5240,8 @@ var WorkflowEngine = class {
4876
5240
  * children kept running with the parent gone and nothing pointing at them — a silent orphan you
4877
5241
  * could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
4878
5242
  * walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
4879
- * `cancel` returns early on a run that is already `completed`/`cancelled`/`dead` (a child that beat
4880
- * the sweep to the finish is never clobbered, and a re-cancel is a no-op), and it writes the
5243
+ * `cancel` returns early on a run that is already terminal (a child that beat the sweep to the
5244
+ * finish is never clobbered, and a re-cancel is a no-op), and it writes the
4881
5245
  * parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
4882
5246
  * second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
4883
5247
  * the same terminal state twice and cascade into already-cancelled children — idempotent, not
@@ -4893,18 +5257,11 @@ var WorkflowEngine = class {
4893
5257
  if (this.draining) return;
4894
5258
  if (reg.executionTimeoutMs == null) continue;
4895
5259
  const deadline = now - reg.executionTimeoutMs;
4896
- const inflight = [
4897
- ...await this.store.listRuns({
4898
- workflow: reg.name,
4899
- status: "running",
4900
- namespace: this.namespace
4901
- }),
4902
- ...await this.store.listRuns({
4903
- workflow: reg.name,
4904
- status: "suspended",
4905
- namespace: this.namespace
4906
- })
4907
- ];
5260
+ const inflight = await this.store.listRuns({
5261
+ workflow: reg.name,
5262
+ statuses: TIMEOUT_SWEPT_STATUSES,
5263
+ namespace: this.namespace
5264
+ });
4908
5265
  for (const run of inflight) {
4909
5266
  if (this.draining) return;
4910
5267
  if (run.createdAt.getTime() > deadline) continue;
@@ -4937,48 +5294,103 @@ var WorkflowEngine = class {
4937
5294
  const results = [];
4938
5295
  for (const listed of await this.orphanedRuns(nowMs)) {
4939
5296
  if (this.draining) break;
4940
- const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4941
- if (!acquired) continue;
4942
- const run = await this.store.getRun(listed.id);
4943
- if (!run || run.status !== "running" && run.status !== "cancelling") {
4944
- await this.store.releaseRunLock(listed.id);
4945
- continue;
4946
- }
4947
- if (run.status === "running") {
4948
- const dispatched = await this.inFlightRemoteStep(run.id);
4949
- if (dispatched) {
4950
- await this.store.updateRun(run.id, {
4951
- status: "suspended",
4952
- wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
4953
- updatedAt: /* @__PURE__ */ new Date()
4954
- });
4955
- await this.store.releaseRunLock(run.id);
4956
- results.push({
4957
- runId: run.id,
4958
- status: "suspended"
4959
- });
4960
- continue;
4961
- }
4962
- }
4963
- const settled = await this.countRecovery(run);
4964
- if (settled) {
4965
- results.push(settled);
4966
- continue;
4967
- }
4968
- await this.store.releaseRunLock(run.id);
4969
- if (run.status !== "cancelling") {
5297
+ const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
5298
+ if (!outcome) continue;
5299
+ if (outcome.dispatch) await this.runDispatcher.dispatch(outcome.result.runId);
5300
+ results.push(outcome.result);
5301
+ }
5302
+ return results;
5303
+ }
5304
+ /** A run whose still-live lease is held by this very `instanceId` (see {@link reclaimOwnLease}). */
5305
+ isOwnLiveLease(run, nowMs) {
5306
+ return run.lockedBy === this.instanceId && (run.lockedUntil ?? 0) > nowMs;
5307
+ }
5308
+ /**
5309
+ * Recover a run whose lease is still live but held by THIS `instanceId` and predates this process
5310
+ * (listed by {@link orphanedRuns}): the previous process with this stable id died without a
5311
+ * graceful shutdown (OOM, SIGKILL), so nothing is renewing that lease and waiting for it to expire
5312
+ * only delays the run. The lease is taken back with `renewRunLock` (a compare-and-set on the
5313
+ * owner; `tryLockRun` refuses a live lease), so the lease alone cannot fence a live execution in
5314
+ * THIS process from its own recovery: the in-process run gate does. A run with a
5315
+ * gate is executing (or about to) here and is skipped; otherwise a gate is held while the run is
5316
+ * recovered, so a resume landing meanwhile queues behind the recovery instead of running beside it.
5317
+ * The gate is released before the re-dispatch, which may resume the run on this instance.
5318
+ */
5319
+ async reclaimOwnLease(listed, nowMs) {
5320
+ if (this.runGates.has(listed.id)) return void 0;
5321
+ let release;
5322
+ const tail = new Promise((resolve) => {
5323
+ release = resolve;
5324
+ });
5325
+ const gate = {
5326
+ tail
5327
+ };
5328
+ this.runGates.set(listed.id, gate);
5329
+ this.closeGateAfter(listed.id, gate, tail);
5330
+ let outcome;
5331
+ try {
5332
+ const fresh = await this.store.getRun(listed.id);
5333
+ const liveHere = fresh?.lockedBy === this.instanceId && (fresh.lockedUntil ?? 0) > nowMs && (fresh.lockedUntil ?? 0) >= this.ownLeaseCutoffMs;
5334
+ if (fresh && !liveHere) outcome = await this.recoverOrphan(listed, nowMs, true);
5335
+ } finally {
5336
+ release(outcome?.result ?? {
5337
+ runId: listed.id,
5338
+ status: listed.status
5339
+ });
5340
+ }
5341
+ return outcome;
5342
+ }
5343
+ /**
5344
+ * One run of a {@link recoverIncomplete} pass: take its lease and decide on it. Resolves the
5345
+ * pass's result for it and whether it must be re-dispatched, or `undefined` when it was not ours.
5346
+ * `ownLease`: the run's live lease is this instance id's own, left by a previous process (see
5347
+ * {@link reclaimOwnLease}), so it is taken back by owner rather than acquired as a free one.
5348
+ */
5349
+ async recoverOrphan(listed, nowMs, ownLease = false) {
5350
+ const acquired = ownLease ? await this.store.renewRunLock(listed.id, this.instanceId, nowMs + this.leaseMs) : await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
5351
+ if (!acquired) return void 0;
5352
+ const run = await this.store.getRun(listed.id);
5353
+ if (!run || run.status !== "running" && run.status !== "cancelling") {
5354
+ await this.store.releaseRunLock(listed.id);
5355
+ return void 0;
5356
+ }
5357
+ if (run.status === "running") {
5358
+ const dispatched = await this.inFlightRemoteStep(run.id);
5359
+ if (dispatched) {
4970
5360
  await this.store.updateRun(run.id, {
4971
- status: "pending",
5361
+ status: "suspended",
5362
+ wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
4972
5363
  updatedAt: /* @__PURE__ */ new Date()
4973
5364
  });
5365
+ await this.store.releaseRunLock(run.id);
5366
+ return {
5367
+ result: {
5368
+ runId: run.id,
5369
+ status: "suspended"
5370
+ },
5371
+ dispatch: false
5372
+ };
4974
5373
  }
4975
- await this.runDispatcher.dispatch(run.id);
4976
- results.push({
4977
- runId: run.id,
4978
- status: run.status === "cancelling" ? "cancelling" : "pending"
5374
+ }
5375
+ const settled = await this.countRecovery(run);
5376
+ if (settled) return {
5377
+ result: settled,
5378
+ dispatch: false
5379
+ };
5380
+ await this.store.releaseRunLock(run.id);
5381
+ if (run.status !== "cancelling") {
5382
+ await this.store.updateRun(run.id, {
5383
+ status: "pending",
5384
+ updatedAt: /* @__PURE__ */ new Date()
4979
5385
  });
4980
5386
  }
4981
- return results;
5387
+ return {
5388
+ result: {
5389
+ runId: run.id,
5390
+ status: run.status === "cancelling" ? "cancelling" : "pending"
5391
+ },
5392
+ dispatch: true
5393
+ };
4982
5394
  }
4983
5395
  /**
4984
5396
  * The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
@@ -4988,12 +5400,20 @@ var WorkflowEngine = class {
4988
5400
  * ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
4989
5401
  * filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
4990
5402
  * recovering at once spread over the batch instead of contending for the same leases.
5403
+ *
5404
+ * Also listed: runs still leased by THIS `instanceId` with a lease taken before this process
5405
+ * booted ({@link ownLeaseCutoffMs}). With a stable `instanceId` (pinned to the pod identity), that
5406
+ * is the previous process, killed without a graceful shutdown; see {@link reclaimOwnLease}.
4991
5407
  */
4992
5408
  async orphanedRuns(nowMs) {
5409
+ const ownedBy = {
5410
+ owner: this.instanceId,
5411
+ lockedBefore: this.ownLeaseCutoffMs
5412
+ };
4993
5413
  if (this.store.listOrphanedRuns) {
4994
- return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
5414
+ return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace, ownedBy));
4995
5415
  }
4996
- return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, RECOVERY_BATCH));
5416
+ return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs || run.lockedBy === ownedBy.owner && run.lockedUntil < ownedBy.lockedBefore).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, RECOVERY_BATCH));
4997
5417
  }
4998
5418
  /**
4999
5419
  * The remote step this run dispatched and is still awaiting a result for, if any — the signature
@@ -5036,6 +5456,7 @@ var WorkflowEngine = class {
5036
5456
  error: error2,
5037
5457
  recoveryAttempts: attempts
5038
5458
  });
5459
+ this.notifyParent(run.id, failedCompletion(error2));
5039
5460
  return {
5040
5461
  runId: run.id,
5041
5462
  status: "dead",
@@ -5242,7 +5663,7 @@ var WorkflowEngine = class {
5242
5663
  error: void 0,
5243
5664
  ...resurrecting ? {
5244
5665
  recoveryAttempts: 0,
5245
- tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
5666
+ tags: withoutSingletonMarkers(run.tags)
5246
5667
  } : {},
5247
5668
  updatedAt: /* @__PURE__ */ new Date()
5248
5669
  });
@@ -5654,6 +6075,16 @@ var WorkflowEngine = class {
5654
6075
  return this.deliverSignal(lateWaiter, reclaimed.payload);
5655
6076
  }
5656
6077
  /**
6078
+ * Wake the run parked on `token` with `payload`, if there is one; otherwise do nothing — unlike
6079
+ * {@link signal}, it never buffers. For a wake-up whose real payload is ALREADY buffered on the
6080
+ * token (an entity op, see `Entities.kick`): a run that isn't parked reads that buffer at its next
6081
+ * wait, so there is nothing to keep.
6082
+ */
6083
+ async wakeSignalWaiter(token, payload) {
6084
+ const waiter = await this.store.takeSignalWaiter(token);
6085
+ return waiter ? this.deliverSignal(waiter, payload) : null;
6086
+ }
6087
+ /**
5657
6088
  * Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
5658
6089
  * signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
5659
6090
  * consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
@@ -6249,9 +6680,31 @@ var WorkflowEngine = class {
6249
6680
  });
6250
6681
  }
6251
6682
  /** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
6683
+ /**
6684
+ * A run's first execution: `pending` -> `running`. A singleton run not yet admitted is stamped
6685
+ * {@link SINGLETON_TRACKED_TAG} in the same write, BEFORE it competes for a slot, so a run this
6686
+ * replica is about to admit or deny is never mistaken for a holder admitted by a replica older
6687
+ * than 0.73 (see `legacySingletonHolders`).
6688
+ */
6689
+ async markStarted(run, singleton) {
6690
+ const tags = singleton ? withSingletonTracked(run.tags) : run.tags;
6691
+ const retag = tags !== run.tags;
6692
+ await this.store.updateRun(run.id, {
6693
+ status: "running",
6694
+ ...retag ? {
6695
+ tags
6696
+ } : {},
6697
+ updatedAt: /* @__PURE__ */ new Date()
6698
+ });
6699
+ run.status = "running";
6700
+ if (retag) run.tags = tags;
6701
+ }
6252
6702
  async singletonWaitResult(run) {
6253
6703
  const current = await this.store.getRun(run.id);
6254
- 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
+ }
6255
6708
  if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
6256
6709
  if (current.status === "suspended") {
6257
6710
  this.emit({
@@ -6279,10 +6732,7 @@ var WorkflowEngine = class {
6279
6732
  };
6280
6733
  }
6281
6734
  }
6282
- const renew = setInterval(() => {
6283
- void this.store.renewRunLock(run.id, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
6284
- }, Math.max(50, Math.floor(this.leaseMs / 2)));
6285
- renew.unref?.();
6735
+ const held = this.holdExecutionLease(run.id);
6286
6736
  try {
6287
6737
  const result = registered?.remote ? await this.runRemoteExecution(run, registered) : await this.runExecution(run, fn, registered);
6288
6738
  if (registered?.singleton && (result.status === "completed" || result.status === "failed" || result.status === "cancelled" || result.status === "dead")) {
@@ -6290,10 +6740,59 @@ var WorkflowEngine = class {
6290
6740
  }
6291
6741
  return result;
6292
6742
  } finally {
6293
- clearInterval(renew);
6743
+ held.stop();
6294
6744
  }
6295
6745
  }
6296
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
+ /**
6297
6796
  * Advance a remote (cross-SDK) workflow one turn: hand its history to the executor (which dispatches
6298
6797
  * a workflow task to the worker and awaits its replay) and apply the decision. Mirrors
6299
6798
  * {@link runExecution}'s settle/suspend; the lease is held by {@link execute}. The result that lands
@@ -6510,6 +7009,7 @@ ${run.workflowVersion}`;
6510
7009
  * keeps its own lease handling (the TS executor's `finally`, the remote path's caller).
6511
7010
  */
6512
7011
  async settleRun(run, outcome) {
7012
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6513
7013
  const updatedAt = /* @__PURE__ */ new Date();
6514
7014
  if (outcome.kind === "completed") {
6515
7015
  const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
@@ -6590,6 +7090,7 @@ ${run.workflowVersion}`;
6590
7090
  }
6591
7091
  async runRemoteExecution(run, registered) {
6592
7092
  if (run.status === "cancelling") {
7093
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6593
7094
  const error2 = {
6594
7095
  message: "cancelled"
6595
7096
  };
@@ -6598,6 +7099,7 @@ ${run.workflowVersion}`;
6598
7099
  error: error2,
6599
7100
  updatedAt: /* @__PURE__ */ new Date()
6600
7101
  });
7102
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6601
7103
  this.emit({
6602
7104
  type: "run.failed",
6603
7105
  runId: run.id,
@@ -6622,11 +7124,7 @@ ${run.workflowVersion}`;
6622
7124
  }
6623
7125
  const remote = registered.remote;
6624
7126
  if (run.status === "pending") {
6625
- await this.store.updateRun(run.id, {
6626
- status: "running",
6627
- updatedAt: /* @__PURE__ */ new Date()
6628
- });
6629
- run.status = "running";
7127
+ await this.markStarted(run, registered?.singleton !== void 0);
6630
7128
  this.emit({
6631
7129
  type: "run.started",
6632
7130
  runId: run.id,
@@ -6648,6 +7146,7 @@ ${run.workflowVersion}`;
6648
7146
  }
6649
7147
  const history = await this.remoteHistory(run.id, checkpoints);
6650
7148
  if (remote.executor.dispatch) {
7149
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6651
7150
  const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
6652
7151
  const silenceMs = this.remoteAdvanceSilenceMs;
6653
7152
  const wakeAt = silenceMs != null ? this.clock() + silenceMs : void 0;
@@ -6663,6 +7162,7 @@ ${run.workflowVersion}`;
6663
7162
  workflow: run.workflow,
6664
7163
  namespace: run.namespace
6665
7164
  });
7165
+ this.handOffLease(run.id);
6666
7166
  await this.store.releaseRunLock(run.id);
6667
7167
  await remote.executor.dispatch(run, history, taskId);
6668
7168
  return {
@@ -6691,6 +7191,7 @@ ${run.workflowVersion}`;
6691
7191
  message: err instanceof Error ? err.message : String(err),
6692
7192
  stack: err instanceof Error ? err.stack : void 0
6693
7193
  };
7194
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6694
7195
  await this.store.updateRun(run.id, {
6695
7196
  status: "failed",
6696
7197
  error: error2,
@@ -6719,6 +7220,7 @@ ${run.workflowVersion}`;
6719
7220
  * — the caller has already ensured this is the decision for the currently-awaited turn.
6720
7221
  */
6721
7222
  async applyDecision(run, decision) {
7223
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
6722
7224
  if (decision.status === "completed") {
6723
7225
  await this.applyCommands(run, decision.commands);
6724
7226
  return this.settleRun(run, {
@@ -7188,6 +7690,7 @@ ${run.workflowVersion}`;
7188
7690
  * cancel flag stays too, re-derived from the status on any later re-drive regardless.
7189
7691
  */
7190
7692
  async compensateAndCancel(run, compensations, replay) {
7693
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
7191
7694
  const pending = await this.unwindCompensations(run, compensations, replay);
7192
7695
  if (pending) return {
7193
7696
  runId: run.id,
@@ -7242,11 +7745,7 @@ ${run.workflowVersion}`;
7242
7745
  async runExecutionTurn(run, fn, registered) {
7243
7746
  if (run.status === "cancelling") this.cancelRequested.add(run.id);
7244
7747
  if (run.status === "pending") {
7245
- await this.store.updateRun(run.id, {
7246
- status: "running",
7247
- updatedAt: /* @__PURE__ */ new Date()
7248
- });
7249
- run.status = "running";
7748
+ await this.markStarted(run, registered?.singleton !== void 0);
7250
7749
  this.emit({
7251
7750
  type: "run.started",
7252
7751
  runId: run.id,
@@ -7283,6 +7782,7 @@ ${run.workflowVersion}`;
7283
7782
  });
7284
7783
  } catch (err) {
7285
7784
  if (err instanceof ContinueAsNew) {
7785
+ if (this.isFenced(run.id)) return await this.echoCurrentStatus(run.id);
7286
7786
  if (this.cancelRequested.has(run.id)) {
7287
7787
  return await this.compensateAndCancel(run, compensations, replay);
7288
7788
  }
@@ -7622,6 +8122,17 @@ ${run.workflowVersion}`;
7622
8122
  signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
7623
8123
  queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
7624
8124
  }, "signalEntity"),
8125
+ // The durable half of a workflow-side entity send: awaited inside the turn (a store write
8126
+ // only, never an inline entity turn), so the ctx can checkpoint after it. See Entities.enqueue.
8127
+ enqueueEntityOp: /* @__PURE__ */ __name((name, key, op, arg, id, reply) => this.entities.enqueue(name, key, op, arg, id, reply), "enqueueEntityOp"),
8128
+ // Start/wake the entity for what enqueueEntityOp buffered — deferred like signalEntity above,
8129
+ // so it never re-enters a still-executing turn. A failed wake loses nothing: the op stays
8130
+ // buffered for the entity's next wait (and recovery's buffered-entity wake).
8131
+ kickEntity: /* @__PURE__ */ __name((name, key) => {
8132
+ queueMicrotask(() => void this.entities.kick(name, key).catch((err) => {
8133
+ console.warn(`[nestjs-durable] waking entity "${name}" key "${key}" failed (its op stays buffered for the next wake):`, err);
8134
+ }));
8135
+ }, "kickEntity"),
7625
8136
  interceptStep: /* @__PURE__ */ __name((invocation, body) => this.interceptStep(invocation, body), "interceptStep")
7626
8137
  };
7627
8138
  }
@@ -7723,6 +8234,7 @@ ${run.workflowVersion}`;
7723
8234
  * own `finally` also releases (the run-execution path) is safe.
7724
8235
  */
7725
8236
  async parkBlocked(run, blocked) {
8237
+ if (this.isFenced(run.id)) return this.echoCurrentStatus(run.id);
7726
8238
  const error2 = {
7727
8239
  message: blocked.reason,
7728
8240
  code: blocked.code,
@@ -7763,7 +8275,7 @@ ${run.workflowVersion}`;
7763
8275
  "blocked"
7764
8276
  ]
7765
8277
  });
7766
- return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
8278
+ return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || poolNamespace(r.namespace) === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
7767
8279
  }
7768
8280
  async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
7769
8281
  const group = stepGroup(step, routing?.namespace);
@@ -8302,17 +8814,14 @@ ${run.workflowVersion}`;
8302
8814
  const run = await this.store.getRun(decision.runId);
8303
8815
  if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
8304
8816
  if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
8305
- if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
8817
+ if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
8306
8818
  return;
8307
8819
  }
8308
8820
  const nowMs = this.clock();
8309
8821
  if (!await this.store.tryLockRun(decision.runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
8310
8822
  return;
8311
8823
  }
8312
- const renew = setInterval(() => {
8313
- void this.store.renewRunLock(decision.runId, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
8314
- }, Math.max(50, Math.floor(this.leaseMs / 2)));
8315
- renew.unref?.();
8824
+ const held = this.holdExecutionLease(decision.runId);
8316
8825
  try {
8317
8826
  const fresh = await this.store.getRun(decision.runId);
8318
8827
  if (!fresh || fresh.awaitingDecisionTaskId !== decision.taskId) return;
@@ -8327,8 +8836,9 @@ ${run.workflowVersion}`;
8327
8836
  fresh.awaitingDecisionTaskId = void 0;
8328
8837
  await this.applyDecision(fresh, decision);
8329
8838
  } finally {
8330
- clearInterval(renew);
8331
- await this.store.releaseRunLock(decision.runId);
8839
+ const fenced = held.lease.lost;
8840
+ held.stop();
8841
+ if (!fenced) await this.store.releaseRunLock(decision.runId);
8332
8842
  }
8333
8843
  }
8334
8844
  /** Earliest clock time a suspended turn's deadline is worth LOOKING at again (`runId` → clock ms) —
@@ -8524,6 +9034,9 @@ function childIdsOf(checkpoints) {
8524
9034
  }
8525
9035
  __name(childIdsOf, "childIdsOf");
8526
9036
 
9037
+ // src/run-history.ts
9038
+ var RUN_HISTORY_FIXTURE_VERSION = 1;
9039
+
8527
9040
  // src/run-facets.ts
8528
9041
  function facetOrigin(origin) {
8529
9042
  return origin?.trim() ? origin : null;
@@ -9077,13 +9590,14 @@ var CodecStateStore = class {
9077
9590
  }
9078
9591
  inner;
9079
9592
  codec;
9593
+ extended;
9080
9594
  /** Atomic claims touch only searchable metadata and bypass payload encoding. */
9081
9595
  tryAdmitSingleton;
9082
9596
  /**
9083
9597
  * Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
9084
- * (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
9085
- * actually aggregates, so `store.runFacets?.()` still reports absence through the wrapper instead
9086
- * 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.
9087
9601
  */
9088
9602
  runFacets;
9089
9603
  /**
@@ -9135,14 +9649,15 @@ var CodecStateStore = class {
9135
9649
  updateSchedule;
9136
9650
  deleteSchedule;
9137
9651
  listSchedules;
9138
- constructor(inner, codec) {
9652
+ constructor(inner, codec, options = {}) {
9139
9653
  this.inner = inner;
9140
9654
  this.codec = codec;
9655
+ this.extended = options.coverage === "extended";
9141
9656
  if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
9142
9657
  const deleteRuns = inner.deleteRuns;
9143
9658
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
9144
9659
  const orphans = inner.listOrphanedRuns;
9145
- if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace) => (await orphans.call(inner, nowMs, limit, namespace)).map((r) => this.decRun(r));
9660
+ if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace, ownedBy) => (await orphans.call(inner, nowMs, limit, namespace, ownedBy)).map((r) => this.decRun(r));
9146
9661
  const updateIf = inner.updateRunIf;
9147
9662
  if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
9148
9663
  const saveIfPending = inner.saveCheckpointIfPending;
@@ -9195,32 +9710,52 @@ var CodecStateStore = class {
9195
9710
  return v === void 0 ? void 0 : this.codec.decode(v);
9196
9711
  }
9197
9712
  encRun(run) {
9198
- return {
9713
+ const next = {
9199
9714
  ...run,
9200
9715
  input: this.enc(run.input),
9201
9716
  output: this.enc(run.output)
9202
9717
  };
9718
+ if (this.extended && run.error !== void 0) {
9719
+ next.error = this.enc(run.error);
9720
+ }
9721
+ return next;
9203
9722
  }
9204
9723
  decRun(run) {
9205
- return run && {
9724
+ if (!run) return run;
9725
+ const next = {
9206
9726
  ...run,
9207
9727
  input: this.dec(run.input),
9208
9728
  output: this.dec(run.output)
9209
9729
  };
9730
+ if (this.extended && run.error !== void 0) {
9731
+ next.error = this.dec(run.error);
9732
+ }
9733
+ return next;
9210
9734
  }
9211
9735
  encCp(cp) {
9212
- return {
9736
+ const next = {
9213
9737
  ...cp,
9214
9738
  input: this.enc(cp.input),
9215
9739
  output: this.enc(cp.output)
9216
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;
9217
9746
  }
9218
9747
  decCp(cp) {
9219
- return cp && {
9748
+ if (!cp) return cp;
9749
+ const next = {
9220
9750
  ...cp,
9221
9751
  input: this.dec(cp.input),
9222
9752
  output: this.dec(cp.output)
9223
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;
9224
9759
  }
9225
9760
  encSpec(spec) {
9226
9761
  return "input" in spec ? {
@@ -9259,6 +9794,9 @@ var CodecStateStore = class {
9259
9794
  };
9260
9795
  if ("input" in patch) next.input = this.enc(patch.input);
9261
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
+ }
9262
9800
  return next;
9263
9801
  }
9264
9802
  async getRun(runId) {
@@ -9294,8 +9832,8 @@ var CodecStateStore = class {
9294
9832
  tryLockRun(runId, owner, leaseUntilMs, nowMs) {
9295
9833
  return this.inner.tryLockRun(runId, owner, leaseUntilMs, nowMs);
9296
9834
  }
9297
- releaseRunLock(runId) {
9298
- return this.inner.releaseRunLock(runId);
9835
+ releaseRunLock(runId, owner) {
9836
+ return owner === void 0 ? this.inner.releaseRunLock(runId) : this.inner.releaseRunLock(runId, owner);
9299
9837
  }
9300
9838
  renewRunLock(runId, owner, leaseUntilMs) {
9301
9839
  return this.inner.renewRunLock(runId, owner, leaseUntilMs);
@@ -9663,7 +10201,7 @@ var InMemoryStateStore = class {
9663
10201
  async createRun(run) {
9664
10202
  this.runs.set(run.id, {
9665
10203
  ...run,
9666
- namespace: run.namespace ?? "default"
10204
+ namespace: run.namespace || "default"
9667
10205
  });
9668
10206
  this.reindexAttributes(run.id, run.searchAttributes);
9669
10207
  }
@@ -9678,7 +10216,7 @@ var InMemoryStateStore = class {
9678
10216
  applyRunPatch(runId, inputPatch) {
9679
10217
  const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
9680
10218
  ...inputPatch,
9681
- tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
10219
+ tags: withoutSingletonMarkers("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)
9682
10220
  } : inputPatch;
9683
10221
  const existing = this.runs.get(runId);
9684
10222
  if (!existing) throw new Error(`run ${runId} not found`);
@@ -9766,10 +10304,10 @@ var InMemoryStateStore = class {
9766
10304
  return true;
9767
10305
  });
9768
10306
  }
9769
- async listOrphanedRuns(nowMs, limit, namespace) {
10307
+ async listOrphanedRuns(nowMs, limit, namespace, ownedBy) {
9770
10308
  return [
9771
10309
  ...this.runs.values()
9772
- ].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
10310
+ ].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs || ownedBy !== void 0 && r.lockedBy === ownedBy.owner && r.lockedUntil < ownedBy.lockedBefore) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
9773
10311
  ...r
9774
10312
  }));
9775
10313
  }
@@ -9803,9 +10341,9 @@ var InMemoryStateStore = class {
9803
10341
  run.lockedUntil = leaseUntilMs;
9804
10342
  return true;
9805
10343
  }
9806
- async releaseRunLock(runId) {
10344
+ async releaseRunLock(runId, owner) {
9807
10345
  const run = this.runs.get(runId);
9808
- if (run) {
10346
+ if (run && (owner === void 0 || run.lockedBy === owner)) {
9809
10347
  run.lockedBy = void 0;
9810
10348
  run.lockedUntil = void 0;
9811
10349
  }
@@ -10155,12 +10693,14 @@ export {
10155
10693
  ContinueAsNew,
10156
10694
  DEFAULT_CONCURRENCY_STATUSES,
10157
10695
  DEFAULT_PARTITION,
10696
+ DEFAULT_STALLED_AFTER_MS,
10158
10697
  DURABLE_OPTIONS,
10159
10698
  DURABLE_OPTIONS_CANONICAL,
10160
10699
  DURABLE_STEP_CONFIG,
10161
10700
  DURABLE_STEP_NAME,
10162
10701
  DurableWorkflow,
10163
10702
  ENGINE_MINTED_TAG_PREFIXES,
10703
+ ENTITY_WORKFLOW,
10164
10704
  EVENT_BUFFER_SCAN_LIMIT,
10165
10705
  EVENT_TOKEN_PREFIX,
10166
10706
  Entities,
@@ -10177,6 +10717,7 @@ export {
10177
10717
  NestedWorkflowCallError,
10178
10718
  NonDeterminismError,
10179
10719
  QueueController,
10720
+ RUN_HISTORY_FIXTURE_VERSION,
10180
10721
  RUN_VALUE_FACET_LIMIT,
10181
10722
  RUN_VALUE_FACET_SCAN,
10182
10723
  RemoteStepError,
@@ -10186,6 +10727,8 @@ export {
10186
10727
  RunGateway,
10187
10728
  SINGLETON_ACTIVE_STATUSES,
10188
10729
  SINGLETON_ADMITTED_TAG,
10730
+ SINGLETON_RESERVED_TAGS,
10731
+ SINGLETON_TRACKED_TAG,
10189
10732
  STATE_STORE,
10190
10733
  STATE_STORE_CANONICAL,
10191
10734
  SUMMARY_CHILD_LIMIT,
@@ -10248,6 +10791,7 @@ export {
10248
10791
  facetOrigin,
10249
10792
  fanPageBounds,
10250
10793
  fanPageEtag,
10794
+ findStalledRuns,
10251
10795
  foldedCount,
10252
10796
  handoffHandlerId,
10253
10797
  handoffName,
@@ -10261,6 +10805,7 @@ export {
10261
10805
  isLegacyDescriptor,
10262
10806
  isNonEmptyRunScope,
10263
10807
  isWorkflowControlFlowSignal,
10808
+ legacySingletonHolders,
10264
10809
  listSignalWaitersOfRuns,
10265
10810
  liveHandoff,
10266
10811
  log,
@@ -10278,8 +10823,10 @@ export {
10278
10823
  pageFanFromStore,
10279
10824
  parentRunIdOf,
10280
10825
  parseDuration,
10826
+ parseWebhookToken,
10281
10827
  partitionOfToken,
10282
10828
  planDispatch,
10829
+ poolNamespace,
10283
10830
  prevCronFireMs,
10284
10831
  queryCheckpointsOf,
10285
10832
  readSearchAttributes,
@@ -10315,6 +10862,8 @@ export {
10315
10862
  tenantGroup,
10316
10863
  useDurableExecution,
10317
10864
  warn,
10865
+ withSingletonTracked,
10866
+ withoutSingletonMarkers,
10318
10867
  workflowName
10319
10868
  };
10320
10869
  //# sourceMappingURL=index.js.map