From e7551cbf8fc0523e9db0dc1c34c92633ba5bf32c Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 20:41:08 -0500 Subject: [PATCH 1/4] =?UTF-8?q?docs(rust):=20plan=20fixes=20=E2=80=94=20D1?= =?UTF-8?q?81-D185,=20how=20step=202=20is=20built?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2 of PLAN_FIXES §6 had four open choices; the org lead answered them before any code: - D181/D182: F12 is a ZoneManager helper that Harmony-patches Zone.InitializeZone, ships in the overlay, and the bridge falls back to IsPositionInZone for scoring without it. - D183: F14's expiry is core's: an `expired` ledger status and ctx.events.expired (MODULE_API 1.11.0), without disturbing Module-uo. - D184: F8 is plugin.loaded / plugin.unloaded with the permission diff. - D185: F2 is the site's labels; no wire change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_FIXES.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/modules/rust/PLAN_FIXES.md b/modules/rust/PLAN_FIXES.md index 20327d7..7119d63 100644 --- a/modules/rust/PLAN_FIXES.md +++ b/modules/rust/PLAN_FIXES.md @@ -1,7 +1,8 @@ # `module-rust` — plan fixes **Status:** plan, approved 2026-09-26; its open questions were answered the same day (D169–D175), and step 1's -shape was settled as work began (D176–D179), and its rig walk's fixes by D180. **Everything in it — fixes and redesigns alike — lands before Module-Rust's cutover +shape was settled as work began (D176–D179), and its rig walk's fixes by D180; step 2's four open choices were +answered before its code (D181–D185). **Everything in it — fixes and redesigns alike — lands before Module-Rust's cutover (phase 19, [`PLAN.md`](PLAN.md) §34, D145; D169).** Everything here comes from the first walk of [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md) with a real player in the game — the Oxide pass, walked by the org lead on the `rust-oxide` rig with every frame checked on the console, the sidecar and the site's @@ -64,6 +65,11 @@ Taken by the org lead during and straight after the walk. | **D178** | **A reload fails on evidence, not on a clock.** While a configuration reload is pending the plugin reads the framework's new log lines each second and rolls back the moment they show the target plugin failing to compile or initialise. The ceiling is 30 s; when it passes, the plugin checks whether the target is loaded (a hook it missed) before restoring anything, and reports what actually loaded (F10). A framework hook for a failed load, if the rig shows one, replaces the log read. Rejected: the clock alone with a longer ceiling, which leaves a broken plugin down for the whole ceiling. | | **D179** | **The configuration page polls the write.** The save is recorded as `reloading`; ingest settles that row from the outcome frame, and the page polls it every couple of seconds until it does. Rejected: pushing the outcome over the admin event stream. | | **D180** | **The step-1 rig walk's fixes stay inside the bridge, and the console is a fair way in.** The walk (2026-09-26) found Carbon's Oxide-compatible `ReloadPlugin` does not reload (W3), Carbon's log is `Carbon.Core.log` (W4), Carbon loads a plugin whose config threw and raises `Loaded` anyway (W5), Oxide's "Could not initialize plugin" line was missed (W2), and Oxide's idle compiler drops a reload with "Timed out waiting for plugin to be compiled" (W1). All are fixed in `RunicGateway.cs` (PROTOCOL.md §19.1), never by changing a third-party plugin. Running server console commands (`c.reload`) is approved, and so is a helper plugin that hooks and runs console commands where one is needed (D168). | +| **D181** | **F12 is fixed by a ZoneManager helper that patches ZoneManager with Harmony** (D168). The patch follows `Zone.InitializeZone` — the one method a zone passes through when it is created, updated or loaded from ZoneManager's data file — and enters every connected player already inside it through ZoneManager's own entry path, so its flags, messages and `OnEnterZone` apply as if they had walked in. It fixes **every** zone on the server, not only the bridge's. Rejected: calling ZoneManager's private `UpdatePlayerZones` by reflection for our zones only; and measuring distance in the bridge with no helper at all. | +| **D182** | **The helper ships in the overlay beside the bridge and is installed by default** (ZoneManager is already a required plugin). An operator may delete it: the bridge still loads, reports the helper missing at hello, and **scores the zones it made by ZoneManager's public `IsPositionInZone`** — so a missing helper costs the zone's flags for somebody already inside, never their score. The site says so. The same fallback applies when the helper loads but cannot find what it patches. | +| **D183** | **F14's expiry is core's to record: the resource ledger gains an `expired` status, and a module reports one with `ctx.events.expired({ kind, ref })`.** `expired` is terminal like `reverted` — teardown never tries to take it back — and the run log says `resource.expired`. It is a MODULE_API minor (1.10.0 → 1.11.0), and it must not disturb Module-uo, which is already built against 1.10.0: additions only, no existing status changes meaning. Rejected: asking core to reconcile, which files an expected expiry as `orphaned` (a fault, and still claimable for a revert); and core marking rows expired from `lease_until` on its own clock. | +| **D184** | **F8's signal is `plugin.loaded` / `plugin.unloaded`, carrying the permissions that plugin added or removed.** The plugin diffs the registered permission list around each load and unload, emits only once the world is ready (a boot is the restart sync's), and the site marks the permission sync dirty when the list is not empty. The same list is §4.1's first evidence of which plugin owns a permission. Rejected: every load, names only. | +| **D185** | **F2 is the site's: NPC attackers are labelled by the killfeed**, which trims a prefab's variant digits (`wolf2` → wolf) and names the NPC families that read badly otherwise (scientists, bandits, animals). No wire change. Rejected: the plugin sending a display name Rust may not have. | ## 2. Fixes @@ -109,7 +115,9 @@ asks it every five seconds (§29.7) — **so after any restart mid-event, everyb nothing until they walk out and back in.** *Fix:* after creating or re-creating a zone, have ZoneManager re-evaluate who is inside; if its API has no way to, that is D168's case (a helper), or the tally measures distance itself for zones the bridge made. Walk it on -Carbon too. +Carbon too. Settled by D181 and D182: ZoneManager has no public way (its `UpdatePlayerZones` is private and +runs only for a connecting player), so a helper patches `Zone.InitializeZone` with Harmony; without the helper the +bridge scores its own zones by `IsPositionInZone`. **F13 — the `world.expired` frame leaves with its kind overwritten.** *(Rust-Plugins, protocol)* Three zones expired in the game exactly on time; all three frames were filed as `kind: "zone"`. Line ~7310 @@ -146,7 +154,7 @@ that, ask in parallel with a short timeout. came back at about 18:44; the grant that had been unresolved landed at 18:57, on the fifteen-minute audit. The page promises "It will land by itself when the plugin is back" — true, thirteen minutes late. *Fix:* emit a frame from `OnPluginLoaded` / `OnPluginUnloaded` when that plugin registers permissions; ingest -marks the server dirty the way `perm.drift` does. §4.1 needs the same signal. +marks the server dirty the way `perm.drift` does. §4.1 needs the same signal. Shape settled by D184. **F11 — the lease target picker was designed and never built.** *(website)* `core.lease`'s own comments say the authoring form reads the chosen lease's target source; `EventEditor.jsx` only ever renders `param.source` @@ -162,7 +170,7 @@ at the same moment did log one), and the retry 2.5 minutes later recorded "0 app restart restored. *Fix:* hold the restart sync until the hello says `worldReady: true`; log a failed sync. **F2 — NPC killers are named by prefab.** *(Rust-Plugins or Module-Rust)* "killed by `wolf2`". Send a display name, -or map prefab to label on the site — whichever the killfeed page already expects. +or map prefab to label on the site — whichever the killfeed page already expects. The site's, by D185. **F4 — a death of a player made by another plugin carries `steamId: null`.** *(Rust-Plugins)* `UserIDString` is empty for players other plugins spawn; the plugin already avoids it for `entity.destroyed`. Use @@ -172,7 +180,8 @@ empty for players other plugins spawn; the plugin already avoids it for `entity. console until the runs were cancelled, when teardown found them "already gone" and counted that a success. This was the design — PROTOCOL.md §15: "The website maps it to nothing. Core learns about it through `reconcile` and `revert`" (D96) — and D170 changes it. *Fix:* after F13, ingest maps `world.expired` to the run's resource row -and marks it `expired`; teardown treats `expired` as already reverted; amend PROTOCOL.md §15. +and marks it `expired`; teardown treats `expired` as already reverted; amend PROTOCOL.md §15. The row is core's +(`event_run_resources`), so core gains the status and the call (D183, MODULE_API 1.11.0). ## 3. Interface and walk-doc corrections @@ -366,7 +375,8 @@ once: - `world.expired` carries `what` (F13). - The configuration reload outcome as an asynchronous frame (F9). -- A plugin-loaded / unloaded frame (F8). +- A plugin-loaded / unloaded frame (F8), with the permissions it added or removed (D184). +- The ZoneManager helper's state at hello (D182). - The permission inventory verb and its reply (§4.1). - ZoneManager's flags at hello, zone options on `world.zone`, the dome option (§4.4). - New tally fields: D159's gathering sources, the animal / NPC split, weapon-class kills, headshots, crafting, @@ -394,8 +404,9 @@ in Rust-Plugins, Rust-Link and Module-Rust, and the three cut over to `main` tog Where the walk already points at one: -- **ZoneManager** — re-evaluating who is inside a zone the bridge just created or restored (F12), if it has no API - for it; and a flying permission per zone (§4.4). +- **ZoneManager** — re-evaluating who is inside a zone when it is created, updated or loaded (F12). It has no API + for it, so this is the first helper, built in step 2 (D181, D182). A flying permission per zone (§4.4) may join + it. - **ZoneDomes** — only if a null `player` turns out to be unsafe in its API. - **Kits** — none needed: D164 uses the existing credit path. -- 2.49.1 From 1a7f52743f462abf2f1ecb3f6ef39a4f55db3d85 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 21:47:42 -0500 Subject: [PATCH 2/4] =?UTF-8?q?docs(rust):=20protocol=2013=20step=202=20?= =?UTF-8?q?=E2=80=94=20expiry,=20plugin=20loads,=20the=20zone=20helper,=20?= =?UTF-8?q?the=20tally=20(=C2=A719.4-19.8,=20MODULE=5FAPI=201.11.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_FIXES.md | 7 +++ rust-link/INSTALL.md | 9 ++- rust-link/PLAYER_WALK.md | 24 +++++++- rust-link/PROTOCOL.md | 117 +++++++++++++++++++++++++++++++++++-- website/EVENTS.md | 16 ++++- website/MODULE_API.md | 23 +++++++- 6 files changed, 185 insertions(+), 11 deletions(-) diff --git a/modules/rust/PLAN_FIXES.md b/modules/rust/PLAN_FIXES.md index 7119d63..b6cdbcc 100644 --- a/modules/rust/PLAN_FIXES.md +++ b/modules/rust/PLAN_FIXES.md @@ -395,6 +395,13 @@ in Rust-Plugins, Rust-Link and Module-Rust, and the three cut over to `main` tog (D176, D177). 2. **Protocol 13 with the remaining fixes:** F13, F14, F12, F1, F3, F8, F7, F2, F4 in the bridge and the module, and F5/F6 in the module. + *As built, 2026-09-27* (decisions D181–D185): Rust-Plugins#18, Module-Rust#22, Rust-Link#19 (the egg), + installer#35 and website#209 (core's `expired` status, MODULE_API 1.11.0). The spec is + [`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §19.4–19.8. Walked on both rigs without a player: the + bridge and the ZoneManager helper compile and load, the helper patches ZoneManager 3.1.14, the hello + carries its state, and `plugin.loaded` / `plugin.unloaded` carry the right permissions. The rows that + need somebody in the game are [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md)'s step-2 section, + rows 3–9. 3. **The redesigns**, each planned in detail before code and walked on both frameworks: the permission manager (§4.1), the event step editor with the kit-weekend template (§4.2, §4.3), zones and domes (§4.4), the map's marker types (§4.5), the chat title conditions (§4.6), and the NPC research (§4.7). diff --git a/rust-link/INSTALL.md b/rust-link/INSTALL.md index dbb6ce5..0628861 100644 --- a/rust-link/INSTALL.md +++ b/rust-link/INSTALL.md @@ -11,7 +11,7 @@ speaks one protocol, never "the latest of each": | Component | What it is | Released from | |---|---|---| -| **The plugin** | `RunicGateway.cs`, one file that runs unchanged on Oxide and Carbon | [Rust-Plugins](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/releases) | +| **The plugin** | `RunicGateway.cs`, one file that runs unchanged on Oxide and Carbon — and beside it, from protocol 13, the optional **ZoneManager helper** `RunicGatewayZones.cs` (PLAN_FIXES D181, D182), installed by default | [Rust-Plugins](https://gitea.whitlocktech.com/RunicGateway/Rust-Plugins/releases) | | **The sidecar** | `rust-link-sidecar`, which the plugin dials on loopback and the website reaches over HTTP | [Rust-Link](https://gitea.whitlocktech.com/RunicGateway/Rust-Link/releases) | The game server opens no port for the bridge: the plugin is the client and the sidecar the @@ -156,7 +156,10 @@ history, and the token is unchanged. plugin tarball, each with a `sha256`. 2. **Download and verify** both against those checksums (`sha256sum -c`, or `Get-FileHash`). 3. **The plugin:** copy `runicgateway-rust-plugin/RunicGateway.cs` from the tarball into - `oxide/plugins/` or `carbon/plugins/`. To name the server, create its config first — + `oxide/plugins/` or `carbon/plugins/`, and every other `.cs` the tarball's `manifest.json` lists + in `files` beside it — from protocol 13 that is `RunicGatewayZones.cs`, which lets ZoneManager count a + player already standing in a zone when it opens or comes back after a restart. It is optional: leave + it out and events still score everybody, but ZoneManager's own flags miss that player. To name the server, create its config first — `oxide/config/RunicGateway.json` or `carbon/configs/RunicGateway.json` — holding `{ "ServerId": "", "Port": 7799 }`. Without it, the plugin calls the server `main`. 4. **The sidecar:** put the binary somewhere stable and give it a config: @@ -215,7 +218,7 @@ With the installer: | | | |---|---| -| `doctor --game rust [--server-id ]` | Per server: the framework; whether the plugin file is still the one deployed; that the plugin's config names this server; the required uMod plugins; the service; and `/health` through to **plugin connected**. A stopped server is a warning; a running one whose plugin never connected is a failure, printed with the framework versions the plugin is known good on | +| `doctor --game rust [--server-id ]` | Per server: the framework; whether the plugin file is still the one deployed; each helper deployed beside it, as a **warning** when missing or edited (the bridge runs without one, and the row says what that costs); that the plugin's config names this server; the required uMod plugins; the service; and `/health` through to **plugin connected**. A stopped server is a warning; a running one whose plugin never connected is a failure, printed with the framework versions the plugin is known good on | | `update --game rust` | Moves the sidecar and every server's plugin to the current bundle, and restarts the sidecars. Always all servers together — they share one binary | | `uninstall --game rust [--server-id ] [--purge]` | Removes the service and the plugin file. **Keeps the plugin's config** — it is the website's, and it names the server. `--purge` also removes the sidecar config (the token) and the database. Removing the last server removes the shared binary too | diff --git a/rust-link/PLAYER_WALK.md b/rust-link/PLAYER_WALK.md index d82c5a7..e7814bb 100644 --- a/rust-link/PLAYER_WALK.md +++ b/rust-link/PLAYER_WALK.md @@ -289,7 +289,7 @@ start the run. | 3 | **Leave step 2's run going and restart the server.** Rejoin, walk back into the zone and run `rgp.inzone` | `zone_list` shows the zone **back** after the restart, at the same place and with the same name, re-created from the bridge's own record because ZoneManager forgets temporary zones. `rgp.inzone` names you again. On the site the run's zone row still reads `confirmed` | | 4 | **`rust.crate.place`**: `crate.hackable` ×1 and `crate.elite` ×3 at the same monument, spread 8. Loot one elite crate empty; start the hack on the locked crate. Then cancel the run | The crates are there and lootable, the hack timer runs. At teardown the untouched crates vanish, and the looted one is simply **gone** already — its row reads `reverted` all the same (§15.2: reverting something a player took is a success) | | 5 | **`rust.npc.place`**: `npc.scientist` ×3 at the monument. Kill one; let the others see you | They are hostile and fight. The one you killed is just gone; at teardown the survivors vanish mid-fight. Every row reads `reverted`. If an NPC spawned **inside a wall or fell through the ground**, note the monument — some offsets land off the navmesh, and that is a finding about where to put things, not a bug to hide | -| 6 | **A zone for 3 minutes.** Stand inside it (`rgp.inzone` names you) and wait out the three minutes | The zone disappears around you **without anybody on the website doing anything**, because the game keeps the deadline (D96). `zone_list` no longer lists it, and the sidecar's `/events?kind=world.expired` shows the frame. Best done on the non-admin account, which is the view a real player has | +| 6 | **A zone for 3 minutes.** Stand inside it (`rgp.inzone` names you) and wait out the three minutes | The zone disappears around you **without anybody on the website doing anything**, because the game keeps the deadline (D96). `zone_list` no longer lists it, and the sidecar's `/events?kind=world.expired` shows the frame, with `what: "zone"` (protocol 13, F13 — before it, the frame was filed as `zone` and this check could not pass). On the run console the zone reads **`expired` — ended by the game on time**, not `confirmed` (F14). Best done on the non-admin account, which is the view a real player has | | 7 | **If players can be arranged, a `core.lease` on `spawn.max_rate`** — these two scalars are left out of the catalogue today because they only matter with people online (§27.5) | With several players on, the population system's spawn rate moves (`spawn.report`) while the lease holds and returns after. If it does, the two `spawn.max_*` keys can be added to the allowlist; if nothing moves even with players, they stay out | **Run steps 1–5 on both frameworks.** One thing is specific to Carbon: ZoneManager was only installed @@ -407,3 +407,25 @@ above, and a staff member at the website. **What counts as a pass:** a real chat line carries a site title and a site style, the voice speaks without a sender, and a popup appears on a real screen. + +## The step-2 fixes (protocol 13, PLAN_FIXES §6 step 2) + +Added 2026-09-27. What [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md) §8 asks the re-walk to show for +step 2's fixes, in the order they are quickest to walk. Player A from the walks above; a staff member +at the website. **Run it on Oxide and on Carbon.** Rows marked *no player* were walked on both rigs on +2026-09-27 without one; the rest need somebody in the game. + +| # | Do this | You should see | +|---|---|---| +| 1 | *No player.* `rgz.status` in the server console | `Runic Gateway Zones 0.1.0: patched`, and the servers page says nothing about the helper. Delete `RunicGatewayZones.cs` and wait a minute: the servers page warns that ZoneManager's flags miss a player already inside a new zone (D182). Put it back | +| 2 | *No player.* `oxide.unload Kits` (Carbon: `c.unload Kits`), grant a site group `kits.admin`, then `oxide.load Kits` | The sidecar's `/events?kind=plugin.loaded` shows Kits with `["kits.admin","kits.rgreward"]`, and the grant lands **within a minute** — not on the fifteen-minute audit (F8) | +| 3 | A fells a tree and picks up a hemp plant | The next `player.tally` counts the wood the inventory shows — the final hit's bonus included — and the cloth from the hemp (F1, D159) | +| 4 | A breaks a wall of their own base | `entity.destroyed` arrives; the next tally's `structures` does **not** move (F3) | +| 5 | Staff open a zone with a timed event while A stands where it will be | ZoneManager counts A at once: the zone's enter message shows, and the run's tally gives A seconds without A walking out and back in (F12) | +| 6 | With A standing in a live event zone, restart the server | After the restart A is in the zone again (ZoneManager says so) and keeps scoring without moving (F12). The site's restart permission sync goes **after** "Server startup complete", not before, and says what it applied (F7) | +| 7 | Staff delete the helper, then repeat step 5 | A still scores (the bridge measures the zone's shape, D182), but the zone's enter message does not show for A | +| 8 | Stop another enabled server, then have A type a made-up code on the website | "That code is unknown or has expired" — not "one of the servers could not be reached … your code is still good" (F5) — in a second or two, not many (F6). A real code from A's server still links | +| 9 | A is killed by a wolf, then by a scientist | The killfeed says **Wolf** and **Scientist**, not `wolf2` or `scientistnpc full any` (F2) | + +**What counts as a pass:** a zone counts somebody who was already standing in it, on both frameworks, +and an expired zone reads `expired` on the run console. diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 6ecf906..04b971c 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -1511,8 +1511,11 @@ erases a zone whose `holdMs` has run out, whether or not the website is heard fr {"kind":"world.expired","type":"event","id":"rg-13-35875416-1","runId":"13"} ``` -The website maps it to nothing. Core learns about it through `reconcile` and `revert`, as with -`lease.expired`. +Until protocol 13 the website mapped it to nothing (D96), and core learned about it only through +`reconcile` and `revert`, as with `lease.expired`. **Protocol 13 changes both halves** (§19.4): the +frame carries the entity's kind as `what` — protocols 9 to 12 wrote it over the frame's own `kind`, so no +`world.expired` ever arrived as one — and the website records the run's resource as `expired` +(PLAN_FIXES D170, D183). ### 15.4 `EventsEnabled`, and the two bounds @@ -1950,8 +1953,9 @@ untouched. Planned in [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md): one bump for every wire change the org lead's first walk with a player in the game found (§5 there). It is built on `edge` in Rust-Plugins, -Rust-Link and Module-Rust and released together (D177). **This section grows as the pieces land**; -the first is the configuration save. +Rust-Link and Module-Rust and released together (D177). **This section grows as the pieces land**: +the configuration save first (§19.1–19.3, step 1), then step 2's fixes (§19.4–19.8, decisions +D181–D185). One fix rides in the same release without a wire change: **the plugin decodes what the sidecar sends as UTF-8** (F15). Protocols 1 to 12 decoded each byte as a character — Latin-1 — so every non-ASCII @@ -2073,3 +2077,108 @@ from `config.outcome` by `(server_id, write_id)`, and only while it is still `re frame moves nothing and a late one still lands. A row past `settle_by` reads as **`lost`**. The page polls `GET /api/v1/admin/rust/config/{serverId}/writes/{writeId}` every two seconds until the write settles (D179). + +### 19.4 `world.expired` is recognisable, and an expiry is recorded (F13, F14) + +The frame gains **`what`**, the entity's kind — the name the website's resource rows already use: + +```json +{"kind":"world.expired","type":"event","id":"rg-13-35875416-1","what":"zone","runId":"13"} +``` + +Protocols 9 to 12 wrote the entity's kind into `kind`, over the frame's own, so every expiry reached the +sidecar as `kind: "zone"` and nothing listening for `world.expired` ever heard one (F13). A website that +reads protocol 12 frames will find them filed under `zone`; nothing is re-filed. + +**The website records it** (D170, D183). Module-Rust hands core the resource the zone step ledgered — +kind `world`, ref `:` (§15, the ref names the server) — through +`ctx.events.expired({ kind, ref })` (MODULE_API 1.11.0). Core marks that row **`expired`**: terminal, +never taken back at teardown, and distinct from `orphaned`, which `reconcile` uses for a thing that +vanished with nobody asking. An expiry for a zone no run ledgered is not an error. + +`world.expired` and `lease.expired` are **staff** kinds (§8.5). Neither was classified before protocol +13 — default deny kept both off public pages — and both are an event's machinery; the public hears what +an event did from core's announcements. + +### 19.5 `plugin.loaded` and `plugin.unloaded` (F8) + +A grant the site made for a plugin that was not loaded stays unresolved until the plugin is back. The +first walk watched one land thirteen minutes after the plugin loaded, on the fifteen-minute audit. So +the bridge now announces a load or an unload of **another** plugin, with the permissions it registered +or dropped (D184): + +```json +{"kind":"plugin.loaded","type":"event","name":"Kits","version":"4.4.9","permissions":["kits.admin","kits.rgreward"]} +``` + +- **`permissions`** is a diff of the framework's registered-permission list, taken **one tick after the + hook**, because the order of "drop an unloaded plugin's permissions" and "call `OnPluginUnloaded`" + is the framework's own. It is sorted, and it may be empty. +- **Only once the world is ready** (§15.5). A boot loads every plugin, and a boot is the restart sync's + job; the bridge takes its baseline list at `OnServerInitialized`. +- **A reload in one tick shows an empty list on both frames.** Carbon's `c.reload` unloads and loads in + the same tick, so by the next the list is what it was. That is the right answer: what a grant + resolves against did not change. Two plugins loaded in one tick share one diff, under the first name. +- Both are **staff** kinds. The website marks the server's permission sync dirty when `permissions` is + not empty, and the next tick (30 s) pushes. §4.1's permission manager will take plugin ownership from + its own inventory read, not from this. + +Walked on both rigs, 2026-09-27: `oxide.unload Kits` / `oxide.load Kits` and Carbon's `c.unload` and +the queued load each carried `["kits.admin","kits.rgreward"]`; the ZoneManager helper's load carried `[]`. + +### 19.6 The ZoneManager helper, and `zoneHelper` at hello (F12) + +ZoneManager counts a player as inside a zone only when the zone's trigger fires on **entry**, so a zone +created, updated or re-created around somebody standing there holds nobody — its flags, messages and +`OnEnterZone` never happen for them, and the bridge's tally, which asks ZoneManager's membership, scored +nobody already in an arena after a restart. ZoneManager has no public way to look again. + +**`RunicGatewayZones.cs`** (D181, D168) ships beside the bridge and is installed by default (D182). It +Harmony-postfixes ZoneManager's `Zone.InitializeZone` — the one method a zone passes through when it is +created, updated (`Zone.Reset`) or loaded from ZoneManager's data file — and one tick later enters every +connected player ZoneManager's own `IsPositionInZone` places inside, through ZoneManager's own +`OnPlayerEnterZone`. A player ZoneManager already counts is skipped, and ZoneManager refuses a second +entry anyway. It fixes **every** zone on the server, not only the bridge's. It sweeps all zones once when +it patches, and re-patches when ZoneManager reloads. `rgz.status` prints its state. + +The bridge never depends on it. `server.hello` carries, whenever ZoneManager is loaded: + +```json +"zoneHelper": { "state": "patched", "version": "0.1.0" } +``` + +`state` is `patched`, `unsupported` (loaded, but this ZoneManager lacks what it patches — `reason` says +what), `no-zonemanager`, or `missing` (the file is not there). **Anything but `patched` and the bridge +scores the zones it made by ZoneManager's public `IsPositionInZone`**, for the participation tally and +the kill credit alike — so a missing helper costs ZoneManager's flags for somebody already inside, never +their score. The website shows the cost on the servers page. + +Walked 2026-09-27: it patched ZoneManager 3.1.14 on Oxide and on Carbon, and the site read +`{"state":"patched","version":"0.1.0"}` from both hellos. **Not yet walked with a player standing in a +zone** — that needs somebody in the game. + +### 19.7 What the tally counts (F1, F3, F4) + +- **`gathered` is everything a player harvests** (D159). `OnDispenserGather` is the swings; + `OnDispenserBonus` (the final hit's bonus), `OnCollectiblePickup` (hemp, mushrooms, stones off the + ground) and `OnGrowableGathered` (a farmed plant) now add to the same per-item counter. All three are + `void` (§8.7) and in `rg.hooks`. A pick-up another plugin vetoes is still counted — the hook fires + before the items move — which is rarer than a second hook is worth. +- **`structures` no longer counts your own base.** A building block the attacker placed, or one on a + cupboard they are authorised on, is not counted. The `entity.destroyed` frame still goes; the site + already skips the raid alert for an authorised attacker (D59). +- **Every Steam id comes from `userID`.** The thirteen frames that read `UserIDString` sent `null` for a + player another plugin spawned; they now use the same `SteamIdOf` as the map. + +### 19.8 The website + +- **The loading hold** (F7). Permission and title pushes wait while the last hello says + `worldReady: false` — the first walk's restart sync went 35 s before the save loaded, timed out, and + the retry reported "0 applied". A human's "sync now" is not held. A failed or refused sync logs a + warning. +- **Link codes** (F5, F6). A code is asked of the servers that minted one in the last six minutes (every + `/link` stores an `account.link.requested`) first, then of the rest, each group **in parallel**. The + answer is "a server could not be reached — your code is still good" only when one of the minting + servers is unreachable; a dead server that minted nothing no longer makes a wrong code look good. +- **NPC attackers** (F2, D185) are named by the killfeed: a family (`scientistnpc_*` → Scientist, + `bradleyapc` → Bradley APC) or the prefab without its variant digits (`wolf2` → Wolf). No wire change. diff --git a/website/EVENTS.md b/website/EVENTS.md index ec75136..fc3014b 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -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 diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 5cd33e5..3f1514e 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -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`, 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 -- 2.49.1 From a199823513b5ad0ed32227f7086ba69172b0e25c Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 21:48:18 -0500 Subject: [PATCH 3/4] docs(rust): say which half of the step-2 walk rows was walked Rows 1-2 of PLAYER_WALK's step-2 section had their game side walked on both rigs; their site side (the servers-page warning, a grant landing within a minute) needs a walk core running Module-Rust#22 on website#209, which the first commit claimed as walked. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_FIXES.md | 6 +++--- rust-link/PLAYER_WALK.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/rust/PLAN_FIXES.md b/modules/rust/PLAN_FIXES.md index b6cdbcc..68a28a6 100644 --- a/modules/rust/PLAN_FIXES.md +++ b/modules/rust/PLAN_FIXES.md @@ -399,9 +399,9 @@ in Rust-Plugins, Rust-Link and Module-Rust, and the three cut over to `main` tog installer#35 and website#209 (core's `expired` status, MODULE_API 1.11.0). The spec is [`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §19.4–19.8. Walked on both rigs without a player: the bridge and the ZoneManager helper compile and load, the helper patches ZoneManager 3.1.14, the hello - carries its state, and `plugin.loaded` / `plugin.unloaded` carry the right permissions. The rows that - need somebody in the game are [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md)'s step-2 section, - rows 3–9. + carries its state, and `plugin.loaded` / `plugin.unloaded` carry the right permissions. Still to walk: + the site side (a walk core running Module-Rust#22 on website#209), and the rows of + [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md)'s step-2 section that need somebody in the game (3–9). 3. **The redesigns**, each planned in detail before code and walked on both frameworks: the permission manager (§4.1), the event step editor with the kit-weekend template (§4.2, §4.3), zones and domes (§4.4), the map's marker types (§4.5), the chat title conditions (§4.6), and the NPC research (§4.7). diff --git a/rust-link/PLAYER_WALK.md b/rust-link/PLAYER_WALK.md index e7814bb..750171f 100644 --- a/rust-link/PLAYER_WALK.md +++ b/rust-link/PLAYER_WALK.md @@ -412,8 +412,10 @@ without a sender, and a popup appears on a real screen. Added 2026-09-27. What [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md) §8 asks the re-walk to show for step 2's fixes, in the order they are quickest to walk. Player A from the walks above; a staff member -at the website. **Run it on Oxide and on Carbon.** Rows marked *no player* were walked on both rigs on -2026-09-27 without one; the rest need somebody in the game. +at the website. **Run it on Oxide and on Carbon.** Rows marked *no player* need nobody in the game. +Their game side was walked on both rigs on 2026-09-27 (the helper patched, the hello carried its state, +the load frames carried Kits' permissions); their site side waits for a walk core running Module-Rust#22 on +website#209. The rest need somebody in the game. | # | Do this | You should see | |---|---|---| -- 2.49.1 From 383f3bdbe58876d8ef5311020c773194f214f212 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sun, 27 Sep 2026 01:05:35 -0500 Subject: [PATCH 4/4] docs(rust): the step-2 site walk, and the run it found stuck pending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walked on a core running Module-Rust#22 on website#209 against rust-oxide: F8, F13/F14, F7, F5/F6 and the helper's state all pass. The expiry walk found a run left cleanup-pending for ever when its last resource expires while it runs; website#209 now settles it, and EVENTS.md §L says so. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_FIXES.md | 12 +++++++++--- rust-link/PLAYER_WALK.md | 5 +++-- website/EVENTS.md | 4 +++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/rust/PLAN_FIXES.md b/modules/rust/PLAN_FIXES.md index 68a28a6..b0a4174 100644 --- a/modules/rust/PLAN_FIXES.md +++ b/modules/rust/PLAN_FIXES.md @@ -399,9 +399,15 @@ in Rust-Plugins, Rust-Link and Module-Rust, and the three cut over to `main` tog installer#35 and website#209 (core's `expired` status, MODULE_API 1.11.0). The spec is [`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §19.4–19.8. Walked on both rigs without a player: the bridge and the ZoneManager helper compile and load, the helper patches ZoneManager 3.1.14, the hello - carries its state, and `plugin.loaded` / `plugin.unloaded` carry the right permissions. Still to walk: - the site side (a walk core running Module-Rust#22 on website#209), and the rows of - [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md)'s step-2 section that need somebody in the game (3–9). + carries its state, and `plugin.loaded` / `plugin.unloaded` carry the right permissions. The site side + was walked the same night on a walk core running Module-Rust#22 on website#209, against `rust-oxide`: the + servers list shows the helper `patched`; a Kits unload and load was synced as `dirty` 24 s later (F8); + a 3-minute zone expired into `expired` with `resource.expired` and teardown never touched it (F13, F14) + — which found a run left `pending` for ever when its last resource expires while it runs, fixed in + website#209; a restart's sync waited for `worldReady` and went after it (F7); and `ZZZZZZ` with five + servers down answered "unknown or has expired", once, in one timeout (F5, F6). Still to walk: the rows + of [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md)'s step-2 section that need somebody in the game + (3–9). 3. **The redesigns**, each planned in detail before code and walked on both frameworks: the permission manager (§4.1), the event step editor with the kit-weekend template (§4.2, §4.3), zones and domes (§4.4), the map's marker types (§4.5), the chat title conditions (§4.6), and the NPC research (§4.7). diff --git a/rust-link/PLAYER_WALK.md b/rust-link/PLAYER_WALK.md index 750171f..b5fded8 100644 --- a/rust-link/PLAYER_WALK.md +++ b/rust-link/PLAYER_WALK.md @@ -414,8 +414,9 @@ Added 2026-09-27. What [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md) §8 asks step 2's fixes, in the order they are quickest to walk. Player A from the walks above; a staff member at the website. **Run it on Oxide and on Carbon.** Rows marked *no player* need nobody in the game. Their game side was walked on both rigs on 2026-09-27 (the helper patched, the hello carried its state, -the load frames carried Kits' permissions); their site side waits for a walk core running Module-Rust#22 on -website#209. The rest need somebody in the game. +the load frames carried Kits' permissions), and their site side on `rust-oxide` the same night (the servers +list showed the helper; the Kits load was synced 24 s later). Row 8's refusal and events step 6 were also +walked there without a player. The rest need somebody in the game. | # | Do this | You should see | |---|---|---| diff --git a/website/EVENTS.md b/website/EVENTS.md index fc3014b..61bb5d1 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -1805,7 +1805,9 @@ It is **terminal and green, like `reverted`**: it releases the target (it is not 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. +sweep no longer scans. A run still going when its last resource expires is settled by the sweep once it +ends: finished, `pending`, and no unresolved row is `complete` — the scan that joins on an unresolved row +could not see it, and the step-2 walk's run 46 sat `pending` until it could. --- -- 2.49.1