docs(link): protocol 6 as built — a guarantee, and the kind that pays for it
`link/v6.md` is the spec of record. It is the first bump that adds a promise rather than data: a command carrying an `idempotencyKey` is executed at most once, and a repeat is answered with the original reply. Also in this PR: - `INTEGRATION.md` — §2 gains v6, the write plane gains a "retrying a command safely" section with the four rules a caller needs, §7 gains 425 and the paragraph on why it is not 409, and the event catalog gains `champ.boss.killed`. - `PLAN.md` — §5.5 records that a per-KILL damage table needs no override even though a per-HIT event does, which is the observation the new kind rests on; §7 documents the key on the inbound frames. - `EVENTS.md` — §A row 7 and two §G capability rows close, and the failure table's "an action succeeds but the ack is lost" loses its "does not exist today". - `EVENTS_PLAN.md` — Phase 11 splits into 11a and 11b, and `installer` leaves the phase: PLAN.md §7.4 made sure no protocol version is hardcoded there, so the bundle gate is version-agnostic and needed no change for 5 either. The live walk's defect is written up in v6.md §6.1 because the naive check confirms it: an active ChampionSpawn registers an unnamed region over its own spawn area, so the innermost region containing a champion boss is guaranteed to have no name — and region registration is deferred, so a lookup at spawn time answers "Britain" while one at the kill does not. CRLF preserved throughout; links and anchors checked by hand, this repo has no CI. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1025,7 +1025,7 @@ site, cued to a GM, completed, recorded.
|
||||
**Trap worth writing into the action declarations.** These three verbs have *different* idempotency.
|
||||
`towncrier` and `news` are keyed by id and re-posting replaces, so a retry is safe. **`broadcast` is
|
||||
not** — a retry is a second announcement to everyone online — and there is no idempotency key on the
|
||||
wire until P11. So `uo.broadcast` ships with `on_failure: 'skip'` rather than a retry, and the
|
||||
wire until P11a. So `uo.broadcast` ships with `on_failure: 'skip'` rather than a retry, and the
|
||||
declaration says why.
|
||||
|
||||
> **Built 2026-09-04** (`Module-uo#28` + `docs#217`). Three actions, one budget dimension, three option
|
||||
@@ -1246,25 +1246,117 @@ push; the ceiling on `run.failed` proved to exclude a moderator.
|
||||
|
||||
---
|
||||
|
||||
### Phase 11 — Protocol: idempotency, lease deadlines, participation (`servuo-plugins` + `link` + `module-uo` + `installer` + `docs`)
|
||||
### Phase 11 — Protocol: idempotency, lease deadlines, participation (`servuo-plugins` + `link` + `module-uo` + `docs`)
|
||||
|
||||
**§N1 answered 2026-09-01 — no longer gated.** A five-repo protocol bump, and the shape TEAMS Phase 1 already walked.
|
||||
**§N1 answered 2026-09-01 — no longer gated.** A protocol bump, and the shape TEAMS Phase 1 already walked.
|
||||
|
||||
- **Plugin:** an idempotency key on every inbound command with a bounded recent-key set that answers a
|
||||
repeat with the original result; a lease deadline timer that restores baseline **without being
|
||||
asked**; a run-scoped participation ledger; `champ.boss.killed` as a first-class kind.
|
||||
**Split into 11a and 11b (org lead, 2026-09-04).** Three independent mechanisms across four repos,
|
||||
each with a different thing to prove on a live ServUO, is one review and one walk too many. They land
|
||||
as **one protocol version**: 11a bumps to 6, and 11b amends 6 **in place** on `edge` — the same rule
|
||||
the org lead set for `MODULE_API_VERSION` on 2026-09-03, and it applies because 6 will not reach
|
||||
`main` until the events cutover. The bundle CI therefore never sees two numbers.
|
||||
|
||||
**`installer` is NOT in this phase, and the line above that said so was wrong.** `PLAN.md` §7.4 made
|
||||
sure of it deliberately: no protocol version is hardcoded anywhere in the installer. `bundle.rs`
|
||||
reads the number out of the bundle document, and `bundle.yml`'s Gate 1 reads both halves out of the
|
||||
released artefacts and refuses a mismatch — version-agnostic, so it needed no change for 5 either.
|
||||
And `link`/`servuo-plugins` are on `edge`, so nothing is released or bundled until the cutover.
|
||||
|
||||
#### Phase 11a — the idempotency key, and `champ.boss.killed`
|
||||
|
||||
- **Plugin:** an idempotency key on every inbound command with a bounded recent-key set that answers
|
||||
a repeat with the original result; `champ.boss.killed` as a first-class kind.
|
||||
- **`overlay.toml`** protocol version bumped **in this PR** — the installer refuses to pair a sidecar
|
||||
and an overlay that disagree, so a bump in a later PR means the next bundle silently fails to
|
||||
compose.
|
||||
- **Sidecar:** `PROTOCOL_VERSION` bumped, the new fields carried, the new kind stored and served.
|
||||
- **`installer`:** joins the phase because of the pairing above.
|
||||
- **Sidecar:** `PROTOCOL_VERSION` bumped; the key carried; the new kind stored and served.
|
||||
- **`module-uo`:** the key sent on every event-driven write, `uo.broadcast` made retryable, and the
|
||||
new kind mapped for visibility and declared as a trigger.
|
||||
- **`docs`:** a new `docs/link/v6.md` as the spec of record, plus `INTEGRATION.md`.
|
||||
|
||||
**Ships:** the game side can refuse a duplicate command and can put the world back on its own. Both
|
||||
are safety properties, not features.
|
||||
**Ships:** the game side can refuse a duplicate command. A safety property, not a feature — and the
|
||||
precondition every world verb in Phase 12 is waiting on.
|
||||
**Verify:** against the local ServUO tree with a **build-offline-first** step and confirmation the new
|
||||
code is live; a deliberate duplicate command; a lease whose website is killed before it expires,
|
||||
proving baseline returns anyway.
|
||||
code is live; a deliberate duplicate command, checked against the shard's own audit trail rather than
|
||||
against the reply.
|
||||
|
||||
> **Built.** `PROTOCOL_VERSION` and `overlay.toml` both **6**, in the same pair of PRs. The whole
|
||||
> sidecar change is one constant and one status mapping; the whole guarantee lives on the shard,
|
||||
> which is the only place it can, because the shard is where the world write happens.
|
||||
>
|
||||
> **Seven decisions (org lead, 2026-09-04), all as recommended.** The 11a/11b split; participation
|
||||
> keyed by character serial; the ledger persisted in the world save; leases as machinery plus one
|
||||
> proven key; reserve-on-receipt with `bridge.busy` for an in-flight repeat; a TTL-plus-cap with a
|
||||
> loud eviction; and top damagers at a `staff` ceiling. The last three are 11a's.
|
||||
>
|
||||
> **What the phase bought immediately:** `uo.broadcast` stopped being un-retryable. Phase 9 shipped it
|
||||
> answering `retry: false` to *everything* including a 503 from a restarting shard, with a comment
|
||||
> naming the line that would change. This is that line. It now defers to `sidecarFailure` — the same
|
||||
> helper its two siblings already used — so the hand-rolled variant that forced every outcome
|
||||
> terminal is gone rather than merely re-tuned.
|
||||
>
|
||||
> **One verb was less idempotent than its own `id` made it look.** Both keyed verbs post under a
|
||||
> run-scoped id and a repeat REPLACES, which is why Phase 9 called them safe to retry. But
|
||||
> `news.add` with `announce: true` makes the criers proclaim the article's title on every post, so a
|
||||
> retry replaced the article silently and proclaimed it **again**. The key stops the second
|
||||
> proclamation. Read in the plugin, not inferred: `BridgeNews` removes the old entry, inserts the
|
||||
> new one, and then announces.
|
||||
>
|
||||
> **The live walk found one defect, and the naive check confirms it.** A champion killed in the
|
||||
> middle of Britain emitted a frame with **no region**. An active `ChampionSpawn` registers a
|
||||
> `ChampionSpawnRegion` over its spawn area with a **null name** and the town region as its parent,
|
||||
> so the most specific region containing a champion boss is the one region on the map guaranteed to
|
||||
> be nameless — and `Mobile.Region` hides that by falling back to the map's unnamed default rather
|
||||
> than to null. Region registration is deferred, too, so a lookup at spawn time answers `"Britain"`
|
||||
> and one at the kill does not: the probe printed `"Britain"` on every run, including the ones whose
|
||||
> frame carried nothing. Fixed by walking outward to the nearest NAMED ancestor, which is the general
|
||||
> answer — house regions, dungeon sub-regions and guarded-zone overlays are all anonymous children of
|
||||
> somewhere a player would name.
|
||||
>
|
||||
> **Two findings that are rules rather than facts about events:**
|
||||
>
|
||||
> - **A trigger is not a stream, and in this module they are disjoint sets.** Push delivery is keyed
|
||||
> on the subscription id, which core's catalog grants only to registered STREAMS. `module-uo`'s
|
||||
> stream ids (`champ.start`, `idoc.warning`, …) and its trigger ids (`uo.champ.started`, …) share
|
||||
> no member, so **no** engagement rule in this module can push — the tickle resolves to zero
|
||||
> endpoints while the send log records success. That is the Phase 10 defect, pre-existing here in
|
||||
> twenty rules. This phase does not fix them; it declines to add a twenty-first, so its rule ships
|
||||
> `['email','inapp']` with a comment saying why. **Flagged for the org lead as its own change.**
|
||||
> - **A new rule needs a new seed GROUP, never an appended one.** `triggers-v1` is stamped once under
|
||||
> a settings guard, so a twenty-seventh entry would reach fresh installs and nothing else.
|
||||
> `champ-boss-killed-v1` is its own group — the same remedy core applied in Phase 10, and the third
|
||||
> application of Engagement Phase 11's seed-key finding.
|
||||
>
|
||||
> **What the rig could not prove: `bridge.busy`.** With today's synchronous handlers a repeat cannot
|
||||
> arrive while the original runs — the Core thread takes one inbound line at a time — so the state is
|
||||
> unreachable on a live shard until a handler defers. Implemented, with `Hold`/`Complete` as the door,
|
||||
> and unit-tested at the sidecar's mapping on all three responders. **11b's leases are the first
|
||||
> thing that can produce it**, and proving it belongs in that walk.
|
||||
>
|
||||
> See [`../link/v6.md`](../link/v6.md) for the spec of record.
|
||||
|
||||
#### Phase 11b — lease deadlines and the participation ledger
|
||||
|
||||
- **Plugin:** a lease deadline timer that restores baseline **without being asked**, with
|
||||
compare-and-set restore reporting `drifted`; a run-scoped participation ledger.
|
||||
- **Leases land as machinery plus ONE proven key** (org lead): the generic registry, the deadline
|
||||
timer, CAS restore and `lease.list` reconcile, proved end to end against one verified live-read
|
||||
`Config.Get` key. The curated allowlist and its boot-time self-check stay in Phase 12, where the
|
||||
rest of the lease surface is.
|
||||
- **The participation ledger counts presence in a declared area plus kill credit inside it, keyed by
|
||||
character serial** — matching `module-uo`'s existing Teams `memberKey`, so one module speaks one
|
||||
member vocabulary. The plugin computes the score; core stores an opaque number it never interprets.
|
||||
- **The ledger is persisted in the world save**, which makes it the Bridge's first persisted state
|
||||
ever. A run spans hours and a restart mid-event is realistic; an in-memory tally would regress the
|
||||
score after one, and the only ways to paper over that are a high-water rule in core (which must
|
||||
stay game-agnostic) or a per-run offset in the module.
|
||||
- **Protocol 6 is amended in place**, not bumped to 7. See the split note above.
|
||||
|
||||
**Ships:** the game side can put the world back on its own, and can say who took part.
|
||||
**Verify:** a lease whose website is killed before it expires, proving baseline returns anyway; a
|
||||
deliberate mid-lease GM edit producing `drifted` rather than a silent overwrite; a tally that survives
|
||||
a shard restart mid-run; and the first live `bridge.busy`, which a deferring handler finally makes
|
||||
reachable.
|
||||
|
||||
---
|
||||
|
||||
@@ -1396,7 +1488,7 @@ flowchart TD
|
||||
P8 --> P9["P9 · UO wave 1<br/>no protocol change"]
|
||||
P5 --> P10["P10 · integrations"]
|
||||
P9 --> P10
|
||||
P0 -.->|"§N1 answered"| P11["P11 · protocol bump"]
|
||||
P0 -.->|"§N1 answered"| P11["P11a+b · protocol 6"]
|
||||
P8 --> P11
|
||||
P11 --> P12["P12 · UO world verbs"]
|
||||
P7 --> P13["P13 · authoring UI"]
|
||||
|
||||
Reference in New Issue
Block a user