fix(automationbench): record the harness's exit status on the k8s surface too - #554
Merged
Conversation
…face too A task that ran for 38 minutes and was killed at the wall clock reported the same thing as a task that never started: nothing. Run 20260910-091433-61fe (automationbench/claude-code/FW-Nemotron-3-Ultra) had 30 such tasks — each with a multi-megabyte trace and real tokens billed against it — and the dashboard could only label them `incomplete`, because the one signal that separates a timeout from a task that never began was never written. Only `run-agent` writes /output/agent/.exit-code. This benchmark is model-only: its preset replaces the standard three-phase pipeline with its own harness, so run-agent never runs, and `python3 run_automationbench.py || true` additionally swallowed the status. `write-result` then coerced the missing file to JSON null (rule 16's exit_code), and null is not a failure code — a wall-clock kill and a task that never launched arrive identically. The compose twin was fixed in 82f9c01 but the preset was not, so the two surfaces disagreed on a rule-16 field — a rule 24b lockstep break, and a rule 24 violation on byte-equivalent results across surfaces. Both surfaces now bound the harness the way run-agent bounds an agent (`timeout -k 30 $TIMEOUT`, rule 14) and record `$?`. The bound is load-bearing, not decoration: without it the only limit left is the pod's activeDeadlineSeconds (timeout + deadlineGrace = 2400s here), whose SIGKILL takes down the whole pod and leaves no shell alive to write .exit-code — so recording alone would still yield null at exactly the moment it matters. A timeout now lands as 124, and the dashboard labels it `timeout` with no dashboard change. Rule 29's per-surface gates cannot catch this class of drift: each surface renders fine on its own, and the divergence lives in `runnerArgs` — the one per-benchmark command a preset is still allowed to define, which is why the "one chart cannot drift from itself" reasoning does not extend to it. Two static gates now hold every runnerArgs preset to recording an exit status and bounding its harness. tau-bench already satisfied both (`exit $rc`); automationbench was the only gap, and the first gate fails on the pre-fix preset. Verified: `helm template --set benchmark=automationbench` renders the inner 1800s bound inside the 2400s pod deadline and leaves `$?` untouched; `docker compose config` passes (rule 27); static suite green. The unrelated task_inspection fixture failure (14 red trace findings) predates this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yoav Katz <katz@il.ibm.com>
Formatting only — `cargo fmt --check` wants the `checked > 0` assert wrapped. No change to what either gate asserts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yoav Katz <katz@il.ibm.com>
yoavkatz
added a commit
that referenced
this pull request
Sep 12, 2026
#554 (the #547 fix) landed on main while this was open and touched the same three files, so both automationbench surfaces conflicted on the harness command. Resolved by keeping BOTH changes on each surface: the edge starter runs first, then main's bounded harness (`timeout -k 30 $TIMEOUT`) and its recorded exit status. The starter sits deliberately OUTSIDE that bound — it is framework setup, not the harness, so a bring-up failure fails the task rather than eating into its wall-clock budget. Comment blocks from both sides are kept; neither explanation subsumes the other. tests/static/check.rs auto-merged: main's two preset gates append at the end and this branch's edge gate inserts mid-file, so all three coexist (35 pass). With #547 now fixed on main, this branch's remaining scope is the edge record alone. Signed-off-by: Yoav Katz <katz@il.ibm.com>
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
Run
20260910-091433-61fehas 30 tasks that ran for ~38 minutes, burned real tokens, and produced multi-megabyte traces — then were killed at the wall clock before the verifier could grade them. The dashboard could only label themincomplete, nevertimeout, because the signal that separates the two was never recorded.Task 10, for example: 150 model turns, 117,248 tokens, $0.08, a 6.5 MB trace whose last span lands mid-conversation at 09:54:43, and no
result.json. Every incomplete task in the run is cut off within seconds of the same wall-clock moment while the graded ones finished early on their own — a deadline expiring, not 30 independent failures.Why
Only
run-agentwrites/output/agent/.exit-code. automationbench is model-only: its preset replaces the standard three-phase pipeline with its own harness, sorun-agentnever runs — andpython3 /app/run_automationbench.py || trueswallowed the status on top of that.write-resultcoerces the missing file to JSON null:Null is not a failure code, so a wall-clock kill and a task that never launched arrive at the dashboard identically. Its classifier reaches
timeoutonly viaisinstance(ec, int) && ec == 124, so these fall through toincomplete— correct given its inputs, and unfixable downstream.compose.yamlwas fixed in 82f9c01; the preset was not. The two surfaces therefore disagreed on a rule-16 field — a rule 24b lockstep break ("changes to the compose base or the chart MUST be reflected in the other in the same commit") and a rule 24 violation of byte-equivalentresult.jsonacross surfaces.The fix
Both surfaces now bound the harness the way
run-agentbounds an agent (timeout -k 30 $TIMEOUT, rule 14) and record$?.The bound is load-bearing, not decoration. Without it the only remaining limit is the pod's
activeDeadlineSeconds(timeout + deadlineGrace= 2400s here), whose SIGKILL takes down the whole pod and leaves no shell alive to write.exit-code— so recording alone would still produce null at exactly the moment it matters. The inner 1800s bound fires first, the harness dies with 124, and the dashboard labels ittimeoutwith no dashboard change.Why CI missed it
Rule 29's gates are per-surface: each renders and parses fine alone. The drift lives in
runnerArgs— the one per-benchmark command a preset may still define — so the rules' "one chart and one container recipe cannot drift from themselves" reasoning does not extend to it.Two static gates now close that:
a_preset_that_replaces_the_agent_phase_records_its_exit_status— any preset overridingrunnerArgsmust record a status (.exit-code, or tau-bench'sexit $rc).a_preset_that_replaces_the_agent_phase_bounds_its_harness— any preset recording its own status must bound the harness withtimeout -k.tau-benchalready satisfied both; automationbench was the only gap across all nine presets. The first gate fails on the pre-fix preset (verified by reverting it).Verification
helm template --set benchmark=automationbenchrenders the 1800s inner bound inside the 2400s pod deadline,$?untouched by Helm.docker compose configpasses (rule 27); renders$$?so the container shell receives$?.task_inspectionfailure (14 red trace-fixture findings) is pre-existing onmain— confirmed on a clean tree, unrelated.Follow-up
The 30 incomplete tasks in that run are rerunnable, but this model wasn't converging inside 30 minutes — a rerun wants a larger
EVAL_TIMEOUT(--set timeoutOverride=), not just a relaunch.🤖 Generated with Claude Code