docs(events): the acceptance walk, and the three contracts it moved (Phase 16a)
Phase 16 is split into 16a (the walk), 16b (the cutover) and 16c (runicgateway.com + .profile), because the phase as written asked for a walk "against released artefacts" BEFORE the cutover and all three component repos release on push to `main`. The walk therefore runs against artefacts built from `edge` the way a release builds them, and 16b re-verifies against the real bundle. `EVENTS_PLAN.md` gains the 16a record: the rig, all three deliberate failures passing, the six defects, the one finding withdrawn, and what each fix was verified against. Three contracts move, each because the walk proved the built thing did not match the written one: **`link/v6.md` — a refusal does not spend its key.** Rule 2 had two cases, throw and return, and needed a third: a handler that ran to completion and deliberately refused did nothing, so freezing that refusal as the key's answer made a refusal that WAITING FIXES impossible to retry past. The section now carries the case `uo.world.save` found it with, and the rule the release rests on — do not answer `*.error` after changing the world. `[bridge status` gains `refused=`. **`website/MODULE_API.md` — `revert`'s `idempotencyKey` identifies a dispatch; it is not a key to send on the undo.** The paragraph explained what the key is FOR and never said what it is not, and `module-uo` read it the other way: every despawn went out under the key its spawn had used, so a store that keys on the key alone answered the undo with the DO's reply and teardown became a no-op that reported success. **`website/EVENTS.md` §I — the public calendar matches a run that OVERLAPS the window.** The row promised "upcoming, live and recent" and the built route served only the first, because it read the start instant and a live run has already started. The default window now reaches back so "recent" has somewhere to live, and projections are forecast from now rather than into that tail. Pairs with `website#`, `Module-uo#` and `servuo-plugins#`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
31
link/v6.md
31
link/v6.md
@@ -80,10 +80,31 @@ A repeat of a key still in flight is answered **`bridge.busy`**: nothing runs, a
|
||||
told to come back. It is deliberately not spelled `bridge.busy.error` — nothing is wrong, the work
|
||||
is happening.
|
||||
|
||||
**2. A key that has begun is never released.** Not even when the handler throws. Releasing it would
|
||||
let a retry re-run a command that may have applied half of itself, which is the exact failure this
|
||||
file exists to prevent. A handler that throws stores a `bridge.error` reply instead, so the retry
|
||||
gets a definite answer and the step fails once rather than looping.
|
||||
**2. A key that has begun is never released — except on a refusal.** Not when the handler throws.
|
||||
Releasing it would let a retry re-run a command that may have applied half of itself, which is the
|
||||
exact failure this file exists to prevent. A handler that throws stores a `bridge.error` reply
|
||||
instead, so the retry gets a definite answer and the step fails once rather than looping.
|
||||
|
||||
**A REFUSAL is the third case**, added by the Phase 16 acceptance walk and amending protocol 7 in
|
||||
place. A handler that ran to completion and answered `*.error` did not do anything — every refusal
|
||||
on this plane is a guard: a missing `runId`, an unknown item, a cap, a rate limit, a write that
|
||||
failed and left the value alone. Remembering it froze the answer for ever, so a refusal that
|
||||
*waiting fixes* could never be retried past. `uo.world.save` is the case that found it: the shard
|
||||
saves at most every 300 seconds, the module documents that as "the one refusal on this plane that
|
||||
waiting fixes", and six attempts over four minutes all replayed one frozen sentence — "the last save
|
||||
was 227 seconds ago" — because the number was the first reply's, not the clock's. A step's key is
|
||||
one value for the life of the step, so the operator's retry control could not escape it either.
|
||||
|
||||
So a refusal releases the key: nothing happened, and the caller may ask again. The refusal is still
|
||||
**emitted** to the caller, which is what ends that attempt; it is simply not remembered as the key's
|
||||
answer. A refusal is recognised by its `kind` ending in `.error`, matched on the suffix so a handler
|
||||
family added later is covered without extending a list. `bridge.error` is excluded deliberately —
|
||||
that is the reply the shard writes when a handler THREW, which is the case whose key must be kept.
|
||||
|
||||
**This puts a rule on handlers, and it is the rule the release rests on: do not answer `*.error`
|
||||
after changing the world.** Report a partial change in an `ok` reply, as `item.grant` does with
|
||||
`granted`/`missed` and `world.despawn` with `removed`/`gone`/`refused`. The shard cannot verify
|
||||
"nothing happened"; it takes the `.error` kind as the claim.
|
||||
|
||||
**3. A replay is stamped with the REPEAT's correlation id.** The sidecar's `reqId` is a fresh
|
||||
per-process counter, so a retry is waiting on an id the first attempt never used. Replaying the
|
||||
@@ -102,7 +123,7 @@ evicted key's repeat *would* be applied a second time — so an eviction that dr
|
||||
its TTL prints a console warning naming the count. If the promise is ever actually breached, an
|
||||
operator reads it here rather than discovering a doubled spawn in the world.
|
||||
|
||||
`[bridge status` reports `idem(keys= seen= replayed= busy= evicted= uncorrelated=)`.
|
||||
`[bridge status` reports `idem(keys= seen= replayed= busy= evicted= uncorrelated= refused=)`.
|
||||
|
||||
#### 2.1.1 How the reply is captured
|
||||
|
||||
|
||||
Reference in New Issue
Block a user