From 8bca376f3583622d547b03435d7b89e428c65040 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp <19+brynary@users.noreply.github.com> Date: Wed, 27 May 2026 10:38:32 -0400 Subject: [PATCH] fix(web): close run phase on terminal events (#427) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run Events no longer leaves the pre-execution Initializing bar open when a run fails before `run.running`. This addresses the waterfall symptom in fabro-sh/fabro#426. The phase derivation now records terminal `run.completed` / `run.failed` events and uses them as fallback boundaries for Submitted, Pending, Runnable, and Initializing phases. The existing `run.running` handoff still takes precedence once execution actually starts. Tested: - `cd apps/fabro-web && bun test app/lib/run-phases.test.ts` - `cd apps/fabro-web && bun run typecheck` - `git diff --check` --- [![Compound Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --- apps/fabro-web/app/lib/run-phases.test.ts | 33 +++++++++++++++++++++++ apps/fabro-web/app/lib/run-phases.ts | 18 ++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/apps/fabro-web/app/lib/run-phases.test.ts b/apps/fabro-web/app/lib/run-phases.test.ts index a0ad5654e..7c2dab22d 100644 --- a/apps/fabro-web/app/lib/run-phases.test.ts +++ b/apps/fabro-web/app/lib/run-phases.test.ts @@ -9,6 +9,7 @@ const T_PENDING = "2026-05-23T12:00:02.000Z"; const T_RUNNABLE = "2026-05-23T12:00:03.000Z"; const T_STARTING = "2026-05-23T12:00:04.000Z"; const T_RUNNING = "2026-05-23T12:00:10.000Z"; +const T_FAILED = "2026-05-23T12:00:12.000Z"; function makeEvent(name: string, ts: string, seq: number): EventEnvelope { return { @@ -114,6 +115,38 @@ describe("deriveRunPhases", () => { expect(phases[1]!.endMs).toBe(Date.parse(T_RUNNING)); }); + test("closes initializing at run.failed when the run never reaches running", () => { + const phases = deriveRunPhases( + [ + makeEvent("run.start_requested", T_REQUESTED, 1), + makeEvent("run.runnable", T_RUNNABLE, 2), + makeEvent("run.starting", T_STARTING, 3), + makeEvent("run.failed", T_FAILED, 4), + ], + CREATED, + ); + expect(phases).toEqual([ + { + kind: "submitted", + label: "Submitted", + startMs: Date.parse(CREATED), + endMs: Date.parse(T_REQUESTED), + }, + { + kind: "runnable", + label: "Runnable", + startMs: Date.parse(T_RUNNABLE), + endMs: Date.parse(T_STARTING), + }, + { + kind: "initializing", + label: "Initializing", + startMs: Date.parse(T_STARTING), + endMs: Date.parse(T_FAILED), + }, + ]); + }); + test("uses run.starting as fallback end for submitted when pre-execution events are missing", () => { const phases = deriveRunPhases( [makeEvent("run.starting", T_STARTING, 1)], diff --git a/apps/fabro-web/app/lib/run-phases.ts b/apps/fabro-web/app/lib/run-phases.ts index 3c77f68ca..bc847eec6 100644 --- a/apps/fabro-web/app/lib/run-phases.ts +++ b/apps/fabro-web/app/lib/run-phases.ts @@ -29,11 +29,12 @@ export function deriveRunPhases( let runnableMs: number | null = null; let startingMs: number | null = null; let runningMs: number | null = null; - let remaining = 5; + let terminalMs: number | null = null; + let remaining = 6; for (const event of events ?? []) { if (remaining === 0) break; - let target: "startRequested" | "pending" | "runnable" | "starting" | "running" | null = null; + let target: "startRequested" | "pending" | "runnable" | "starting" | "running" | "terminal" | null = null; switch (event.event) { case "run.start_requested": if (startRequestedMs == null) target = "startRequested"; @@ -50,6 +51,10 @@ export function deriveRunPhases( case "run.running": if (runningMs == null) target = "running"; break; + case "run.completed": + case "run.failed": + if (terminalMs == null) target = "terminal"; + break; } if (target == null) continue; const ms = Date.parse(event.ts); @@ -60,6 +65,7 @@ export function deriveRunPhases( case "runnable": runnableMs = ms; break; case "starting": startingMs = ms; break; case "running": runningMs = ms; break; + case "terminal": terminalMs = ms; break; } remaining -= 1; } @@ -70,7 +76,7 @@ export function deriveRunPhases( kind: "submitted", label: PHASE_LABEL.submitted, startMs: createdMs, - endMs: startRequestedMs ?? pendingMs ?? runnableMs ?? startingMs ?? runningMs, + endMs: startRequestedMs ?? pendingMs ?? runnableMs ?? startingMs ?? runningMs ?? terminalMs, }); if (pendingMs != null) { @@ -78,7 +84,7 @@ export function deriveRunPhases( kind: "pending", label: PHASE_LABEL.pending, startMs: pendingMs, - endMs: runnableMs ?? startingMs ?? runningMs, + endMs: runnableMs ?? startingMs ?? runningMs ?? terminalMs, }); } @@ -87,7 +93,7 @@ export function deriveRunPhases( kind: "runnable", label: PHASE_LABEL.runnable, startMs: runnableMs, - endMs: startingMs ?? runningMs, + endMs: startingMs ?? runningMs ?? terminalMs, }); } @@ -96,7 +102,7 @@ export function deriveRunPhases( kind: "initializing", label: PHASE_LABEL.initializing, startMs: startingMs, - endMs: runningMs, + endMs: runningMs ?? terminalMs, }); }