feat(events): a trigger can start a run
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 35s
PR Checks / client-build (pull_request) Successful in 44s
PR Checks / server-tests (pull_request) Successful in 12m45s

Rust PLAN_REDESIGNS §14.3.12 (D370), stage 9 of §14.3. A definition's
published version may carry `starts`: a trigger, a condition and a
cooldown. A matching firing creates a run of it, now.

- spec: `starts` validated by the phase gate's own function (same
  grammar, same dormant rule; a dormant start refuses publish), a
  cooldown from 0m to 7d (30m default), at most ten, event.* refused.
- events/starts.js: the emit path's third subscriber, beside dispatch
  and the gate observer. An unverified version starts nothing; one run
  at a time per definition and the cooldown are one conditional UPDATE
  on the new event_trigger_starts table; the occurrence key makes one
  firing reported twice one run. Ignored firings are counted by why.
- event_runs.origin: the firing (trigger, label, time, subject, matched
  condition, declared variables), also on the run.created audit line.
- Editor: a "Started by the game" panel using the gate's condition
  builder, with the ignored-firings line. Run console: "Started because
  ... fired".
- No module contract change and no MODULE_API bump.

Tests: eventStarts (server 21, client 8); eventRunnerSql +7 against a
real MariaDB (75/75); Module-uo proof on uo.champ.boss_up against the
dev database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-16 01:51:55 -05:00
parent eb773693d3
commit c9e992588b
18 changed files with 1352 additions and 20 deletions

View File

@@ -2704,3 +2704,43 @@ ALTER TABLE event_runs ADD COLUMN IF NOT EXISTS results_published_at DATETIME NU
-- that no longer resolves answers that honestly.
ALTER TABLE announce_jobs ADD COLUMN IF NOT EXISTS run_id BIGINT NULL;
ALTER TABLE announce_jobs ADD INDEX IF NOT EXISTS idx_announce_run (run_id);
-- ── A trigger can start a run (Rust PLAN_REDESIGNS §14.3.12, D370) ─────────
-- Why a run exists, when a game firing started it: the trigger, when it fired,
-- its subject and its variables, so the console can say "started because a
-- champion boss is up at Abyss" and the audit can say what. NULL for a run an
-- admin or the schedule started, which `run.created`'s `source` already says.
--
-- A column rather than only a log line because the run log is pruned and the
-- run is not, and "why did this run happen" outlives "what did each step do".
ALTER TABLE event_runs ADD COLUMN IF NOT EXISTS origin JSON NULL;
-- One row per definition that a trigger has tried to start: the cooldown's
-- clock, the claim that makes "one run at a time" true, and the count of
-- firings it ignored, which the editor shows ("2 firings ignored while
-- running").
--
-- **The claim is a conditional UPDATE on this row**, the event_run_budget
-- argument once more: two firings arriving together each try it, and the
-- cooldown, the one-at-a-time check and a stale-claim guard are all in its
-- WHERE, so exactly one of them owns the start. `claimed_at` is held only
-- between the claim and the run row existing — a minute is a crashed process,
-- not a slow insert.
--
-- The counts are since the LAST trigger-started run, and a new start zeroes
-- them: what an operator asks is "is it ignoring things right now", and a
-- lifetime total cannot answer that.
CREATE TABLE IF NOT EXISTS event_trigger_starts (
definition_id INT NOT NULL PRIMARY KEY,
claimed_at DATETIME NULL,
last_started_at DATETIME NULL, -- the firing's time, which is the run's scheduled_for
last_run_id BIGINT NULL,
ignored_running INT NOT NULL DEFAULT 0,
ignored_cooldown INT NOT NULL DEFAULT 0,
ignored_unverified INT NOT NULL DEFAULT 0,
last_ignored_at DATETIME NULL,
last_ignored_reason VARCHAR(32) NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_evtrig_def FOREIGN KEY (definition_id) REFERENCES event_definitions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;