Skip to content

feat(evalboard): replace the turn-budget signal with time per passed task - #125

Open
bai-uipath wants to merge 5 commits into
mainfrom
bai/wall-clock-evalboard
Open

feat(evalboard): replace the turn-budget signal with time per passed task#125
bai-uipath wants to merge 5 commits into
mainfrom
bai/wall-clock-evalboard

Conversation

@bai-uipath

@bai-uipath bai-uipath commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

The consumer half of Proposal: optimizing for wall clock time instead of expected turns. The producer is coder_eval_uipath#86, which derives each task's expected wall clock from run history and stamps expected_seconds into run.json.

Safe to merge in either order: runs without the stamp render as unscored rather than erroring.

Screenshots

image
image

Why the turn budget had to go

Problem Evidence
The numbers were unmaintained 147 commits in skills touched expected_turns; six revised a value
The seed was a best-case extreme Bulk-seeded from min_turns_success, the luckiest run ever seen; 44.7% of passing budgeted tasks exceed budget on a healthy night
Post-seed values are guesses 49% are multiples of 5; 204 tasks share a copy-pasted 300 against a p90 actual of 52, so they are permanently green
Coverage holes sit on the slow tasks 249 of 1002 tasks have no budget and hold 34.7% of task-seconds; of the 25 slowest, 14 have no budget
A turn is not a unit of time Seconds per visible turn: p10 5.2s, p50 8.1s, p90 15.5s, max 156s. A Read and a 20-minute deploy both count 1
Not comparable across harnesses On identical tasks Codex uses 0.50x Claude's turns and scores 98.3% within budget against Claude Code's 77.3%
Nothing consumed it One evalboard chart, no Slack line, no gate

What changes on the dashboard

  • Home page: the "Within Expected Turns (%)" card becomes Time per Passed Task, a per-harness trend of seconds per passed task. Hovering a point gives that run's seconds and its within-expected share. There is no headline figure over the chart: those numbers belong to one run and the chart spans many.
  • Task grid: a new sortable vs Expected column carries the ratio and the tint. Duration stays untinted, so a long task is never mistaken for a slow one. Sorting by ratio is a genuinely different ranking than sorting by duration: on the 2026-08-18 codex nightly the two top-tens share zero tasks, and the ratio sort surfaces a uipath-troubleshoot cluster the duration sort cannot see.
  • Task detail and trends: the ratio is printed beside the duration (7m29s 3.1×) and gets its own column in the per-task run history, so the number is readable without hovering.
  • Watchlist: the third attention segment becomes time-overage; "turn-overage offenders" becomes slow-task offenders, reading 2m18s / 1m22s expected instead of turn counts.

Ratios render to one decimal. The second decimal is fake precision: the baseline is a min over a handful of runs (p10 over ten), and a task's own night-to-night spread is wider than the digit it would add.

The dashboard derives no baseline of its own. It reads the expected_seconds the runner stamped, so a number rendered here still matches the Slack ping that announced that run, and an unscored task (no passing run yet on that harness, or a run predating the stamp) reads as unscored everywhere rather than as "on target".

Two bugs that only running it against real data exposed

Both were invisible to the tests and are fixed here with regression coverage.

The board disagreed with the runner about its own headline. A codex nightly that stamps 3m12s rendered as 1m17s. timePerPassedTaskForTasks counted mature-skipped rows in the denominator while the runner excludes them from both sides, so it divided real seconds by 681 carried-forward passes that never ran. withinExpectedTimeRateForTasks had the same leak and survived only by accident, because those rows carry no stamped line.

The delta compared across harnesses. Consecutive nightlies alternate harness, so a codex run was being measured against the previous claude-code run and reported ▼ 75% vs prev, which described the schedule rather than a change in speed. That delta is now gone from the card entirely, for the reason above.

One semantic change worth reviewing

withinExpectedTimeRateForTasks scores passing tasks only. The turn rate it replaces counted a budgeted failure as over budget. Two reasons that does not carry over: a task that crashed in 10 seconds did not blow a time budget, and folding failures in would make a pass-to-timeout regression read as an efficiency gain once the slow pass stopped counting. Failure is already the pass rate's job, and the seconds burned failing are already in the headline's numerator (total seconds of every task that ran, over the number that passed).

expected_turns is deprecated, not deleted

The field stays on RunLimits, accepted and ignored, marked deprecated, with a CE031 exemption that states why. Deleting it now would fail every one of the ~930 skills task YAMLs that still declare it, since RunLimits sets extra="forbid", and would break any external suite that sets it (this package publishes to public PyPI and has an ADOPTERS.md). Removal belongs in its own release once those YAMLs are cleaned, and keeping the field here means no release-and-pin chain is on this PR's critical path.

Every consumer is gone: the orchestrator's one-shot warning and its two call sites, reports_stats.expected_turns_overage, the HTML header badge, the markdown Run-time Note, and the per-row expected_turns / expected_turns_overage report fields. Docs updated to describe the derived line instead.

Testing done

  • pnpm vitest run: 523 passed, 43 files. tsc --noEmit: clean. next build: clean.
  • Run end-to-end against real stamped data, not fixtures. Twelve real nightlies were replayed locally, each stamped using only the history that preceded it, and the board driven off them. That is what surfaced both bugs above, and it is the only check that compares what the board renders against what the runner wrote.
  • New regression tests for both bugs: mature-skipped passes leaving both sides of the ratio, and a mature-skipped pass not counting as within expected.
  • Component tests for the new column: the ratio rendering, the tint buckets, Duration staying untinted, and that sorting by ratio produces a different order than sorting by duration.
  • uv run pytest -m "not live and not lint": 4175 passed. Four failures are pre-existing on main in this environment and untouched by this branch: test_litellm_route (us. vs eu. Bedrock profile, no litellm file in the diff) and three test_reports_stats_nonfinite cases (cohens_dstatistics.stdev on NaN; verified against 3.12.5 as well, so it is environment-local rather than a version issue, and cohens_d is byte-identical to main).
  • ruff format --check + ruff check: clean. Custom CE lint: 344 passed.

Left to do

  • Not seen on a deployed instance. Everything above is local. The chart, the new column and the hover have not been rendered against the live blob-backed data source.
  • Not exercised on mobile. The grid's card layout carries the ratio under the duration rather than as a fifth stat; that path is tested but never viewed on a real device.
  • The branch is behind main by a few non-conflicting commits and needs an update before merge.
  • Manual deploy after merge: workflow_dispatch on coder_eval_uipath/.github/workflows/deploy-evalboard.yml with ref: main. That workflow does not fire on merge.
  • Optional follow-up, not in this PR: the harness legend renders twice on the home page, once under each chart, with identical contents. De-duplicating it means coupling the two chart components, so it is left alone.

🤖 Generated with Claude Code

bai-uipath and others added 3 commits August 18, 2026 13:08
…task

The dashboard's only efficiency signal was "Within Expected Turns", a rate
computed against a hand-written `run_limits.expected_turns` in each task YAML.
That signal did not work: 147 commits touched the field and exactly six revised a
value, 249 of 1002 tasks carried no budget at all (holding 34.7% of task-seconds),
a fifth of the suite shared a copy-pasted 300 against a p90 actual of 52, and a
turn is not a unit of time — seconds per visible turn ran p10 5.2s to p90 15.5s,
max 156s, so a Read and a 20-minute deploy both counted 1.

Efficiency is now measured in seconds against `expected_seconds`, derived per task
per harness from that task's own passing history and stamped into run.json by the
eval runner (coder_eval_uipath#86). The dashboard reads the stamp rather than
deriving anything, so a number here still matches the Slack ping that announced
the run months later, and a task with no line reads as *unscored* everywhere
rather than as "on target".

- Home page: "Within Expected Turns (%)" becomes "Time per Passed Task", showing
  the latest run's seconds per passed task with its delta against the previous
  run, the within-expected share, and the same per-harness trend chart.
- Task grid, task detail and trends: the tint moves from the Turns cell to the
  Duration cell, with the ratio and the line it was measured against in the
  hover. Turn counts stay, untinted — data, not a score.
- Watchlist: the third attention segment becomes time-overage, and the
  "turn-overage offenders" panel becomes slow-task offenders.

`withinExpectedTimeRateForTasks` scores passing tasks only, which is a deliberate
departure from the turn rate it replaces (that counted a budgeted failure as over
budget). A task that crashed in 10 seconds did not blow a time budget, and folding
failures in would make a pass-to-timeout regression read as an efficiency gain
once the slow pass stopped counting. Failure is the pass rate's job; the seconds
burned failing are in the headline's numerator.

`RunLimits.expected_turns` stays accepted and ignored, marked deprecated, and
exempted in CE031 with the reason. Deleting it now would fail every one of the
~930 task YAMLs that still declare it (RunLimits forbids extras) and break
external suites that set it; the removal belongs in its own release once those
YAMLs are cleaned. Every consumer is gone: the orchestrator's one-shot warning,
`expected_turns_overage`, the HTML badge, the markdown Run-time Note, and the
per-row report fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same content, fewer paragraphs: the comments now say what the code does and keep
only the non-obvious constraints. Also drops the stale description of the min /
p10 split, which the runner no longer has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gregates

Found by running the board against real stamped nightlies. A codex run whose
runner block said 3m12s per passed task rendered as 1m17s, because the front page
counted mature-skipped rows in the denominator while the runner excludes them
from both sides: real seconds divided by tasks that never ran. The within-expected
rate had the same leak, surviving only because those rows carry no stamped line.

The headline's "vs prev" also compared across harnesses, so a codex run was
measured against the previous Claude Code run and reported the schedule instead
of a change in speed. It now compares against the previous run of the same
harness and says which.

Tracks the runner: a 2x gate, and a task is unscored only when its harness has
never passed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bai-uipath
bai-uipath marked this pull request as ready for review August 18, 2026 22:58
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Claude finished @bai-uipath's task in 1m 17s —— View job


Review in progress

Todo List

  • Read .github/code_review.md for review standards
  • Read CLAUDE.md for project conventions
  • Analyze the full PR diff
  • Review changed files with full context
  • Perform cross-file consistency checks
  • Analyze what's missing
  • Design-level scrutiny
  • Post comprehensive review

Starting review of PR #125: feat(evalboard): replace the turn-budget signal with time per passed task...

bai-uipath and others added 2 commits August 18, 2026 16:21
The card was titled Time per Passed Task and plotted the within-expected rate,
so the heading and the line described different numbers. It now plots what it
says: seconds per passed task, per harness, on a zero-based axis.

The headline figure above the chart is gone. Both numbers it carried, the
per-task time and the within-expected rate, belong to one run, and the chart
spans a dozen, so neither reads correctly as a title over the series. The rate
moves into the point hover instead, which HarnessTooltip now supports through an
optional second line and a caller-supplied value formatter.

The "vs prev" delta goes with it. Consecutive nightlies alternate harness, so it
was comparing a codex run against the previous claude-code run and reporting the
schedule rather than a change in speed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ratio lived in a duration cell's tooltip, so finding the tasks running at 2x
their line meant hovering every row. It gets its own column now, vs Expected,
sortable and tinted, with the duration left plain: a long task is not a slow one,
and the two sorts rank differently (on the 2026-08-18 codex nightly the top ten
by duration and the top ten by ratio share no tasks).

Ratios render to one decimal. The baseline is a min over a handful of runs, p10
over ten, and a task's own night-to-night spread is wider than the second
decimal, so the extra digit was precision the number does not have. The same
format carries to the task detail page and to the per-task run history on trends.

Also drops the column-help popovers. Each cost a click to read one sentence and
crowded the header it sat in; the text is a native title tooltip now, which
leaves col-help as the strings themselves. The watchlist badge reads the shared
tolerance constant rather than repeating 1.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant