Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/delivery-not-eligible-gloss-both-surfaces.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/spec': patch
---

`DELIVERY_NOT_ELIGIBLE`'s ledger gloss now describes every surface that raises it, not just `redeliver`

The `ERROR_CODE_LEDGER` entry for `DELIVERY_NOT_ELIGIBLE` glossed the code as
*"delivery row is in a non-terminal state"*. That named one refusal on one
surface, and the code has since been reused on a second: `INotificationOutbox.ack`
refuses any row that is not `in_flight`, which covers an unclaimed `pending` row
**and** an already-terminal one. So the old wording was not merely incomplete —
it was backwards for half the code's uses, describing terminal rows as the
acceptable ones when `ack` refuses exactly those.

The reuse itself is the ruled shape, not a defect: one concept — *this delivery
row's state does not permit the requested operation* — on two delivery surfaces,
with a second near-synonym code rejected for the vocabulary sprawl ADR-0112
exists to prevent. Only the comment lagged.

The gloss is now stated per surface, because the two refuse opposite halves of
the state space and no single status predicate covers both:

- **`IHttpOutbox.redeliver`** (`HttpRedeliverError`) refuses a row that is NOT
terminal — `redeliver` means send this again, so it wants
`success`/`failed`/`dead`. It also raises the same code when the producer's
`RedeliverGuard` refuses or itself throws (fail-closed: "we could not check"
must never read as "allowed"), and when the terminal re-check at the write
misses because a dispatcher tick re-claimed the row mid-call.
- **`INotificationOutbox.ack`** (`NotificationAckError`) refuses a row that is
not `in_flight` — both the unclaimed `pending` row (the ack-as-cancel trap)
and the already-terminal one — plus the `SqlNotificationOutbox` compare-and-set
read-back that shows the claim was lost mid-ack.

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.
29 changes: 28 additions & 1 deletion packages/spec/src/api/error-code-ledger.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -510,7 +510,34 @@ export const ERROR_CODE_LEDGER = {
],
'@objectstack/service-messaging': [
'DELIVERY_NEVER_SENT', // [#8069] terminal delivery row with 0 attempts — a PARKED record of a delivery that could never be prepared, not one that failed. Redelivering it would be a FIRST send, and the row carries no HMAC signature because the secret that would have produced one is exactly what went missing, so it would go out unsigned (#7799). Distinct from DELIVERY_NOT_ELIGIBLE: that one says "wrong state, try when it settles"; this one says "never, fix the configuration instead"
'DELIVERY_NOT_ELIGIBLE', // delivery row is in a non-terminal state
// "this delivery row's state does not permit the requested operation" —
// ONE concept on TWO delivery surfaces of this package, deliberately
// sharing one spelling (PR #11858's contract-review PASS ruled option B;
// a second near-synonym code was rejected for the vocabulary sprawl
// ADR-0112 exists to prevent). Stated per-surface because the two refuse
// OPPOSITE halves of the state space — no single status predicate glosses
// both, and the older "non-terminal state" wording named only the first:
// - `IHttpOutbox.redeliver` (`HttpRedeliverError`; the operator
// redeliver button) refuses a row that is NOT terminal —
// `redeliver` means send this AGAIN, so it wants `success`/`failed`/
// `dead` (`assertHttpRedeliverable`). Also raised when the producer's
// `RedeliverGuard` refuses, or itself throws (fail-closed on purpose:
// "we could not check" must never read as "allowed"), and when the
// terminal re-check AT THE WRITE misses because a dispatcher tick
// 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.
// 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".
'DELIVERY_NOT_ELIGIBLE',
],
'@objectstack/trigger-api': [
'ENQUEUE_FAILED', // queue accepted the call but publish threw
Expand Down
Loading