A module's perform() may answer { ok: false, retry: true, retryAfterMs }
and the runner waits that long instead of the flat EVENT_STEP_RETRY_MS
(Rust PLAN_REDESIGNS §14.3.13, D371). Dangerous Treasures' warm-up is the
first user.
- dispatch.classify reads it on the retry shape only, clamped to 60 000 to
1 800 000 ms; a value that is not a number is dropped and logged, and the
step retries on the flat delay.
- It is one attempt of EVENT_STEP_MAX_ATTEMPTS and degrades the run like any
retry. A run paused or cancelled in the wait is not picked up.
- Every step.retry log line now carries retryAt (and retryAfterMs when
hinted); the run console reads "retrying at 20:10".
- MODULE_API 1.15.0, additions only; the engagement manifest is regenerated.
Module-uo (^1.10.0) answers none of it and is unaffected.
- Test harness: resourcesDb.finishedRunsWithNothingLeft was never stubbed in
eventRunner.test.js, so a tick that reached the cleanup sweep with a
terminal run waited on the dead pool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
213 lines
14 KiB
JavaScript
213 lines
14 KiB
JavaScript
// The module API version — the single number a module's `coreApi` range is
|
||
// checked against (docs/website/MODULE_API.md §1.1).
|
||
//
|
||
// Bump minor when a member is ADDED to ctx or a new register* call appears;
|
||
// major when one is removed, its signature changes, or its behaviour changes
|
||
// without a signature change. A core-internal refactor behind an unchanged
|
||
// member is not a bump.
|
||
//
|
||
// Deliberately separate from PROTOCOL_VERSION (which versions the shard wire and
|
||
// has nothing to say about a website module) and from any module's own version.
|
||
|
||
// 1.15.0 — a failed step may say when to try again: `{ ok: false, retry: true,
|
||
// retryAfterMs }` (Rust PLAN_REDESIGNS §14.3.13, D371; docs/website/EVENTS.md §F).
|
||
// Clamped to one minute–thirty minutes, never refused; absent, the flat
|
||
// `EVENT_STEP_RETRY_MS` as before. It is one attempt of `EVENT_STEP_MAX_ATTEMPTS`
|
||
// like any retry, and ignored beside `retry: false`. One optional envelope
|
||
// member, so minor. Module-uo declares `^1.10.0` and answers none: an envelope
|
||
// without it is classified exactly as it was. This took 1.15.0 ahead of
|
||
// STEAM_SIGNIN.md, which had claimed it and moves to the next minor.
|
||
//
|
||
// 1.14.0 —an action param may hold several values: `multiple: true`, with an
|
||
// optional `max` (1–100, 20 if left out), on a `string` or `int` param (Rust
|
||
// PLAN_REDESIGNS §12.3, D344; docs/website/EVENTS.md §F). The step stores a JSON
|
||
// array; a bare value is read as a list of one; `required` means at least one;
|
||
// `perform()` and `cost()` are handed the array. Refused on a duration and on a
|
||
// template's inputs. The editor draws it as a multi-select.
|
||
// One optional flag, so minor. Module-uo declares `^1.10.0` and uses none of it:
|
||
// a param that does not say `multiple` is checked, stored and drawn exactly as it
|
||
// was, and the declaration the registry hands back does not grow the key.
|
||
//
|
||
// 1.13.0 — the event step editor (Rust PLAN_REDESIGNS §2; docs/website/EVENTS.md
|
||
// §F, §I). Four additions, each optional:
|
||
// • `api.registerEventTemplates([{ id, label, description, inputs, build }])` —
|
||
// a named starting point for a new event; inputs in, an ordinary draft out,
|
||
// and nothing about the template kept in the definition (D192);
|
||
// • a param's `duration: 'minutes'`, so the editor can see a timed step and
|
||
// keep its phase open long enough for it;
|
||
// • a lease's `example` for its value, so a lease step shows that lease's
|
||
// example rather than core's one for every lease;
|
||
// • an option row's `note`, shown under the field while it is the chosen one.
|
||
// Additions only, so minor. Module-uo declares `^1.10.0` and uses none of them:
|
||
// its actions, leases and sources come out of the registry exactly as they went
|
||
// in, and its steps render with the new controls and nothing else changed.
|
||
//
|
||
// 1.12.0 — an event action may declare `progress(envelope)`: how a live run's
|
||
// step stands, for its public page (docs/website/EVENTS.md §I; RunicNPC D304,
|
||
// D305). Core accepts two shapes, `{ label, left, of }` and `{ label, percent }`,
|
||
// puts them in the public run as `progress` and renders them as text it does not
|
||
// interpret. Optional and additive, so minor: Module-uo declares `^1.10.0` and
|
||
// registers no action with it, so its actions and pages are unchanged.
|
||
//
|
||
// 1.11.0 — `ctx.events.expired({ kind, ref })`, and `expired` as a resource
|
||
// ledger status (docs/website/EVENTS.md §L; Rust PLAN_FIXES D183). A game that
|
||
// ends something at its own deadline — a Rust zone erased when its time is up —
|
||
// could until now reach core only through `ctx.events.reconcile()`, which files
|
||
// it `orphaned`: amber, "it vanished", and still claimable for a revert. The new
|
||
// call files it as the plan working: terminal, green, never taken back.
|
||
//
|
||
// Additions only, so minor, and checked against the one module already built on
|
||
// 1.10.0: Module-uo never calls it, reads no ledger status, and every existing
|
||
// status keeps its meaning — `expired` joins neither `HELD` nor `UNRESOLVED`, so
|
||
// the sweep, the manual retry and `cleanup_status` see exactly what they saw.
|
||
|
||
// 1.10.0 — the event contract opens to modules: `api.registerEventActions`,
|
||
// `api.registerEventBudgets`, `api.registerEventLeases` and
|
||
// `api.registerEventOptionSources` (docs/website/EVENTS.md §F, EVENTS_PLAN.md
|
||
// Phase 7). Four names, and only one of them is new machinery: the ACTION
|
||
// registry has staged core's `core.announce`, `core.wait` and `core.cue` on every
|
||
// boot since Events Phase 1, and `loader.js` simply had no method that delegated
|
||
// to it. What Phase 7 adds is the facade, the three declarations beside it, and
|
||
// the fail-closed rule they exist for — a `cost()` naming a dimension no module
|
||
// registered is REFUSED at save, at the dry run and at dispatch, so a module
|
||
// cannot spend a budget it did not declare.
|
||
//
|
||
// Additions only, so minor: a module written against 1.9.0 registers no actions
|
||
// and the deployment simply has fewer verbs an event can use. That is §F's own
|
||
// posture stated as a version rule — core with no module installed is still an
|
||
// event engine that can announce, wait, cue a human and publish results.
|
||
//
|
||
// **A lease is DECLARED here and acquired by nothing.** Core owns a lease's
|
||
// duration and its conflict check, and both live in the resource ledger, which is
|
||
// Phase 8's. It is in 1.10.0 rather than in 1.11.0 so that the module contract is
|
||
// one version a module author reads once, not two.
|
||
//
|
||
// 1.9.0 - a sixth registration call: `api.registerEngagementSeeds({ templates,
|
||
// ruleGroups })` (docs/website/ENGAGEMENT.md Phase 11b, decision 7). A module
|
||
// could declare a trigger from 1.7.0 and could never say what the mail should
|
||
// READ like: `templateSeeds.js` and `coreRules.js` are core files with core
|
||
// arrays in them, so a module's notification was core's generic body or nothing.
|
||
// Additions only, so minor: every module written against 1.8.0 keeps working and
|
||
// simply seeds nothing.
|
||
//
|
||
// **What a module has to know about it beyond the new name**, because the two
|
||
// halves behave differently on purpose:
|
||
//
|
||
// - **Templates are re-ensured on every boot**, under `seed_key` /
|
||
// `seed_version` / `customized` - so bumping a body's `seedVersion` reaches
|
||
// every deployment except the ones where an operator edited that row, and a
|
||
// template added in a later module version reaches everyone.
|
||
// - **Rules are one-shot, per named GROUP.** Re-ensuring one would resurrect a
|
||
// rule an operator deleted and reset one they enabled, so each group carries
|
||
// its own settings guard. A rule appended to an existing group therefore
|
||
// reaches FRESH INSTALLS ONLY; one that must reach deployments already
|
||
// stamped takes a new group key. That is 11a's seed-key finding as an API
|
||
// rather than as a warning, and the module makes the choice knowingly.
|
||
//
|
||
// Two things it deliberately does not permit. A seeded rule is always
|
||
// `enabled = 0` - it is not a parameter - which is Q3's invariant surviving
|
||
// contact with the largest seed set in the workstream. And a module may not mark
|
||
// a template `protected`: that flag means "the system breaks without this body",
|
||
// which is true of a password reset and of nothing a module ships, and a module
|
||
// setting it would take an operator's delete button away.
|
||
//
|
||
// It runs from `modules/lifecycle.js` `boot()` rather than `seedDefaults()`, and
|
||
// that is forced rather than chosen: core seeds before `app.js` is required, and
|
||
// requiring `app.js` is what runs the loader.
|
||
|
||
// 1.8.0 - a seventh value in the audience ceiling lattice: `admin`, a child of
|
||
// `staff` (docs/website/ENGAGEMENT.md Phase 11, decision 1). A module may now
|
||
// declare `ceiling: 'admin'` on a trigger or an audience, so the set of values
|
||
// `registerEventTriggers` and `registerAudiences` accept grew. Additions only,
|
||
// so minor: every declaration valid before is valid now, no stored value
|
||
// changes, and module-uo's `coreApi: "^1.3.0"` still resolves.
|
||
//
|
||
// It exists because Phase 11's operator-facing triggers - `uo.audit.staff_action`,
|
||
// `uo.economy.milestone`, `uo.world.saved` - are described everywhere as
|
||
// admin-audience, and the narrowest value the lattice had was `staff`, which
|
||
// means admin / editor / moderator. Ceilinging them there would have permitted a
|
||
// rule that mails the staff audit digest to every editor.
|
||
//
|
||
// **What a module has to know about it beyond the new name.** `admin` is the one
|
||
// pair in the tree with real containment - every admin is staff - so it is the
|
||
// only place `permits` is true between two non-`authenticated` values:
|
||
// `permits('staff', 'admin')` holds and nothing else of that shape does. A
|
||
// trigger ceilinged `staff` therefore accepts an `admin` audience, which is the
|
||
// intended narrowing, and the reverse is refused as it should be.
|
||
|
||
// 1.7.0 — the engagement contract (docs/website/ENGAGEMENT.md Phase 2).
|
||
// Additions only, so minor: `api.registerEventTriggers([...])`,
|
||
// `api.registerAudiences([...])`, `ctx.events.emit(triggerId, envelope)` and
|
||
// `ctx.inbox.push(userId, item)`. module-uo's `coreApi: "^1.3.0"` still resolves.
|
||
//
|
||
// **As in 1.6.0, the number covered the whole surface and the members arrived by
|
||
// phase, and all of them have now arrived.** `ctx.inbox.push` threw until Phase 7
|
||
// built the in-app channel and the table behind it — the same choice, for the
|
||
// same reason: a member of 1.7.0 that were absent would have made the version a
|
||
// lie, and one that silently accepted data into a table that did not exist would
|
||
// have been worse than either. **Filling it in is NOT a bump**: the signature is
|
||
// the one 1.7.0 declared, and a module written against it needs no change. What a
|
||
// module WILL see differently is the throw becoming a write, which is the whole
|
||
// point of the phase.
|
||
//
|
||
// One thing here is not a member and is still part of the contract: a trigger id
|
||
// and a notification-stream id share ONE namespace (ENGAGEMENT.md §7.2, settled
|
||
// by the org lead in Phase 2). An id has exactly one owner across both facets,
|
||
// so a module cannot attach a payload contract to another module's stream. That
|
||
// tightens a rule rather than changing a signature, and nothing registrable
|
||
// before this bump becomes unregistrable after it — the id grammar was RELAXED
|
||
// in the same change (`_` is now legal inside a segment).
|
||
//
|
||
// 1.6.0 — the Team surface (docs/website/TEAMS.md Part 11). Additions only, so
|
||
// minor: `api.registerTeamProvider({ getTeams, getTeamMembers, getTeamLeaders })`,
|
||
// `ctx.teams.publish(event)`, `ctx.teams.reconcile({ reason })`,
|
||
// `ctx.teams.activity.push(items)`, `api.registerSlashCommands([...])`, and the
|
||
// client slots `team.overview` / `team.member.row`. module-uo's `coreApi:
|
||
// "^1.3.0"` still resolves.
|
||
//
|
||
// **The number covers the whole surface; the members arrived by phase, and all of
|
||
// them have now arrived.** `activity.push` landed with the Team activity feed
|
||
// (§4, phase 3) and `registerSlashCommands` with the Discord commands (§7.1,
|
||
// phase 7); until each did, it was present and THREW rather than being absent or,
|
||
// worse, silently accepting data into a table that did not exist. Nothing in
|
||
// 1.6.0 throws any more.
|
||
//
|
||
// 1.5.0 — a CLIENT addition: `PublicLayout` takes an optional `shell` prop that
|
||
// renders the page body wrapper core's own pages write by hand (MODULE_API.md
|
||
// §3.4). Minor, not major: §3.4 makes *changing* a kit component's props a major
|
||
// bump because that breaks a call already written, and adding an optional one
|
||
// breaks nothing — omitting `shell` is 1.4.0's behaviour exactly. Nothing on the
|
||
// server changed; this file bumps for the reason below. Found by the Integration
|
||
// Kit's acceptance run (docs/modules/kit-acceptance.md), where a module built
|
||
// exactly as the kit teaches rendered outside the site's page column.
|
||
//
|
||
// 1.4.0 — no member changed. §2.7 gained one prohibition: a module does not open
|
||
// a connection to a game server from the website process; it talks to a sidecar,
|
||
// which owns the durable copy of the game's state. Minor rather than major
|
||
// because the SURFACE is identical to 1.3.0 — module-uo's `coreApi: "^1.3.0"`
|
||
// still resolves, and it already complies — but a module written against 1.3.0
|
||
// could satisfy every member and still be built the wrong way round, which is
|
||
// what this number now says. The one §2.7 rule with no CI behind it: an outbound
|
||
// socket is not statically detectable the way an internal require is.
|
||
//
|
||
// 1.3.0 — three CLIENT additions from Phase 3 slice 3: a nav item may carry an
|
||
// `icon`, core declares a `player.invite.accepted` slot, and `window.__rg.api`
|
||
// gained `BASE` (which §3.5 always specified and shared.js never published).
|
||
// Nothing on the server changed; this file bumps for the reason below.
|
||
//
|
||
// 1.2.0 — the CLIENT registry gained `registerExtension` and core gained client
|
||
// extension slots (MODULE_API.md §3.7): the twin of this half's declareSlot /
|
||
// registerExtension, for module content inside a core *page* rather than under a
|
||
// core route prefix. Nothing on the server changed, and this file bumps anyway —
|
||
// the two halves state ONE version, because a module declares a single `coreApi`
|
||
// range and is served one chunk (client/src/modules/version.js).
|
||
//
|
||
// 1.1.0 — `ctx` gained `activity.log`, `users.getById` and `site.baseUrl`, each
|
||
// because module-uo's extraction needed it and none of them could be vendored:
|
||
// an admin action a module performs belongs in core's one audit log, the
|
||
// extension slot needs the user its prefix names, and §2.7 forbids a module
|
||
// reading core's `APP_BASE_URL` for itself. Additions only, so minor.
|
||
const MODULE_API_VERSION = '1.15.0'
|
||
|
||
module.exports = { MODULE_API_VERSION }
|