Skip to content

fix(voice): make model downloads atomic and route stream write errors - #1574

Merged
vastsa merged 4 commits into
vastsa:mainfrom
SeanLiu326:fix/voice-download-atomic-write
Oct 12, 2026
Merged

vastsa merged 4 commits into
vastsa:mainfrom
SeanLiu326:fix/voice-download-atomic-write

Conversation

@SeanLiu326

Copy link
Copy Markdown
Contributor

Fixes #1571

Summary

  • Model downloads write to <target>.part and rename into place only after the stream closes, so an interrupted download can no longer leave a truncated file for ModelManager.initializeStates's existsSync to report as downloaded — re-downloading after a mid-download crash/force-quit just works.
  • The WriteStream's error listener is attached before the copy loop starts, and its rejection races the source reads: an ENOSPC/EIO mid-download now surfaces as a normal download failure (state error, retryable from the UI) instead of an unhandled error event in the Electron main process.
  • The stream's close (handle release) is awaited before the rename and before unlinking a failed .part — both fail with EBUSY/EPERM on Windows against an open handle.

Rationale

Described in #1571: the crash path and the unrecoverable truncated-file state share one root cause — the final path was written directly with errors unobserved mid-stream. The temp-file + rename split removes the truncated-state failure mode entirely, and the error routing turns the crash into a retryable failure.

Affected specs / ADRs / E2E scenarios

  • No spec covers the local model download lifecycle (docs/spec/03-runtime/20-speech.md is the cloud provider surface), so none updated.
  • E2E test:e2e:live-voice etc.: NOT RUN — those cover the realtime call path, and download E2E would need real hub traffic. Validation below is a new unit suite over the real downloader with a stubbed hub.

Validation (on latest origin/main eec988c)

  • New apps/desktop/test/voice-model-download.test.mjs (3 tests, over the real model-downloader.ts with @huggingface/hub stubbed through module hooks): completed download lands at the final path with no .part residue; mid-stream source failure rejects and leaves neither file; an unwritable .part path rejects as a download failure instead of an uncaught stream error. 5× repeat runs stable.
  • tsc --noEmit in packages/voice-runtime and apps/desktop: pass.
  • Adjacent suites re-run green: voice-service-idle-unload (7), voice-settings-surface (2).

Compatibility / remaining risk

  • The .part suffix lives inside the model cache directory; nothing else reads that directory, and stale .part files from crashes are inert (never match the final path).
  • download() short-circuits only on the final path now, so pre-existing truncated files from older builds — if any user has one — become re-downloadable, which is the intended repair.
  • No API changes; ModelManager is untouched.

Two defects from vastsa#1571: the WriteStream's error listener attached only
after the copy loop, so an ENOSPC/EIO mid-download was an unhandled
'error' event in the Electron main process — an app crash; and bytes
went straight to the final path, so an interrupted download left a
truncated model that existsSync then reported as downloaded forever,
with re-download a silent no-op.

Write to <target>.part and rename into place after the stream closes;
route stream errors into the copy loop through a racing rejection; and
await the stream's close (handle release) before renaming or unlinking,
both of which fail with EBUSY/EPERM on Windows against an open handle.
An interrupted download now cleans up its .part file and re-downloading
just works.
When disk errors win the stream race, the generator now releases the open response reader before cleaning up the partial file. This prevents a failed download from continuing to consume the response in the background.
@vastsa
vastsa merged commit 647548d into vastsa:main Oct 12, 2026
5 checks passed
@vastsa

vastsa commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Thanks for the fix. The writer-error crash and partial final-file risks were real. I added cancellation of the response reader when a write fails, so the remote stream stops instead of continuing in the background. The updated candidate passed the targeted regression tests, typecheck, PR-base check, and Electron E2E, then merged as 647548d.

@SeanLiu326
SeanLiu326 deleted the fix/voice-download-atomic-write branch October 15, 2026 09:06

This branch was previously deployed

1 inactive deployment
Preview — 51e712d7 Deployed Oct 12, 2026 by vercel[bot]
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.

[Bug] Voice model download: a mid-stream write error crashes the main process, and a truncated model is permanently treated as downloaded

2 participants