Skip to content

fix(mobile): a dropped connection no longer discards a dictation - #35567

Open
cjol wants to merge 2 commits into
pingdotgg:mainfrom
cjol:fix/mobile-voice-keeps-recording-on-disconnect
Open

cjol wants to merge 2 commits into
pingdotgg:mainfrom
cjol:fix/mobile-voice-keeps-recording-on-disconnect

Conversation

@cjol

@cjol cjol commented Nov 12, 2026

Copy link
Copy Markdown

Problem

On mobile, a dictation sent to an environment is deleted when the connection drops. The recording stops, no error is shown, and after reconnecting there is nothing to retry, so the whole message has to be spoken again.

To reproduce: choose an environment for voice input in Settings → Voice, start dictating, then turn on airplane mode.

Change

The voice route changes whenever the connection drops or comes back, and VoiceInputProvider cancelled the dictation on every route change. Cancelling mid-recording deletes the audio file without any message. A recording that survived anyway still could not be sent: its transcriber was tied to the connection object it was prepared on, and a reconnect replaces that object, so the send failed with "Your transcription choice changed."

  • Only a change of voice destination (the grant) cancels a dictation. A reconnect no longer does.
  • The environment transcriber looks up the current connection each time it sends. A recording made across a reconnect goes to the same chosen environment. With no connection, sending fails with "Connection lost. Your recording is saved; retry once reconnected."
  • Mobile turns on retryFailedRecording, as web already does. A failed send keeps the audio, and the existing retry (the mic button, or Retry in the voice control) sends it again without re-recording. Dismissing the error still deletes it.

On-device transcription uses the live start path, so its recordings are not kept and its behaviour is unchanged. The one exception is Android builds without the live recorder, where a failed on-device transcription is now kept for retry, matching web.

Scope

A bug fix. I hit it as a daily user: a dropped connection mid-dictation lost the whole message. I found no issue or open PR for it, and it needs none because losing a recording without a message is plainly unintended and the reproduction above takes a minute.

The change is in shared mobile code, so it covers iOS and Android. Web loses a dictation the same way: useEnvironmentSpeechInput disposes its controller whenever the connection object changes, and in streaming mode it keeps no local copy of the audio. That code is separate, so it is not changed here.

Verification

Test. Added a case to voiceDestination.test.ts: prepare a recording, drop the connection, send (it fails with the "saved" error), reconnect, send again (it transcribes). On main it fails with code: "cancelled" ("Your transcription choice changed."). With the fix it passes.

Android emulator (API 35, arm64). Dev client built from this branch, an isolated dev server with the Canary 180M local model, and airplane mode to drop the connection. I drove the emulator with adb rather than T3's device panel, because agent device access was off in my session.

Before (main): recording at 0:04, then airplane mode on. The recording bar disappears, the mic stops, and no error is shown. After reconnecting there is nothing to retry. The server never receives a transcribe request.

Before: recording, then the connection drops and the recording disappears with no error

before.mp4

After, stopped while offline: recording continues through the drop (0:08). Tapping finish shows "Connection lost. Your recording is saved; retry once reconnected." After reconnecting, tapping the mic sends the saved recording. The server logs POST /api/voice/transcribe 200.

After: recording continues offline, finishing shows the saved-recording error, and the mic retries it after reconnecting

after.mp4

After, short blip: the connection drops at 0:03 and comes back. Recording continues (0:14), and finishing transcribes straight away.

After, short blip: recording continues through a brief drop and transcribes on finish

blip.mp4

The emulator's microphone records silence (host audio input froze this emulator), so each run ends with "No speech was detected." rather than text. The server log shows the upload in each case.

Not checked: iOS. The code path is the same, but I only ran it on Android.

Claude Opus 5.5 in Claude Code (T3 Code)

🤖 Generated with Claude Code

The voice route changes on every disconnect and reconnect, and the
provider cancelled the dictation on each change, deleting the audio.
Only a change of voice destination cancels now. The environment
transcriber looks up the current connection when it sends, so a
recording made across a reconnect can still be sent, and mobile keeps
a failed recording for retry, as web already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched Author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Nov 12, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — Dictation now survives reconnects and uses the current connection to the selected environment. Enabling failed-recording retention and retry for all mobile sessions changes the default audio lifecycle and requires human review.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

Run configuration

Commits

Reviewing files that changed from the base of the PR and between ba9646b and 1c5effb.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

No actionable comments were generated in the recent review.

Walkthrough

Voice transcription now resolves the current host connection during preparation and upload. If the host disconnects, the saved recording can be retried after reconnection. The provider cancels sessions on grant changes rather than route changes.

Changes: Voice input reconnect recovery

Layer / File(s) Summary
Resolve connections for transcription
apps/mobile/src/features/voice-input/voiceDestination.ts, apps/mobile/src/features/voice-input/voiceDestination.test.ts
The transcriber looks up the current connection during preparation and upload. If no connection is available during upload, it returns a saved-recording error. The test verifies that transcription succeeds after reconnection.
Configure provider session lifecycle
apps/mobile/src/features/voice-input/VoiceInputProvider.tsx
The provider mounts the voice route, cancels the session when the grant changes, and enables retries for failed recordings.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant VoiceInputSession
  participant voiceRouteAtom
  participant environmentTranscriber
  participant HostConnection
  VoiceInputSession->>voiceRouteAtom: Request transcription route
  voiceRouteAtom->>environmentTranscriber: Provide current connection getter
  environmentTranscriber->>HostConnection: Resolve connection for upload
  HostConnection-->>environmentTranscriber: No connection
  environmentTranscriber-->>VoiceInputSession: Return saved-recording transcription error
  HostConnection->>HostConnection: Reconnect
  VoiceInputSession->>environmentTranscriber: Retry saved recording
  environmentTranscriber->>HostConnection: Resolve current connection
  HostConnection-->>environmentTranscriber: Return connection
  environmentTranscriber-->>VoiceInputSession: Return transcription text
Loading

Possibly related PRs

Suggested labels: bugfix

Suggested reviewers: juliusmarminge, macroscopeapp

Merge Risk: Minimal · previous review ba964

This change keeps a dictation alive across a dropped connection and lets a saved recording be retried after reconnecting. No concrete merge-blocking risk was found.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: a dropped connection no longer discards a dictation. It follows the conventional commit form and matches the objectives and the file summaries, which show the grant-on…
Description check Passed The description follows the template. It has Problem, Change, Scope, and Verification sections, and each is filled in with specific detail. The Problem section gives reproduction steps. The Change sec…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: a dropped connection no longer discards a dictation. It follows the conventional commit form and matches the objectives and the file summaries, which show the grant-only cancellation and the retry changes. The title is 64 characters, which is within the usual limit.

Full details: Description check

Explanation

The description follows the template. It has Problem, Change, Scope, and Verification sections, and each is filled in with specific detail. The Problem section gives reproduction steps. The Change section explains the fix and its scope. The Scope section explains why no issue is linked and states that the fix is a bug fix. The Verification section lists a focused test, Android emulator runs with before and after evidence, and what was not checked (iOS). The description does not link an issue, which the template asks for when one exists. The author gives a reason for omitting it. The model and harness are named at the end, as the template requests.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 1001bc7 and ba9646b.

Files selected for processing: 3

  • apps/mobile/src/features/voice-input/VoiceInputProvider.tsx
  • apps/mobile/src/features/voice-input/voiceDestination.test.ts
  • apps/mobile/src/features/voice-input/voiceDestination.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@juliusmarminge juliusmarminge added the takeover:in-progress A takeover lead is working on this PR label Nov 14, 2026
@juliusmarminge

Copy link
Copy Markdown
Member

Evidence

Real Android app, Pixel 9 emulator, API 36, portrait 1080x2424, light theme. Fresh isolated state on each commit; identical synthetic project and environment-local Parakeet transcription model.

  1. Confirm online dictation reaches the transcription endpoint.
  2. Start dictation, enable airplane mode, then finish offline if the recorder survives; reconnect and retry.
  3. Repeat, reconnecting before tapping Finish.
Before After
73fb6cd009 1c5effb2e7
Offline-finish video: recording disappears Offline-finish video: saved-audio error, reconnect, successful retry
Reconnect-before-Finish video: recording lost Reconnect-before-Finish video: timer continues, Finish transcribes
Before: idle composer after silent recording loss After: retained audio transcribed on retry; silent mic produces No speech detected

Base received only the online control's transcription request. Head received successful HTTP 200 transcription responses for its control, offline retry, repeated offline capture, and reconnect-before-Finish flow. The microphone is silent, so No speech was detected is the expected end-to-end result. The composer exposes retry through its microphone button, labelled Start dictation; retry goes straight to Transcribing without starting another recording.

Driver script · Seed script · Runtime transcript and request counts · Base server log, redacted · Head server log, redacted

Videos were visually inspected. Both stills were captured after identical screenshot pairs separated by at least 500 ms.

Not shown:

  • Spoken-word recognition accuracy was not exercised because the emulator microphone is silent; retained-audio retry was verified through the real transcription endpoint and its No speech was detected result.
  • iOS and cloud-transcription destinations were not exercised; this proof covers Android with an environment-local speech model.

@juliusmarminge juliusmarminge added takeover:needs-julius Takeover done; route gate says Julius decides and removed takeover:in-progress A takeover lead is working on this PR labels Nov 14, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). takeover:needs-julius Takeover done; route gate says Julius decides vouch:unvouched Author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants