From 656a48fecdc72d63edabf7b9fbd7a8cc7fb75d99 Mon Sep 17 00:00:00 2001 From: uipreliga Date: Thu, 10 Sep 2026 14:31:27 -0700 Subject: [PATCH 01/10] =?UTF-8?q?fix(evalboard):=201/6=20=E2=80=94=20show?= =?UTF-8?q?=20what=20the=20wall=20clock=20does=20not=20account=20for?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reporting fixes to the same surface: time figures that are each well-formed but never say what they should reconcile to. A. The task page's timeline strip gains an `Unaccounted` cell — task wall clock minus generation minus tool execution — so a harness that stops reporting one of them is visible on the page instead of reading as fast. Tinted red at or above a 25% residual. A negative residual (parallel tool calls, or a tool closing inside a generation window) renders signed and untinted rather than clamped: an overlap is a signal, not unreported time. `fmtMs` is now sign-aware so that reads as `-1.2s`, not `-1200ms`. B. The run list's Duration column now counts only what ran. Mature-skipped rows are carried-forward passes with no duration, so summing over them divides real seconds by a task count that never executed — a codex nightly rendered "1300 tasks · 15h 29m" describing 397 tasks. The two duplicated duration derivations in runs.ts collapse into one exported `deriveRunDuration`, which excludes those rows from both the sum and the `every()` completeness guard, and reports `executedTasks` alongside. Both run tables now name that count next to the duration when the two differ. `tasksExecuted` is carried through `RunSummary` / `RunOverview` / `ScopedRun` / `RunListingRow` rather than recomputed per consumer: the whole-run count comes from the same helper that produced the duration (over run.json's task_results), while `overview.tasks` drops rows with no task_id, so a recount could disagree with the duration's own denominator. A run with no mature skips renders exactly as before. Co-Authored-By: Claude Opus 5 (1M context) --- evalboard/app/page.tsx | 52 +++++++-- .../__tests__/message-timeline.test.tsx | 102 ++++++++++++++++++ .../app/runs/[id]/[...task]/_sections.tsx | 55 +++++++++- evalboard/app/runs/[id]/[...task]/page.tsx | 1 + evalboard/lib/__tests__/overview.test.ts | 36 +++++++ evalboard/lib/__tests__/runs.test.ts | 93 ++++++++++++++++ evalboard/lib/overview.ts | 15 +++ evalboard/lib/runs.ts | 81 +++++++++----- 8 files changed, 399 insertions(+), 36 deletions(-) diff --git a/evalboard/app/page.tsx b/evalboard/app/page.tsx index 7a66f245..ee20f570 100644 --- a/evalboard/app/page.tsx +++ b/evalboard/app/page.tsx @@ -73,6 +73,40 @@ function fmtCost(c: number | null): string { return `$${c.toFixed(2)}`; } +// The Duration cell for a run row. `taskDurationSeconds` is compute time over +// the rows that actually ran, so when the nightly carried some forward as +// mature passes the cell says how many that was — otherwise "1300 tasks · +// 15h 29m" reads as a per-task rate over 1300 tasks when it describes 397. +function RunDurationCell({ + seconds, + tasksRun, + tasksExecuted, +}: { + seconds: number | null; + tasksRun: number; + tasksExecuted: number; +}) { + const skipped = tasksRun - tasksExecuted; + if (skipped <= 0) { + return ( + + {fmtDuration(seconds)} + + ); + } + return ( + + {fmtDuration(seconds)} +
+ {tasksExecuted} run +
+ + ); +} + // Rail-level q filter: substring match on tag name only. This is narrower // than getRunListing's q (which also matches taskId / humanized id) by // design — rails are a tag namespace, the table is a task namespace. @@ -428,9 +462,11 @@ export default async function Page({ {fmtCost(r.totalCostUsd)} - - {fmtDuration(r.taskDurationSeconds)} - + ); })} @@ -572,11 +608,11 @@ export default async function Page({ {fmtCost(r.totalCostUsd)} - - {fmtDuration( - r.taskDurationSeconds, - )} - + ); })} diff --git a/evalboard/app/runs/[id]/[...task]/__tests__/message-timeline.test.tsx b/evalboard/app/runs/[id]/[...task]/__tests__/message-timeline.test.tsx index cf5ade72..70db7a22 100644 --- a/evalboard/app/runs/[id]/[...task]/__tests__/message-timeline.test.tsx +++ b/evalboard/app/runs/[id]/[...task]/__tests__/message-timeline.test.tsx @@ -476,3 +476,105 @@ describe("MessageTimelineSection — sub-agent grouping", () => { expect(screen.getByText("Message timeline (1)")).toBeInTheDocument(); }); }); + +// The strip must reconcile: generation + tool exec are shown against the wall +// clock they should add up to, so a harness that stops reporting one of them +// is visible on the page instead of silently reading as fast. +describe("MessageTimelineSection — Unaccounted cell", () => { + // Each summary cell is