diff --git a/.changeset/delivery-not-eligible-ack-claim-ownership.md b/.changeset/delivery-not-eligible-ack-claim-ownership.md new file mode 100644 index 0000000000..2576ea92d5 --- /dev/null +++ b/.changeset/delivery-not-eligible-ack-claim-ownership.md @@ -0,0 +1,27 @@ +--- +'@objectstack/spec': patch +--- + +`DELIVERY_NOT_ELIGIBLE`'s `INotificationOutbox.ack` gloss now describes the post-#11859 refusal set + +The `ERROR_CODE_LEDGER` entry for `DELIVERY_NOT_ELIGIBLE` enumerates `ack`'s +refusal cases per surface. #11859 widened that set on the notification surface — +the claim credential now rides the record `claim()` returns and `ack()` takes it +back — and two statements in the bullet stopped being the whole truth: + +- **The refusal set is wider than "not `in_flight`".** `ack` now also refuses a + row that **is** `in_flight` but is no longer held by the claim being + completed: ownership joined the predicate, and the credential is the + (`claimed_by`, `claimed_at`) **pair**, so a claim lost to the `claimTtlMs` + reap plus a re-claim by any node — including the caller's own later claim — + matches nothing and nothing is written. It additionally refuses a record + handed back carrying no claim credential at all. +- **"Also raised by `SqlNotificationOutbox`" was too narrow.** + `MemoryNotificationOutbox` raises the lost-claim refusal too; both backends + are pinned on one table in `outbox-ack-claim-ownership.integration.test.ts`, + and both route through the shared refusal messages in `outbox.ts` so the two + cannot drift into different wordings for one contract violation. + +Comment only. No code is registered or removed, no wire value changes, and no +acceptance or refusal behaviour moves — `packages/spec` publishes +`src/**/*.zod.ts`, so the corrected gloss ships to consumers reading the ledger. diff --git a/packages/spec/src/api/error-code-ledger.zod.ts b/packages/spec/src/api/error-code-ledger.zod.ts index f804174b4f..09df76ca40 100644 --- a/packages/spec/src/api/error-code-ledger.zod.ts +++ b/packages/spec/src/api/error-code-ledger.zod.ts @@ -569,13 +569,20 @@ export const ERROR_CODE_LEDGER = { // re-claimed the row mid-call — both `SqlHttpOutbox` and // `MemoryHttpOutbox` report that miss instead of a false success // (#11009). - // - `INotificationOutbox.ack` (`NotificationAckError`; #11453) refuses a - // row that is not `in_flight` — which covers BOTH an unclaimed - // `pending` row (the ack-as-cancel trap) and an already-terminal one, - // because `ack` records the outcome of a delivery the caller CLAIMED. - // Also raised by `SqlNotificationOutbox` when its compare-and-set - // read-back shows the claim was lost mid-ack (a slow send outrunning - // `claimTtlMs`), so nothing was written. + // - `INotificationOutbox.ack` (`NotificationAckError`; #11453, #11859) + // refuses a row that is not `in_flight` — an unclaimed `pending` row + // (the ack-as-cancel trap) or an already-terminal one, because `ack` + // records the outcome of a delivery the caller CLAIMED — AND, since + // #11859, an `in_flight` row no longer held by the claim being + // completed: `ack` takes back the record `claim()` returned and the + // compare-and-set binds its (`claimed_by`, `claimed_at`) credential, + // so a claim lost to the `claimTtlMs` reap plus a re-claim (by ANY + // node, including the caller's own later claim) matches nothing and + // nothing is written. Also raised for a record handed back carrying + // no claim credential at all. BOTH backends raise every one of these + // refusals — `SqlNotificationOutbox` and `MemoryNotificationOutbox`, + // pinned on one table in + // `outbox-ack-claim-ownership.integration.test.ts`. // Distinct from DELIVERY_NEVER_SENT: this one says "wrong state for THIS // operation, try when it settles"; that one says "never, fix the // configuration instead".