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:
@@ -21,6 +21,8 @@
|
||||
const db = require('./eventRuns.db')
|
||||
const stepsDb = require('./eventRunSteps.db')
|
||||
const logDb = require('./eventRunLog.db')
|
||||
const gatesDb = require('./eventPhaseGates.db')
|
||||
const gates = require('../../events/gates')
|
||||
const definitionsDb = require('./eventDefinitions.db')
|
||||
const versionsDb = require('./eventVersions.db')
|
||||
|
||||
@@ -140,12 +142,30 @@ async function create(
|
||||
return { ok: true, created: true, run: await db.getById(runId) }
|
||||
}
|
||||
|
||||
/** A run, its steps and its status counts — what the run console reads. */
|
||||
/**
|
||||
* A run, its steps, its status counts and its phase gates — the run console.
|
||||
*
|
||||
* The gates arrive already DESCRIBED rather than as rows (Phase 5): the panel's
|
||||
* whole value is that it reads the way the condition builder reads, and those
|
||||
* words come from `engagement/conditions.js`'s own operator labels. Rendering
|
||||
* them in the browser would be a second implementation of a grammar the server
|
||||
* owns, and the first clause the two spelled differently would meet its operator
|
||||
* at two in the morning.
|
||||
*
|
||||
* Every gate the run has opened is returned, not only the current phase's. A
|
||||
* completed phase's gate answers "how long did phase 2 actually wait, and what
|
||||
* released it" — which is the same question as the live one, asked afterwards.
|
||||
*/
|
||||
async function detail(runId) {
|
||||
const run = await db.getById(runId)
|
||||
if (!run) return null
|
||||
const [steps, counts] = await Promise.all([stepsDb.listForRun(runId), stepsDb.statusCounts(runId)])
|
||||
return { run, steps, counts }
|
||||
const [steps, counts, gateRows] = await Promise.all([
|
||||
stepsDb.listForRun(runId),
|
||||
stepsDb.statusCounts(runId),
|
||||
gatesDb.listForRun(runId),
|
||||
])
|
||||
const now = new Date()
|
||||
return { run, steps, counts, gates: gateRows.map((g) => gates.describe(g, now)) }
|
||||
}
|
||||
|
||||
module.exports = { create, detail, renderConcurrencyKey }
|
||||
|
||||
Reference in New Issue
Block a user