Spawn failures are infrastructure evidence, not model evidence#35
Open
mlava wants to merge 1 commit into
Open
Conversation
When an engine binary is missing, the wrapper exits 127 (or 126) without ever invoking a model — but every such attempt wrote a FAIL row to the per-model log and then spent the task's retry on a failure a retry cannot fix. In the field this poisoned a scoreboard with 36 rows in one run, dragging an innocent model's pass rate from 1.00 to 0.40, and the operator had to hand-edit runs.jsonl to correct the record. The fix draws one line: no model evidence, no model row. - worker_never_ran() names the two signatures: exit 126/127 with no token usage (wrapper/shell command-not-found), or an error before exec with no exit code. Token usage always wins — a late 127 from a wrapper's cleanup after a real model run still counts as model evidence. - verdict_for() returns INFRA for these attempts, checked before verify.ok so a stale artifact passing the check cannot credit a model that never ran. - _log_attempt() skips the model-log row (and the steering observation) for such attempts, writing a breadcrumb to the worker log instead. This single choke point also covers _record_prepare_error, whose rows carried a model name for a model that never ran. - INFRA is not in the retry set, so the retry is skipped, with a log line saying why and what to fix; the run summary counts INFRA tasks and points at the engine bin/PATH config. Tests: unit coverage for every branch of the classification (including the rc=1-no-tokens case that must KEEP reaching the log, and timeout staying model evidence), plus an end-to-end run against a wrapper that exits 127 — asserting verdict INFRA, attempts == 1, an empty model log, and the summary hint. The pre-existing test_design_reference/test_scoreboard_page failures on main are untouched by this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 14, 2026
Contributor
|
Review outcome: #35 rides with #36 (which contains it) — the joint-design invitation and maintainer constraints for unifying with #33's engine-down breaker are posted on #36 and #33. Short version: one run-scoped engine-health mechanism, your spawn detector + @jeffhamons' API-terminal detector feeding it, visible-but-unranked eval rows rather than suppression, and truthful display states. |
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.
Problem
When an engine binary is missing from PATH, the wrapper script exits 127 (
opencode-sandboxed.sh: opencode not found on PATH) without ever invoking a model. Today each such attempt:runs.jsonl.fail FAIL 2), so triage starts from zero.Fix — one rule: no model evidence, no model row
worker_never_ran()names the two signatures: exit 126/127 with no token usage (shell command-not-found / not-executable, which is how wrapper-script engines fail), or an error before exec with no exit code. Token usage always wins — a late 127 from a wrapper's cleanup after a real model run still counts as model evidence.verdict_for()returns a newINFRAverdict for these attempts, checked beforeverify.okso a check passing against stale artifacts can't credit a model that never ran._log_attempt()gates onworker_never_ran()at a single choke point: no model-log row, no steering observation, breadcrumb written to the worker log instead. This also covers_record_prepare_error, whose ERROR rows carried a model name for a model that never ran.INFRAis not in the retry set, so the retry is skipped, with a worker-log line saying why and what to fix.bin/PATH), so the failure class is visible at a glance.Tests
tests/test_infra_spawn_failures.py:test_mock_engine.py) against a/bin/shwrapper that exits 127: asserts verdictINFRA,attempts == 1, an empty model log, the summary hint, and the worker-log breadcrumbs8/8 pass. The pre-existing
test_design_reference/test_scoreboard_pagefailures on main reproduce identically with this change stashed, so they're unrelated.Field evidence
Run
protocol-spec-audit-20260714T120644Z: 16 tasks × 2 attempts, all rc=127, 0 tokens, ~10–20s each — every row logged as model FAIL. Same signature recurred the same day in a second run because the fix lived in the operator's shell environment rather than the harness.🤖 Generated with Claude Code