[object Object]

← back to Exo

fix: detect completed downloads by checking final file exists (#1582)

7024ddcf3e4fbcb840f21cf90647da5565da447d · 2026-02-23 10:12:07 -0800 · Alex Cheema

## Summary

Split from #1547 per review feedback.

When scanning existing download status, `get_downloaded_size()` returns
bytes from either the final file or its `.partial` counterpart — so a
`.partial` file with all bytes downloaded (e.g. process killed before
hash verification and rename) could be falsely reported as complete.

The previous approach (commit 3b54e7df) added a byte-comparison fallback
in the coordinator (`downloaded >= total > 0`), but this suffered from
the same `.partial` conflation issue.

**Fix:** Check whether the final (non-`.partial`) file actually exists
on disk before marking status as `"complete"` in `download_utils.py`.
This is the only reliable signal that hash verification passed and the
rename from `.partial` succeeded. The coordinator-level byte comparison
is removed since the source now reports correctly.

### Changes
- `download_utils.py`: Add `final_file_exists` check — only report
`"complete"` when the renamed, hash-verified file exists with matching
size
- `coordinator.py`: Revert to simple `progress.status == "complete"`
check, removing the byte-comparison fallback

**Note:** The corresponding byte-comparison workaround in #1547 should
also be removed.

## Test plan
- [x] basedpyright — 0 errors
- [x] ruff check — passes
- [x] pytest — 218 passed, 1 skipped (1 pre-existing Rust bindings
failure)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 7024ddcf3e4fbcb840f21cf90647da5565da447d
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date:   Mon Feb 23 10:12:07 2026 -0800

    fix: detect completed downloads by checking final file exists (#1582)
    
    ## Summary
    
    Split from #1547 per review feedback.
    
    When scanning existing download status, `get_downloaded_size()` returns
    bytes from either the final file or its `.partial` counterpart — so a
    `.partial` file with all bytes downloaded (e.g. process killed before
    hash verification and rename) could be falsely reported as complete.
    
    The previous approach (commit 3b54e7df) added a byte-comparison fallback
    in the coordinator (`downloaded >= total > 0`), but this suffered from
    the same `.partial` conflation issue.
    
    **Fix:** Check whether the final (non-`.partial`) file actually exists
    on disk before marking status as `"complete"` in `download_utils.py`.
    This is the only reliable signal that hash verification passed and the
    rename from `.partial` succeeded. The coordinator-level byte comparison
    is removed since the source now reports correctly.
    
    ### Changes
    - `download_utils.py`: Add `final_file_exists` check — only report
    `"complete"` when the renamed, hash-verified file exists with matching
    size
    - `coordinator.py`: Revert to simple `progress.status == "complete"`
    check, removing the byte-comparison fallback
    
    **Note:** The corresponding byte-comparison workaround in #1547 should
    also be removed.
    
    ## Test plan
    - [x] basedpyright — 0 errors
    - [x] ruff check — passes
    - [x] pytest — 218 passed, 1 skipped (1 pre-existing Rust bindings
    failure)
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
---
 src/exo/download/download_utils.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/exo/download/download_utils.py b/src/exo/download/download_utils.py
index dde07018..38dbe3e6 100644
--- a/src/exo/download/download_utils.py
+++ b/src/exo/download/download_utils.py
@@ -823,6 +823,7 @@ async def download_shard(
 
     for file in filtered_file_list:
         downloaded_bytes = await get_downloaded_size(target_dir / file.path)
+        final_file_exists = await aios.path.exists(target_dir / file.path)
         file_progress[file.path] = RepoFileDownloadProgress(
             repo_id=shard.model_card.model_id,
             repo_revision=revision,
@@ -832,7 +833,9 @@ async def download_shard(
             total=Memory.from_bytes(file.size or 0),
             speed=0,
             eta=timedelta(0),
-            status="complete" if downloaded_bytes == file.size else "not_started",
+            status="complete"
+            if final_file_exists and downloaded_bytes == file.size
+            else "not_started",
             start_time=time.time(),
         )
 

← dc89ba66 feat: add info button to model picker variant rows (#1589)  ·  back to Exo  ·  fix: add download/resume buttons to pending downloads (#1581 e8c13371 →