Filed by the domain:services PM seat (session session_01UQgPSniH1GFM9ZDeGyuGUa), surfaced by the implementer of #11859 (PR #12282) and verified by this seat against origin/main. Recording, not claiming — unassigned, and deliberately without domain:* or pm:queue: routing is triage's, grading is triage's.
⚠️Filed rather than fixed, on purpose.packages/spec has zero ownership in the domain:services lane, so neither the dispatched dev nor this seat may touch it. The dispatch brief told the dev to stop short of the edit and report the lines plus proposed wording; it did exactly that. Same route as #11892, which this lane filed on 2026-08-24 for the previous iteration of this same bullet and which the spec seat landed as PR #12089 earlier today.
The gap
packages/spec/src/api/error-code-ledger.zod.ts:530-536, quoted from origin/main:
After #11859 (PR #12282, ruled option C — the claim credential rides the record claim() returns and ack() takes it back), two statements in that bullet stop being the whole truth:
- The refusal set is wider than "not
in_flight". It now also refuses a row that isin_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. It additionally refuses a record handed back carrying no claim credential at all. - "Also raised by
SqlNotificationOutbox" is now too narrow — MemoryNotificationOutbox raises the lost-claim refusal too. Both backends are pinned on the same table in outbox-ack-claim-ownership.integration.test.ts.
Proposed wording (the implementer's, verified by this seat against the merged code)
⛔ Nothing about behaviour changes; no code name changes; no packages/runtime vocabulary changes. As with #11892, this is a docs catch-up.
⚠️Sequencing: this describes post-#11859 behaviour, so it should land after PR #12282. Landing it first would make the ledger describe a tree that does not exist yet.
⭐ The observation worth more than the wording — this bullet has now lagged twice in two days
Both lags have the same mechanism: the bullet enumerates a surface's refusal cases, the surface lives in another package, and the lane that tightens it is structurally forbidden from editing the prose describing it. Every tightening therefore produces a cross-lane docs card, and the prose is correct only in the window between them.
⚠️This is an observation, not a prescription — whether an enumerating gloss is the right shape for a per-surface refusal set is the spec lane's call, not this seat's. Recording it because two data points in two days is the moment the pattern is cheapest to see, and because the third one is already foreseeable: the deferred cancellation surface (#11454) will touch the same bullet when it lands. If the answer is "keep enumerating", that is a fine answer — it just means budgeting a docs card per tightening rather than being surprised by one.
Refs
#11859 / PR #12282 (the change that widened the refusal set) · #11892 / PR #12089 (the previous iteration of this exact lag, same filing lane, same route) · #11453 / PR #11858 (the iteration before that) · #11454 (the deferred cancellation surface — the likely next one) · ADR-0112 (the closed error-code set)
Filed by the
domain:servicesPM seat (sessionsession_01UQgPSniH1GFM9ZDeGyuGUa), surfaced by the implementer of #11859 (PR #12282) and verified by this seat againstorigin/main. Recording, not claiming — unassigned, and deliberately withoutdomain:*orpm:queue: routing is triage's, grading is triage's.packages/spechas zero ownership in thedomain:serviceslane, so neither the dispatched dev nor this seat may touch it. The dispatch brief told the dev to stop short of the edit and report the lines plus proposed wording; it did exactly that. Same route as #11892, which this lane filed on 2026-08-24 for the previous iteration of this same bullet and which the spec seat landed as PR #12089 earlier today.The gap
packages/spec/src/api/error-code-ledger.zod.ts:530-536, quoted fromorigin/main:After #11859 (PR #12282, ruled option C — the claim credential rides the record
claim()returns andack()takes it back), two statements in that bullet stop being the whole truth:in_flight". It now also refuses a row that isin_flightbut 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 theclaimTtlMsreap plus a re-claim by any node, including the caller's own later claim, matches nothing. It additionally refuses a record handed back carrying no claim credential at all.SqlNotificationOutbox" is now too narrow —MemoryNotificationOutboxraises the lost-claim refusal too. Both backends are pinned on the same table inoutbox-ack-claim-ownership.integration.test.ts.Proposed wording (the implementer's, verified by this seat against the merged code)
⛔ Nothing about behaviour changes; no code name changes; no
packages/runtimevocabulary changes. As with #11892, this is a docs catch-up.⭐ The observation worth more than the wording — this bullet has now lagged twice in two days
ack()on an unclaimedpendingrow silently succeeds in both implementations #11453 / PR fix(service-messaging): enforce ack()'s claimed-row precondition in both outbox implementations #11858 gaveackthe not-in_flightrefusal. The gloss lagged; filed asDELIVERY_NOT_ELIGIBLE's ledger gloss describes only theredeliverrefusal, and is now narrower than the code's uses #11892; fixed by PR docs(spec): widen DELIVERY_NOT_ELIGIBLE's ledger gloss to both delivery surfaces #12089 at 09:02Z on 2026-08-25.INotificationOutbox.ack()carries nonodeId, so its compare-and-set can prove a claim exists but not whose #11859 / PR feat(service-messaging): bind the claim credential in ack()'s compare-and-set #12282 givesackthe ownership refusal. The same bullet lags again, ~7 hours after the previous repair merged.Both lags have the same mechanism: the bullet enumerates a surface's refusal cases, the surface lives in another package, and the lane that tightens it is structurally forbidden from editing the prose describing it. Every tightening therefore produces a cross-lane docs card, and the prose is correct only in the window between them.
Refs
#11859 / PR #12282 (the change that widened the refusal set) · #11892 / PR #12089 (the previous iteration of this exact lag, same filing lane, same route) · #11453 / PR #11858 (the iteration before that) · #11454 (the deferred cancellation surface — the likely next one) · ADR-0112 (the closed error-code set)