feat(events): an expired ledger status and ctx.events.expired (MODULE_API 1.11.0) #209

Merged
whitlocktech merged 2 commits from feat/events-expired-status into main 2026-09-27 06:10:46 +00:00
Member

Core's half of Rust PLAN_FIXES F14. Decision D183 is in docs#288.

Until now, a game that ended a resource at its own deadline (a Rust zone erased when its time is up) could only reach core through ctx.events.reconcile(). That filed it as orphaned: shown amber as "gone", and still claimable for a revert. This PR records it as the plan working.

What changes

  • event_run_resources.status gains expired. It is terminal like reverted: the sweep never tries to give it back, live_marker releases the target, and it is in neither HELD nor UNRESOLVED. The ENUM ALTER sits below the CREATE and re-runs as a no-op on every boot. I checked this on MariaDB 11.8 with the stored generated column that depends on status.
  • ctx.events.expired({ kind, ref }) marks the calling module's pending, confirmed or orphaned rows for that target as expired and writes resource.expired to the run log. A revert already in flight is left to finish. The owner is bound by the loader, never read from the arguments, the same way reconcile does it. If this was the last unresolved row of a finished run, cleanup moves to complete, including from incomplete, which the sweep no longer scans.
  • The run console shows it green, as "ended by the game on time". The swagger description is updated and the spec regenerated.
  • MODULE_API_VERSION goes 1.10.0 → 1.11.0 in both halves. engagement-triggers.json is regenerated, since it embeds the version.

Module-uo is unaffected (the org lead's condition)

  • It declares coreApi: ^1.10.0, calls none of this, and reads no ledger status; the matches are comments only.
  • I ran its CI frozen-manifest job against this branch in a scratch clone. Core loaded it (registered module "uo" v0.6.0, 12 event actions), and the result was routes.manifest.json is current — 76 routes, all documented.
  • Its own suites on that clone: server 723/723, client 45/45.
  • eventModuleContract.test.js now asserts MODULE_API_VERSION satisfies ^1.10.0, as well as pinning 1.11.0.

Tests

  • 6 new tests in eventCleanup.test.js: terminal, wins over orphaned, leaves a revert in flight alone, owner and target scoping, malformed input and targets nobody ledgered, and the cleanup-status transitions. Also a moduleLoader probe for ctx.events keys and the owner binding.
  • eventRunnerSql.test.js: 68/68 against a real MariaDB 11.8.
  • Server: 2069 pass. Three failures are not from this change:
    • routeManifest ×2 also fail on main in this working copy, and pass in a fresh clone of this branch (route manifest up to date (280 routes)).
    • eventRunnerSql skips without a DB and passed against one; it failed once in a full-suite run.
    • teamActivity hangs to its file timeout on main too.
  • Client: 400/400, and the build passes.

Follow-ups, not in this PR: EVENTS.md §L and MODULE_API.md ride in the step-2 docs PR. The Integration-kit pin moves only when someone re-reads the chapters against a shipped core, per its own ci/core-ref.json rule. Module-Rust will declare ^1.11.0 when it starts calling this.

Tracking: Module-Rust#21.

  • AI-assisted (Claude Code)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Core's half of Rust PLAN_FIXES **F14**. Decision **D183** is in docs#288. Until now, a game that ended a resource at its own deadline (a Rust zone erased when its time is up) could only reach core through `ctx.events.reconcile()`. That filed it as `orphaned`: shown amber as "gone", and still claimable for a revert. This PR records it as the plan working. **What changes** - `event_run_resources.status` gains **`expired`**. It is terminal like `reverted`: the sweep never tries to give it back, `live_marker` releases the target, and it is in neither `HELD` nor `UNRESOLVED`. The ENUM `ALTER` sits below the `CREATE` and re-runs as a no-op on every boot. I checked this on MariaDB 11.8 with the stored generated column that depends on `status`. - **`ctx.events.expired({ kind, ref })`** marks the calling module's `pending`, `confirmed` or `orphaned` rows for that target as `expired` and writes `resource.expired` to the run log. A revert already in flight is left to finish. The owner is bound by the loader, never read from the arguments, the same way `reconcile` does it. If this was the last unresolved row of a finished run, cleanup moves to `complete`, including from `incomplete`, which the sweep no longer scans. - The run console shows it green, as "ended by the game on time". The swagger description is updated and the spec regenerated. - `MODULE_API_VERSION` goes **1.10.0 → 1.11.0** in both halves. `engagement-triggers.json` is regenerated, since it embeds the version. **Module-uo is unaffected (the org lead's condition)** - It declares `coreApi: ^1.10.0`, calls none of this, and reads no ledger status; the matches are comments only. - I ran its CI `frozen-manifest` job against this branch in a scratch clone. Core loaded it (`registered module "uo" v0.6.0`, 12 event actions), and the result was `routes.manifest.json is current — 76 routes, all documented`. - Its own suites on that clone: server 723/723, client 45/45. - `eventModuleContract.test.js` now asserts `MODULE_API_VERSION` satisfies `^1.10.0`, as well as pinning 1.11.0. **Tests** - 6 new tests in `eventCleanup.test.js`: terminal, wins over orphaned, leaves a revert in flight alone, owner and target scoping, malformed input and targets nobody ledgered, and the cleanup-status transitions. Also a `moduleLoader` probe for `ctx.events` keys and the owner binding. - `eventRunnerSql.test.js`: 68/68 against a real MariaDB 11.8. - Server: 2069 pass. Three failures are not from this change: - `routeManifest` ×2 also fail on `main` in this working copy, and pass in a fresh clone of this branch (`route manifest up to date (280 routes)`). - `eventRunnerSql` skips without a DB and passed against one; it failed once in a full-suite run. - `teamActivity` hangs to its file timeout on `main` too. - Client: 400/400, and the build passes. **Follow-ups, not in this PR:** EVENTS.md §L and MODULE_API.md ride in the step-2 docs PR. The Integration-kit pin moves only when someone re-reads the chapters against a shipped core, per its own `ci/core-ref.json` rule. Module-Rust will declare `^1.11.0` when it starts calling this. Tracking: Module-Rust#21. - [x] AI-assisted (Claude Code) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-09-27 02:17:07 +00:00
feat(events): an expired ledger status and ctx.events.expired (MODULE_API 1.11.0)
All checks were successful
PR Checks / client-build (pull_request) Successful in 35s
PR Checks / server-tests (pull_request) Successful in 5m48s
PR Checks / bot-tests (pull_request) Successful in 7m51s
cc1f49af29
A game that ends a resource at its own deadline — a Rust zone erased when its
time is up — could 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 (Rust PLAN_FIXES F14, D170, D183):

- event_run_resources.status gains `expired`, terminal like `reverted`: the
  sweep never takes it back, live_marker releases the target, and it joins
  neither HELD nor UNRESOLVED. The ENUM ALTER re-runs as a no-op on every boot
  (checked on MariaDB 11.8 with the stored generated column depending on it).
- ctx.events.expired({ kind, ref }) marks the calling module's own pending,
  confirmed or orphaned rows for that target expired and logs
  `resource.expired`; a revert in flight is left to finish. The owner is bound
  by the loader, like reconcile. A finished run whose last unresolved row this
  was goes to cleanup `complete`, even from `incomplete`.
- The run console shows it green, "ended by the game on time".

Additions only, so minor. Module-uo (coreApi ^1.10.0) calls none of it and
reads no ledger status; the contract test now asserts the range still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-09-27 03:19:24 +00:00
fix(events): settle a finished run whose last resource expired while it ran
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 31s
PR Checks / client-build (pull_request) Successful in 31s
PR Checks / server-tests (pull_request) Successful in 20m48s
224e2b4dbc
Found walking Rust PLAN_FIXES step 2 (run 46 on the walk core): a zone expired
while its run was still going, so expireResource left the run's cleanup status
to its terminal path. When the run was cancelled the sweep never selected it,
because runsNeedingCleanup joins on an unresolved row and it had none, so
cleanup_status sat at `pending` for ever over an empty ledger.

The sweep now settles finished runs that are `pending` with no unresolved row
as `complete` (never over `incomplete`, which is a human's to clear). Checked
against the walk database: the query returns run 46 and nothing else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Author
Member

Walked, and one fix added (224e2b4).

I ran this branch plus Module-Rust#22 as the walk core against the rust-oxide rig. A 3-minute event zone expired in the game, and its row went expired with resource.expired in the run log. Cancelling the run then left it alone (0 revert attempts).

That walk found a gap: when a run's last resource expires while the run is still going, cleanup_status stayed pending for ever once the run ended. runsNeedingCleanup joins on an unresolved row, and there wasn't one. The sweep now settles finished pending runs that have no unresolved row as complete, and never touches incomplete. There's a new test for it. I checked the query against the walk database: it returned exactly that run, which went complete on the next sweep.

CI is green on 224e2b4 (server, client, bot).

🤖 Generated with Claude Code

**Walked, and one fix added (224e2b4).** I ran this branch plus Module-Rust#22 as the walk core against the `rust-oxide` rig. A 3-minute event zone expired in the game, and its row went `expired` with `resource.expired` in the run log. Cancelling the run then left it alone (0 revert attempts). That walk found a gap: when a run's last resource expires **while the run is still going**, `cleanup_status` stayed `pending` for ever once the run ended. `runsNeedingCleanup` joins on an unresolved row, and there wasn't one. The sweep now settles finished `pending` runs that have no unresolved row as `complete`, and never touches `incomplete`. There's a new test for it. I checked the query against the walk database: it returned exactly that run, which went `complete` on the next sweep. CI is green on 224e2b4 (server, client, bot). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
whitlocktech approved these changes 2026-09-27 06:10:36 +00:00
whitlocktech merged commit f0e7d2aa2a into main 2026-09-27 06:10:46 +00:00
whitlocktech deleted branch feat/events-expired-status 2026-09-27 06:10:48 +00:00
Sign in to join this conversation.
No description provided.