fix(cookbook): recover completed downloads from DOWNLOAD_OK in background reconciler (#4000)

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) <noreply@anthropic.com>
This commit is contained in:
Max Hsu
2026-06-15 14:36:39 +08:00
committed by GitHub
parent 2966ad6ef6
commit 65c7321ace
2 changed files with 28 additions and 2 deletions
+11 -1
View File
@@ -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;
@@ -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():