diff --git a/.github/workflows/devdex-eval.yml b/.github/workflows/devdex-eval.yml index d554749..35b4a5a 100644 --- a/.github/workflows/devdex-eval.yml +++ b/.github/workflows/devdex-eval.yml @@ -59,7 +59,7 @@ jobs: - name: Install run: | python -m pip install --upgrade pip - pip install pytest + pip install pytest numpy - name: Dataset + scorer tests # the DIRECTORY, not a hand-listed set -- a new test file must not need a CI edit to run run: python -m pytest devdex/tests -q diff --git a/benchmark/register_arm.py b/benchmark/register_arm.py index 03f0e06..2664e3d 100644 --- a/benchmark/register_arm.py +++ b/benchmark/register_arm.py @@ -67,5 +67,8 @@ def run_agent_mode(engine, track="all", limit=None, n_runs=1, workers=3, dataset if limit: cmd += ["--limit", str(limit)] print(f"\n=== {t}: {' '.join(cmd[1:])}", flush=True) - rc |= subprocess.call(cmd, env=env, cwd=str(ROOT)) + status = subprocess.call(cmd, env=env, cwd=str(ROOT)) + if status == 78: + return status # rejected resume: do not run further tracks or report old records + rc |= status return rc diff --git a/benchmark/run_benchmark.py b/benchmark/run_benchmark.py index 07c55c7..4f6af89 100644 --- a/benchmark/run_benchmark.py +++ b/benchmark/run_benchmark.py @@ -202,6 +202,8 @@ def main(): rc = register_arm.run_agent_mode( engine=label, track=a.track, limit=a.limit, n_runs=a.n_runs, workers=a.workers, datasets=DATA) + if rc == 78: + return rc # saved records were rejected; they must not become a new score if rc: print(f"\none or more tracks exited non-zero (rc={rc}); scoring what landed", flush=True) diff --git a/devdex/harness/runner_sdk.py b/devdex/harness/runner_sdk.py index 82c7e9f..b9e48cb 100644 --- a/devdex/harness/runner_sdk.py +++ b/devdex/harness/runner_sdk.py @@ -776,7 +776,7 @@ def _is_dead(row): return bool(err) and any(d in err for d in _DEAD_ON_RESUME) -def _resume(pass_tag, track, arm, items): +def _resume(pass_tag, track, arm, items, *, gt_sha=None, model=None): """Load a partial cell if one exists and return (done_rows, remaining_items).""" path = _out(f"{pass_tag}_{track}_{arm}.json") if not os.path.exists(path): @@ -790,6 +790,19 @@ def _resume(pass_tag, track, arm, items): else: keep = [r for r in done if not _is_dead(r)] redo = {r.get("qid") for r in done if _is_dead(r)} + # Qids can remain stable while their queries/golds or driver change. Reusing those + # measurements would mix experiments; _one_pass later stamps the current config on all + # rows, concealing that mismatch. Unknown legacy identity is not evidence of a match. + for row in keep: + for key, expected in (("gt_sha", gt_sha), ("model", model)): + if expected is not None and row.get(key) != expected: + print( + f"cannot resume {path}: qid={row.get('qid')!r} has {key}={row.get(key)!r}, " + f"expected {expected!r}. Start a fresh run with a new --label and without " + "--auto-resume, or use a fresh --output-dir; saved records are unchanged.", + file=sys.stderr) + # EX_CONFIG: the suite must not score/relabel the incompatible saved cell. + raise SystemExit(78) seen = {r.get("qid") for r in keep} todo = [x for x in items if x["id"] not in seen] if done: @@ -830,7 +843,7 @@ def _one_pass(items, pass_tag, pass_no): "skill": SKILL_NAME, "skill_sha": SKILL_SHA, "skill_provenance": SKILL_PROV} _CFG_SNAPSHOT.update(cfg) - rows, items = _resume(pass_tag, TRACK, ARM_NAME, items) + rows, items = _resume(pass_tag, TRACK, ARM_NAME, items, gt_sha=GT_SHA, model=MODEL) if not items: print("nothing to do — cell already complete", flush=True) return rows diff --git a/devdex/run_eval.py b/devdex/run_eval.py index 1c8d010..f5f39c7 100644 --- a/devdex/run_eval.py +++ b/devdex/run_eval.py @@ -63,7 +63,8 @@ def main(): ap.add_argument("--resume-dir", default=None, help="continue into an existing runs/-