Skip to content

🐛 fix(models): track model availability - #168

Merged
smb merged 2 commits into
mainfrom
fix/issue-167-model-availability
Sep 12, 2026
Merged

smb merged 2 commits into
mainfrom
fix/issue-167-model-availability

Conversation

@smb

@smb smb commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

After a model finished downloading as part of a preview, the inspector kept
describing it as missing and the primary action kept offering to download it
again — a 927.6 MiB download the user had just waited for.

Root cause

model_available was never derived from the weight being present on disk. It
was set in exactly two places: from a ModelAvailabilityChanged event, which
only startup and the Model Manager dispatch, and as a side effect of a preview
job advancing from JobState.PREPARING_MODEL to PREVIEWING.

So the download succeeding was not itself observed. A preview that failed,
timed out or was cancelled after the download completed left the flag False
while the weight was cached, and the three surfaces that read it all agreed on
the wrong answer:

surface showed
action button (ui/presenter.py) Download & Preview
status row (ui/presentation_model.py) Not cached
download notice (ui/inspector.py) … 927.6 MiB download required

Only opening the Model Manager corrected it. The maintainer hit this with a
segmentation process timed out, which is why the timeout and the stale label
arrived together.

Fixing only the timeout path would have left the same defect on the failure
and cancellation paths, so the fix records the fact instead: the completed
download dispatches availability itself, independently of what the job does
next.

Reported only after verification

The terminal (size, size) progress is emitted after the checksum is
verified and the file is atomically promoted, never on bytes received. A weight
that fails verification is deleted, so announcing availability earlier would
have been worse than the stale label — the interface would claim a model is
ready that is about to be removed. This also makes completion observable when
the server sends no Content-Length, which the previous progress path could
not report at all.

German copy, also from the issue

  • Herunterladen & Vorschau was a word-for-word rendering of
    Download & Preview: a bare infinitive beside a noun, which is not how a
    German action label reads. Now Modell laden & Vorschau, naming what the
    button fetches first.
  • %s — %s Download erforderlich becomes the sentence
    Für %s ist ein Download von %s erforderlich.

Catalogues: 529 strings, 529 finished, 0 unfinished in both languages.

Tests

test_completed_model_stays_available_when_preview_setup_fails and
test_completed_model_stays_available_when_preview_is_cancelled pin the flag
together with the button, the status row and the download notice, so the three
cannot drift apart again.
test_download_reports_completion_after_verified_weight_is_promoted pins that
the callback does not fire until the target file exists, and
test_unknown_content_length_reports_post_promotion_completion pins the case
the old path could not report.

Verification

uv run ruff check .                          All checks passed!
uv run mypy src                              no issues found in 121 source files
uv run mypy --platform linux src             no issues found in 121 source files
uv run mypy --platform win32 src             no issues found in 121 source files
uv run python scripts/check_guardrails.py    Guardrail check passed
QT_QPA_PLATFORM=offscreen uv run pytest -q   2058 passed, 4 skipped in 53.92s

Closes #167

- 🐛 Defects:
  - `model_available` was never derived from the weight being on disk. It
    was set only by a `ModelAvailabilityChanged` event, which the Model
    Manager dispatches, or as a side effect of a preview advancing from
    `PREPARING_MODEL` to `PREVIEWING`. A preview that failed, timed out or
    was cancelled after the download finished therefore left the flag
    `False` although the weight was cached — and all three surfaces that
    read it agreed on the wrong answer: the action button kept offering
    `Download & Preview`, the status row kept reading `Not cached`, and the
    download notice kept demanding 927.6 MiB. Only opening the Model
    Manager corrected it.
  - `PreviewController._notification` now dispatches
    `ModelAvailabilityChanged(True)` when the download stage reports
    completion, so the fact is recorded independently of what the job does
    next.
  - `ModelDownloader._download_locked` emits its terminal `(size, size)`
    progress **after** the checksum is verified and the file is atomically
    promoted, never before. A weight that fails verification is deleted, so
    reporting it as available on bytes received would have been worse than
    the stale label. This also makes completion observable when the server
    sends no Content-Length.
- 🧪 Tests:
  - `test_completed_model_stays_available_when_preview_setup_fails` and
    `test_completed_model_stays_available_when_preview_is_cancelled` pin
    the flag together with the button, the status row and the download
    notice, so the three cannot drift apart again.
  - `test_download_reports_completion_after_verified_weight_is_promoted`
    pins that the callback does not fire until the target file exists, and
    `test_unknown_content_length_reports_post_promotion_completion` pins
    the case the old progress path could not report at all.
- 📝 Copy:
  - `Herunterladen & Vorschau` was a word-for-word rendering of
    `Download & Preview` — a bare infinitive beside a noun, which is not
    how a German action label reads. It is now `Modell laden & Vorschau`,
    naming what the button fetches first. `%s — %s Download erforderlich`
    becomes the sentence `Für %s ist ein Download von %s erforderlich`.

Trigger: repro — select a model that is not cached, start a preview, let
the download finish and the preview then fail with
`segmentation process timed out`; the inspector still reports the model as
not cached and the button still offers to download it.

Closes #167
Comment thread src/matteloop/jobs/models/download.py
- 🐛 Defects:
  - Two early returns leave a verified weight on disk without reporting
    the terminal progress: the pre-flight check before any request is
    made, and the post-download check after a concurrent writer won the
    race. A caller that reads that progress as "the weight is usable now"
    never hears it on either path.
  - That is the same defect this branch exists to fix, on the cached path:
    `ModelChanged` sets `model_available` to `False`, and selecting a model
    that is already downloaded then takes the pre-flight return, so nothing
    dispatches `ModelAvailabilityChanged(True)`. If the preview that
    follows fails, times out or is cancelled, the inspector again reports a
    cached model as missing and offers to download it.
  - Every path that leaves a usable weight now reports completion.
- 🧪 `test_an_already_cached_weight_still_reports_completion` downloads
  twice and asserts the second, fully cached call reports completion as the
  first did. It fails on the pre-flight path without this change; the
  post-download path was found by that test rather than by reading, because
  fixing only the reported line left the test red.

Trigger: reported by the review bot on PR #168 — the cache-hit early return
at `download.py:266` skips the terminal completion progress.

Refs #167
@smb

smb commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Gitar's finding is correct and fixed in a46fd9c — and it is the same defect this PR exists to fix, on the cached path.

ModelChanged sets model_available to False (core/parameters.py). Selecting a model that is already downloaded then takes the early return, so nothing dispatches ModelAvailabilityChanged(True). If the preview that follows fails, times out or is cancelled, the inspector once more reports a cached model as missing and offers to download it.

There were two such returns, not one. Gitar named the post-download check at download.py:266, which handles a concurrent writer winning the race. Fixing only that left the new test red, which exposed the pre-flight check at :177 — the one that actually runs when a weight is already cached, before any request is made. Both now report completion.

test_an_already_cached_weight_still_reports_completion downloads twice and asserts the second, fully cached call reports completion as the first did. It found the second path; reading the reported line alone would not have.

Verification after the fix: ruff, mypy on all three platforms, guardrails, 2059 passed, 4 skipped.

@sonarqubecloud

Copy link
Copy Markdown

@gitar-bot

gitar-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

Gitar's finding is correct and fixed in a46fd9c — and it is the same defect this PR exists to fix, on the cached path.

ModelChanged sets model_available to False (core/parameters.py). Selecting a model that is already downloaded then takes the early return, so nothing dispatches ModelAvailabilityChanged(True). If the preview that follows fails, times out or is cancelled, the inspector once more reports a cached model as missing and offers to download it.

There were two such returns, not one. Gitar named the post-download check at download.py:266, which handles a concurrent writer winning the race. Fixing only that left the new test red, which exposed the pre-flight check at :177 — the one that actually runs when a weight is already cached, before any request is made. Both now report completion.

test_an_already_cached_weight_still_reports_completion downloads twice and asserts the second, fully cached call reports completion as the first did. It found the second path; reading the reported line alone would not have.

Verification after the fix: ruff, mypy on all three platforms, guardrails, 2059 passed, 4 skipped.

Confirmed against a46fd9c. Both early returns in download.py (the pre-flight cache-hit at what's now line 177, and the post-download race-winner check at line 271) now call progress(artifact.size_bytes, artifact.size_bytes) before returning target, and ModelChanged does set model_available = False unconditionally in core/parameters.py, so the cached-selection path you describe was a real gap that's now closed on both returns. Thanks for tracking down the second one.

@gitar-bot

gitar-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Fixes model availability tracking so the inspector, button, and status row no longer show stale Not cached / Download & Preview states after a model finishes downloading. The fix records availability independently when the download completes and is verified, rather than relying only on job state transitions, and adds test coverage to keep the three UI surfaces in sync. Cache-hit early return skip of terminal completion progress has been addressed.

✅ 1 resolved
✅ Edge Case: Cache-hit early return skips terminal completion progress

📄 src/matteloop/jobs/models/download.py:266-270 📄 src/matteloop/jobs/models/download.py:295
When the fully streamed and checksum-verified .part is discarded because target_name already exists and verifies (download.py:266-270), the function returns without reaching the new progress(artifact.size_bytes, artifact.size_bytes) at line 295. On that path no (total, total) event is emitted, so the controller never dispatches ModelAvailabilityChanged(True) and the progress bar never reaches 100% — the exact stale-flag failure mode this PR fixes would persist if this branch is ever hit with model_available still False. It is a concurrency/idempotency corner (another flight already promoted the weight), so impact is limited, but for consistency emit the terminal completion before the early return too (e.g. progress(artifact.size_bytes, artifact.size_bytes) prior to return target).

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 7 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

@smb
smb merged commit 8a7bb52 into main Sep 12, 2026
10 checks passed
@smb
smb deleted the fix/issue-167-model-availability branch September 12, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model stays "not cached" after downloading during a preview

1 participant