feat(events): the resource ledger, leases and cleanup (Phase 8) #190
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/events-phase-8"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Event System Phase 8 (
EVENTS_PLAN.md). Docs half: RunicGateway/docs#216.One table (
event_run_resources), one core action (core.lease), one option source (core.options.leases), one route added and none moved, one body field (cancel's{ cleanup }), and two members added to MODULE_API 1.10.0 in place.The version was amended, not bumped
reconcile()andctx.events.reconcile()joined 1.10.0 rather than becoming 1.11.0 (org lead, 2026-09-03). A protocol owes a bump once it has landed onmain; while it is onedgeit is amended in place — the rule the Teams workstream arrived at, applied to a module API for the first time. 1.10.0 has not shipped, so the whole module contract reaches an author as one version they read once, which was the argument for putting the lease declaration in it a phase early.This branch therefore does not touch
MODULE_API_VERSION. The integration kit stays exactly as Phase 7 left it: red on purpose until the Phase 16 cutover re-pinsci/core-ref.json. Please still do not "fix" it there.Four decisions, settled 2026-09-03, all as recommended
core.lease. §F puts the duration bound and the two-events-one-target conflict check on core's side of the seam, and a lease verb per module would be both of those re-implemented once per module — advisory everywhere, and wrong in the first one that forgot. It isrisk: 'change', so it is the first core action that is default-off, admin-only and cap-checked like any module verb.refdoes not exist until the module answers, so there is nothing to write a row about — what core writes beforehand iskind: '@step',ref= that key. On the answer the reported resources are insertedconfirmedand the placeholder is resolved; if the answer never comes it stands, and cleanup callsrevert({ idempotencyKey, resources: [] }). That is why §F'sreverttakes the key at all. A lease skips it and reserves its real target instead — the same rule in a stronger form, and the only moment the conflict refusal can happen before the world has been written to.event_run_stepsrows. The step-shaped reading buys the console's per-step retry for free and costs a second retry counter besiderevert_attempts— two counters that disagree the first time a step reverts three of its four resources.reconcileis declared here and TRIGGERED BY THE MODULE, throughctx.events.reconcile(). Core has no concept of the game being up (§F), so it cannot decide when to ask; module-uo already watchesbootId. Core asks once at its own boot, which is the one reconnect it can see.Four things the build settled
driftedandorphanedhold a target for ever, so one bad night would disable a lease permanently with no control able to clear it. Recorded as a dated amendment.statusalone.TEAMS.md§2.5 had to be corrected on this exact shape: MariaDB refusesON DELETE SET NULLon a foreign key whose column is a base column of a stored generated column (error 1901), andstep_idmust stay SET NULL because a record of what was changed in the world has to outlive the row that scheduled it.cancelanswers at once.Verify
npm test— 2025 tests, 1935 pass, 89 skipped, 1 fail. That one is the pre-existingengagementManifest.test.jsCRLF failure, in a file this branch does not touch (edgebefore: 1950/1876/73/1). +75 tests.eventRunnerSql.test.jsgained 16 tests; 65 pass against the container. The realschema.sqlwas applied to a fresh database and to an existing one —CREATE TABLE IF NOT EXISTSnext to a generated column is where a migration silently does nothing.utils/dbbuilds at require time fromDB_NAME— andutils/db's owndotenv.config()readsserver/.env. So on a developer's machine those tests were reaching that developer's real schema while the fixtures they asserted against were written into the throwaway database next door. They passed only because both tables happened to exist in both.npm run routes:manifestandnpm run swagger— one route added, none moved.ECONNREFUSED.The live walk found three defects, and two of them are the phase's real finding
Driven by a throwaway
rigmodule inwebsite/modules/— a real directory scanned by the real loader, deleted before commit — whose behaviour is driven by a JSON file on disk, so a revert could be made to fail and then succeed on the same boot. 34 assertions, all green at the end.rigdeclaredcoreApi: "^1.10.0"and module-uo's"^1.9.0"still resolved, on the same boot.Defect 1 — a lease was never given back at all.
core.leasereserves its own ledger row, so it never went through the ledger's dirty-marking, so a run holding only a lease keptcleanup_status = 'not_required'and the cleanup leg — which selected onpending— never looked at it. Every unit test passed: they call the sweep directly, and a test that calls the sweep never asks what would have SELECTED the run.Defect 2 —
EVENT_REVERT_MAX_ATTEMPTSmeant one attempt, not three. The first failing sweep moved the run toincomplete, which took it out of the leg's own scan for ever. Visible only asrevert_attemptssitting at 1 through half a minute of live ticks — and the test covering the bound asserted<= 3, which 1 satisfies. A bound has two halves, and a test that only asserts the ceiling passes against a floor. Both halves are asserted now.Both are the same mistake: deriving "is there anything to do" from a summary column instead of from the rows that are the fact. The scan now includes
not_required, andincompleteis written only once there is nothing left to try — so it means "finished with, and not finished", which is also what makes it the honest marker for a run an admin cancelled without cleanup.Defect 3 — the first fix for defect 2 made the console lie. Spending every row's
revert_attemptswas a tidy way to take acleanup: falserun out of a counter-bounded scan, and the run page then rendered "3 attempts" beside resources nothing had ever tried — which reads as "core tried three times and could not". Found by opening the page, exactly like Phase 7's React defect. A counter that means two things is a counter a screen cannot render.What the walk proved beyond the defects
Three wisps recorded and given back with the run reaching
complete; a failing revert leaving the runcompleted+incompletewith the reason on each row; the cleanup route rescuing it, and a moderator refused it; the route refusing a run still in flight and one that recorded nothing; a lease taken with its baseline and applied value in the payload, and a second run refused it by name; a GM's mid-event edit producingdriftedwith the world left alone, and a later run still able to lease that target; andcancel { cleanup: false }refused to a moderator, allowed to an admin, and logged.The two that needed the process to die
The plan's own verify line. With the module's
perform()hanging: the placeholder existed while the dispatch was in flight and nothing was named —unresolvedResources: 1over an empty list, which is rule 1 visible in the wild. Aftertaskkilland a restart the reclaim re-dispatched the same idempotency key, the retry re-used its own placeholder rather than writing a second, and everything was given back. Then, with the module reporting one of two resources as no longer in force, core's boot-timereconcileAll()marked the otherorphaned— neverreverted— and logged it.The console, in all three states
A clean run: neutral border, "Everything this run created or borrowed has been given back", green rows, no button. An unresolved run: amber border, "3 of these are still unresolved", "still out there", and Try cleanup again — clicked, answered
200, and flipped the panel green in place. A drifted lease: "someone else moved it", its deadline, its genuine attempt count, and the sentence naming the value that is there now.Correction to this PR's Verify section, for the record.
It says the one failing test was "the pre-existing
engagementManifest.test.jsCRLF failure, in a file this branch does not touch". That diagnosis was wrong, and #189's body says the same thing, so it has now been repeated twice.It was not CRLF, and it was not benign:
engagement-triggers.jsonwas genuinely stale. It embedsmoduleApiVersion, and #189 bumpedMODULE_API_VERSIONto 1.10.0 without regenerating it, so the committed file has said1.9.0since that merge.The CRLF part was real but secondary — the check compares bytes, and under
core.autocrlf=truegit checks the LF blob out as CRLF, so on Windows it fails for a spurious reason as well. That noise is precisely why the genuine staleness went unnoticed: the failure had already been written off as environmental, by me, twice.Both are fixed in #191, which also proves the check is still live by breaking it deliberately. Nothing in Phase 8 itself is affected — the ledger, the lease and the cleanup are unchanged — but the "1 fail" number quoted above should be read as one real failure this branch did not cause and should not have waved past, rather than as environmental noise.