fix(recording): stop native capture after fatal transcription worker failure
Problem
A fatal transcription-worker initialization failure can occur after the native recording session has already been published as live. The worker emits an active transcription-error and exits, but the frontend routes that event directly into post-stop processing without first invoking the native stop_recording command.
The frontend briefly marks the recording stopped while microphone/system capture, the recording manager, and native IS_RECORDING remain active. Backend polling can restore the Stop controls, so manual recovery is possible, but the user may reasonably assume the error already stopped capture.
This behavior is pre-existing and was verified while reviewing PR #767. PR #767 added lifecycle phase classification but preserved the old active-error behavior; it did not introduce or amplify this defect.
Root cause and verified call path
- Record startup stores the manager and sets the native session live before launching the transcription worker:
frontend/src-tauri/src/audio/recording_commands.rs:379-399. - If
get_or_init_transcription_enginefails, the worker emitstranscription-errorwithphase: "active"and returns:frontend/src-tauri/src/audio/transcription/worker.rs:59-69. RecordingControlshandles active errors withonRecordingStop(false):frontend/src/components/RecordingControls.tsx:277-296.- The page maps that callback directly to
useRecordingStop.handleRecordingStop:frontend/src/app/page.tsx:46-50,236-238. handleRecordingStopassumes native stop already happened and only performs frontend post-stop/transcript processing:frontend/src/hooks/useRecordingStop.ts:121-146.- The only normal owner of the native stop invocation is
RecordingControls.stopRecordingAction:frontend/src/components/RecordingControls.tsx:142-162. The error listener bypasses it. - Backend state polling runs every 500 ms and can restore
isRecordingfrom the still-live native session:frontend/src/contexts/RecordingStateContext.tsx:85-119.
The readiness preflight makes this path less frequent but does not make it impossible: engine configuration/state is read again by the worker, and get_or_init_transcription_engine remains fallible after the session becomes live.
User impact
- Microphone and system capture can continue after a fatal transcription error is shown.
- The session continues without a functioning transcription worker.
- Frontend post-stop state can race with backend polling and present contradictory recording state.
- The user must notice the restored Stop control or tray state and stop manually.
- Continued capture has privacy, device-use, and unbounded-duration/storage implications.
Severity is P2 because polling/manual Stop provides recovery and the error is visible, but native capture must not remain active after a fatal worker failure.
Reproduction
- Arrange for model readiness validation to pass, then inject or force
get_or_init_transcription_engineto returnErrafter native recording startup. - Start a recording from the main UI or tray.
- Observe the
transcription-errorand frontend post-stop transition. - Query native recording state or inspect active audio devices.
Expected: native shutdown runs exactly once before post-stop processing; capture ends and accepted audio is flushed/finalized.
Actual: the transcription task exits, but native recording remains live until the user stops it manually.
Required behavior
- A fatal transcription-worker initialization failure after the session becomes live must initiate exactly one complete native recording shutdown.
- Native shutdown must finish before frontend post-stop processing assumes recording has stopped.
- Use the normal stop/flush/finalize path so accepted audio is preserved; do not only flip flags, abort tasks, or drop streams.
- Preserve cleanup and error presentation if shutdown itself fails.
- Avoid self-await/deadlock if shutdown is initiated near the worker task whose handle is stored in
TRANSCRIPTION_TASK. - Concurrent fatal-error, UI Stop, and tray Stop requests must remain idempotent and must not finalize or emit stop completion twice.
- Startup-phase errors must continue to avoid native Stop because no live session exists.
- Distinguish fatal worker initialization from recoverable per-chunk transcription errors. Do not make every active error terminate recording unless that is an explicit product decision.
- After shutdown, frontend state, backend state, tray state, and audio-device ownership must agree that recording stopped.
Verification
Add deterministic fault-injection coverage for the observable lifecycle:
- Force worker initialization to fail immediately after native start; assert native
is_recordingbecomes false, streams/manager are released, saver finalization completes, and one stop-completion event is emitted. - Assert frontend post-stop processing starts only after native stop resolves.
- Race fatal initialization failure against UI Stop and tray Stop; assert one shutdown/finalization.
- Emit a recoverable per-chunk error and confirm recording remains live if that class is intended to be non-fatal.
- Confirm startup-phase model/runtime failures still return without entering native stop.
- Confirm normal Record and Stop behavior remains unchanged.
Pre-existing attribution
The behavior exists on the PR base before PR #767:
- Worker initialization error event: commit
1e81fdb3(October 2025). - Frontend
transcription-errorcallback toonRecordingStop(false): commit3dc877f6(September 2025). - Post-stop-only
handleRecordingStop: commitfc8e6826(October 2025).
PR #767 changes the event payload to include phase and restricts the callback to active errors. The active failure still follows the same pre-existing path.
Reviewer verification: https://github.com/Zackriya-Solutions/meetily/pull/767#issuecomment-5573128804
Related but distinct issues
- #775 covers final VAD/closed-transcription-channel degradation after initialization; it explicitly leaves model-initialization behavior unchanged.
- #781 covers recording-saver initialization, draining, and final-save error propagation.
- #529 covers model download/availability before recording starts.
- #698 describes a native Local Whisper access-violation crash, not a handled worker-initialization error.
No existing issue was found that owns the native-stop/post-stop ordering defect described here.
Scope boundaries
- Do not redesign transcription providers or audio mixing.
- Do not fold the saver lifecycle work from #781 into this issue.
- Do not treat all per-chunk errors as fatal without explicit classification.
- Keep the fix centered on ownership and ordering of fatal active-session shutdown.
Source: Zackriya-Solutions/meetily