[macOS] Hiding the floating composer or closing global dictation can silently discard an active recording

Open 💬 0 comments Opened Jul 15, 2026 by pharmatorium

Environment

  • Codex desktop app: 26.707.72221 (build 5307)
  • Bundled Codex CLI: 0.144.2
  • macOS: 26.5.2 (25F84), Apple Silicon (arm64)
  • Locale: de-DE
  • Subscription: not provided; the failure occurs locally before model execution

Summary

Hiding or closing the composer while voice dictation is active can silently discard the in-progress recording. No transcript is inserted, no recovery option is presented in the UI, and the user receives no warning that the action will destroy the recording.

This is a data-loss bug. The user had already spoken the content and reasonably expected the app either to finish transcription, preserve a draft, or block the hide/close action.

Reproduction A: floating composer

  1. Open a Codex task with the browser/floating composer visible.
  2. Start dictation and begin speaking.
  3. While dictation is active, invoke the composer hide action (German UI: Composer ausblenden).
  4. Reopen the composer.

Reproduction B: global dictation window

  1. Start global desktop dictation.
  2. Begin speaking.
  3. Close the global dictation window while the recording is active.

Actual behavior

  • The dictation disappears.
  • No transcript is inserted into the composer or restored draft.
  • No recoverable recording is exposed.
  • There is no confirmation, warning, or explicit discard action.

Expected behavior

Closing or hiding UI must never implicitly delete an active recording. The app should do one of the following:

  1. stop and finalize the recording, finish transcription, and persist the transcript in the task draft;
  2. keep recording/transcription alive outside the unmounted overlay;
  3. prevent hiding until capture is durably saved; or
  4. show an explicit warning and require the user to choose whether to discard.

Hypothesis from local bundle inspection

The installed app bundle contains two lifecycle paths that could cause this class of loss:

  1. The browser action thread.browser.options.hideFloatingComposer unmounts the floating composer. The unmount cleanup attempts stop/finalize with an insert action, and transcript insertion has a persisted-draft fallback. However, finalization remains asynchronous across the unmount boundary. A concrete loss path exists while microphone startup is pending: cleanup marks the hook inactive before a recorder exists; when microphone startup resolves, the continuation stops the tracks and returns without retaining audio. The exact failing line for the user-observed case after recording had already begun is not isolated from the bundle alone.
  2. The global dictation close event clears the active session and closes the recorder window without first taking the normal stop/finalize path. The normal stop event calls the recorder stop routine; the close event does not. Because audio chunks exist only in the recorder renderer until stop/finalization creates a blob, closing that renderer destroys the only copy.

No dictation-specific error or crash was found in the observed failure window. This does not independently identify the failure cause, but it provides no evidence of an ASR crash.

Suggested fix

  • Move recorder ownership and pending audio out of any composer/window component that can unmount.
  • Make composer stop/finalize expose a dictation-scoped completion promise. Defer floating-composer unmount until that promise resolves to a persisted transcript or a recoverable error.
  • For global dictation, make stop/finalize expose a session-scoped completion promise. On close, set a pending-close state, stop exactly once, and defer recorder-window destruction and active-session clearing until completion has been accepted.
  • As defense in depth, preserve the recording temporarily in encrypted local storage before ASR so a transcription failure cannot destroy the user's input, with an explicit short retention period and discard policy.

Regression tests

  • Recorder already active for more than 250 ms + hide floating composer -> recording is finalized once and transcript appears in the persisted task draft.
  • Active global dictation + close window -> stop/finalize runs exactly once, completion is accepted before session clearing, and no duplicate transcription occurs.
  • Microphone startup pending + hide/close -> no silent discard; the action is deferred, cancelled explicitly, or safely finalized.
  • Transcription error after hide/close -> raw audio remains recoverable and retryable.

Related reports

  • #29951 — macOS desktop audio recording stops when switching threads
  • #28765 — pending dictation lost after ASR failure and window/thread switch
  • #21408 — long dictation can fail without recoverable audio
  • #21445 — permission prompt interrupts active dictation and discards audio

No private dictation content, session transcript, or full local logs are included in this report.

View original on GitHub ↗