Repository navigation
fix(dictation): every dictation ends with a reason code, and phases have deadlines (#243) - #1340
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e has a deadline (#243) 56 of 148 recorded sessions ended with no outcome row, 25 of them telling the user "No speech detected" after the microphone took seconds to open, and every provider error read "Deepgram transcription failed". Outcomes now carry one shared code, the user reads the code's curated sentence, and connect (10 s), first-audio (2 s) and final (30 s) have deadlines taken from the recorded maxima. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 27, 2026
…nd terminal delivery (#1340 review) An unknown getUserMedia error's own text reached the user; a microphone opening after unmount started an ownerless recorder; a connect timeout during stop produced two endings; shutdown dropped its OUTCOME; the recorded 408 was downgraded to no-speech; terminal paste results were never awaited; a hung chunk push stranded stopping. Each is fixed with a fail-first case on the recorded sessions (the 408 added to the fixture). Refs #243 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fixed 250 ms delay raced the hook's 180 ms accidental-tap timer under load, ending the recording as too-short before main was ever called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unt, attribute late paste failures (#1340 round 2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en it holds the transcript (#1340 q67) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t cancel and the too-short branch (#1340 review b) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…his append's id (#1340 steering q71) Main mints the History row id before its un-awaited append and returns it as historyId. After a failed terminal paste the renderer looks for that id: found is 'saved', read-but-missing is 'absent', and a failed read is 'unknown' with hedged copy. Matching text let an older identical dictation claim a failed append as saved, and a failed read was reported as a failed save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Disposition: steering q71 (fixed in
|
…tation counts as newer (#1340 verification a/c) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Disposition: verification pass (a and c at
|
…ication 2 a/c survivor) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Disposition, verification 2: a, b and c are all MERGE-READY at
|
Owner
Author
|
OWNER-APPROVED: the dictation deadlines and reason wording are accepted as written. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #243. It delivers reason codes and deadlines for connect, first audio, the drain before stop, final, and terminal insertion. It stays Refs: the issue asks that every session end with a coded outcome, and application quit (for a renderer-side session) and #1299's never-released recording still do not.
Problem (from the owner's 148 recorded
dictation-debugjournals)getUserMediatook most of that (up to 3,836 ms), so the recorder ran 2–611 ms and captured nothing.stream-startawaits an unbounded keychain read.Change
One shared code union,
DictationOutcomeReason(src/shared/types/dictation.ts), grouped by phase:no-speech.*,cancelled.*,mic.*,recorder.*,config.*,provider.*,network,connect.timeout,final.timeout,delivery.*,unknown.classifyProviderFailure(status)maps 400/401/403/408/429/5xx/none.Exactly one OUTCOME row per session, with
code. Main writes it when the provider decided; the renderer writes it for everything it decides (taps, hidden panes, microphone errors, late mic, deadlines, unmount). Main's stream-cancel row is now an IPC row, so a cancel is not a second outcome. Delivery that did not happen after a success is aTRANSCRIPT delivery:skippedrow with a code.The user reads the code's sentence, never provider, IPC or browser text (q22/q39).
dictationReasonMessageis a fixed, bounded table. Main's error answers gainreason;messagestays for diagnostics only. Microphone errors keepdictationAudioInputError's curated sentences, which name the chosen device. Its label is capped at 80 characters, and an unrecognized error now gets a fixed sentence instead of the error's own text.Single terminal owner. A deadline or recorder error that fails a recording while
stop()is awaiting owns the ending, andstop()stands down. Unmounting while the microphone is still opening writescancelled.unmount; the late stream is closed and no recorder is built. A transcription aborted by quit journalscancelled.shutdown.mic.opened-latereplaces the false "No speech detected" when nothing reached main and the microphone took 500 ms or more of the press: "Nothing was recorded — the microphone took 3.3 s to open. Hold the key until the indicator shows listening." (wording UNCONFIRMED).Deadlines (
DICTATION_DEADLINES_MS), set against the recorded maxima:connect.timeout: ends the recording nowrecorder.no-audio: a silent mic still encodes silence, so only a capture producing nothing trips itfinal.timeout: ends the recording and cancels main's half (a hung push stranded "stopping")final.timeout: aborts through the controller quit already usesdelivery.failedInsertion. The composer sink is a synchronous draft write (recorded 3 / 11 / 21 ms) that cannot hang. A terminal sink is an async session write that can answer false or reject.
TRANSCRIPT committedis now written only once it answers true; false, rejection, or no answer within 5 s is adelivery.failedrow and a sentence. (The first head wrongly said all insertion was synchronous.)What the sentence says about History depends on what is known (q67, q71). Main's History write is not awaited and can fail, so main mints the row id first and returns it as
historyId. The renderer then reads History (serialised behind the append) and looks for that id, not for matching text:An older dictation with the same words can no longer make a failed append look saved.
A failure that lands while a newer dictation is starting or recording names "the previous transcript" and leaves the newer attempt's overlay alone.
Stream ownership. The stream id main returns is kept from the moment it answers, and every exit (stop, failure, unmount, short press) releases main's session through one helper. A drain that hangs before the id is published still cancels main's session. Unmounting during device enumeration asks for no microphone.
The recorded 408 is a timeout, not no-speech. It is excluded from the short-clip downgrade, so it is now
provider.timeout.Plan:
docs/plans/2026-09-26-dictation-reason-codes.md.Tests (31 new: 11 red on main; 8 red on the round-1 head; 5 red on the round-2 head; 5 red on the q67 head; 4 mutation-pinned)
A fixture of six real recorded sessions' lifecycle rows: five shapes plus the one recorded HTTP 408,
testing/fixtures/dictation/lifecycle-sessions-2026-09.json. It has no audio samples, chunks or dictated text; device ids are zeroed, and one personal name and the Deepgram request id are replaced at the same length.Main (
dictationReasons.test.ts):provider.bad-audio/provider.timeout/network, with matching OUTCOME codes and sentences;cancelled.shutdownrow;config.missing-api-key;final.timeout;Renderer (
useComposerDictation.renderer.test.tsx), driven with recorded timings:cancelled.short-pressand no message;mic.opened-lateand "took 3.3 s", never "No speech detected";OverconstrainedErrorgivesmic.unavailable;recorder.no-audio;connect.timeout;cancelled.unmount;unknown;final.timeout;delivery.failed, nevercommitted;committed;The recorded-timing tests (the 49 ms tap and the 3,280 ms mic open) fake only
Date, so they are exact under load.src/main/ipc/,src/main/dictation*,tile-tree/,features/voice-dictation: 401/401 in one batch atdeafa064; the dictation suites are 94/94 at154bcd68.npx tsc -bis clean.Out of scope / residuals
a543abd7, plus the test-only6620b459). Residuals:clearTimeout(finalDeadline), the no-speech branch's hardcoded code, orcleanup()'s timer clears changes nothing observable today;mic.opened-late, is a separate change.mic.opened-latewording are UNCONFIRMED defaults, tuned on one machine's recordings. The terminal-insertion bound (5 s) is a judgment: terminal delivery was never journaled before this. Real microphone and Deepgram behaviour cannot be run here.mic.opened-late; 11 (mic open 125–203 ms) are still "No speech detected", which is right for them.cancelled.shutdownonly if a transcription was in flight.🤖 Generated with Claude Code