#782·meetily

fix(recording): stop native capture after fatal transcription worker failure

Author: safvanatzackCreated Sep 8, 2026Updated Sep 8, 2026
Labelsbugbackend

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

  1. 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.
  2. If get_or_init_transcription_engine fails, the worker emits transcription-error with phase: "active" and returns: frontend/src-tauri/src/audio/transcription/worker.rs:59-69.
  3. RecordingControls handles active errors with onRecordingStop(false): frontend/src/components/RecordingControls.tsx:277-296.
  4. The page maps that callback directly to useRecordingStop.handleRecordingStop: frontend/src/app/page.tsx:46-50,236-238.
  5. handleRecordingStop assumes native stop already happened and only performs frontend post-stop/transcript processing: frontend/src/hooks/useRecordingStop.ts:121-146.
  6. The only normal owner of the native stop invocation is RecordingControls.stopRecordingAction: frontend/src/components/RecordingControls.tsx:142-162. The error listener bypasses it.
  7. Backend state polling runs every 500 ms and can restore isRecording from 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

  1. Arrange for model readiness validation to pass, then inject or force get_or_init_transcription_engine to return Err after native recording startup.
  2. Start a recording from the main UI or tray.
  3. Observe the transcription-error and frontend post-stop transition.
  4. 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

  1. A fatal transcription-worker initialization failure after the session becomes live must initiate exactly one complete native recording shutdown.
  2. Native shutdown must finish before frontend post-stop processing assumes recording has stopped.
  3. Use the normal stop/flush/finalize path so accepted audio is preserved; do not only flip flags, abort tasks, or drop streams.
  4. Preserve cleanup and error presentation if shutdown itself fails.
  5. Avoid self-await/deadlock if shutdown is initiated near the worker task whose handle is stored in TRANSCRIPTION_TASK.
  6. Concurrent fatal-error, UI Stop, and tray Stop requests must remain idempotent and must not finalize or emit stop completion twice.
  7. Startup-phase errors must continue to avoid native Stop because no live session exists.
  8. 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.
  9. 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_recording becomes 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-error callback to onRecordingStop(false): commit 3dc877f6 (September 2025).
  • Post-stop-only handleRecordingStop: commit fc8e6826 (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