@ewhauser/world-celld 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as isPositiveSafeInteger, a as queueClaimName, b as strictIntegerSetting, c as queuePayloadRegistryKey, f as HOOK_CLAIM_LEASE_MS, g as isNonNegativeSafeInteger, h as QUEUE_CLAIM_STALE_MS, l as validateNativeQueueEnvelope, m as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, n as QUEUE_PAYLOAD_REGISTRY_PREFIX, o as queueOrphanName, p as LIFECYCLE_COMPACTION_RETRY_MS, r as isInlineRunBody, s as queuePayloadObjectKey, t as NATIVE_QUEUE_MAX_MESSAGE_BYTES, u as validateNativeQueueSendOptions, v as isRecord$1, y as queueDelayDeadline } from "./queue-protocol-DKYt8qZJ.js";
1
+ import { S as strictIntegerSetting, _ as QUEUE_CLAIM_STALE_MS, a as isInlineRunBody, b as isRecord$1, c as queueOrphanName, d as validateNativeQueueEnvelope, f as validateNativeQueueSendOptions, g as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, h as LIFECYCLE_COMPACTION_RETRY_MS, i as QUEUE_RESERVATION_GRACE_MS, l as queuePayloadObjectKey, m as HOOK_CLAIM_LEASE_MS, n as QUEUE_PAYLOAD_REGISTRY_PREFIX, r as QUEUE_PUBLICATION_LEASE_MS, s as queueClaimName, t as NATIVE_QUEUE_MAX_MESSAGE_BYTES, u as queuePayloadRegistryKey, v as isNonNegativeSafeInteger, x as queueDelayDeadline, y as isPositiveSafeInteger } from "./queue-protocol-C1zbXwa_.js";
2
2
  import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
3
3
  import { CreateEventSchema, EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, applyAttributeChanges, eventIdToSlot, getMaxEventsPerRun, isChildEntityCreationEvent, isTerminalStepStatus, isTerminalWorkflowRunStatus, slotToEventId, validateAttributeChanges } from "@workflow/world";
4
4
  //#region src/worker/auth.ts
@@ -56,6 +56,11 @@ function cellQueueClaim(cell) {
56
56
  }),
57
57
  hold: (params) => cell.holdInflight(params),
58
58
  release: (messageId) => cell.releaseInflight(messageId),
59
+ abandonReservation: (messageId) => cell.abandonQueueMessageReservation(messageId),
60
+ confirmPublication: (messageId, expiresAt) => cell.confirmQueueMessagePublication({
61
+ messageId,
62
+ expiresAt
63
+ }),
59
64
  complete: (messageId) => cell.completeQueueMessage(messageId)
60
65
  };
61
66
  }
@@ -80,6 +85,15 @@ function runQueueClaim(run, claimName) {
80
85
  claimName,
81
86
  messageId
82
87
  }),
88
+ abandonReservation: (messageId) => run.abandonRunQueueMessageReservation({
89
+ claimName,
90
+ messageId
91
+ }),
92
+ confirmPublication: (messageId, expiresAt) => run.confirmRunQueueMessagePublication({
93
+ claimName,
94
+ messageId,
95
+ expiresAt
96
+ }),
83
97
  complete: (messageId) => run.completeRunQueueMessage({
84
98
  claimName,
85
99
  messageId
@@ -96,7 +110,7 @@ function deliveryQueueClaim(envelope, runStub) {
96
110
  //#region src/worker/queue-delivery.ts
97
111
  /** Internal delivery contract. No Request/Response objects cross the RPC boundary. */
98
112
  const MAX_QUEUE_DELIVERY_RESPONSE_BYTES = 65536;
99
- const QUEUE_ORPHAN_GRACE_MS$1 = 432e6;
113
+ const QUEUE_ORPHAN_GRACE_MS = 432e6;
100
114
  const PERMANENT_QUEUE_STATUSES = /* @__PURE__ */ new Set([
101
115
  404,
102
116
  409,
@@ -210,13 +224,13 @@ async function deliverQueueMessage(env, secret, input, attempt) {
210
224
  messageId: envelope.messageId,
211
225
  runId: envelope.runId,
212
226
  key: envelope.payloadKey,
213
- expiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS$1
227
+ expiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
214
228
  });
215
229
  if (claim) await claim.hold({
216
230
  messageId: envelope.messageId,
217
231
  retryAt: notBefore,
218
232
  expiresAt: notBefore + QUEUE_CLAIM_STALE_MS,
219
- reservationExpiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS$1
233
+ reservationExpiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
220
234
  });
221
235
  return {
222
236
  kind: "suspend",
@@ -427,8 +441,8 @@ function readStringProp(value, key) {
427
441
  }
428
442
  }
429
443
  /**
430
- * Map a failure eventData.error (which may be a string, an Error-shaped
431
- * object, or anything else) to the structured error stored on entities.
444
+ * Map a run failure eventData.error (which may be a string, an Error-shaped
445
+ * object, or anything else) to the structured error stored on runs.
432
446
  * Matches upstream: string errors keep their text, errorCode is preserved.
433
447
  */
434
448
  function toStructuredError(error, opts) {
@@ -904,14 +918,17 @@ async function applyEvent(store, ctx) {
904
918
  }
905
919
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
906
920
  if (step.retryAfter && step.retryAfter.getTime() > now.getTime()) return failure("TOO_EARLY", `Cannot start step "${data.correlationId}": retryAfter timestamp has not been reached yet`, { retryAfterSeconds: Math.ceil((step.retryAfter.getTime() - now.getTime()) / 1e3) });
907
- const updated = StepSchema.parse(compact({
908
- ...step,
909
- status: "running",
910
- startedAt: step.startedAt ?? now,
911
- attempt: step.attempt + 1,
912
- retryAfter: void 0,
913
- updatedAt: now
914
- }));
921
+ const updated = StepSchema.parse({
922
+ ...compact({
923
+ ...step,
924
+ status: "running",
925
+ startedAt: step.startedAt ?? now,
926
+ attempt: step.attempt + 1,
927
+ retryAfter: void 0,
928
+ updatedAt: now
929
+ }),
930
+ error: step.error
931
+ });
915
932
  const event = buildEvent({ ...data });
916
933
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
917
934
  await putEvent(event);
@@ -926,13 +943,16 @@ async function applyEvent(store, ctx) {
926
943
  case "step_completed": {
927
944
  const step = validatedStep;
928
945
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
929
- const updated = StepSchema.parse(compact({
930
- ...step,
931
- status: "completed",
932
- output: data.eventData.result,
933
- completedAt: now,
934
- updatedAt: now
935
- }));
946
+ const updated = StepSchema.parse({
947
+ ...compact({
948
+ ...step,
949
+ status: "completed",
950
+ output: data.eventData.result,
951
+ completedAt: now,
952
+ updatedAt: now
953
+ }),
954
+ error: step.error
955
+ });
936
956
  const event = buildEvent({ ...data });
937
957
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
938
958
  await putEvent(event);
@@ -946,13 +966,13 @@ async function applyEvent(store, ctx) {
946
966
  case "step_failed": {
947
967
  const step = validatedStep;
948
968
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
949
- const updated = StepSchema.parse(compact({
969
+ const updated = StepSchema.parse({
950
970
  ...step,
951
971
  status: "failed",
952
- error: toStructuredError(data.eventData.error),
972
+ error: data.eventData.error,
953
973
  completedAt: now,
954
974
  updatedAt: now
955
- }));
975
+ });
956
976
  const event = buildEvent({ ...data });
957
977
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
958
978
  await putEvent(event);
@@ -966,13 +986,13 @@ async function applyEvent(store, ctx) {
966
986
  case "step_retrying": {
967
987
  const step = validatedStep;
968
988
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
969
- const updated = StepSchema.parse(compact({
989
+ const updated = StepSchema.parse({
970
990
  ...step,
971
991
  status: "pending",
972
- error: toStructuredError(data.eventData.error),
992
+ error: data.eventData.error,
973
993
  retryAfter: data.eventData.retryAfter ? new Date(data.eventData.retryAfter) : void 0,
974
994
  updatedAt: now
975
- }));
995
+ });
976
996
  const event = buildEvent({ ...data });
977
997
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
978
998
  await putEvent(event);
@@ -1008,7 +1028,7 @@ async function applyEvent(store, ctx) {
1008
1028
  }
1009
1029
  if (await store.get(markerKey) !== void 0) {
1010
1030
  const existingHook = await store.get(hookKey);
1011
- if (existingHook && !holder) return {
1031
+ if (existingHook && !holder && existingHook.token === token) return {
1012
1032
  ok: true,
1013
1033
  hook: existingHook,
1014
1034
  hookToIndex: existingHook,
@@ -1977,7 +1997,8 @@ const BINDINGS = {
1977
1997
  "scheduleCleanup",
1978
1998
  "cleanupNow",
1979
1999
  "rearmCleanup",
1980
- "resolveHookTokenClaim"
2000
+ "resolveHookTokenClaim",
2001
+ "resolveExpiredHookClaim"
1981
2002
  ])
1982
2003
  },
1983
2004
  streams: {
@@ -2004,7 +2025,6 @@ const BINDINGS = {
2004
2025
  /** Request body cap: oversize payloads get a clear 413 instead of an OOM. */
2005
2026
  const MAX_BODY_BYTES = 33554432;
2006
2027
  const MAX_STREAM_WRITE_BODY_BYTES = 8391680;
2007
- const QUEUE_ORPHAN_GRACE_MS = 432e6;
2008
2028
  function errorResponse(status, name, message) {
2009
2029
  return Response.json({ error: {
2010
2030
  name,
@@ -2157,6 +2177,7 @@ function createRouter(env) {
2157
2177
  } catch (error) {
2158
2178
  return errorResponse(400, "BadRequest", error.message);
2159
2179
  }
2180
+ let enqueueClaim;
2160
2181
  try {
2161
2182
  const namespace = env.WORKFLOW_DB;
2162
2183
  const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
@@ -2173,26 +2194,26 @@ function createRouter(env) {
2173
2194
  });
2174
2195
  if (new TextEncoder().encode(sized).byteLength > 128e3) return errorResponse(413, "PayloadTooLarge", `native queue envelope exceeds ${NATIVE_QUEUE_MAX_MESSAGE_BYTES} bytes`);
2175
2196
  const now = Date.now();
2176
- const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_ORPHAN_GRACE_MS;
2197
+ const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_RESERVATION_GRACE_MS;
2177
2198
  const run = envelope.runId ? namespace.get(namespace.idFromName(envelope.runId)) : void 0;
2178
- let claim;
2179
2199
  let admission;
2180
2200
  if (envelope.idempotencyKey) {
2181
2201
  const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
2182
2202
  let scope = run && envelope.runId ? runClaimScopes.get(envelope.runId) : "cell";
2183
2203
  let reservation;
2184
2204
  if (run && envelope.runId && scope !== "cell") {
2205
+ enqueueClaim = runQueueClaim(run, claimName);
2185
2206
  const reserved = await run.reserveRunQueueMessage({
2186
2207
  claimName,
2187
2208
  messageId: envelope.messageId,
2188
- expiresAt: reservationExpiresAt
2209
+ expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
2210
+ publicationPending: true
2189
2211
  });
2190
2212
  if (!reserved.ok) return errorResponse(410, "RunExpiredError", reserved.message);
2191
2213
  scope = reserved.scope;
2192
2214
  rememberRunClaimScope(envelope.runId, reserved.scope);
2193
2215
  if (reserved.scope === "run") {
2194
2216
  reservation = reserved;
2195
- claim = runQueueClaim(run, claimName);
2196
2217
  brokerEnvelope = {
2197
2218
  ...brokerEnvelope,
2198
2219
  claimScope: "run"
@@ -2204,27 +2225,34 @@ function createRouter(env) {
2204
2225
  }
2205
2226
  if (!reservation) {
2206
2227
  const cell = namespace.get(namespace.idFromName(claimName));
2228
+ enqueueClaim = cellQueueClaim(cell);
2207
2229
  reservation = await cell.reserveQueueMessage({
2208
2230
  messageId: envelope.messageId,
2209
- expiresAt: reservationExpiresAt
2231
+ expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
2232
+ publicationPending: true
2233
+ });
2234
+ }
2235
+ if (!reservation.admitted) {
2236
+ if (reservation.publicationPending) return errorResponse(503, "QueuePublicationPending", "the reserved Queue message has not been confirmed by the broker; retry publication");
2237
+ return new Response(rpcStringify({ messageId: reservation.messageId }), {
2238
+ status: 200,
2239
+ headers: { "content-type": "application/json" }
2210
2240
  });
2211
- claim = cellQueueClaim(cell);
2212
2241
  }
2213
- if (!reservation.admitted) return new Response(rpcStringify({ messageId: reservation.messageId }), {
2214
- status: 200,
2215
- headers: { "content-type": "application/json" }
2216
- });
2217
2242
  } else if (inlineRunBody && run) admission = run.getQueueAdmission();
2218
2243
  const encoded = JSON.stringify(brokerEnvelope);
2219
2244
  if (admission) {
2220
2245
  const admitted = await admission;
2221
2246
  if (!admitted.ok) {
2222
- await claim?.complete(envelope.messageId);
2247
+ await enqueueClaim?.complete(envelope.messageId);
2223
2248
  return errorResponse(410, "RunExpiredError", admitted.message);
2224
2249
  }
2225
2250
  }
2226
2251
  if (run && envelope.runId && payloadKey) {
2227
- if (!payloadStore) return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
2252
+ if (!payloadStore) {
2253
+ await enqueueClaim?.abandonReservation(envelope.messageId);
2254
+ return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
2255
+ }
2228
2256
  const orphanExpiresAt = reservationExpiresAt;
2229
2257
  const registered = await run.registerQueuePayload({
2230
2258
  messageId: envelope.messageId,
@@ -2232,7 +2260,7 @@ function createRouter(env) {
2232
2260
  orphanExpiresAt
2233
2261
  });
2234
2262
  if (!registered.ok) {
2235
- await claim?.complete(envelope.messageId);
2263
+ await enqueueClaim?.complete(envelope.messageId);
2236
2264
  return errorResponse(410, "RunExpiredError", registered.message);
2237
2265
  }
2238
2266
  const orphan = namespace.get(namespace.idFromName(queueOrphanName(envelope.messageId)));
@@ -2245,30 +2273,18 @@ function createRouter(env) {
2245
2273
  });
2246
2274
  } catch (error) {
2247
2275
  await run.unregisterQueuePayload(envelope.messageId);
2248
- await claim?.complete(envelope.messageId);
2249
- throw error;
2250
- }
2251
- try {
2252
- await payloadStore.write(payloadKey, envelope.body, {
2253
- runId: envelope.runId,
2254
- messageId: envelope.messageId
2255
- });
2256
- } catch (error) {
2257
- await claim?.complete(envelope.messageId);
2258
- throw error;
2259
- }
2260
- let finalized;
2261
- try {
2262
- finalized = await run.finalizeQueuePayload(envelope.messageId);
2263
- } catch (error) {
2264
- await claim?.complete(envelope.messageId);
2265
2276
  throw error;
2266
2277
  }
2278
+ await payloadStore.write(payloadKey, envelope.body, {
2279
+ runId: envelope.runId,
2280
+ messageId: envelope.messageId
2281
+ });
2282
+ const finalized = await run.finalizeQueuePayload(envelope.messageId);
2267
2283
  if (!finalized.ok) {
2268
2284
  await payloadStore.delete(payloadKey);
2269
2285
  await run.unregisterQueuePayload(envelope.messageId);
2270
2286
  await orphan.cancelQueuePayloadOrphan(envelope.messageId);
2271
- await claim?.complete(envelope.messageId);
2287
+ await enqueueClaim?.complete(envelope.messageId);
2272
2288
  return errorResponse(410, "RunExpiredError", finalized.message);
2273
2289
  }
2274
2290
  }
@@ -2276,11 +2292,17 @@ function createRouter(env) {
2276
2292
  contentType: "text",
2277
2293
  ...options
2278
2294
  });
2295
+ await enqueueClaim?.confirmPublication(envelope.messageId, reservationExpiresAt);
2279
2296
  return new Response(rpcStringify({ messageId: envelope.messageId }), {
2280
2297
  status: 200,
2281
2298
  headers: { "content-type": "application/json" }
2282
2299
  });
2283
2300
  } catch (error) {
2301
+ try {
2302
+ await enqueueClaim?.abandonReservation(envelope.messageId);
2303
+ } catch (cleanupError) {
2304
+ console.error("world-celld could not abandon a failed Queue reservation", cleanupError);
2305
+ }
2284
2306
  const err = error;
2285
2307
  return errorResponse(500, err.name ?? "Error", err.message ?? String(error));
2286
2308
  }
@@ -2651,6 +2673,16 @@ async function runIsActive(env, runId) {
2651
2673
  if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
2652
2674
  return await namespace.get(namespace.idFromName(runId)).getLifecycleStatus() === "active";
2653
2675
  }
2676
+ /** Resolve a stale shard reservation in its authoritative event writer. */
2677
+ async function resolveExpiredHookClaim(env, owner, claimId, token) {
2678
+ const namespace = env.WORKFLOW_DB;
2679
+ if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
2680
+ return await namespace.get(namespace.idFromName(owner.runId)).resolveExpiredHookClaim({
2681
+ hookId: owner.hookId,
2682
+ claimId,
2683
+ token
2684
+ });
2685
+ }
2654
2686
  //#endregion
2655
2687
  //#region src/worker/durable-objects/HookIdDO.ts
2656
2688
  const MAX_RELEASE_BATCH$1 = 64;
@@ -2679,52 +2711,69 @@ var HookIdDO = class extends DurableObject {
2679
2711
  return await runIsActive(this.env, owner.runId) ? raw : null;
2680
2712
  }
2681
2713
  async reserve(hookId, owner, proposedClaimId) {
2682
- return await this.ctx.storage.transaction(async (txn) => {
2683
- const record = recordKey$1(hookId);
2684
- const claim = claimKey$1(hookId);
2685
- const values = await txn.get([record, claim]);
2686
- const indexed = values.get(record);
2687
- if (indexed !== void 0) return {
2688
- claimed: false,
2689
- holder: ownerFromSerializedHook(indexed)
2690
- };
2691
- let existing = values.get(claim);
2692
- const now = this.now();
2693
- if (existing && existing.expiresAt <= now) {
2694
- await txn.delete([claim, existing.deadlineKey]);
2695
- existing = void 0;
2696
- }
2697
- if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2698
- claimed: false,
2699
- holder: existing.owner
2700
- };
2701
- if (existing === void 0 || existing.claimId !== proposedClaimId) {
2702
- if (existing) await txn.delete(existing.deadlineKey);
2703
- const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2704
- const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2705
- await txn.put(claim, {
2706
- owner,
2707
- claimId: proposedClaimId,
2708
- reservedAt: now,
2709
- expiresAt,
2710
- deadlineKey
2711
- });
2712
- await txn.put(deadlineKey, {
2713
- hookId,
2714
- claimId: proposedClaimId,
2715
- expiresAt
2716
- });
2717
- await this.armAtMost(txn, expiresAt);
2714
+ for (;;) {
2715
+ const observed = await this.ctx.storage.get(claimKey$1(hookId));
2716
+ const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
2717
+ const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId) : void 0;
2718
+ const result = await this.ctx.storage.transaction(async (txn) => {
2719
+ const record = recordKey$1(hookId);
2720
+ const claim = claimKey$1(hookId);
2721
+ const values = await txn.get([record, claim]);
2722
+ const indexed = values.get(record);
2723
+ if (indexed !== void 0) return {
2724
+ claimed: false,
2725
+ holder: ownerFromSerializedHook(indexed)
2726
+ };
2727
+ let existing = values.get(claim);
2728
+ const now = this.now();
2729
+ if (existing && existing.expiresAt <= now) {
2730
+ if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
2731
+ if (committed) {
2732
+ await this.renewClaim(txn, hookId, existing, now);
2733
+ return sameOwner(existing.owner, owner) ? {
2734
+ claimed: true,
2735
+ claimId: existing.claimId
2736
+ } : {
2737
+ claimed: false,
2738
+ holder: existing.owner
2739
+ };
2740
+ }
2741
+ await txn.delete([claim, existing.deadlineKey]);
2742
+ existing = void 0;
2743
+ }
2744
+ if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2745
+ claimed: false,
2746
+ holder: existing.owner
2747
+ };
2748
+ if (existing === void 0 || existing.claimId !== proposedClaimId) {
2749
+ if (existing) await txn.delete(existing.deadlineKey);
2750
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2751
+ const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2752
+ await txn.put(claim, {
2753
+ owner,
2754
+ claimId: proposedClaimId,
2755
+ reservedAt: now,
2756
+ expiresAt,
2757
+ deadlineKey
2758
+ });
2759
+ await txn.put(deadlineKey, {
2760
+ hookId,
2761
+ claimId: proposedClaimId,
2762
+ expiresAt
2763
+ });
2764
+ await this.armAtMost(txn, expiresAt);
2765
+ return {
2766
+ claimed: true,
2767
+ claimId: proposedClaimId
2768
+ };
2769
+ }
2718
2770
  return {
2719
2771
  claimed: true,
2720
- claimId: proposedClaimId
2772
+ claimId: existing.claimId
2721
2773
  };
2722
- }
2723
- return {
2724
- claimed: true,
2725
- claimId: existing.claimId
2726
- };
2727
- });
2774
+ });
2775
+ if (result) return result;
2776
+ }
2728
2777
  }
2729
2778
  async publish(hookId, serializedHook, owner, claimId) {
2730
2779
  if (!await runIsActive(this.env, owner.runId)) {
@@ -2741,10 +2790,7 @@ var HookIdDO = class extends DurableObject {
2741
2790
  const indexed = values.get(record);
2742
2791
  if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook id ${hookId} is owned by another hook`);
2743
2792
  const existingClaim = values.get(claim);
2744
- if (existingClaim && existingClaim.expiresAt <= this.now()) {
2745
- await txn.delete([claim, existingClaim.deadlineKey]);
2746
- return { stored: false };
2747
- }
2793
+ if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook id ${hookId} reservation expired`);
2748
2794
  if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook id ${hookId} is reserved by another hook`);
2749
2795
  if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook id ${hookId} has no active reservation`);
2750
2796
  await txn.put(record, serializedHook);
@@ -2794,21 +2840,51 @@ var HookIdDO = class extends DurableObject {
2794
2840
  const current = await storage.getAlarm();
2795
2841
  if (current === null || at < current) await storage.setAlarm(at);
2796
2842
  }
2843
+ async renewClaim(txn, hookId, claim, now) {
2844
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2845
+ const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2846
+ await txn.delete(claim.deadlineKey);
2847
+ await txn.put(claimKey$1(hookId), {
2848
+ ...claim,
2849
+ expiresAt,
2850
+ deadlineKey
2851
+ });
2852
+ await txn.put(deadlineKey, {
2853
+ hookId,
2854
+ claimId: claim.claimId,
2855
+ expiresAt
2856
+ });
2857
+ await this.armAtMost(txn, expiresAt);
2858
+ }
2797
2859
  async compactClaims() {
2798
2860
  const now = this.now();
2861
+ const due = await this.ctx.storage.list({
2862
+ prefix: CLAIM_DEADLINE_PREFIX$1,
2863
+ end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
2864
+ limit: 129
2865
+ });
2866
+ const page = Array.from(due).slice(0, 128);
2867
+ const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey$1(item.hookId)));
2868
+ const committed = /* @__PURE__ */ new Map();
2869
+ await Promise.all(page.map(async ([, item]) => {
2870
+ const claim = observed.get(claimKey$1(item.hookId));
2871
+ if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
2872
+ const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId);
2873
+ committed.set(claimKey$1(item.hookId), hook !== null);
2874
+ }));
2799
2875
  await this.ctx.storage.transaction(async (txn) => {
2800
- const due = await txn.list({
2801
- prefix: CLAIM_DEADLINE_PREFIX$1,
2802
- end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
2803
- limit: 129
2804
- });
2805
- const page = Array.from(due).slice(0, 128);
2806
2876
  const claims = await txn.get(page.map(([, item]) => claimKey$1(item.hookId)));
2807
- const deletes = page.flatMap(([deadline, item]) => {
2877
+ const deletes = [];
2878
+ for (const [deadline, item] of page) {
2808
2879
  const key = claimKey$1(item.hookId);
2809
2880
  const claim = claims.get(key);
2810
- return claim?.claimId === item.claimId && claim.expiresAt <= now ? [deadline, key] : [deadline];
2811
- });
2881
+ const snapshot = observed.get(key);
2882
+ if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
2883
+ else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
2884
+ if (committed.get(key)) await this.renewClaim(txn, item.hookId, claim, now);
2885
+ else deletes.push(deadline, key);
2886
+ }
2887
+ }
2812
2888
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
2813
2889
  if (due.size > 128) {
2814
2890
  await txn.setAlarm(now + 1);
@@ -2851,51 +2927,68 @@ var HookTokenDO = class extends DurableObject {
2851
2927
  return await runIsActive(this.env, owner.runId) ? raw : null;
2852
2928
  }
2853
2929
  async reserve(token, owner, proposedClaimId) {
2854
- return await this.ctx.storage.transaction(async (txn) => {
2855
- const record = recordKey(token);
2856
- const claim = claimKey(token);
2857
- const values = await txn.get([record, claim]);
2858
- const indexed = values.get(record);
2859
- if (indexed !== void 0) return {
2860
- claimed: false,
2861
- holder: ownerFromSerializedHook(indexed)
2862
- };
2863
- let existing = values.get(claim);
2864
- const now = this.now();
2865
- if (existing && existing.expiresAt <= now) {
2866
- await txn.delete([claim, existing.deadlineKey]);
2867
- existing = void 0;
2868
- }
2869
- if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2870
- claimed: false,
2871
- holder: existing.owner
2872
- };
2873
- if (existing === void 0) {
2874
- const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2875
- const deadlineKey = claimDeadlineKey(token, expiresAt);
2876
- await txn.put(claim, {
2877
- owner,
2878
- claimId: proposedClaimId,
2879
- reservedAt: now,
2880
- expiresAt,
2881
- deadlineKey
2882
- });
2883
- await txn.put(deadlineKey, {
2884
- token,
2885
- claimId: proposedClaimId,
2886
- expiresAt
2887
- });
2888
- await this.armAtMost(txn, expiresAt);
2930
+ for (;;) {
2931
+ const observed = await this.ctx.storage.get(claimKey(token));
2932
+ const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
2933
+ const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId, token) : void 0;
2934
+ const result = await this.ctx.storage.transaction(async (txn) => {
2935
+ const record = recordKey(token);
2936
+ const claim = claimKey(token);
2937
+ const values = await txn.get([record, claim]);
2938
+ const indexed = values.get(record);
2939
+ if (indexed !== void 0) return {
2940
+ claimed: false,
2941
+ holder: ownerFromSerializedHook(indexed)
2942
+ };
2943
+ let existing = values.get(claim);
2944
+ const now = this.now();
2945
+ if (existing && existing.expiresAt <= now) {
2946
+ if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
2947
+ if (committed) {
2948
+ await this.renewClaim(txn, token, existing, now);
2949
+ return sameOwner(existing.owner, owner) ? {
2950
+ claimed: true,
2951
+ claimId: existing.claimId
2952
+ } : {
2953
+ claimed: false,
2954
+ holder: existing.owner
2955
+ };
2956
+ }
2957
+ await txn.delete([claim, existing.deadlineKey]);
2958
+ existing = void 0;
2959
+ }
2960
+ if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2961
+ claimed: false,
2962
+ holder: existing.owner
2963
+ };
2964
+ if (existing === void 0) {
2965
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2966
+ const deadlineKey = claimDeadlineKey(token, expiresAt);
2967
+ await txn.put(claim, {
2968
+ owner,
2969
+ claimId: proposedClaimId,
2970
+ reservedAt: now,
2971
+ expiresAt,
2972
+ deadlineKey
2973
+ });
2974
+ await txn.put(deadlineKey, {
2975
+ token,
2976
+ claimId: proposedClaimId,
2977
+ expiresAt
2978
+ });
2979
+ await this.armAtMost(txn, expiresAt);
2980
+ return {
2981
+ claimed: true,
2982
+ claimId: proposedClaimId
2983
+ };
2984
+ }
2889
2985
  return {
2890
2986
  claimed: true,
2891
- claimId: proposedClaimId
2987
+ claimId: existing.claimId
2892
2988
  };
2893
- }
2894
- return {
2895
- claimed: true,
2896
- claimId: existing.claimId
2897
- };
2898
- });
2989
+ });
2990
+ if (result) return result;
2991
+ }
2899
2992
  }
2900
2993
  async finalize(token, serializedHook, owner, claimId) {
2901
2994
  if (!await runIsActive(this.env, owner.runId)) {
@@ -2912,10 +3005,7 @@ var HookTokenDO = class extends DurableObject {
2912
3005
  const indexed = values.get(record);
2913
3006
  if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook token ${token} is owned by another hook`);
2914
3007
  const existingClaim = values.get(claim);
2915
- if (existingClaim && existingClaim.expiresAt <= this.now()) {
2916
- await txn.delete([claim, existingClaim.deadlineKey]);
2917
- return { stored: false };
2918
- }
3008
+ if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook token ${token} reservation expired`);
2919
3009
  if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook token ${token} is reserved by another hook`);
2920
3010
  if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook token ${token} has no active reservation`);
2921
3011
  await txn.put(record, serializedHook);
@@ -2965,21 +3055,51 @@ var HookTokenDO = class extends DurableObject {
2965
3055
  const current = await storage.getAlarm();
2966
3056
  if (current === null || at < current) await storage.setAlarm(at);
2967
3057
  }
3058
+ async renewClaim(txn, token, claim, now) {
3059
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
3060
+ const deadlineKey = claimDeadlineKey(token, expiresAt);
3061
+ await txn.delete(claim.deadlineKey);
3062
+ await txn.put(claimKey(token), {
3063
+ ...claim,
3064
+ expiresAt,
3065
+ deadlineKey
3066
+ });
3067
+ await txn.put(deadlineKey, {
3068
+ token,
3069
+ claimId: claim.claimId,
3070
+ expiresAt
3071
+ });
3072
+ await this.armAtMost(txn, expiresAt);
3073
+ }
2968
3074
  async compactClaims() {
2969
3075
  const now = this.now();
3076
+ const due = await this.ctx.storage.list({
3077
+ prefix: CLAIM_DEADLINE_PREFIX,
3078
+ end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
3079
+ limit: 129
3080
+ });
3081
+ const page = Array.from(due).slice(0, 128);
3082
+ const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey(item.token)));
3083
+ const committed = /* @__PURE__ */ new Map();
3084
+ await Promise.all(page.map(async ([, item]) => {
3085
+ const claim = observed.get(claimKey(item.token));
3086
+ if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
3087
+ const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId, item.token);
3088
+ committed.set(claimKey(item.token), hook !== null);
3089
+ }));
2970
3090
  await this.ctx.storage.transaction(async (txn) => {
2971
- const due = await txn.list({
2972
- prefix: CLAIM_DEADLINE_PREFIX,
2973
- end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
2974
- limit: 129
2975
- });
2976
- const page = Array.from(due).slice(0, 128);
2977
3091
  const claims = await txn.get(page.map(([, item]) => claimKey(item.token)));
2978
- const deletes = page.flatMap(([deadline, item]) => {
3092
+ const deletes = [];
3093
+ for (const [deadline, item] of page) {
2979
3094
  const key = claimKey(item.token);
2980
3095
  const claim = claims.get(key);
2981
- return claim?.claimId === item.claimId && claim.expiresAt <= now ? [deadline, key] : [deadline];
2982
- });
3096
+ const snapshot = observed.get(key);
3097
+ if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
3098
+ else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
3099
+ if (committed.get(key)) await this.renewClaim(txn, item.token, claim, now);
3100
+ else deletes.push(deadline, key);
3101
+ }
3102
+ }
2983
3103
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
2984
3104
  if (due.size > 128) {
2985
3105
  await txn.setAlarm(now + 1);
@@ -3456,6 +3576,15 @@ var StreamDO = class extends DurableObject {
3456
3576
  size: validateSegmentSize(entry[1], start)
3457
3577
  });
3458
3578
  }
3579
+ let covered = startIndex;
3580
+ for (let index = 0; index < planned.length; index++) {
3581
+ const { start, size } = planned[index];
3582
+ if (start > covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
3583
+ if (index > 0 && start !== covered) throw new Error(`Overlapping persisted stream segment at index ${start}`);
3584
+ if (start + size.count <= covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
3585
+ covered = start + size.count;
3586
+ }
3587
+ if (covered < endIndex) throw new Error(`Missing persisted stream segment at index ${covered}`);
3459
3588
  const selected = [];
3460
3589
  let plannedBytes = 0;
3461
3590
  for (const segment of planned) {
@@ -4011,6 +4140,28 @@ var WorkflowRunDO = class extends DurableObject {
4011
4140
  async getRun() {
4012
4141
  return this.readKey("run");
4013
4142
  }
4143
+ /** Fence both supported event-claim forms before an expired shard claim is released. */
4144
+ async resolveExpiredHookClaim(request) {
4145
+ if (typeof request?.hookId !== "string" || request.hookId.length === 0 || typeof request.claimId !== "string" || request.claimId.length === 0 || request.token !== void 0 && typeof request.token !== "string") throw new TypeError("world-celld expired hook claim resolution is invalid");
4146
+ return await this.ctx.storage.transaction(async (txn) => {
4147
+ const { tombstone } = await this.retentionState(txn);
4148
+ if (tombstone) return null;
4149
+ const run = await txn.get("run");
4150
+ if (run && isTerminalWorkflowRunStatus(run.status)) return null;
4151
+ const hook = await txn.get(`${HOOK_KEY_PREFIX}${request.hookId}`);
4152
+ if (hook && (request.token === void 0 || hook.token === request.token)) return hook;
4153
+ const cancellation = {
4154
+ hookId: request.hookId,
4155
+ token: request.token,
4156
+ canceledAt: this.now()
4157
+ };
4158
+ await txn.put({
4159
+ [hookClaimCancellationKey(`${request.claimId}:${request.claimId}`)]: cancellation,
4160
+ [hookClaimCancellationKey(`-:${request.claimId}`)]: cancellation
4161
+ });
4162
+ return null;
4163
+ });
4164
+ }
4014
4165
  /** Single authoritative lifecycle answer used after derivative compaction. */
4015
4166
  async getLifecycleStatus() {
4016
4167
  return await this.ctx.storage.transaction(async (txn) => {
@@ -4642,7 +4793,7 @@ var WorkflowRunDO = class extends DurableObject {
4642
4793
  * the run expiry check.
4643
4794
  */
4644
4795
  async reserveRunQueueMessage(params) {
4645
- if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld run queue reservation is invalid");
4796
+ if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt) || params.publicationPending !== void 0 && typeof params.publicationPending !== "boolean") throw new TypeError("world-celld run queue reservation is invalid");
4646
4797
  return await this.ctx.storage.transaction(async (txn) => {
4647
4798
  const key = runQueueClaimKey(params.claimName);
4648
4799
  const values = await txn.get([
@@ -4662,17 +4813,19 @@ var WorkflowRunDO = class extends DurableObject {
4662
4813
  };
4663
4814
  const state = values.get(key) ?? {};
4664
4815
  const existing = state.reservation;
4665
- if (existing && existing.expiresAt > this.now() && existing.messageId !== params.messageId) return {
4816
+ if (existing && existing.expiresAt > this.now() && (existing.messageId !== params.messageId || params.publicationPending)) return {
4666
4817
  ok: true,
4667
4818
  scope: "run",
4668
4819
  admitted: false,
4669
- messageId: existing.messageId
4820
+ messageId: existing.messageId,
4821
+ ...existing.publicationPending ? { publicationPending: true } : {}
4670
4822
  };
4671
4823
  await txn.put(key, {
4672
4824
  ...state,
4673
4825
  reservation: {
4674
4826
  messageId: params.messageId,
4675
- expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
4827
+ expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
4828
+ ...params.publicationPending ? { publicationPending: true } : {}
4676
4829
  }
4677
4830
  });
4678
4831
  return {
@@ -4683,6 +4836,21 @@ var WorkflowRunDO = class extends DurableObject {
4683
4836
  };
4684
4837
  });
4685
4838
  }
4839
+ async confirmRunQueueMessagePublication(params) {
4840
+ if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld run queue publication confirmation is invalid");
4841
+ await this.ctx.storage.transaction(async (txn) => {
4842
+ const key = runQueueClaimKey(params.claimName);
4843
+ const state = await txn.get(key);
4844
+ if (state?.reservation?.messageId !== params.messageId) return;
4845
+ await txn.put(key, {
4846
+ ...state,
4847
+ reservation: {
4848
+ messageId: params.messageId,
4849
+ expiresAt: Math.max(state.reservation.expiresAt, params.expiresAt)
4850
+ }
4851
+ });
4852
+ });
4853
+ }
4686
4854
  /** Delivery-side run expiry check and in-flight claim in one transaction. */
4687
4855
  async claimRunQueueMessage(params) {
4688
4856
  if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isPositiveSafeInteger(params.staleMs)) throw new TypeError("world-celld run queue claim is invalid");
@@ -4694,9 +4862,19 @@ var WorkflowRunDO = class extends DurableObject {
4694
4862
  key
4695
4863
  ]);
4696
4864
  if (this.tombstoneFrom(values)) return { expired: true };
4697
- const state = values.get(key) ?? {};
4698
- const existing = state.claim;
4865
+ let state = values.get(key) ?? {};
4699
4866
  const now = this.now();
4867
+ if (state.reservation?.publicationPending && state.reservation.messageId === params.messageId) {
4868
+ state = {
4869
+ ...state,
4870
+ reservation: {
4871
+ messageId: params.messageId,
4872
+ expiresAt: Math.max(state.reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
4873
+ }
4874
+ };
4875
+ await txn.put(key, state);
4876
+ }
4877
+ const existing = state.claim;
4700
4878
  if (existing && existing.expiresAt > now) {
4701
4879
  if (existing.messageId !== params.messageId) return {
4702
4880
  expired: false,
@@ -4768,6 +4946,18 @@ var WorkflowRunDO = class extends DurableObject {
4768
4946
  else await txn.delete(key);
4769
4947
  });
4770
4948
  }
4949
+ /** Failed enqueue cleanup must preserve delivery already accepted by the broker. */
4950
+ async abandonRunQueueMessageReservation(params) {
4951
+ if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0) throw new TypeError("world-celld run queue reservation abandonment is invalid");
4952
+ await this.ctx.storage.transaction(async (txn) => {
4953
+ const key = runQueueClaimKey(params.claimName);
4954
+ const state = await txn.get(key);
4955
+ if (state?.reservation?.messageId !== params.messageId) return;
4956
+ const { reservation: _abandoned, ...rest } = state;
4957
+ if (rest.claim) await txn.put(key, rest);
4958
+ else await txn.delete(key);
4959
+ });
4960
+ }
4771
4961
  async registerQueuePayload(registration) {
4772
4962
  if (!registration || typeof registration.messageId !== "string" || registration.messageId.length === 0 || typeof registration.key !== "string" || registration.key.length === 0 || !isNonNegativeSafeInteger(registration.orphanExpiresAt)) throw new TypeError("world-celld queue payload registration is invalid");
4773
4963
  return await this.ctx.storage.transaction(async (txn) => {
@@ -4824,22 +5014,24 @@ var WorkflowRunDO = class extends DurableObject {
4824
5014
  });
4825
5015
  }
4826
5016
  async reserveQueueMessage(params) {
4827
- if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld queue message reservation is invalid");
5017
+ if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt) || params.publicationPending !== void 0 && typeof params.publicationPending !== "boolean") throw new TypeError("world-celld queue message reservation is invalid");
4828
5018
  return await this.ctx.storage.transaction(async (txn) => {
4829
5019
  const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
4830
5020
  const existing = values.get(QUEUE_RESERVATION_KEY);
4831
5021
  const claim = values.get("claim");
4832
5022
  const now = this.now();
4833
- if (existing && existing.expiresAt > now && existing.messageId !== params.messageId) {
5023
+ if (existing && existing.expiresAt > now && (existing.messageId !== params.messageId || params.publicationPending)) {
4834
5024
  await txn.setAlarm(Math.min(existing.expiresAt, claim?.expiresAt ?? existing.expiresAt));
4835
5025
  return {
4836
5026
  admitted: false,
4837
- messageId: existing.messageId
5027
+ messageId: existing.messageId,
5028
+ ...existing.publicationPending ? { publicationPending: true } : {}
4838
5029
  };
4839
5030
  }
4840
5031
  const reservation = {
4841
5032
  messageId: params.messageId,
4842
- expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
5033
+ expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
5034
+ ...params.publicationPending ? { publicationPending: true } : {}
4843
5035
  };
4844
5036
  await txn.put(QUEUE_RESERVATION_KEY, reservation);
4845
5037
  await txn.setAlarm(Math.min(reservation.expiresAt, claim?.expiresAt ?? reservation.expiresAt));
@@ -4849,6 +5041,20 @@ var WorkflowRunDO = class extends DurableObject {
4849
5041
  };
4850
5042
  });
4851
5043
  }
5044
+ async confirmQueueMessagePublication(params) {
5045
+ if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld queue publication confirmation is invalid");
5046
+ await this.ctx.storage.transaction(async (txn) => {
5047
+ const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
5048
+ const existing = values.get(QUEUE_RESERVATION_KEY);
5049
+ if (existing?.messageId !== params.messageId) return;
5050
+ const reservation = {
5051
+ messageId: params.messageId,
5052
+ expiresAt: Math.max(existing.expiresAt, params.expiresAt)
5053
+ };
5054
+ await txn.put(QUEUE_RESERVATION_KEY, reservation);
5055
+ await txn.setAlarm(Math.min(reservation.expiresAt, values.get("claim")?.expiresAt ?? reservation.expiresAt));
5056
+ });
5057
+ }
4852
5058
  async completeQueueMessage(messageId) {
4853
5059
  if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue messageId must be a non-empty string");
4854
5060
  await this.ctx.storage.transaction(async (txn) => {
@@ -4868,13 +5074,32 @@ var WorkflowRunDO = class extends DurableObject {
4868
5074
  else await txn.deleteAlarm();
4869
5075
  });
4870
5076
  }
5077
+ /** Abandon the enqueue reservation without releasing an active delivery claim. */
5078
+ async abandonQueueMessageReservation(messageId) {
5079
+ if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue reservation messageId must be a non-empty string");
5080
+ await this.ctx.storage.transaction(async (txn) => {
5081
+ const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
5082
+ if (values.get(QUEUE_RESERVATION_KEY)?.messageId !== messageId) return;
5083
+ await txn.delete(QUEUE_RESERVATION_KEY);
5084
+ const claim = values.get("claim");
5085
+ if (claim) await txn.setAlarm(claim.expiresAt);
5086
+ else await txn.deleteAlarm();
5087
+ });
5088
+ }
4871
5089
  async claimInflight(params) {
4872
5090
  if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isPositiveSafeInteger(params.staleMs)) throw new TypeError("world-celld queue claim is invalid");
4873
5091
  return await this.ctx.storage.transaction(async (txn) => {
4874
5092
  const values = await txn.get(["claim", QUEUE_RESERVATION_KEY]);
4875
5093
  const existing = values.get("claim");
4876
- const reservation = values.get(QUEUE_RESERVATION_KEY);
5094
+ let reservation = values.get(QUEUE_RESERVATION_KEY);
4877
5095
  const now = this.now();
5096
+ if (reservation?.publicationPending && reservation.messageId === params.messageId) {
5097
+ reservation = {
5098
+ messageId: params.messageId,
5099
+ expiresAt: Math.max(reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
5100
+ };
5101
+ await txn.put(QUEUE_RESERVATION_KEY, reservation);
5102
+ }
4878
5103
  if (existing && existing.expiresAt > now) {
4879
5104
  if (existing.messageId !== params.messageId) return { claimed: false };
4880
5105
  if (existing.retryAt === void 0 || existing.retryAt > now) return {