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

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
This commit is contained in:
2026-09-26 21:13:07 -05:00
parent 1a76b98d76
commit cc1f49af29
14 changed files with 309 additions and 10 deletions

View File

@@ -327,6 +327,42 @@ async function markOrphaned(id, detail = null) {
)
}
/**
* The rows one module holds for one target that the game may have let go of on
* its own — what `ctx.events.expired` looks for (MODULE_API 1.11.0).
*
* `orphaned` is included on purpose: a reconcile that ran before the module heard
* the expiry filed the row as vanished, and the expiry is the better answer.
* `reverting` is not: a revert already in flight finds the thing gone, which §L
* calls a success, and it settles as `reverted` on its own.
*/
async function expirableByTarget(owner, kind, ref) {
const rows = await query(
`SELECT ${COLUMNS} FROM event_run_resources
WHERE owner_module = ? AND kind = ? AND ref = ?
AND status IN ('pending', 'confirmed', 'orphaned')
ORDER BY id`,
[owner, kind, ref],
)
return rows.map(hydrate)
}
/**
* The game ended it by itself, as it was told to — a zone reaching its deadline.
* Terminal like `reverted`, so teardown never tries to give it back, and unlike
* `orphaned`, which is a thing that vanished with nobody asking. Guarded on the
* status so a row a revert has just claimed is left to that revert.
*/
async function markExpired(id, detail = null) {
const result = await query(
`UPDATE event_run_resources
SET status = 'expired', last_error = ?
WHERE id = ? AND status IN ('pending', 'confirmed', 'orphaned')`,
[detail === null ? null : String(detail).slice(0, 500), id],
)
return (result.affectedRows || 0) > 0
}
/**
* Terminal runs that still owe the world something — the cleanup leg's scan.
*
@@ -385,5 +421,7 @@ module.exports = {
liveForModule,
modulesWithLiveRows,
markOrphaned,
expirableByTarget,
markExpired,
runsNeedingCleanup,
}