[object Object]

← back to Exo

Fix first start in offline mode crash (#1782)

248919c2a821d6e923aca6b7b4f789b9210bcd78 · 2026-03-24 13:58:23 +0000 · ciaranbor

## Motivation

Running exo in offline mode on a machine where a model has never been
downloaded causes a crash

## Changes

- `download_shard` now catches `FileNotFoundError` from
`fetch_file_list_with_cache` and returns a `not_started` progress
instead of propagating the exception

## Why It Works

A status query should never crash its caller. By returning
`not_started`, the coordinator's existing offline guard (`if
self.offline:` at line 198) is reached and emits a graceful
`DownloadFailed` event. This also eliminates the warning spam on startup
where the status iterator catches the same exception for every
predefined model.

## Test Plan

### Manual Testing
- Start exo with `--offline` on a machine with no cached models, request
a model via the API — should get a graceful failure instead of a crash

Files touched

Diff

commit 248919c2a821d6e923aca6b7b4f789b9210bcd78
Author: ciaranbor <81697641+ciaranbor@users.noreply.github.com>
Date:   Tue Mar 24 13:58:23 2026 +0000

    Fix first start in offline mode crash (#1782)
    
    ## Motivation
    
    Running exo in offline mode on a machine where a model has never been
    downloaded causes a crash
    
    ## Changes
    
    - `download_shard` now catches `FileNotFoundError` from
    `fetch_file_list_with_cache` and returns a `not_started` progress
    instead of propagating the exception
    
    ## Why It Works
    
    A status query should never crash its caller. By returning
    `not_started`, the coordinator's existing offline guard (`if
    self.offline:` at line 198) is reached and emits a graceful
    `DownloadFailed` event. This also eliminates the warning spam on startup
    where the status iterator catches the same exception for every
    predefined model.
    
    ## Test Plan
    
    ### Manual Testing
    - Start exo with `--offline` on a machine with no cached models, request
    a model via the API — should get a graceful failure instead of a crash
---
 src/exo/download/download_utils.py | 31 ++++++++++++++++++++++++-------
 1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/src/exo/download/download_utils.py b/src/exo/download/download_utils.py
index 3f6f1dc9..ba9e7742 100644
--- a/src/exo/download/download_utils.py
+++ b/src/exo/download/download_utils.py
@@ -744,13 +744,30 @@ async def download_shard(
         logger.debug(f"Downloading {shard.model_card.model_id=} with {allow_patterns=}")
 
     all_start_time = time.time()
-    file_list = await fetch_file_list_with_cache(
-        shard.model_card.model_id,
-        revision,
-        recursive=True,
-        skip_internet=skip_internet,
-        on_connection_lost=on_connection_lost,
-    )
+    try:
+        file_list = await fetch_file_list_with_cache(
+            shard.model_card.model_id,
+            revision,
+            recursive=True,
+            skip_internet=skip_internet,
+            on_connection_lost=on_connection_lost,
+        )
+    except FileNotFoundError:
+        not_started_progress = RepoDownloadProgress(
+            repo_id=str(shard.model_card.model_id),
+            repo_revision=revision,
+            shard=shard,
+            completed_files=0,
+            total_files=0,
+            downloaded=Memory.from_bytes(0),
+            downloaded_this_session=Memory.from_bytes(0),
+            total=Memory.from_bytes(0),
+            overall_speed=0.0,
+            overall_eta=timedelta(0),
+            status="not_started",
+            file_progress={},
+        )
+        return target_dir, not_started_progress
     filtered_file_list = list(
         filter_repo_objects(
             file_list, allow_patterns=allow_patterns, key=lambda x: x.path

← 49951e1b Sync custom model cards across nodes (#1768)  ·  back to Exo  ·  Improve batch performance and stats reporting (#1777) 7df3774c →