fix: fail jobs stuck running past 90min; recover re-queued completions#66
Conversation
…ions Two reliability fixes for stuck eval jobs: - Job-level timeout: the reaper only re-queued running jobs whose AGENT hadn't heartbeated in 5min, but a zombied/superseded agent keeps last_seen_at fresh (its heartbeats stop updating it, or a new process on the same token keeps the shared row alive), so such jobs hung "running" forever. Add failTimedOutRunning Jobs(): the background reaper now fails any job running longer than a global MAX_JOB_RUN_MINUTES = 90 (module-level constant in index.ts), regardless of heartbeat — terminal, with a clear "exceeded maximum running time" error. - Completion no longer silently drops results: /complete rejected any completion where job.eval_agent_id != agentId, so a valid finish arriving after the job was re-queued (eval_agent_id cleared by the reaper/re-register) was 403'd and the result lost. Now accept from the current lease holder (superseded still fenced) when the job is unassigned or assigned to this agent; reject only when it's actively assigned to a DIFFERENT agent. Also idempotent: a duplicate completion returns "already completed" without creating a second result. Verified: a job stuck running 91min → failed by the reaper with the clear message; a re-queued (eval_agent_id NULL) job completed by the current agent → 200 + result saved (was 403); duplicate completion → idempotent (test). 🤖 Built with SMT <smt@agora.build>
🤖 Claude Code ReviewReviewed the diff on the merge ref ( 1. Idempotency guard is read-then-write, not atomic — duplicate results possible ( 2. Minor / non-blocking
|
🤖 Codex Code ReviewFindings
No injection or credential exposure issues found in the changed code. |
…lize (review) Address the review of #66's completion change: - Revert the over-broad relaxation (High): a completion is again accepted only when the job is assigned to THIS agent, so a valid token can no longer complete an arbitrary unassigned job. The normal re-run re-claims the job (eval_agent_id set again), so legitimate finishes still record. - No resurrecting terminal jobs (High): finalizeRunningJob transitions only from 'running', so a late completion of a job already failed by the 90-min timeout stays failed and saves no result. - Authorize before finalizing (Medium): the assignment check runs before the terminal/idempotency handling, so a stranger's token can't probe job state. - Idempotency is now atomic (Medium): finalizeRunningJob is a single conditional UPDATE (running → terminal, RETURNING), so exactly one completion wins — no duplicate result on concurrent retries. Verified: duplicate completion → "already finalized" (idempotent); a timed-out failed job completed late by its agent → stays failed, 0 results. 🤖 Built with SMT <smt@agora.build>
|
Reworked the completion path to be secure (5aeca79), addressing all four findings:
Net: the primary fix is the 90-min job timeout (stuck jobs fail with a clear message instead of hanging). The completion path is now strict, terminal-safe, and atomically idempotent. Verified: duplicate completion → "already finalized"; timed-out job completed late → stays failed, 0 results. |
🤖 Codex Code ReviewFindings
No security injection or credential exposure issues found in the touched code. |
🤖 Claude Code ReviewReviewed the full diff ( Correctness1. Result becomes permanently unrecoverable if const finalized = await storage.finalizeRunningJob(...) // running → completed
...
} catch (resultError) {
await storage.completeEvalJob(parseInt(jobId), "Failed to save eval results"); // → failed
return res.status(500).json(...);
}Because The old code, being unguarded on status, could re-attempt 2. Transient "completed with no result" window (order inversion). Finalize now marks the job Minor
Good
Net: ship-worthy after reconsidering finding #1 — the unrecoverable-on-save-failure path is the one real behavioral gap. |
…eview) Codex #1: with finalize-first, a transient createEvalResult failure flipped the job to failed, and the agent's retry then hit "already finalized" and lost the result forever. On save-failure, reset the job back to running (resetJobToRunning) so the retry can re-finalize WITH the result; the 90-min reaper is the backstop. 🤖 Built with SMT <smt@agora.build>
|
Addressed finding #1 in the latest commit: on a transient |
🤖 Codex Code ReviewFindings
Open Question
|
🤖 Claude Code ReviewThe change is well-reasoned and the atomic-finalize approach is sound. A few notes, all low severity: Logic — lost-result race on duplicate completions (low, backstop exists)
Consistency — agent left
Maintainability —
Minor — 404 vs 403 disclosure
Things that are correct and worth confirming stayed correct:
Overall: solid fix, ship-able. The duplicate-completion result-loss race is the only item I'd consider hardening (e.g. have "already finalized" verify a result exists, or reorder so the result is saved before the winner is observable), but the reaper makes it non-critical. |
Fixes a job that hangs at running forever when its agent dies/zombies, and a completion whose result is silently dropped after the job was re-queued (the reported
eval-jobs/25762).Root causes
last_seen_at, while a new process on the same token keeps it fresh. So the reaper sees a healthy agent and never reclaims the job. There was no job-level timeout.eval_agent_id(→ NULL)./completethen rejected any completion wherejob.eval_agent_id !== agentId(403) and discarded the results — so a valid finish arriving after a re-queue was lost.Fixes
MAX_JOB_RUN_MINUTES = 90, module-level constant inserver/index.ts). The reaper now fails any job running longer than that, regardless of heartbeat — terminal, with a clear"exceeded maximum running time"error, so it stops instead of running forever. Chosen behavior: fail (not re-queue)./completeno longer drops valid results. The lease is already fenced against superseded duplicates; now a completion from the current lease holder is accepted when the job is unassigned (re-queued) or assigned to this agent — rejected only if actively assigned to a different agent. Also idempotent: a duplicate completion returns"already completed"with no second result.Verification
completed_atset.eval_agent_id NULL) completed by the current agent → 200, result saved (was 403).sato avoid the localnadaemon).npm run checkclean; suite failures unchanged (pre-existing/environmental).Generated with SMT smt@agora.io