feat(events): the minimal admin surface (Phase 3)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 30s
PR Checks / server-tests (pull_request) Successful in 5m26s
PR Checks / client-build (pull_request) Successful in 8m30s

Three screens, an Events nav group and the six live run controls Phase 1 left
absent on purpose because nothing was in flight. An admin can now author,
publish, start and watch an event that announces things and cues a human; a
moderator can stop one that is going wrong.

Six controls, not eight. `advance` is absent because a phase today advances when
its steps go terminal — the per-step skip already does that — and Phase 5 is what
gives a phase an advance condition. Cancel takes `{ reason }`, not `{ cleanup }`,
until Phase 8's ledger exists. Every control is a compare-and-set on the status it
may act from, so a console rendered thirty seconds ago cannot act on a run that
has moved.

Fixes a defect in the Phase 2 runner: `advanceRun` drained up to
EVENT_STEPS_PER_TICK steps while only checking the run's status at the top of the
tick, so a pause pressed mid-batch did nothing for up to 24 more steps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
This commit is contained in:
2026-09-02 08:39:35 -05:00
parent 2ba397eff7
commit 7b570c8ea1
20 changed files with 3775 additions and 8 deletions

View File

@@ -3766,6 +3766,101 @@
]
}
},
"/api/v1/admin/events/runs/{runId}/cancel": {
"post": {
"tags": [
"Admin · Events"
],
"summary": "Cancel a run",
"description": "Legal from every non-terminal status, `scheduled` included. Pending steps and any parked cue are cancelled with it; a step with a live lease is left alone, because nothing can recall a command already sent and a second writer on that row would race the process dispatching it. `cleanup` is not a parameter yet — the resource ledger it would work over arrives in Phase 8, and a flag that changes nothing is worse than one that is not there.",
"parameters": [
{
"name": "runId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The cancelled run and how many steps were closed out with it",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"run": {
"type": "object",
"additionalProperties": true
},
"cancelledSteps": {
"type": "integer"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Not an admin or moderator",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "The run has already reached a terminal status",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Why. Recorded on the run and in its log, with the actor."
}
}
}
}
}
}
}
},
"/api/v1/admin/events/runs/{runId}/log": {
"get": {
"tags": [
@@ -3842,6 +3937,494 @@
]
}
},
"/api/v1/admin/events/runs/{runId}/pause": {
"post": {
"tags": [
"Admin · Events"
],
"summary": "Pause a run in flight",
"description": "A paused run is excluded from the runner\\'s sweep and nothing advances it until resume. Legal from `starting` and `running` only — a `scheduled` occurrence that should not happen is cancelled, not paused, because resuming one after its grace window had passed would produce a `missed` from a button labelled resume. Takes effect at once even mid-tick: the runner re-reads the run\\'s status between steps.",
"parameters": [
{
"name": "runId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The paused run",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"run": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Not an admin or moderator",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "The run is not in flight",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Recorded in the run log with the actor"
}
}
}
}
}
}
}
},
"/api/v1/admin/events/runs/{runId}/resume": {
"post": {
"tags": [
"Admin · Events"
],
"summary": "Resume a paused run",
"description": "Where the run goes back to is derived rather than remembered: a paused run with a `current_phase` was running, one without never got past `starting`. `last_error` is cleared — the operator has just dealt with it — and `health` is not, because \"this run has already had trouble\" stays true whoever pressed resume.",
"parameters": [
{
"name": "runId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The resumed run",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"run": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Not an admin or moderator",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "The run is not paused",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/admin/events/runs/{runId}/steps/{stepId}/confirm": {
"post": {
"tags": [
"Admin · Events"
],
"summary": "Confirm a parked step — the GM cue",
"description": "The other half of `core.cue`. The action posts an instruction and parks the step `running` with a NULL lease — genuinely in flight, nothing holding it, so no sweep takes it back and a cue posted on Friday is still waiting on Monday. This ends it, as `done` rather than `skipped`: a person saying they did the thing is the step having succeeded. The optional note is what they did, and it is kept on the step and in the log.",
"parameters": [
{
"name": "runId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "stepId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The confirmed step",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"step": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Not an admin or moderator",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "No such run, or no such step on it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "The step is not waiting on anyone",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"note": {
"type": "string",
"description": "What was actually done in-client"
}
}
}
}
}
}
}
},
"/api/v1/admin/events/runs/{runId}/steps/{stepId}/retry": {
"post": {
"tags": [
"Admin · Events"
],
"summary": "Re-queue the failed step a paused run is stopped at, and resume it",
"description": "One action rather than two, because there is no state in which you would want half of it: retry is legal only while the run is paused, and a paused run is paused AT this step. The step must be the one its phase is stopped at — a failed step under an `on_failure` of `skip` is one the run has already moved past, and re-queueing that would put a pending row behind the runner\\'s cursor. `attempts` returns to zero: the attempt ceiling bounds what the runner does unattended, and a named person deciding is the thing it is unattended from.",
"parameters": [
{
"name": "runId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "stepId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The re-queued step and the run, with whether the resume took",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"step": {
"type": "object",
"additionalProperties": true
},
"run": {
"type": "object",
"additionalProperties": true
},
"resumed": {
"type": "boolean"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Not an admin or moderator",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "No such run, or no such step on it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "The run is not paused, or the run is not stopped at this step",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/admin/events/runs/{runId}/steps/{stepId}/skip": {
"post": {
"tags": [
"Admin · Events"
],
"summary": "Skip a step nobody is going to run",
"description": "A step that has not started, or a parked cue. This is what the `skipped` status was reserved for, and why all three `on_failure` dispositions write `failed` instead — a status meaning both \"a human decided against this\" and \"this was attempted three times and never worked\" would make the console summary unreadable. A step with a live lease cannot be skipped; a failed one does not need to be, because resuming the run already carries the phase past it.",
"parameters": [
{
"name": "runId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "stepId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The skipped step",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"step": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Not an admin or moderator",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "No such run, or no such step on it",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "The step or its run is in a status that cannot be skipped",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"reason": {
"type": "string"
}
}
}
}
}
}
}
},
"/api/v1/admin/events/series": {
"get": {
"tags": [