Skip to content

VEC-3 fix: recover model downloads after a failed transfer - #102

Merged
IchenDEV merged 19 commits into
mainfrom
agent/vec-3-model-download-recovery
Sep 22, 2026
Merged

IchenDEV merged 19 commits into
mainfrom
agent/vec-3-model-download-recovery

Conversation

@IchenDEV

@IchenDEV IchenDEV commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fix model downloads that become unrecoverable after a failed, cancelled, or stalled transfer. Retries now use generation-isolated staging, reject late publication from cancelled writers, preserve complete published models, and keep expensive cleanup/candidate preparation off the MainActor.

The accepted product code and tests are fixed at 1cba5718aef0dd61254838fa77d8181cb4af820a. Later commits are documentation-only; no product-code change or full regression rerun is part of the remaining evidence follow-up.

Root cause

  1. Concurrent or retried writers could use shared cache/download paths, allowing an old writer to corrupt or replace a newer generation.
  2. Cancellation changed UI state before dependency I/O necessarily returned, so a late old writer could still attempt publication.
  3. Hub snapshots could publish symlinks back into disposable staging caches.
  4. Candidate preparation, rollback backup creation, and recursive cleanup could block the MainActor.
  5. Abnormal exits could leave managed generation, promotion, backup, and retired-cleanup artifacts behind.

Fix

  • Give Whisper, LLM, and ASR downloads a token-scoped .utter-generations/<token>/download root and isolated Hub cache from operation start.
  • Serialize token validation, publication, Cancel, and Delete arbitration; reject every late write from a retired generation.
  • Materialize Hub symlinks into regular files before deleting staging and atomically publish prepared candidates with rollback restoration.
  • Prepare candidates/backups and delete retired trees in detached work; keep only short same-volume arbitration on the MainActor.
  • Sweep all managed orphan artifacts at startup through the real ModelCatalog initializer path, off the MainActor, and gate fresh download entry points until it completes.
  • Allow a replacement ModelCatalog.downloadWhisper request to start while the cancelled dependency call is still suspended, without sharing its staging root or publication rights.
  • Retain the 120-second no-progress watchdog and distinguish user cancellation from transport failure.

Fixed code identity

  • Code commit: 1cba5718aef0dd61254838fa77d8181cb4af820a
  • Whole tree: d15b2c7b02262bf4823646903730122dba90a56f
  • Sources tree: ca355f4ff185ece0cc99a3c73cb9921510018ccc
  • Tests tree: 7d69e1226b6a109bffc5ae3c3b0a83dace33c9d3
  • Current materials baseline: 263071e0885fc0e9245c35794e850fca2c76dadb (documentation only; Sources/Tests identical to 1cba5718)
  • Follow-up documentation commit produced from the review patch: 249eca3fd79df4bced0ec42c55baba7dcb383e47 (documentation only; Sources/Tests identical to 1cba5718)
  • Toolchain-provenance correction: bf859aa3139c54b0e560ed9123a116407d1c9ee8 (documentation only; Sources/Tests identical to 1cba5718)
  • Raw-timing correction: 47a0c276e4abf80f76c935c282dcbcda13e6329c (documentation only; Sources/Tests identical to 1cba5718)

Accepted macOS verification at 1cba5718

  • Raw-log toolchain: macOS 27.2, Xcode 27.0 (build 27A266a), Apple Swift 6.4 (swiftlang-6.4.0.34.1, clang-2100.3.34.1).
  • swift build: exit 0.
  • Focused P1 counterexamples: 3 executed, 0 failures, including:
    • testModelCatalogInitializerStartupCleanupKeepsMainActorResponsive
    • testStartupCleanupKeepsMainActorResponsive
    • testApplicationCatalogResumeEntryStartsBeforeOldDependencyReturns
  • Full suite: 670 XCTest executed, 14 skipped, 0 failures.
  • Skip taxonomy: 4 cases require OPENTYPE_LIVE_DOWNLOAD_INTEGRATION=1; the other 10 are environment/model gated (ANE model, Apple Speech sample, chat-template probe, foreground bundle identity, Espresso fallback, prompt dump, Qwen native ASR ×2, streaming ASR ×2).
  • Release-style bash scripts/build-app.sh --app-only: exit 0; signed dist/Utter.app with a 3.7 MB default.metallib.
  • Live Whisper interruption → Resume → atomic commit → model load → real transcription: exit 0 (docs/assets/demos/en-sample.m4a, 0.15 seconds).
  • Historical live Hub evidence remains attributed to its recorded source SHA: full 278 MB weights, symlink materialization, tokenizer round trip, MLX container load, and real text generation.
  • Existing mutation summary: synchronous initializer cleanup exited 1; Task { @MainActor in } cleanup exited 1; the restored test exited 0 after each mutation and returned to tree d15b2c7b….

Raw accepted regression log: mac-p34-1cba5718-verification.log.

Audit-grade mutation transcript

The audit-grade mutation transcript was captured on macOS using mac-p102-mutation-harness.sh. It created an independent clean checkout fixed at 1cba5718aef0dd61254838fa77d8181cb4af820a, performed exactly four focused runs, streamed and recorded complete combined stdout/stderr, verified target assertion failure markers, and verified exact tree restoration.

Its raw header records macOS 27.2, Xcode 27.0 (build 27A266a), and Apple Swift 6.4 (swiftlang-6.4.0.34.1, clang-2100.3.34.1). The Xcode 16.2 / Swift 6.0.3 claim in handoff prose was a transcription error, not execution evidence. Earlier live-download and model-loading results retain their original commit and log attribution below.

The four focused runs were:

  1. Mutation A: initializer performs synchronous cleanup — failed at UtilityTests.swift:409-413 (5 expected failures).
  2. Restore A exactly — same focused test exited 0 (passed in 0.033s) and tree equaled d15b2c7b02262bf4823646903730122dba90a56f.
  3. Mutation B: cleanup uses Task { @MainActor in } — failed at UtilityTests.swift:411-412 (2 expected failures).
  4. Restore B exactly — same focused test exited 0 (passed in 0.032s) and tree equaled d15b2c7b02262bf4823646903730122dba90a56f.

Measured results (mac-p102-mutation-harness.log):

  • Log SHA-256: 5b6ea6e1d45fec7a56d60513023ab0c3e305e6f152398b98ad3c68660e484240
  • Mutation A exit / target assertions / elapsed: exit 1 / failed with 5 expected assertion failures at UtilityTests.swift:409-413 / elapsed 268s (includes initial scratch build).
  • Restored A exit / exact tree / elapsed: exit 0 (passed in 0.033s) / matched exact baseline d15b2c7b02262bf4823646903730122dba90a56f (git diff --exit-code 0) / elapsed 14s.
  • Mutation B exit / target assertions / elapsed: exit 1 / failed with 2 expected assertion failures at UtilityTests.swift:411-412 / elapsed 14s.
  • Restored B exit / exact tree / elapsed: exit 0 (passed in 0.032s) / matched exact baseline d15b2c7b02262bf4823646903730122dba90a56f (git diff --exit-code 0) / elapsed 12s.
  • Final full-tree diff against 1cba5718aef0dd61254838fa77d8181cb4af820a: exit 0, 0 harness failures, porcelain status clean.

This evidence recapture does not require another product-code build, full XCTest run, live-download run, or Release build.

Evidence lineage

  • 0167504b920de6b36cb36f1b8e703ce486d69e8f: baseline recovery implementation.
  • 28a847f6018871f70210f119c4d650aed1e13040: live Whisper/Hub interruption and Resume verification.
  • 6bcc5adc69c42f21aa905c05b74b6e041fc0b4c7: detached candidate preparation, rollback, and startup cleanup integration.
  • 522947265e83f07c730b00c2130db5c45727de55: MLX container load and real generation with default.metallib.
  • 3442f83641869f912247bef796e624efd813605a: managed-artifact reclaim, O(1) retirement, detached delete, and token recheck.
  • eb4c08a39b2104cbb1045fe0cd7eb589bd1c8e3f: historical documentation baseline before the final P1 continuation.
  • 1cba5718aef0dd61254838fa77d8181cb4af820a: accepted final code/test tree and macOS verification target.
  • 332e38dafabbe46f3514514a21aba5fe7163ce5d: initial documentation of the measured p3/p4 results.
  • 263071e0885fc0e9245c35794e850fca2c76dadb: documentation-only rerun totals, skip taxonomy, mutation summary, and resource-risk decision point.
  • 249eca3fd79df4bced0ec42c55baba7dcb383e47: documentation-only follow-up recording the audit-grade mutation transcript and measured results.
  • bf859aa3139c54b0e560ed9123a116407d1c9ee8: documentation-only correction of the raw-log toolchain provenance and stale pending wording.
  • 47a0c276e4abf80f76c935c282dcbcda13e6329c: documentation-only correction of the Restored A timing to the raw-log value.

Residual risk and release gates

  • Release-owner decision: if underlying URLSession/I/O never returns, a cancelled transfer retains its background Task and staging root. Repeated permanent hangs can accumulate tasks and staging directories until process restart. Write isolation and immediate retry remain intact, but the release owner must either accept this dependency-layer risk or require a resource ceiling/process isolation.
  • The 120-second stall threshold is a product-visible heuristic and remains subject to reviewer approval.
  • Intent/spec/verification approval, final documentation review, external CI, licensing, and publication approval remain required.
  • Do not merge or publish until the release-owner decision and human review gates are complete.

Fixes VEC-3

IchenDEV and others added 2 commits September 21, 2026 11:09
A download that died mid-file left an .incomplete partial that no later
retry could reconcile, so the model could never be downloaded again.

- ModelDownloadRecovery clears stale *.incomplete markers (whisper repo
  tree plus the shared Hugging Face cache) on retry and on delete, without
  touching completed files
- ModelDownloadTasks now passes a run token, exposes isCurrent to guard
  terminal writes, and makes cancel drop the entry so a retry can start
  even when the transfer ignores cancellation
- DownloadStallWatchdog fails a transfer that stops reporting progress so
  the row offers Resume instead of a frozen bar
- cancelDownload immediately marks the model paused

Tests: ModelDownloadRecoveryTests, retry-gate and watchdog coverage.
Co-authored-by: multica-agent <github@multica.ai>
Independent review found the cancel-then-retry path could run two writers
against the same cache paths, whisper cleanup could delete another variant's
partial, and a stalled transfer could be kept alive by an unchanged callback.

- ModelDownloadTasks keeps the single-writer slot while a cancelled run is
  still exiting, so a retry waits for it instead of racing it on disk
- Whisper partial cleanup only removes files under the requested variant
- DownloadProgressSignal only counts real byte/fraction growth as progress
- tests assert a single active writer with ordered start/end, whisper variant
  scoping, and unchanged-callback stall behaviour

Co-authored-by: multica-agent <github@multica.ai>
@IchenDEV IchenDEV changed the title fix: recover model downloads after a failed transfer VEC-3 fix: recover model downloads after a failed transfer Sep 21, 2026
IchenDEV and others added 11 commits September 21, 2026 15:58
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
…acOS

Co-authored-by: multica-agent <github@multica.ai>
…scope

Co-authored-by: multica-agent <github@multica.ai>
…ation, and startup cleanup

Co-authored-by: multica-agent <github@multica.ai>
…n with default.metallib

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
…he main actor

Applies the p2 managed-cleanup patch on 421cb90: startup reclaim now covers
generation roots, prepared candidates, rollback backups, and retired cleanup
roots; runtime cleanup retires a tree in O(1) and deletes it detached, so a
model-sized copy never blocks the MainActor arbitration point.

- ModelStorage: managed temporary prefixes, retireManagedDirectory,
  cleanupOrphanedGenerationStaging over all managed roots
- Whisper/LLM/ASR publication paths retire candidates/backups detached and
  re-check the token while cleanup is pending
- counterexamples: application Cancel allows Resume before the old writer
  returns, startup cleanup removes every orphan, cleanup stays responsive

Co-authored-by: multica-agent <github@multica.ai>
- verification: managed-artifact restart reclaim with mutation evidence,
  O(1) retire + detached delete responsiveness, application-layer Resume
  before the old writer returns; suite figures at 3442f83 (667 executed,
  14 skipped, 0 failures)
- plan: mark the reclaim, responsiveness, and token re-check items

Co-authored-by: multica-agent <github@multica.ai>
IchenDEV and others added 6 commits September 21, 2026 21:36
Applies p3 (startup+resume P1) and p4 (initializer seam + Sendable) on
eb4c08a, verified on macOS with Xcode 27.

- ModelCatalog.init takes a path-scoped startup-cleanup factory; production
  uses the detached helper, and download entry points gate on the task
- the initializer counterexample drives the real wiring with an
  enter/exit barrier and a detached watchdog, so a synchronous or
  MainActor-child cleanup fails it
- a Whisper seam lets the Resume counterexample enter through the real
  ModelCatalog.downloadWhisper while the old dependency is still suspended
- mark the Whisper progress callback @sendable and capture only progress
  scalars before crossing actors

Co-authored-by: multica-agent <github@multica.ai>
Fills the pending rows with measured values from the macOS Mac mini / Xcode 27
run: focused 3 executed 0 failures, full suite 670 executed 14 skipped 0
failures, Release build exit 0, live Whisper transcription 0.15s, and both
startup-cleanup mutations exiting non-zero with a passing recovery run.

Co-authored-by: multica-agent <github@multica.ai>
… harness

- verification: startup responsiveness and application-entry Resume now read as
  pass at 1cba571 with the measured mutation exit codes, recovery proof and
  tree hashes; remove the stale pending sentences and old-head readings
- plan: tick the final rerun, mutation rerun and skip taxonomy rows; the only
  open item is the release-owner resource-ceiling decision, kept visible
- correct the skip taxonomy everywhere: 4 live-download gates plus 10
  environment/model gates

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
@IchenDEV
IchenDEV merged commit 2075084 into main Sep 22, 2026
3 checks passed
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.

1 participant