Repository navigation
A generation-0 failure before the durable run is linked makes every later generation unrecoverable #378
Description
Impact
Any failure in generation 0 that happens after eval plan creates the eval run directory but before a
durable run is linked converts a transient error into a permanently unrecoverable run. Every
subsequent recovery generation dies in ~92 seconds without doing any work, so the entire
maxNoProgressGenerations budget (default 3, packages/modal/src/defaults.ts:15) burns down and the run
is lost. Recovery cannot succeed no matter how many generations are allowed.
Observed on R54 (aave-v4-v0012-main-issue368fix-r54-invariant-only, image from 0088f80).
Evidence
Generation 0 failed at workflow submission (an unrelated npm publish race, #377). Generations 1 and 2 then
each died in ~92s. They wrote no events at all, which is why the node-error channel and the volume's
event log were both silent — the failure is above them. The cause is only in the sandbox's own stderr:
worker terminated: Error: resume requires exactly one linked durable run, found 0
at locateModalResumeWorkspace (/opt/ultrafuzz/packages/modal/dist/resume.js:77)
at async Object.run (/opt/ultrafuzz/packages/modal/dist/worker.js:96)
The linkage record on the volume confirms it — one record, failed, and never linked:
control/.ultrafuzz/evals/runs/<eval-run-id>/runs.jsonl
1 record: status=failed ultrafuzz_run_id=None
so productRunIds.size === 0 and resume.ts:120 throws.
Root cause: the routing predicate disagrees with the resume precondition
worker.ts:144 chooses resume-vs-fresh with:
async function hasExistingEvalWorkspace(): Promise<boolean> {
const evalRoot = path.join(WORK_ROOT, "control", ".ultrafuzz", "evals", "runs");
return (await readdirIfExists(evalRoot)).length > 0; // worker.ts:213
}That directory is created by eval run (packages/evals/src/runner.ts:127 writes eval.json), not by
eval plan — eval plan is a dry run and writes nothing. So the directory appears once model work has
begun, but the ultrafuzz_run_id linkage is only recorded once a durable run actually exists. Between
those two moments the predicate reports "resumable" for a workspace that provably has nothing to resume, and
locateModalResumeWorkspace — which requires exactly one linked durable run — immediately throws.
That window is not narrow: it spans the whole of workflow compilation and submission, which is where
generation 0 died here.
The part that makes this clearly a bug rather than a missing feature: prepareWorkspace() already handles
this exact state. Its else branch (worker.ts:336) exists precisely for "a pre-model workspace is
already on disk" — it asserts target, control, ground-truth/findings.yml, the suite and the source
proof are present, then re-runs references sync, validate and eval plan. That branch is
unreachable whenever eval plan got far enough to create the runs directory, which is exactly when it
is needed.
So the two halves disagree about what "existing workspace" means:
hasExistingEvalWorkspace→ "the eval runs directory is non-empty"locateModalResumeWorkspace→ "exactly one linked durable run exists"
Suggested fix
Route on the same condition the resume path requires: a workspace is resumable only if a durable run was
actually linked. With no linked run, fall through to prepareWorkspace(), whose non-empty branch already
does the right thing.
Two properties any fix must preserve, both worth explicit tests:
- Never silently discard real progress. If a linked durable run exists, behaviour must not change —
this must not become a path that quietly restarts a run that had done model work. - Distinguish "nothing linked" from "more than one linked."
productRunIds.size > 1is genuine
corruption and must keep failing loudly; only the0case is safe to treat as not-yet-started, because
0 linked runs means by construction that no durable run was ever recorded.
Acceptance criteria
- A regression test that fails without the fix: a workspace whose
runs.jsonlholds only unlinked
records routes to the pre-model path and does not throwexactly one linked durable run. - A test that a linked durable run still resumes exactly as before.
- A test that
productRunIds.size > 1still throws. - Recovery is shown to survive a generation-0 failure that occurs after
eval plan.
Note on diagnosis
Worth recording because it cost time: all three generations failed at near-identical durations (93s, 92s,
94s), which made a single shared cause look obvious, and the one cause already in hand was the npm E404 from
generation 0. That inference was wrong — generations 1 and 2 never reached npm. Only the sandbox stderr
distinguished them. Neither the event log nor the node-error channel can see a failure this early, which is
the same visibility gap as #361.
Activity
aviggiano commentedon Aug 8, 2026
Correction to the body, made after filing. I originally wrote that the eval run directory is created by
eval plan"before any model work". That is wrong:eval planis a dry run that writes nothing(
packages/cli/src/commands/eval/plan.ts), and the directory is created byeval run—eval.jsoniswritten at
packages/evals/src/runner.ts:127. The body is corrected.The diagnosis and the fix are unaffected — the predicate still disagrees with the resume precondition — but
the corrected timing matters for the fix, in a way that makes it less trivial than the original text
implied:
modelWorkStartedis already set totruebeforerunEvalis called, so a run in this window has begunmodel work even though nothing is linked. The vulnerable window is therefore workflow compilation and
submission, not a pre-model gap. Any fix must be justified by "no durable run was ever linked, so there is
nothing to resume" — not by "no model work had started", which is false here.
fix(modal): repair the missing durable-run link instead of restarting
fix(modal): make the repaired row scoreable and the disk predicate ho…
fix(modal): derive the run metadata path, and only delete a provably …
fix(modal): classify every run root, and stop restating the metadata …
fix(modal): classify eval run entries the same way as run roots
Resume a durable run whose journal link was never written (#381)