Phase B.3 audit of commit 9f67ab2 (atomic model download + manifest —
Rev-1, Rev-5). Existing coverage is solid: the transcription-side
download_file has fixture tests for resume-and-verify, restart-on-200,
SHA-mismatch cleanup, 5xx rejection, Rev-1 preserve-existing-file, and
the Rev-5 manifest tmp+rename atomicity. The llm-side download_impl
has resume-and-verify and the Rev-1 preserve-existing-file regression.
One real residual found in crates/llm/src/model_manager.rs that the
original commit did not close.
When a stale .part exists (resume_from > 0) and the server returns a
200 full-body response to a Range request, download_impl returns
DownloadError::ResumeUnsupported without unlinking the .part. Every
subsequent download_model() call computes the same resume_from > 0,
sends the same Range request, gets the same 200, and fails the same
way — the download is wedged until the user manually invokes
delete_model(). That is itself a reversibility kill in the same
family as Rev-1: stale partial state stuck on disk, no automatic
recovery, the user has to discover an out-of-band command to escape.
The transcription-side download_file handles this case by treating
200-on-resume as a fresh-start (line 268: "Server ignored our Range
header — treat as fresh start"). The llm-side does not have an
analogous restart code path, but the simpler fix is sufficient: unlink
the .part before returning ResumeUnsupported. The next call sees
resume_from = 0, sends no Range header, the server returns 200, and
download_impl writes the new payload into a fresh .part and renames
atomically over dest. Single retry recovers.
Fix:
* crates/llm/src/model_manager.rs:
- download_impl: tokio::fs::remove_file(&tmp).await.ok() before
returning ResumeUnsupported, with a comment that names this as
a Phase B.3 audit residual and explains the wedge scenario.
- New test resume_unsupported_unlinks_part_so_retry_starts_fresh
— spins a server that ignores Range and returns 200, plants a
sentinel .part, asserts ResumeUnsupported AND .part removed AND
dest not written.
Verification:
* cargo test -p lumotia-llm --lib model_manager
→ 5/5 pass including the new test.
* cargo fmt --check → clean.
* cargo clippy -p lumotia-llm --all-targets -- -D warnings → clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>