From 65c7321aceee4bce1c5695ce2d9fceae523a43c3 Mon Sep 17 00:00:00 2001 From: Max Hsu Date: Mon, 15 Jun 2026 14:36:39 +0800 Subject: [PATCH] fix(cookbook): recover completed downloads from DOWNLOAD_OK in background reconciler (#4000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard background status reconciler (_pollBackgroundStatus) only recovered "done" for dependency installs when the backend reported a finished task as "stopped". A real model download whose tmux pane is gone after DOWNLOAD_OK (so the dead-session check misses the landed snapshot) fell through to `task.type === 'download' ? 'crashed'`, so a completed download was shown as crashed (and stalled on the Serve tab). Recover "done" from the terminal DOWNLOAD_OK sentinel, mirroring the dep-install recovery already present. The background poll runs blind, so it keys off the conclusive exit-0 sentinel only — not the `/snapshots/` path, which can be printed mid-stream for multi-file downloads and would risk marking an incomplete download done. Fixes #3897 Co-authored-by: Claude Opus 4.8 (1M context) --- static/js/cookbookRunning.js | 12 +++++++++++- ...ookbook_dependency_completion_regression.py | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/static/js/cookbookRunning.js b/static/js/cookbookRunning.js index 47f7a1b62..61dadd51d 100644 --- a/static/js/cookbookRunning.js +++ b/static/js/cookbookRunning.js @@ -3533,12 +3533,22 @@ async function _pollBackgroundStatus() { // dead-session check inspects). Recover "done" from the retained output's // exit-0 sentinel so a clean install isn't downgraded to crashed. const depDone = !!task.payload?._dep && _depInstallSucceeded(task.output); + // A finished model download whose tmux pane is gone is also reported + // "stopped" (the dead-session check can miss the landed snapshot). + // Recover "done" from the terminal `DOWNLOAD_OK` sentinel — emitted + // only after the runner exits 0 — so a completed download isn't + // downgraded to crashed. This background poll runs blind (no live + // stream to debounce against), so unlike the reconnect loop it keys + // off the conclusive exit sentinel only, never the `/snapshots/` path, + // which can be printed mid-stream for multi-file downloads. + const downloadDone = task.type === 'download' + && String(task.output || '').includes('DOWNLOAD_OK'); const nextStatus = live.status === 'completed' ? 'done' : (live.status === 'error' ? 'error' : (live.status === 'stopped' - ? (depDone ? 'done' : (task.type === 'download' ? 'crashed' : 'stopped')) + ? ((depDone || downloadDone) ? 'done' : (task.type === 'download' ? 'crashed' : 'stopped')) : null)); if (nextStatus && task.status !== nextStatus) { updates.status = nextStatus; diff --git a/tests/test_cookbook_dependency_completion_regression.py b/tests/test_cookbook_dependency_completion_regression.py index 1533bdaca..1427cebaa 100644 --- a/tests/test_cookbook_dependency_completion_regression.py +++ b/tests/test_cookbook_dependency_completion_regression.py @@ -74,7 +74,23 @@ def test_background_poll_recovers_done_for_stopped_dependency_install(): source = _read("static/js/cookbookRunning.js") assert "const depDone = !!task.payload?._dep && _depInstallSucceeded(task.output);" in source - assert "depDone ? 'done' : (task.type === 'download' ? 'crashed' : 'stopped')" in source + assert "(depDone || downloadDone) ? 'done' : (task.type === 'download' ? 'crashed' : 'stopped')" in source + + +def test_background_poll_recovers_done_for_completed_download(): + """When the backend reports a finished model download as "stopped" (its + tmux pane is gone after DOWNLOAD_OK, so the dead-session check can miss the + landed snapshot), the reconciler must recover "done" from the terminal + DOWNLOAD_OK sentinel instead of downgrading the card to crashed. The + background poll keys off DOWNLOAD_OK only (not the "/snapshots/" path, which + can appear mid-stream for multi-file downloads).""" + source = _read("static/js/cookbookRunning.js") + + normalized = " ".join(source.split()) + assert ( + "const downloadDone = task.type === 'download' " + "&& String(task.output || '').includes('DOWNLOAD_OK');" + ) in normalized def test_dependency_install_payload_keeps_env_path_for_refresh():