From 7d51820bfe43d7df5f612a0aa0e9b1c4ab004677 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 15 Oct 2026 02:41:12 -0500 Subject: [PATCH] =?UTF-8?q?docs(events):=20a=20step's=20retry=20delay,=20M?= =?UTF-8?q?ODULE=5FAPI=201.15.0=20(PLAN=5FREDESIGNS=20=C2=A714.3.15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MODULE_API.md: 1.15.0, the failure envelope's retryAfterMs (clamped to one to thirty minutes, one attempt, ignored beside retry: false), the rule bullet and the registration line. - EVENTS.md §F: the envelope rule and the flat delay as a default. - STEAM_SIGNIN.md: its module contract moves to 1.16.0. - PLAN_REDESIGNS: §14.3.15, the stage-2 record (six settled points, the test-harness gap found on the way), and pointers from §9's summary and §14.3.13. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 42 ++++++++++++++++++++++++++++++++-- website/EVENTS.md | 12 +++++++++- website/MODULE_API.md | 30 ++++++++++++++++++++++-- website/STEAM_SIGNIN.md | 3 ++- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index d8e9f4f..263ac71 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -929,7 +929,8 @@ pulls) is research, not a walk, so it still comes first: all of them on both rigs (§14.2, D352); **it ran on 2026-10-14 (§14.2.1)**. Then one integration plan, answered before code: **written on 2026-10-14 as §14.3**, from D361–D369, and answered as D370–D376. Then the build, in §14.3.11's nine stages (two of them core), §13 first. **Stage 1, §13, was built on 2026-10-15 - (§14.3.14)**, with D378 and D379. + (§14.3.14)**, with D378 and D379. **Stage 2, core's `retryAfterMs` (MODULE_API 1.15.0), was built on + 2026-10-15 (§14.3.15).** **Then one walk, on Oxide and Carbon, of everything still owed (D331):** §2.7; §11's rows; §12.9; §13's and §14's rows; the in-game rows @@ -2822,6 +2823,8 @@ ten minutes"*. **Tests (website).** The delay honoured; clamped at both ends; counted as an attempt; no retry for a run paused or cancelled in the wait; `MODULE_API.md` and the classify rules updated. +> **Built on 2026-10-15 as MODULE_API 1.15.0** (§14.3.15). + #### 14.3.14 Stage 1, built: Rust's own world events (2026-10-15) Built on `edge` in Rust-Plugins, Rust-Link and Module-Rust, specified in @@ -2895,6 +2898,41 @@ Carbon's console; and a configuration save's reload from the website on Carbon ( **Not changed:** the Android app. It reads the API leniently, so it shows neither world events nor the two columns until it learns them. +#### 14.3.15 Stage 2, built: a step's retry delay (2026-10-15) + +Built in `website` as **MODULE_API 1.15.0**, specified in [MODULE_API.md](../../website/MODULE_API.md) §1.1 and +[EVENTS.md](../../website/EVENTS.md) §F, as §14.3.13 planned it. It took 1.15.0 ahead of Steam sign-in, whose plan +now names 1.16.0. No module changes: Module-uo (`^1.10.0`) answers none of it, and Module-Rust stays on +`^1.14.0` until stage 5, when Dangerous Treasures answers it and its pin moves. + +**Settled in the code, and overrulable:** + +1. **A `retryAfterMs` that is not a number is ignored, not clamped.** `"600000"`, `NaN` and `Infinity` are + dropped and logged, and the flat delay applies; the failure is unchanged. Zero and negatives are numbers and + clamp to the one-minute floor. +2. **Rounded to whole milliseconds.** +3. **Only on the retry shape.** Beside `retry: false` it is not carried, because a terminal failure is never + tried again. A missing `retry` still means retry, so the hint is read there too. +4. **Every retry line now says when**: `step.retry` carries `retryAt` whether or not the module hinted, and + `retryAfterMs` only when it did. The console reads *"failed, attempt 1 of 3, retrying at 20:10: not ready + yet"*. The step table's existing *"due 20:10"* was already there. +5. **`revert()` and `reconcile()` are unchanged.** Cleanup has its own retry counter (`MAX_REVERT_ATTEMPTS`), and + nothing in this plan asks for a delay there. +6. **Paused and cancelled need no new code**: the runner's `findDue` reads only running runs, so a step waiting on + its delay is simply not picked up. The tests prove it rather than adding a guard. + +**Tests.** Server: six new in `test/eventRunner.test.js` (the hinted delay honoured and the flat delay not; +an unhinted line's `retryAt`; three attempts at the thirty-minute ceiling fail on schedule; paused in the wait; +cancelled in the wait; `classify()`'s clamps and refusals), 67 in the file. **Found on the way:** the file's +stubs never covered `finishedRunsWithNothingLeft` (MODULE_API 1.11.0), so a tick that reached the cleanup sweep +with a terminal run waited on the dead pool; the cancelled-run test was the first to do it, and it is stubbed +now. Client: the retry line, an older line, an unreadable time. Full suites: server 2102 pass; the only failures +are `routeManifest.test.js`'s two and they fail identically on an untouched `main` here, because they read this +machine's installed `modules/`; client 422/422 and the build. + +**No walk rows of its own.** Stage 5's Dangerous Treasures rows exercise it: a step answering *"not ready"* +shows *"retrying at …"* and runs then. + ### 14.4 Questions for the org lead **Answered on 2026-10-14:** Q17 as proposed, out (D358). **Q18 against the proposal: the end walk waits for @@ -2919,7 +2957,7 @@ it does not hold the end walk (D359). What this module takes from it: -- **A verified Steam id for a signed-in account**, read through the module contract (MODULE_API 1.15.0 there). +- **A verified Steam id for a signed-in account**, read through the module contract (MODULE_API 1.16.0 there; 1.15.0 went to §14.3.13). - **A Steam identity as a linked Rust player**, beside the in-game `/link` code (R1). Whether it counts as a link is that plan's Q1, and what unlinking removes is its Q2. - When it lands, Module-Rust's account screens and the link flow are updated in their own PR. Rust's grants diff --git a/website/EVENTS.md b/website/EVENTS.md index 4a6db59..37d0943 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -518,7 +518,9 @@ contract would be a contract the modules were never asked about. An admin's own Three numbers govern a step, and they live in the runner rather than in a column because no authoring surface would ever show them: `EVENT_STEP_MAX_ATTEMPTS` (3), `EVENT_STEP_RETRY_MS` (60 000, flat), and -`EVENT_RUN_LEASE_MS` (15 minutes). A step's own lease is not one of them — it is computed from that +`EVENT_RUN_LEASE_MS` (15 minutes). The flat delay is the default rather than the only one since +MODULE_API 1.15.0: a failure that answers `retryAfterMs` is retried at that, clamped to one to thirty +minutes (§F). A step's own lease is not one of them — it is computed from that action's declared `budgetMs` plus a minute, because a registry that lets an action declare an hour would otherwise have its steps reclaimed and re-dispatched fifty-nine minutes before they answered. @@ -1000,6 +1002,14 @@ silently did nothing. "keep what you have" because staleness is cheap; an action's default is **"nothing happened, retry"**, because the expensive mistake is recording a world change that did not occur. A rejected promise, a throw, a timeout, a non-object and a missing `ok` are all `{ ok: false, retry: true }`. +- **A retryable failure may say when to try again** (MODULE_API 1.15.0; Rust PLAN_REDESIGNS + §14.3.13, D371): `{ ok: false, retry: true, retryAfterMs }`. `classify()` clamps it to 60 000 – + 1 800 000 ms rather than refusing it, because it is the module's hint and not an author's value; + anything that is not a number is dropped and logged, and the flat delay applies. It is still one + attempt of `EVENT_STEP_MAX_ATTEMPTS`, it degrades the run like any retry, and it is ignored beside + `retry: false`. The `step.retry` log line carries `retryAt` on every retry (and `retryAfterMs` when + hinted), so the console says *"retrying at 20:10"*. A run paused or cancelled in the wait is not + retried, because `findDue` reads only running runs. - **`cost` is declared by the module and enforced by core, before dispatch.** A module says what a verb consumes; core owns the caps and the arithmetic. A module cannot spend a budget it did not declare, and cannot under-report to escape a cap without lying in code the operator installed — diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 14f029d..2e149c8 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -26,13 +26,33 @@ 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.14.0' +const MODULE_API_VERSION = '1.15.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.15.0 — a failed step may say when to try again** (`website/EVENTS.md` §F; Rust +`modules/rust/PLAN_REDESIGNS.md` §14.3.13, D371). One optional envelope member and no removal, so +minor. + +- **`{ ok: false, retry: true, retryAfterMs }`** from `perform()`: the runner retries the step that + many milliseconds later instead of after the flat `EVENT_STEP_RETRY_MS` (60 s). Dangerous Treasures' + warm-up is the first user: *"not ready yet; try in ten minutes"*. +- **Clamped, never refused**, to **60 000 – 1 800 000** (one to thirty minutes): it is the module's + hint about its own verb, not an author's value. A value that is not a number is dropped and logged, + and the step retries on the flat delay. Read only on the retry shape: beside `retry: false` it means + nothing, because a terminal failure is never tried again. +- **It is one attempt** of `EVENT_STEP_MAX_ATTEMPTS`, and the run goes `degraded` on it as on any + retry. A hint changes *when*, never *how many*. A run paused or cancelled in the wait is not + retried; the staff Retry on a paused step works as before. +- **Every `step.retry` log line carries `retryAt`** (ISO time), and `retryAfterMs` when the module + gave one; the run console reads *"retrying at 20:10"*. +- `revert()` and `reconcile()` answers are unchanged: cleanup keeps its own retry counter. +- Module-uo's `^1.10.0` still resolves and answers none of it. This took 1.15.0 ahead of + `website/STEAM_SIGNIN.md`, which had claimed it and now names 1.16.0. + **1.14.0 — a param that holds several values** (`website/EVENTS.md` §F; Rust `modules/rust/PLAN_REDESIGNS.md` §12.3, D344). One optional flag and no removal, so minor. @@ -253,6 +273,8 @@ field: showing what it had, because staleness is cheap, whereas an action that half-ran and was recorded as done is a world change nothing will ever come back for. `retry` is opted OUT of — a module that means "this will never work" says `retry: false`. +- **A retry may say when** (1.15.0): `{ ok: false, retry: true, retryAfterMs }`, clamped to one to + thirty minutes, still one attempt. Without it, the flat `EVENT_STEP_RETRY_MS`. - **`inForce()` is a fourth question, not a fourth spelling of `read()`.** Optional, and answering `{ ok: true, held: false }` is the only thing that takes a lease's ledger row out — everything else, including a throw and a lease that declares no `inForce()` at all, leaves the row alone. @@ -447,6 +469,10 @@ 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.15.0 — a failure's `retryAfterMs`** (`website/EVENTS.md` §F; Rust PLAN_REDESIGNS §14.3.13, +D371). A retryable failure may say when to try again, clamped to one to thirty minutes; one optional +envelope member, so minor; see §1.1. Module-uo's `^1.10.0` still resolves. + **1.14.0 — a param's `multiple`** (`website/EVENTS.md` §F; Rust PLAN_REDESIGNS §12.3, D344). An action param may hold a list of `string` or `int` values, at most `max`; one optional flag, so minor; see §1.1. Module-uo's `^1.10.0` still resolves. @@ -870,7 +896,7 @@ api.registerSlashCommands([{ name, description, options, access, handler }]) // api.registerEventTriggers([{ id, label, kind, subjectKey, audience, ceiling, version, variables }]) // 1.7.0 api.registerAudiences([{ id, label, params, ceiling, resolve }]) // 1.7.0 api.registerEngagementSeeds({ templates, ruleGroups }) // 1.9.0 -api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0; a param's multiple + max 1.14.0 +api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0; a param's multiple + max 1.14.0; a failure's retryAfterMs 1.15.0 api.registerEventBudgets([{ id, label, unit }]) // 1.10.0 api.registerEventLeases([{ id, label, type, min, max, values, example, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0; example 1.13.0 api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0; a row's note 1.13.0 diff --git a/website/STEAM_SIGNIN.md b/website/STEAM_SIGNIN.md index 9f7fe33..5c7f671 100644 --- a/website/STEAM_SIGNIN.md +++ b/website/STEAM_SIGNIN.md @@ -60,7 +60,8 @@ assertion is single-use, which the existing single-use tx cookie already enforce - Admin → Auth shows Steam with an enable switch and, per Q3, an optional Web API key stored like a client secret. - The account page lists Steam identities with a *Link Steam* button, and each linked Steam account is shown with its id (or name, Q3) and an *Unlink* button. -- **Module contract, MODULE_API 1.15.0 (additions only):** a read of a user's verified identities for one provider, +- **Module contract, MODULE_API 1.16.0 (additions only; 1.15.0 was taken by a step's `retryAfterMs`, Rust + PLAN_REDESIGNS §14.3.13):** a read of a user's verified identities for one provider, and a hook when one is linked or unlinked. Names are settled at build. Module-uo uses neither, and runs its usual frozen-manifest proof against the new core. - OpenAPI annotations and the regenerated spec for anything new or changed; `BACKEND_DESIGN.md` §3/§4/§6.