Skip to content

feat(metrics): per-(task version, agent, model) trial metrics - #1225

Open
stateofkate wants to merge 6 commits into
stagingfrom
kate/task-version-model-metrics
Open

feat(metrics): per-(task version, agent, model) trial metrics#1225
stateofkate wants to merge 6 commits into
stagingfrom
kate/task-version-model-metrics

Conversation

@stateofkate

@stateofkate stateofkate commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds task_version_model_metrics: trial metrics at (task_version_id, agent, model), a grain finer than the existing task_version_browse_summaries. Makes per-model pass rate and trajectory length selectable rather than only filterable — _subject_metric_value computes exactly this today and discards it, since it is only ever consumed by a WHERE.

Step counts are stored as a distribution split by outcome (min / p50 / max, each with its own _n), not a mean. A mean hides the two things worth catching, because they sit at opposite extremes and average to something unremarkable:

  • a pass at very few steps — a trivially-green verifier rather than a capable model
  • a failure at the step ceiling — truncation being read as incapability

The prod dry run found an instance in the first 2,400 task versions: grok-build on xai/v9m-rl-learnability-tp8, 3 passing trials at exactly 1 step each (min = p50 = max = 1).

Design notes

Rides the existing refresh, no new maintenance mechanism. One call at the end of refresh_task_browse_summaries, so all 18 existing hook sites cover the finer grain and it runs under the advisory locks already held. No trigger, no dirty queue, no reconciliation worker. The bucket taxonomy and scope predicate are reused verbatim from task_browse_metrics, so this does not add a third definition of either.

Recompute per group, never delta. trial_handler.py resets a retried trial to RUNNING with reward and total_steps nulled, so trials move backwards out of terminal buckets. Delta arithmetic is only correct under exactly-once in-order delivery. A distribution cannot be maintained incrementally at all — you cannot add a trial to a median.

percentile_disc, not _cont. Most groups hold four trials or fewer, where interpolation returns a step count no trial ever had.

The table is inert until backfilled — the migration does no inline work, so it stays fast on a hot trials table.

Verification

  • Full migration chain on a fresh database, tvm_metrics_001 at head
  • 11 tests, all against real Postgres — recompute (bucket split, percentile_disc medians, NULL steps excluded rather than zeroed, in-flight exclusion, agent/model grain, backwards transitions, soft-delete/supersession, stale-row cleanup) and backfill (terminates past a trial-less version, idempotent on re-run)
  • Backfill executed for real, 25 seeded versions → 48 rows; the trial-less version correctly produced none; re-run idempotent (48 → 48); resume-from-midpoint processed 12
  • The empty-version guard was proven to fail against the buggy shape: the original "versions missing a row" cursor returns the same trial-less version on every call, forever
  • Read-only dry run against prod: 2,400 versions in 1.46s, plan is GroupAggregate over an Incremental Sort with Presorted Key: task_version_id — it seeks, no sequential scan
  • ruff clean. Pre-existing suite failures confirmed unrelated by toggling the hook off and getting byte-identical 15 failed, 197 passed both ways

Rollout

Merging applies the migration through the normal deploy. The table is empty until:

python -m oddish.core.backfill_task_version_model_metrics --limit 500   # smoke
python -m oddish.core.backfill_task_version_model_metrics              # full

Prod has 119,750 task versions. Projected ~540k metric rows and ~1.2 minutes of query time — that figure measures the SELECT only, since the upsert could not be timed against a table that does not exist there yet, so treat it as a floor. The projection also extrapolates from the first 2,400 versions in id order, and rows-per-version rose steadily across batches, so the row count is likely understated.

Caveat worth knowing before reading the output

trials.total_steps was only added 2026-06-18, so it is NULL on ~89% of historical trials and the step columns will be NULL for most older groups. The live path has been clean since late June (post-migration trials with harbor_sha set: 0.00% missing). Step data is trustworthy from roughly August onward; before that it is a thin, agent-biased slice. Backfilling total_steps itself is a separate, optional workstream — out of scope here.


Note

Medium Risk
Touches hot trial aggregation paths and adds concurrent refresh/backfill logic guarded by advisory locks; migration is schema-only but operational backfill is heavy on trials.

Overview
Introduces task_version_model_metrics, a denormalized rollup one grain finer than task browse summaries: each row is (task_version_id, agent, model) with pass/partial/fail and unscored/cancelled counters, reward/runtime sums, and step distributions (min / percentile_disc p50 / max) split by pass vs fail (partial is count-only). Ratios stay computed at read time; the migration creates the table and indexes only—no inline backfill on deploy.

refresh_task_version_model_metrics fully recomputes affected groups from trials (same browse scope and bucket taxonomy as existing browse metrics), upserts rows, deletes stale (agent, model) keys, and takes the same per-version advisory locks as browse summary refresh. That refresh is now invoked at the end of refresh_task_browse_summaries, so existing summary hooks keep the new table warm without new call sites.

A batched, resumable backfill (backfill_task_version_model_metrics) walks task versions by id keyset (not “missing row”) for initial population after merge.

Reviewed by Cursor Bugbot for commit 68508de. Bugbot is set up for automated code reviews on this repo. Configure here.

Adds task_version_model_metrics at a grain finer than
task_version_browse_summaries, so pass rate and trajectory length can be
selected per model rather than only filtered on.

Rides the existing refresh_task_browse_summaries hook and its advisory
locks instead of adding a second maintenance mechanism. Recompute per
group, never delta: retried trials reset to RUNNING with reward and
total_steps nulled, so they move backwards out of terminal buckets.

Step counts are stored as a distribution split by outcome (min/p50/max
via percentile_disc, each with its own _n) because a mean hides both a
suspiciously cheap pass and a failure capped at the step ceiling.

Table is inert until backfilled.
Populates task_version_model_metrics for existing task versions, which the
migration deliberately leaves empty so it stays fast on a hot trials table.

Keyset pagination over task version ids rather than "versions missing a row":
a version whose trials are all out of scope legitimately produces no row, so a
missing-row cursor hands back the same page forever. Resumable with --after-id.

Tests cover the recompute (bucket split, percentile_disc medians, NULL steps
excluded rather than zeroed, in-flight exclusion, agent/model grain, backwards
transitions on retry, soft-delete and supersession, stale-row cleanup) and the
backfill itself (terminates past a trial-less version, idempotent on re-run).
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
oddish-app Ready Ready Preview Aug 13, 2026 6:14pm

Comment thread oddish/src/oddish/core/task_version_model_metrics.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Left a non-blocking comment because Cursor Bugbot reported an unresolved high-severity finding (unscored counts include in-flight trials) and the Bugbot check finished as skipped. Assigned reviewers for the browse-summary metrics path.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Oddish preview

Commit: 698c726b56a4d27468bf50c41862e05a4de364d8

Surface Link Target
Frontend https://pr-1225.oddish.app Vercel preview for 698c726
Backend oddish-pr-1225 oddish-pr-1225
Database project rswbeuzjzvdgnnsirlfv project rswbeuzjzvdgnnsirlfv

Vercel deployment URL: https://oddish-p6h21r2wj.oddish.app

Plan:

  • Frontend deploy: true
  • Backend deploy: true
  • Migrations: false

This comment is updated by the PR Preview workflow.

The recompute assumed its only caller was refresh_task_browse_summaries, which
already holds a sorted advisory lock per version id. The backfill calls it
directly and held no lock, so a backfill batch racing a live refresh could
overwrite a fresh row with the snapshot it aggregated moments earlier -- silent
staleness rather than a crash, and invisible to a single-threaded fixture test.

pg_advisory_xact_lock is re-entrant within a transaction, so taking an
already-held lock on the browse-summary path costs nothing, and the shared sort
order keeps both callers in one global ordering.

Proven by holding the lock from a second connection and asserting the recompute
blocks; the test fails with DID NOT RAISE when the lock is removed.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 68508de. Configure here.

Comment thread oddish/src/oddish/core/task_version_model_metrics.py
@github-actions
github-actions Bot had a problem deploying to Preview August 13, 2026 17:34 Failure
@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

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