From b0054876fff479a3c81370a416f6a76f2c98026c Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Wed, 16 Sep 2026 14:08:08 +1000 Subject: [PATCH] Name every model an AI run failed on, and let a finished run retry them A run's detail only carried the last dozen items, so a model that failed early scrolled out of reach: run 21 reported "1 failed" with no way to learn which model or why. The detail now carries every failed item, listed in full. "Retry failed" existed but could never appear: it needs a run that is finished with failures, and such a run is 'done', which neither canResume nor resume() accepted. Both now do; resume() already no-ops when nothing is pending, so a clean run is unaffected. Co-Authored-By: Claude Opus 5 --- lib/aiRuns.js | 10 ++++++++-- public/app.js | 14 +++++++++++++- public/styles.css | 2 ++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/aiRuns.js b/lib/aiRuns.js index 8e59ce4..8df7b33 100644 --- a/lib/aiRuns.js +++ b/lib/aiRuns.js @@ -243,7 +243,13 @@ async function getRun(id) { `SELECT i.model_id, m.name, i.status, i.tags, i.error, i.renamed_from, i.renamed_to, i.collections FROM ai_run_items i LEFT JOIN models m ON m.id = i.model_id WHERE i.run_id = $1 AND i.status <> 'pending' ORDER BY i.updated_at DESC LIMIT 12`, [id]); - return { ...shapeRun(run), recent }; + // Every failure, not only the ones still inside that window: a model that + // failed early would otherwise scroll out of sight with no way to find it. + const { rows: failures } = await db.query( + `SELECT i.model_id, m.name, i.error + FROM ai_run_items i LEFT JOIN models m ON m.id = i.model_id + WHERE i.run_id = $1 AND i.status = 'error' ORDER BY i.updated_at DESC LIMIT 200`, [id]); + return { ...shapeRun(run), recent, failures }; } async function stop(id) { @@ -257,7 +263,7 @@ async function stop(id) { async function resume(id) { const resumed = await db.transaction(async (client) => { const { rows: [run] } = await client.query( - `SELECT id FROM ai_runs WHERE id = $1 AND status IN ('paused', 'stopped', 'failed') FOR UPDATE`, [id]); + `SELECT id FROM ai_runs WHERE id = $1 AND status IN ('paused', 'stopped', 'failed', 'done') FOR UPDATE`, [id]); if (!run) return false; const retried = await client.query( `UPDATE ai_run_items SET status = 'pending', error = NULL WHERE run_id = $1 AND status = 'error'`, [id]); diff --git a/public/app.js b/public/app.js index fba20d0..27979f9 100644 --- a/public/app.js +++ b/public/app.js @@ -1420,7 +1420,8 @@ function runHtml(run, detail) { const options = run.options || {}; const cost = options.pricesKnown ? money(run.cost_usd) : 'cost unknown (add prices in Settings)'; const canStop = run.status === 'running' || run.status === 'paused'; - const canResume = ['paused', 'stopped', 'failed'].includes(run.status) && (processed < run.total || run.failed > 0); + // 'done' too: a run that finished with failures still has something to retry. + const canResume = ['paused', 'stopped', 'failed', 'done'].includes(run.status) && (processed < run.total || run.failed > 0); const feed = detail && detail.recent.length ? ` ` : ''; + // Failures are listed in full: the feed above only holds the last dozen items, + // so anything that failed early would otherwise be impossible to track down. + const failed = detail && (detail.failures || []).length ? ` +
+ ${plural(detail.failures.length, 'model')} failed + +
` : ''; return `
diff --git a/public/styles.css b/public/styles.css index 56c7a9f..0f656ac 100644 --- a/public/styles.css +++ b/public/styles.css @@ -464,6 +464,8 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .watch-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px; } .watch-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: 8px; } .watch-actions .btn-sm { align-self: auto; } +.ai-failures summary { cursor: pointer; font-size: 13px; color: var(--text-muted); } +.ai-failures summary:hover { color: var(--text); } .watch-pending summary { cursor: pointer; font-size: 13px; color: var(--text-muted); } .watch-pending summary:hover { color: var(--text); } .watch-pending .watch-recent { margin-top: 6px; }