From 062838ac8ef857d9a6ed04a21c479c4268faa1f6 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Mon, 6 Jul 2026 07:59:44 +0100 Subject: [PATCH] fix(dictation): ignore stale transcript events after stop/send When the user manually sends (Enter/click) while recording, the submit flow already calls stopRecording(). However, queued data channel messages could still fire handleRealtimeEvent after the peer connection teardown began, writing stale transcript text back into the now-empty composer. Fix: pass the run ID captured at startRecording into the data channel message handler closure. handleRealtimeEvent now checks activeRunIdRef against the captured run ID and drops events from stale runs. This guarantees that once stopRecording() increments the run counter, no further transcript events from that session can mutate composer state. --- .../dictation/hooks/useRealtimeDictation.ts | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/desktop/src/features/dictation/hooks/useRealtimeDictation.ts b/desktop/src/features/dictation/hooks/useRealtimeDictation.ts index a607f1515..15a7d6ed0 100644 --- a/desktop/src/features/dictation/hooks/useRealtimeDictation.ts +++ b/desktop/src/features/dictation/hooks/useRealtimeDictation.ts @@ -104,29 +104,36 @@ export function useRealtimeDictation({ useEffect(() => cleanupResources, [cleanupResources]); - const handleRealtimeEvent = useCallback((event: TranscriptEvent) => { - if (event.type === "error") { - console.error("OpenAI realtime server error", event); - toast.error(event.error?.message ?? "Voice input error"); - return; - } + const handleRealtimeEvent = useCallback( + (runId: number, event: TranscriptEvent) => { + // Ignore events from a stale run (e.g. user sent/stopped while + // transcripts were still in-flight from the data channel). + if (activeRunIdRef.current !== runId) return; - if ( - event.type !== TRANSCRIPT_DELTA_EVENT && - event.type !== TRANSCRIPT_COMPLETED_EVENT && - event.type !== BUFFER_COMMITTED_EVENT - ) { - return; - } + if (event.type === "error") { + console.error("OpenAI realtime server error", event); + toast.error(event.error?.message ?? "Voice input error"); + return; + } - const prevText = getTranscriptText(segmentStateRef.current); - const merged = mergeTranscriptEvent(segmentStateRef.current, event); + if ( + event.type !== TRANSCRIPT_DELTA_EVENT && + event.type !== TRANSCRIPT_COMPLETED_EVENT && + event.type !== BUFFER_COMMITTED_EVENT + ) { + return; + } - if (merged === prevText) return; + const prevText = getTranscriptText(segmentStateRef.current); + const merged = mergeTranscriptEvent(segmentStateRef.current, event); - onTranscriptTextRef.current(merged); - setIsTranscribing(event.type !== TRANSCRIPT_COMPLETED_EVENT); - }, []); + if (merged === prevText) return; + + onTranscriptTextRef.current(merged); + setIsTranscribing(event.type !== TRANSCRIPT_COMPLETED_EVENT); + }, + [], + ); const startRecording = useCallback(async () => { if (!isEnabled || isStarting || isRecording) return; @@ -187,7 +194,7 @@ export function useRealtimeDictation({ dataChannelRef.current = dataChannel; dataChannel.addEventListener("message", (message) => { try { - handleRealtimeEvent(JSON.parse(String(message.data))); + handleRealtimeEvent(runId, JSON.parse(String(message.data))); } catch { // Ignore non-JSON events }