feat(events): conditions, phase advancement and the diagnosis panel (Phase 5)
A phase used to advance on one fact - every step terminal. It can now also carry
an advance CONDITION: `{ after: '30m' }` or `{ on: '<triggerId>', where:
<conditions>, count: n }`, reusing `engagement/conditions.js` unchanged. The
phase's real deliverable is the diagnosis panel: "why didn't phase 3 start?"
answered in the condition builder's own words, with the tally, the elapsed time
and the last related firing whether or not it counted.
`POST /admin/events/runs/:runId/advance` arrives beside it. It has been absent
since Phase 3 for want of a meaning; a phase with a gate can wait on a boss that
will never spawn, and that is the one state "force it anyway" names.
One new table, `event_run_phase_gates`. The emit path writes the tally at the
moment a firing happens - a gate waiting on three spawns counts things that
occur between two ticks, and a tally held in a process's memory is one a restart
silently zeroes - and the runner's tick reads it.
A gate that never opens is HELD, with no automatic advance and no authored
timeout (org lead, 2026-09-02). What the engine owes instead is visibility:
`EVENT_PHASE_STALL_MS` takes the run's health to `stalled`, and `setHealth` is
now escalation-only so a later retry cannot demote it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
This commit is contained in:
@@ -38,6 +38,7 @@ const runsDb = require('../src/model/events/eventRuns.db')
|
||||
const stepsDb = require('../src/model/events/eventRunSteps.db')
|
||||
const logDb = require('../src/model/events/eventRunLog.db')
|
||||
const seriesDb = require('../src/model/events/eventSeries.db')
|
||||
const gatesDb = require('../src/model/events/eventPhaseGates.db')
|
||||
const activity = require('../src/model/activity/activity.model')
|
||||
const db = require('../src/utils/db')
|
||||
|
||||
@@ -54,6 +55,7 @@ for (const [name, mod] of [
|
||||
['stepsDb', stepsDb],
|
||||
['logDb', logDb],
|
||||
['seriesDb', seriesDb],
|
||||
['gatesDb', gatesDb],
|
||||
['activity', activity],
|
||||
]) {
|
||||
originals[name] = { mod, fns: { ...mod } }
|
||||
@@ -76,6 +78,7 @@ function installStubs() {
|
||||
steps: new Map(),
|
||||
log: [],
|
||||
series: new Map(),
|
||||
gates: [],
|
||||
occurrences: new Set(),
|
||||
nextDefinition: 1,
|
||||
nextVersion: 1,
|
||||
@@ -250,6 +253,15 @@ function installStubs() {
|
||||
return counts
|
||||
}
|
||||
|
||||
// Phase 5 gave `runs.detail()` a third leg, and an unstubbed one is a real
|
||||
// query against the dead port this file points at: the run-console test hung
|
||||
// for ten seconds and then failed with ECONNREFUSED, saying nothing whatever
|
||||
// about the route. **This is the third time a new leg has caught a stubbing
|
||||
// file out** — Phase 4's expansion leg did it to `eventRunner.test.js`, where
|
||||
// it merely made the file slow. Worth the comment: when the runner or a model
|
||||
// gains a leg, every file that stubs the layer under it needs the stub.
|
||||
gatesDb.listForRun = async (runId) => store.gates.filter((g) => g.run_id === runId)
|
||||
|
||||
logDb.listForRun = async (runId) => store.log.filter((l) => l.run_id === runId).reverse()
|
||||
logDb.write = async ({ runId, stepId = null, kind, phase = null, detail = null }) => {
|
||||
store.log.push({ id: store.log.length + 1, run_id: runId, step_id: stepId, kind, phase, detail, at: new Date() })
|
||||
|
||||
Reference in New Issue
Block a user