diff --git a/apps/fabro-web/app/components/stage-renderers/helpers.test.ts b/apps/fabro-web/app/components/stage-renderers/helpers.test.ts index 33c661081..a7be45a24 100644 --- a/apps/fabro-web/app/components/stage-renderers/helpers.test.ts +++ b/apps/fabro-web/app/components/stage-renderers/helpers.test.ts @@ -195,15 +195,11 @@ describe("parseParallelOverview", () => { const overview = parseParallelOverview(events); expect(overview).toEqual({ branchCount: 3, - successCount: 2, - failureCount: 1, - durationMs: 12000, results: [ { id: "branch-a", index: null, itemLabel: null, status: "succeeded" }, { id: "branch-b", index: null, itemLabel: null, status: "succeeded" }, { id: "branch-c", index: null, itemLabel: null, status: "failed" }, ], - isComplete: true, }); }); @@ -249,7 +245,6 @@ describe("parseParallelOverview", () => { }), ]; const overview = parseParallelOverview(events); - expect(overview.isComplete).toBe(false); expect(overview.branchCount).toBe(4); expect(overview.results).toEqual([]); }); diff --git a/apps/fabro-web/app/components/stage-renderers/helpers.ts b/apps/fabro-web/app/components/stage-renderers/helpers.ts index 868dcfc4d..afbed444f 100644 --- a/apps/fabro-web/app/components/stage-renderers/helpers.ts +++ b/apps/fabro-web/app/components/stage-renderers/helpers.ts @@ -172,35 +172,28 @@ export interface ParallelBranchSummary { export interface ParallelOverview { branchCount: number | null; - successCount: number | null; - failureCount: number | null; - durationMs: number | null; results: ParallelBranchSummary[]; - isComplete: boolean; } /** * Roll up the `parallel.started` (announces branch count) and - * `parallel.completed` (carries the rolled-up results) events for a parallel + * `parallel.completed` (carries the per-branch results) events for a parallel * stage. Pre-completion, only the announce data is available. + * + * Only branch identity is parsed. The event's own `success_count`, + * `failure_count` and `duration_ms` rollups are deliberately ignored: the + * renderer counts the branch rows it actually draws, and duration comes from + * the stage record via `StageMetaBar`. */ export function parseParallelOverview(events: EventEnvelope[]): ParallelOverview { let branchCount: number | null = null; - let successCount: number | null = null; - let failureCount: number | null = null; - let durationMs: number | null = null; let results: ParallelBranchSummary[] = []; - let isComplete = false; for (const event of events) { const props: UnknownRecord = event.properties ?? {}; if (event.event === "parallel.started") { branchCount = getNumber(props, "branch_count") ?? branchCount; } else if (event.event === "parallel.completed") { - isComplete = true; - successCount = getNumber(props, "success_count") ?? successCount; - failureCount = getNumber(props, "failure_count") ?? failureCount; - durationMs = getNumber(props, "duration_ms") ?? durationMs; const rawResults = getArray(props, "results") ?? []; results = rawResults .map((entry) => { @@ -218,14 +211,7 @@ export function parseParallelOverview(events: EventEnvelope[]): ParallelOverview } } - return { - branchCount, - successCount, - failureCount, - durationMs, - results, - isComplete, - }; + return { branchCount, results }; } export interface ReducerTranscript { diff --git a/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx b/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx index 4354d7fb6..2ebc5c822 100644 --- a/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx +++ b/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx @@ -144,18 +144,23 @@ describe("ParallelChildren", () => { expect(statValue(renderer, "Failed")).toBe("0"); }); - test("uses the stage duration when cancellation interrupts the parallel summary", () => { + test("shows the recorded stage duration when cancellation interrupts the fan-out", () => { const renderer = renderParallel( [startedEvent(2)], [], - { - ...parallelStage, + makeStage({ + id: "fork@1", + name: "fork", + nodeId: "fork", + handler: "parallel", status: StageState.CANCELLED, duration: "53m 29s", - }, + }), ); - expect(statValue(renderer, "Duration")).toBe("53m 29s"); + // No `parallel.completed` event is emitted for an interrupted fan-out, so + // the stage record is the only duration there is. + expect(textContent(renderer.root)).toContain("53m 29s"); }); test("keeps looped fork links scoped to the selected fork visit", () => { diff --git a/apps/fabro-web/app/components/stage-renderers/parallel-children.tsx b/apps/fabro-web/app/components/stage-renderers/parallel-children.tsx index 02f939d30..fe9d9b04d 100644 --- a/apps/fabro-web/app/components/stage-renderers/parallel-children.tsx +++ b/apps/fabro-web/app/components/stage-renderers/parallel-children.tsx @@ -5,13 +5,7 @@ import { StageState } from "@qltysh/fabro-api-client"; import type { EventEnvelope } from "@qltysh/fabro-api-client"; import type { Stage } from "../stage-sidebar"; -import { - ACTIVE_STAGE_STATES, - formatStageLabel, - stageStatusLabel, - stageStatusTone, -} from "../../lib/stage-sidebar"; -import { formatDurationMs } from "../../lib/format"; +import { formatStageLabel, stageStatusLabel, stageStatusTone } from "../../lib/stage-sidebar"; import { StageMetaBar } from "./meta-bar"; import { parseParallelOverview } from "./helpers"; import type { ParallelBranchSummary } from "./helpers"; @@ -187,18 +181,13 @@ export function ParallelChildren({ else if (row.status === StageState.FAILED) failureCount += 1; } - let duration = stage.duration === "--" ? "—" : stage.duration; - if (overview.durationMs != null) { - duration = formatDurationMs(overview.durationMs); - } else if (ACTIVE_STAGE_STATES.has(stage.status)) { - duration = "running"; - } - return (
+ {/* The meta bar owns duration for every stage renderer, including the + live clock while running, so the tiles below stay outcome-only. */} -
+
0 ? "danger" : "default"} /> -
diff --git a/lib/components/fabro-workflow/src/event/sink.rs b/lib/components/fabro-workflow/src/event/sink.rs index 52eb229d1..25d1896ba 100644 --- a/lib/components/fabro-workflow/src/event/sink.rs +++ b/lib/components/fabro-workflow/src/event/sink.rs @@ -163,14 +163,45 @@ impl RunEventLogger { let (tx, mut rx) = mpsc::unbounded_channel(); tokio::spawn(async move { + // A dropped run event is unrecoverable history loss, so the first + // one is an ERROR worth investigating. A broken sink fails for + // every event that follows, so report the rest as a count at flush + // instead of one ERROR per event. Flush runs per stage and per + // agent turn, so only losses since the last summary are reported. + let mut write_failures: u64 = 0; + let mut summarized_failures: u64 = 0; while let Some(command) = rx.recv().await { match command { RunEventCommand::Event(event) => { if let Err(err) = sink.write_run_event(&event).await { - tracing::error!(error = %err, "Failed to write run event"); + write_failures += 1; + if write_failures == 1 { + tracing::error!( + run_id = %event.run_id, + event = %event.body.event_name(), + error = %err, + "Failed to write run event", + ); + } else { + tracing::debug!( + run_id = %event.run_id, + event = %event.body.event_name(), + failures = write_failures, + error = %err, + "Failed to write run event", + ); + } } } RunEventCommand::Flush(tx) => { + if write_failures > summarized_failures { + tracing::error!( + lost = write_failures - summarized_failures, + total = write_failures, + "Run events were lost to write failures", + ); + summarized_failures = write_failures; + } let _ = tx.send(()); } }