docs(rust): protocol 13 step 2 — expiry, plugin loads, the zone helper, the tally (§19.4-19.8, MODULE_API 1.11.0)
The spec for PLAN_FIXES §6 step 2, as built (D181-D185): - PROTOCOL.md §19.4 world.expired's `what` and the website recording an expiry (amends §15's "maps it to nothing"); §19.5 plugin.loaded / plugin.unloaded with the permission diff; §19.6 the ZoneManager helper and `zoneHelper` at hello; §19.7 what the tally counts (F1, F3, F4); §19.8 the website (F7 hold, F5/F6 link fleet, F2 names). - MODULE_API.md 1.11.0 and ctx.events.expired; EVENTS.md §L the `expired` status, terminal and green, and `resource.expired`. - rust-link/INSTALL.md: the helper in the tarball, by hand, and in doctor. - PLAYER_WALK.md: events step 6 can pass now; a step-2 section whose rows 1-2 were walked on both rigs without a player and 3-9 need one. - PLAN_FIXES §6: step 2 as built, with its PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -426,7 +426,7 @@ tables carry no module prefix. (The count said "nine" over a list of ten from th
|
||||
| `event_run_steps` | `run_id`, `phase`, `seq`, `action_id`, `params` JSON, `action_version`, `status`, `due_at`, `attempts`, `on_failure`, `idempotency_key`, `claimed_by`, `claim_expires_at`, `last_error`. `INDEX (status, due_at)` | The work queue, claimed with the outbox's compare-and-set. |
|
||||
| `event_action_settings` | `action_id` (the primary key), `enabled`, `caps` JSON (`{dimension: perRunCap}`), `updated_by`, `updated_at`. | **The deployment's switchboard, and the whole of the permission model beyond the role.** One row per action an admin has an opinion about; **a missing row is not "disabled", it is the default for the action's risk class** — see [§K](#k--security-model). Not a grant table — nobody is named, because the role check already answered who. Rows outlive their actions, so uninstalling a module and re-installing it restores the caps the operator chose. |
|
||||
| `event_run_budget` | `run_id`, `dimension`, `consumed`, `cap` **nullable**, `effective_from`. `UNIQUE (run_id, dimension)` | Consumption is incremented with a conditional update — `… SET consumed = consumed + ? WHERE run_id=? AND dimension=? AND (cap IS NULL OR consumed + ? <= cap)` — so the cap holds under concurrent steps without a transaction. **A NULL cap is uncapped and still a row**, so the console's meter counts what nothing bounds, and a *missing* row keeps its one meaning: a step spending a dimension its own run's version never priced, which is refused. `effective_from` names the action whose cap won, so a number on the meter traces back to a switch. |
|
||||
| `event_run_resources` | `run_id`, `step_id` **`SET NULL`**, `owner_module`, `kind` and `ref` (both module-opaque), `payload` JSON, `lease_until` nullable, `status` `ENUM('pending','confirmed','reverting','reverted','orphaned','drifted')`, `revert_attempts`, `last_error`, optional `member_key`. `UNIQUE (owner_module, kind, ref)` among the rows core still believes are ITS — see the amendment below | **The cleanup ledger, and it holds both kinds of thing an event owns** — objects it created (`kind: 'creature'`, `ref` = a serial) and values it leased (`kind: 'override'`, `payload` = baseline + applied). `drifted` is the compare-and-set refusal; the unique index is what stops two events leasing one target. `@step` is a reserved `kind` core owns (rule 1, below); a module reporting one is refused. |
|
||||
| `event_run_resources` | `run_id`, `step_id` **`SET NULL`**, `owner_module`, `kind` and `ref` (both module-opaque), `payload` JSON, `lease_until` nullable, `status` `ENUM('pending','confirmed','reverting','reverted','orphaned','drifted','expired')` (`expired` from MODULE_API 1.11.0), `revert_attempts`, `last_error`, optional `member_key`. `UNIQUE (owner_module, kind, ref)` among the rows core still believes are ITS — see the amendment below | **The cleanup ledger, and it holds both kinds of thing an event owns** — objects it created (`kind: 'creature'`, `ref` = a serial) and values it leased (`kind: 'override'`, `payload` = baseline + applied). `drifted` is the compare-and-set refusal; the unique index is what stops two events leasing one target. `@step` is a reserved `kind` core owns (rule 1, below); a module reporting one is refused. |
|
||||
| `event_run_participants` | `run_id`, `user_id` nullable `SET NULL`, `member_key` module-opaque **and NOT NULL**, `score` `DECIMAL(18,4)`, `rank_at`, `joined_at`, `meta` JSON. `UNIQUE (run_id, member_key)` | Results and profile history read it. `SET NULL` not `CASCADE`, matching `engagement_sends`: a record of what happened must survive an account deletion. The unique key is what makes a retried collect step an upsert rather than a doubled leaderboard, and `rank_at` carries the suffix because `rank` is a reserved word from MariaDB 10.2 — one forgotten pair of backticks away from a syntax error in a query nothing runs until a run completes at four in the morning. **Written only from an action's success envelope** (Phase 10): core stores what a module tells it and sources nothing, because a `member_key` → account mapping is one game's. |
|
||||
| `event_run_phase_gates` | `run_id`, `phase`, `kind` `ENUM('after','on')`, `after_seconds`, `trigger_id`, `conditions` JSON, `needed`, `tally`, `entered_at`, `due_at`, `last_event` JSON, `satisfied_at`, `satisfied_by`, `forced_by`. `UNIQUE (run_id, phase)`, `INDEX (trigger_id, satisfied_at)` | **What a phase is waiting for, and how far it has got** (Phase 5). The one fact in this feature that is not derivable from a row somebody already wrote: `{ on: …, count: 3 }` counts things that happen *between* two ticks, and the runner is not running when they happen. The unique key is what makes opening a gate an `INSERT IGNORE`; the index is the emit path's only query and the one index here on a hot path. |
|
||||
| `event_run_log` | `run_id`, `step_id` nullable, `kind` (closed set), `phase`, `detail` JSON, `at`. | `activity_log.detail` is `TEXT` and unqueryable. "Why didn't phase 3 start?" must be a query. |
|
||||
@@ -1795,6 +1795,18 @@ through a revert — a revert that finds nothing there is a SUCCESS (§L, and wh
|
||||
whereas a resource the module reports missing is a thing that vanished while nobody was looking.
|
||||
Those are two different sentences to the operator reading the console the morning after.
|
||||
|
||||
**`expired` is the third sentence** (MODULE_API 1.11.0, amended 2026-09-27 for Rust PLAN_FIXES F14,
|
||||
D170, D183). Some game objects carry their own deadline down the wire and the game ends them when it
|
||||
passes, without being asked again — a Rust zone the plugin erases when its time is up. Reached only
|
||||
through reconcile, that looked like `orphaned`: amber, "gone", and still claimable for a revert. So a
|
||||
module that hears the game end one says so with `ctx.events.expired({ kind, ref })`, and core marks the
|
||||
module's `pending`, `confirmed` or `orphaned` row for that target `expired` and logs `resource.expired`.
|
||||
It is **terminal and green, like `reverted`**: it releases the target (it is not one of the three that
|
||||
hold it), it is not "unreverted", and the sweep never tries to give it back. A row a revert has already
|
||||
claimed (`reverting`) is left to that revert, which finds nothing and succeeds. A finished run whose last
|
||||
unresolved row this was goes to `cleanup_status = 'complete'`, including from `incomplete`, which the
|
||||
sweep no longer scans.
|
||||
|
||||
---
|
||||
|
||||
## Versioning, and editing a live event
|
||||
@@ -1998,7 +2010,7 @@ Phase 3 — "The Boss" has not started.
|
||||
| What a phase is waiting for, its tally and its last related firing | `event_run_phase_gates`, served already-rendered as a run's `gates` |
|
||||
| A phase opening a gate, and a gate opening — on a firing, a deadline or a human | `event_run_log`, kinds `phase.gate` and `phase.advanced` |
|
||||
| Module acknowledgement, or its absence with the budget exceeded | `event_run_steps.last_error` |
|
||||
| Resources created, confirmed, leased, reverted, orphaned, drifted | `event_run_resources`, plus six `event_run_log` kinds: `resource.recorded`, `resource.orphaned`, `cleanup.reverted`, `cleanup.failed`, `cleanup.swept`, `cleanup.retry`. `resource.recorded` is written at the ANSWER rather than at the placeholder, because a placeholder is a promise and the operator's question is about the world |
|
||||
| Resources created, confirmed, leased, reverted, orphaned, drifted | `event_run_resources`, plus seven `event_run_log` kinds: `resource.recorded`, `resource.orphaned`, `resource.expired` (1.11.0), `cleanup.reverted`, `cleanup.failed`, `cleanup.swept`, `cleanup.retry`. `resource.recorded` is written at the ANSWER rather than at the placeholder, because a placeholder is a promise and the operator's question is about the world |
|
||||
|
||||
One caution carried over from the engagement retention work: the run log is high-cardinality and grows per event, so it needs a
|
||||
retention sweep from the start — `engagementRetentionPrune` is the pattern, and the rule it learned is
|
||||
|
||||
@@ -26,13 +26,23 @@ here extends the contract first, in this file, before the module is written agai
|
||||
Core exports a single integer-major semver string from `server/src/modules/version.js`:
|
||||
|
||||
```js
|
||||
const MODULE_API_VERSION = '1.10.0'
|
||||
const MODULE_API_VERSION = '1.11.0'
|
||||
```
|
||||
|
||||
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
|
||||
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
|
||||
module chunk evaluates, which is earlier than any network round trip could answer.
|
||||
|
||||
**1.11.0 — `ctx.events.expired({ kind, ref })`** (`website/EVENTS.md` §L; Rust
|
||||
`modules/rust/PLAN_FIXES.md` F14, D183). One addition and no removal, so minor. A game that ends
|
||||
something at its own deadline — a Rust zone the plugin erases when its time is up — tells core, and core
|
||||
marks the calling module's live row for that target **`expired`**: terminal like `reverted`, never taken
|
||||
back at teardown, and not `orphaned`, which `reconcile` uses for a thing that vanished with nobody
|
||||
asking. The owner is bound, as `reconcile`'s is; it is fire-and-forget and returns `undefined`; a
|
||||
`{ kind, ref }` no run ledgered is not an error. **Module-uo is unaffected**: it declares `^1.10.0`,
|
||||
calls none of this and reads no ledger status, and its frozen-manifest job, server and client suites
|
||||
passed against the 1.11.0 core before the bump merged (website#209).
|
||||
|
||||
**1.10.0 — the event contract opens to modules: `api.registerEventActions(...)`,
|
||||
`api.registerEventBudgets(...)`, `api.registerEventLeases(...)` and
|
||||
`api.registerEventOptionSources(...)`** (`website/EVENTS.md` §F, `EVENTS_PLAN.md` Phases 7 and 8).
|
||||
@@ -100,6 +110,11 @@ api.registerEventActions([{
|
||||
// claim about a world that no longer exists.
|
||||
ctx.events.reconcile()
|
||||
|
||||
// 1.11.0. The game ended one of this module's resources at its OWN deadline —
|
||||
// the same `{ kind, ref }` the action reported when it made it. Core files it
|
||||
// `expired`: terminal, and not the `orphaned` a reconcile would have said.
|
||||
ctx.events.expired({ kind: 'world', ref: 'srv-a:rg-13-35875416-1' })
|
||||
|
||||
api.registerEventOptionSources([{
|
||||
id: 'uo.options.creatures', label: 'Creatures',
|
||||
async resolve() { return [{ value: 'Orc', label: 'Orc', group: 'Humanoid' }] },
|
||||
@@ -352,6 +367,11 @@ branches lacks — so it is the only place `permits` is true between two values
|
||||
between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise
|
||||
unchanged, and so is every rule about composition narrowing rather than widening.
|
||||
|
||||
**1.11.0 — `ctx.events.expired`** (`website/EVENTS.md` §L, Rust PLAN_FIXES D183). One addition, so
|
||||
minor; see §1.1. The resource ledger gains an `expired` status, and nothing that existed changes
|
||||
meaning — `expired` joins neither the statuses that hold a target nor the ones that leave a run's
|
||||
cleanup unfinished. Module-uo's `^1.10.0` still resolves and was proved against it.
|
||||
|
||||
**1.10.0 — the event contract** (`website/EVENTS.md` §F). Four additions, no removals and no changed
|
||||
signature, so minor; `module-uo`'s `coreApi: "^1.9.0"` still resolves and it registers no actions
|
||||
until `EVENTS_PLAN.md` Phase 9. `api.registerEventActions([...])`, `api.registerEventBudgets([...])`,
|
||||
@@ -636,6 +656,7 @@ module-uo does not need is on the list.
|
||||
| `ctx.teams.reconcile` | `({ reason }) => void`, returns at once | `model/teams/teamSync` | after a fresh account link (1.6.0) |
|
||||
| `ctx.teams.activity.push` | `(items) => Promise<void>`, fire-and-forget | `model/teams/teamActivity` | the Team provider's module (1.6.0) |
|
||||
| `ctx.events.emit` | `(triggerId, envelope) => void`, fire-and-forget | `utils/engagementEmit` | `module-uo`'s `utils/shardEngagement.js`, off the shard feed (1.7.0) |
|
||||
| `ctx.events.expired` | `({ kind, ref }) => void`, fire-and-forget, owner bound | `events/cleanup` `expireResource` | `module-rust`'s ingest of `world.expired` (1.11.0) |
|
||||
| `ctx.inbox.push` | `(userId, item) => void`, fire-and-forget | the in-app channel (live since Phase 7) | `module-uo` reaches both through `server/core.js` (1.7.0) |
|
||||
|
||||
**`ctx.events.emit(triggerId, envelope)`** fires an event the module DECLARED with
|
||||
|
||||
Reference in New Issue
Block a user