Repository navigation
Read UK staging contract v3: a real blocked run status, block details and failure classes (dashboard and collector) #206
Description
Why
UK builds report how they ended badly today:
- every failure is
error_code: BUILD_FAILEDwith nofailure_class; - a build whose gates refused the candidate closes as
completed, so this dashboard infers "blocked" fromblocking_failure_count(build-monitor.ts, the comment at the top says so); - a build refused at the preflight gates returns without any gate counts, so the dashboard shows it as passed;
- killed builds stay
running.
A microcosm PR (stacked on PolicyEngine/microcosm#1099) fixes this at the source. It gives blocked runs a real blocked status, classifies failures, and moves the UK staging documents to schema_version 3. This issue is the dashboard and collector half. It has to be deployed and qualified before the microcosm PR merges: an old collector answers a blocked event with 422, and #1099's delivery queue retries that batch forever, so the run's terminal event would never arrive.
The v3 contract (what microcosm will write)
Staging run documents (run_manifest.json, progress.json, events.ndjson, calibration_progress.json):
schema_version: 3on all four documents of a run;schema_namevalues unchanged. v2 documents stay valid and must keep reading exactly as today.- Run
status:running | completed | blocked | failed. - New top-level field
blockonrun_manifestandprogress:null, or exactly
{ "phase": <safe identifier>, "blocking_failure_count": <int ≥ 1>, "blocking_gate_ids": [<string>, ...] }.
phaseis one ofpreflight,terminal,assembled,transferredtoday; treat it as an open identifier.
Rules:status == "blocked"requiresblockandfailure: null;status == "failed"requiresfailureandblock: null; any other status requires both null. failuregains a fifth key,failure_class: a lowercase identifier ornull. Exactly five keys:error_code,error_type,message,local_diagnostic_reference,failure_class.- Event
statusaddsblocked. A blocked run ends with one terminal event:
event_type: "stage",stage_id: "blocked",status: "blocked",details: { phase, blocking_failure_count, blocking_gate_ids, gate_statuses }.
Completed and failed runs end as today (complete/failed). - The delivery summary keeps
contract_version: 2(its shape does not change; microcosm's publisher pins it).
Error codes and classes microcosm will write on failed runs: INTERRUPTED / interrupted (Ctrl-C), TERMINATED / terminated (SIGTERM), OUT_OF_MEMORY / out_of_memory, GRAPH_NODE_FAILED / error, BUILD_FAILED / error (fallback). #1099's emitter also sends build_failure, unexpected_process_exit and dry_run_refusal (US).
Collector events (#1099's emitter): a blocked run sends event_type: "run", status: "blocked", stage_id: "blocked", details: { phase, blocking_failure_count, blocking_gate_ids, gate_statuses }. Failed run events add error_code to the existing error_type, failure_class, failed_during details.
What to change
Version handling: treat schema_version >= 2 as the structured contract. It is hard-coded to 2 in several places, and a v3 run would otherwise be parsed with the v1 transition logic or skip checks (line numbers at main 2da3bf3):
frontend/lib/microcosm/build-monitor.ts:721(schemaVersion = … === 2 ? 2 : 1);frontend/lib/microcosm/staging-artifact.ts:389, 436, 537-545(run listing) and857, 882, 891(the diagnostics path falls back to the v1 layout and skips its digest check);frontend/lib/microcosm/staging-contract.ts:725(event sequence check) and766-773(the mixed-version check, thenif (!versions.has(2)) returnskips every consistency check).
Validator (staging-contract.ts): accept v3 as specified above (status enums, block, five-key failure, the rules), keep v2 exact; mixed versions within one run stay an error.
blocked as a final status everywhere:
frontend/lib/microcosm/build-runs.ts:134, 234andstaging-artifact.ts:399(otherwise blocked runs are refetched forever with a short cache life);MARKER_STAGESand the terminal-stage handling inbuild-monitor.ts(~L38, 449-471);- the tone mapping in
frontend/components/microcosm/microcosm-staging-view.tsx:99.
Monitor logic (build-monitor.ts):
runState: mapblockedstraight to theblockedstate, with the failure line built fromblock(phase, count, gate ids); keep today'sblocking_failure_countinference only for v2 runs;- read
failure_classfromprogress.failure(v3), falling back to the terminal event details; stopStatistics: count failure classes for blocked runs too (today onlyfailedruns are counted, ~L1705);- labels in
build-monitor-view.tsx(~L88-103) forbuild_failure,unexpected_process_exit,dry_run_refusal.
Collector (telemetry-service/telemetry_collector/):
models.TelemetryEvent.status: addblocked;storage._apply_event: addblockedto the terminal stage set (~L122); arun/blockedevent sets run statusblocked,ended_at, and keeps the block details; failed events keeperror_codeinfailure;- no database migration:
statusis Text andfailureis JSONB.
Fixtures
The microcosm PR adds packages/microcosm-build/tests/fixtures/staging/v3/ (completed, calibration, failed with a class, blocked) with its own SHA256SUMS, generated by tools/generate_staging_contract_fixtures.py. Copy them byte for byte next to the v2 copy and pin their digest in staging-contract.test.ts; the v2 digest 0d54c630…455e5 must not change. Until they are frozen on the microcosm branch, test against hand-written v3 documents that follow the spec above; the branch link will be added here.
Done when
bun test frontend/lib/microcosmandtsc --noEmitpass, with new cases: a v3 blocked run atpreflightand atterminalreads as blocked with its gates; a v3 failed run carries its code and class into the stop statistics; v2 runs read exactly as before; a v3 document with a sixthfailurekey orblockon a failed run is rejected.- Collector
pytestpasses with ablockedrun event case. - Deployed, and a
blockedevent posted to the qualification collector returns 2xx. Then the microcosm PR can merge.
Activity
Close UK runs as blocked when their gates refuse the candidate (stagi…
juaristi22 commentedon Oct 9, 2026
Three additions from PolicyEngine/microcosm#1147's review round 1 (none change the v3 fixtures; they add vocabulary and relax two expectations):
failedruns, to label inbuild-monitor-view.tsxand count instopStatistics:BUILD_REFUSED/refused: the build returned non-zero without raising and without a recorded gate block (a guard; also what_run_with_telemetrysends when an attempt returns non-zero without closing the emitter itself).RUNG_ABORTED/aborted: a sampled spine build hit a named edge and discarded the attempt (Logbookdiscarded).GATE_BLOCK_UNRECORDED/unrecorded_gate_block: the gates refused the candidate but the staging contract rejected the block's details (a gate id that reads as a sensitive key, say), so the run closedfailed, notblocked.block.blocking_gate_idsmay be[]when the refusal named no gate;blocking_failure_countis always ≥ 1. There is no placeholder id (microcosm no longer writesunidentified_gate). Render it as "n blocking failures, no gate named".gate_statusesis optional in the blocked terminal event'sdetailsand in the collector'srun/blockedevent: present when the refusing report could be read, absent otherwise. Do not require it.One operational note: microcosm's emitter now treats a 4xx other than 408/429 on a run's events as the collector's settled answer and makes the run local-only (one warning), instead of retrying. So an old collector no longer wedges delivery when PolicyEngine/microcosm#1147 lands first; the blocked runs would just be missing from the hosted view until #206 deploys. #206 first is still the plan.