docs(events): the Event System record — the cutover step 16b missed (edge → main)
#232
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
|
||||
|
||||
|
||||
@@ -1847,7 +1847,7 @@ no URL moved.
|
||||
| `DELETE /admin/events/series/:seriesId` | admin, editor | delete it, detaching its definitions; answers with how many |
|
||||
| `GET /admin/events/calendar` | staff | the calendar for a window: materialised runs and projected occurrences (Phase 4) |
|
||||
| `GET/PUT /admin/events/actions` | admin | which actions are enabled on this deployment, and their per-run caps (Phase 6). `admin` on the read as well as the write; the PUT takes one action at a time |
|
||||
| `GET /public/events` | — | **the calendar** (Phase 14a): upcoming, live and recent, by series. Runs and projections interleaved and each saying which it is, ascending by instant. Instants are UTC and every entry carries the EVENT's own zone; the reader's zone places them. Rehearsals and unlisted events are absent. Defaults to now through 31 days out and the window may span at most 92 — the anonymous surface is the one with no login in front of it |
|
||||
| `GET /public/events` | — | **the calendar** (Phase 14a): upcoming, live and recent, by series. Runs and projections interleaved and each saying which it is, ascending by instant. Instants are UTC and every entry carries the EVENT's own zone; the reader's zone places them. Rehearsals and unlisted events are absent. A run is an INTERVAL, not an instant: an entry is in the window when the run OVERLAPS it, so one that began before the window and has not ended is still "what is on" (Phase 16a — reading the start instant alone made this route serve only the first of its three words, while the event's own page said `live`). Defaults to seven days back through 31 days out — the tail is where "recent" lives — and the window may span at most 92; the anonymous surface is the one with no login in front of it. Projections are forecast from NOW, never into the tail, since a slot the runner has already passed did not happen |
|
||||
| `GET /public/events/:slug` | — | **one event** (Phase 14a): storyline, arc, what is live, what is next, what happened recently, and a results table once one is published. Takes an optional `?run=`, which is what an announcement's link carries, so a mail about last Friday's occurrence does not open next Friday's; a run belonging to some other event is **ignored rather than refused**, because a stale link in a months-old mail should land on the event it was about. A draft, an archived definition and an unlisted one all answer 404 |
|
||||
| `GET /public/events/series/:slug` | — | **the arc** (Phase 14a). A series with no listed events is a 404, not an empty page: the arc is a label on its definitions, so a page for an empty one would publish the fact that an operator has named something they have not announced |
|
||||
| `GET /player/events/history` | auth | **this account's participation** (Phase 14a) — the run, when it was, the score a module reported, and the rank once results were published (null until then, which is a real state rather than an error). Self-scoped on the session with **no id parameter**, deliberately: a route that took one would be a middleware mistake away from publishing who attended what. Keyset-paged on the participation row's id. It obeys the calendar's two exclusions, so attending an unannounced event does not disclose that it exists |
|
||||
|
||||
@@ -1820,6 +1820,65 @@ website, emulator — running a real multi-phase event, including three delibera
|
||||
Then `edge` → `main`, in the order every previous cutover used: the protocol side first, the module,
|
||||
core, docs, then the kit's re-pin and `runicgateway.com`.
|
||||
|
||||
> **Split into 16a (the walk) and 16b (the cutover)** (org lead, 2026-09-09), on the same argument
|
||||
> 12a/12b and 14a/14b were split on. The two sentences above cannot both hold: `link`,
|
||||
> `servuo-plugins` and `Module-uo` all release on push to **`main`**, so no released artefact
|
||||
> carrying events can exist until after the cutover. Engagement Phase 13 met the same wall and
|
||||
> resolved it the other way, cutting over first and walking from `main`. Here the walk goes first
|
||||
> against artefacts built from `edge` exactly the way a release builds them, because every walk in
|
||||
> this workstream has found defects and a defect found on `edge` is a reviewed PR rather than a
|
||||
> hotfix to `main`. **16b re-verifies against the real released bundle** — install, boot, run one
|
||||
> event — so the delivery path is still proved, just second. A third leg, **16c**, carries
|
||||
> `runicgateway.com` and `.profile`.
|
||||
>
|
||||
> **16a WALKED, and it is four repos** — `Module-uo`, `website`, `servuo-plugins`, `docs`. The whole
|
||||
> rig: real ServUO 57.4 (208k items, 42k mobiles) → a `cargo --release` sidecar on protocol 7 → core
|
||||
> with the module installed from a release-shaped bundle → the Android app on an emulator. The
|
||||
> overlay was deployed from a tarball built the way CI builds one, into a tree with `Scripts/Custom/
|
||||
> Bridge` and `Saves/Bridge` deleted first, so it was a first install rather than an upgrade.
|
||||
>
|
||||
> **All three deliberate failures pass.** (1) A mid-run process kill landed mid-TEARDOWN — sharper
|
||||
> than mid-step, since a phase executes in about a second — with the run `completed`, cleanup
|
||||
> `pending`, a lease half-returned and 21 world objects up: teardown resumed on restart and all 15
|
||||
> steps still read `attempts = 1`, so nothing re-executed. (2) The sidecar killed during a phase gate
|
||||
> left the run `degraded` rather than failed, `core.lease` retrying with a reason, and the four world
|
||||
> writes behind it **parked at `attempts = 0`**; the shard reconnected on its own. (3) A cap of 5
|
||||
> against a step asking for 12 answered `refused` — its own status — with `code: "cap"` and *"asks
|
||||
> for 12 of `uo.creatures`; 0 of 5 is already spent this run"*, and the dry run had already refused
|
||||
> it in the author's own words.
|
||||
>
|
||||
> **Six defects, all in code already merged to `edge`, and the suites were green on either side of
|
||||
> every one.** Two were blocking or worse. **The spawn atlas could not import on a stock ServUO
|
||||
> tree** — a case-sensitive JS dedupe against an `..._ai_ci` PRIMARY KEY, four colliding decoration
|
||||
> spellings in ServUO's own files, and the whole transaction lost; with no atlas every option source
|
||||
> answers empty and no world verb can be authored at all. **Teardown of all five world verbs was a
|
||||
> no-op that reported success** — `revertOwned` sent the despawn under the step's key, which is the
|
||||
> key the SPAWN used, so the shard replayed the spawn's reply and `OnDespawn` never ran; the ledger
|
||||
> read `reverted` while the shard held all 21 objects, and the same despawn under a fresh key removed
|
||||
> every one. Then: **the public calendar served neither live nor recent runs** though §I promises all
|
||||
> three, so the site said `live` on one page and showed nothing on the other; **a resource left
|
||||
> `reverting` by a crash was never reclaimed**, and the manual cleanup route answered 200 while doing
|
||||
> nothing, which stranded a lease and blocked the NEXT run of the same event; **a transient refusal
|
||||
> under an idempotency key was permanent**, because the shard's store had no case for a handler that
|
||||
> ran and deliberately did nothing; and **three facts every announcement computes were declared by no
|
||||
> trigger** and silently dropped.
|
||||
>
|
||||
> One reported defect was **withdrawn**: `skip` refusing a `failed` step is not a dead end, because
|
||||
> `resume` carries a run past any settled step — the route's own docs say so and the rig confirmed
|
||||
> it. The runner claims only `pending` steps, so `failed` and `refused` are both settled.
|
||||
>
|
||||
> Every fix is verified against the rig, not only against tests: the atlas imports 309 decor types
|
||||
> and 6,455 points; a full four-phase run's teardown leaves the shard owning **0**; a lease stranded
|
||||
> by a real crash is reclaimed in one sweep and `cleanup_status` reaches `complete`; the same save
|
||||
> key 25 seconds apart answers "15 seconds ago" then "40 seconds ago"; and `/site/events` shows a
|
||||
> live run as **Happening now** beside recent ones, in the browser and in the app. Each new test was
|
||||
> confirmed to FAIL without its fix.
|
||||
>
|
||||
> **`Module-uo`'s `revert` no longer forwards core's key at all** — `MODULE_API.md` now says why that
|
||||
> key identifies a lost dispatch rather than addressing the undo. **Protocol 7 is amended in place**:
|
||||
> a refusal releases its key, with the rule that pays for it written down — *do not answer `*.error`
|
||||
> after changing the world*.
|
||||
|
||||
**Two documents that are cutover-window work by construction.**
|
||||
- **`runicgateway.com`** — `checkFacts` reads `main`, so any claim about events is unverifiable until
|
||||
the cutover lands. Same 12a/12b split the engagement workstream needed.
|
||||
|
||||
@@ -1193,6 +1193,16 @@ rather than implementation and belong here:
|
||||
with the key and an EMPTY list, meaning *"a command went out under this key and core never learned
|
||||
what it did"*. Answering that honestly is what makes an unattended world write recoverable; a
|
||||
module that cannot answer it says so, and the row stays visible to an operator.
|
||||
- **That key IDENTIFIES a dispatch; it is not a key to send on the undo.** It names the command core
|
||||
lost the answer to, so the module can ask the game about it. Forwarding it as the outgoing key of
|
||||
the reverting command is a different thing entirely, and on a game whose at-most-once store keys on
|
||||
the key alone — as the uo-link shard's does — the undo is then recognised as a repeat of the DO and
|
||||
answered with the original reply. `module-uo` made exactly this mistake: teardown of all five world
|
||||
verbs was a no-op that reported success, because every despawn carried the key its spawn had gone
|
||||
out under. Found by the Phase 16 acceptance walk, with the ledger reading `reverted` and the shard
|
||||
still holding every object. A command that undoes needs a key of its own or none at all; a repeated
|
||||
undo is usually harmless by construction ("already gone" is a success), which is what makes *none*
|
||||
the right answer more often than not.
|
||||
- **Core owns cleanup, and it is derived rather than authored.** There is no `on_teardown` on an
|
||||
action and no cleanup phase in a spec: an operator cannot be relied on to write the undo, and an
|
||||
aborted run never reaches the phase they wrote it in. Cleanup is one sweep over the ledger and it
|
||||
|
||||
Reference in New Issue
Block a user