Skip to content

fix(dictation): redact API keys from streaming transcriber errors - #852

Merged
kevincodex1 merged 30 commits into
Gitlawb:mainfrom
euxaristia:fix/streaming-dictation-key-redaction
Aug 15, 2026
Merged

fix(dictation): redact API keys from streaming transcriber errors#852
kevincodex1 merged 30 commits into
Gitlawb:mainfrom
euxaristia:fix/streaming-dictation-key-redaction

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Defence-in-depth redaction for server-echoed streaming dictation errors (Deepgram close reasons, OpenAI Realtime error events) so an API key that appears in those messages does not reach the UI. The websocket dial path itself does not surface the raw key in library error text.

Also fixes a real main-line bug: a stale completion from a cancelled dictation session could stop the mic on a newer session. Dictation sessions now carry a non-zero session ID; startup, partials, and finals ignore mismatches.

Changes

  • Deepgram / OpenAI Realtime stream errors go through providerio.Redact with the explicit key; %s flatten; ctx.Err() preserves context.Canceled
  • Session ID on batch startup (dictationStartedMsg), partials, and finals; stale started/partial/final messages are ignored
  • Cancel-race and live-session partial/started regression tests

Test plan

  • go test ./internal/dictation/ ./internal/tui/ -count=1 -run "Dictation|Stale|Deepgram|OpenAIRealtime|Voice"
  • CGO_ENABLED=1 go test -race ./internal/dictation -count=1 -run "TestOpenAIRealtimeStreamTranscribe.*(Cancel|Race|Redaction|WriteCancel|DialCancel|StartupCancel)$" (pass)

Refs: session-ID gate closes stale-completion mic kill on main.

Summary by CodeRabbit

  • Bug Fixes

    • Improved dictation cancellation handling so canceled sessions no longer trigger unintended transcript processing or auto-submit actions.
    • Prevented outdated transcription results from appearing after a new dictation session starts.
    • Improved error messages by redacting sensitive API keys.
    • Preserved clear cancellation errors during connection, startup, streaming, and writing interruptions.
    • Ensured transcription remains responsive when cancellation occurs while waiting for audio or input.
  • Tests

    • Added coverage for cancellation races, stale results, and sensitive information redaction across supported transcription providers.

euxaristia and others added 12 commits July 22, 2026 02:54
Streaming dictation (Deepgram, OpenAI Realtime) injected API keys into
websocket URLs/headers and echoed unredacted transport errors and
server-side error payloads. Wrap those error endpoints with
providerio.Redact and switch the connect-error format specifier from %w
to %s so the raw error cannot leak via errors.Unwrap().

Adds regression tests asserting the API key is not present in the
returned stream error for both transcribers.

Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com>
…edaction tests

- Redact API key in OpenAI Realtime session configuration error.
- Return redacted OpenAI Realtime write error from writeErrCh instead of discarding.
- Move TestOpenAIRealtimeStreamTranscribeErrorRedaction and
  TestDeepgramStreamTranscribeErrorRedaction next to their source files.

Refs Gitlawb#710
…close

Address CodeRabbit nitpick: the server now drains the client's audio frames and
waits for CloseStream before rejecting the connection with an API-key-bearing
policy-violation close reason. This ensures StreamTranscribe observes the close
reason (and redacts it) instead of failing earlier on a write error.

Refs Gitlawb#710
…action

Formatting the read error with %s dropped the unwrap chain, so an Esc
abort of a Deepgram or OpenAI Realtime dictation no longer matched
errors.Is(msg.err, context.Canceled) in the TUI and produced a spurious
error toast on top of the cancellation notice. Return the cancellation
sentinel itself before redacting — it carries no key — and keep the
redacted flat string for genuine failures. Adds regression tests that
cancel a live stream and assert the sentinel survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Key cancellation detection on ctx.Err() instead of
errors.Is(err, context.Canceled), which was racing against
writer-goroutine transport errors that could overwrite the error
value right before the check. Applied consistently across both
transcriber implementations. Fix goroutine leaks and a possible hang
in the cancellation regression tests.
Add regression tests for Esc-abort during WebSocket dial and right after
accept (session update / first write) so those redaction sites keep
returning context.Canceled for the TUI errors.Is check.
Esc can cancel after conn.Read already has an error frame. Check ctx.Err()
after partial callbacks and on the realtimeError path so the stream still
returns context.Canceled instead of a redacted failure notice.

Refs Gitlawb#710
handleDictationTranscribed suppressed context.Canceled but then fell
through to the msg.submit branch. A delta -> Esc -> already-buffered
realtime error race returns a nonempty transcript alongside
context.Canceled, so with stt.autoSubmit on, Esc could submit the
composer's restored pre-existing text instead of doing nothing.

Treat a canceled result as terminal before the auto-submit path is
reached, and add a TUI-level test covering the race.
…fter Esc and synchronize write cancellation test
…on 0 messages

Ensure the first dictation session in a process gets stale-message protection by initializing sessionID to 1 in newDictationController and removing msg.sessionID != 0 exemptions in handleDictationTranscribed and handleDictationPartial. Clean up unreachable redaction guards in providerio.

Refs Gitlawb#710
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 295134a1-8e7e-4a8d-b968-dde402c69da8

📥 Commits

Reviewing files that changed from the base of the PR and between 3fa9f8e and 2f14ac2.

📒 Files selected for processing (1)
  • internal/dictation/transcriber_openai_realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/dictation/transcriber_openai_realtime_test.go

Walkthrough

Deepgram and OpenAI Realtime transcription now preserve context.Canceled and redact API keys. TUI dictation sessions use session IDs to reject stale results and prevent canceled transcripts from triggering processing.

Changes

Dictation reliability

Layer / File(s) Summary
Provider error handling
internal/dictation/transcriber_deepgram.go, internal/dictation/transcriber_openai_realtime.go
Provider errors preserve context cancellation. Other errors redact API keys. Writers observe cancellation while waiting for input.
Provider cancellation and redaction tests
internal/dictation/transcriber_deepgram_test.go, internal/dictation/transcriber_openai_realtime_test.go
WebSocket tests cover API-key redaction and cancellation during dialing, startup, streaming, writing, and error races.
TUI session filtering
internal/tui/dictation.go, internal/tui/dictation_stream.go, internal/tui/dictation_test.go
Dictation messages carry session IDs. Resets advance the ID. Canceled and stale results are ignored before transcript processing or auto-submit.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DictationController
  participant StreamingTranscriber
  participant Context
  participant Composer
  DictationController->>StreamingTranscriber: start stream with sessionID
  StreamingTranscriber->>DictationController: return partial or completed result with sessionID
  DictationController->>Context: check cancellation
  DictationController->>DictationController: validate sessionID
  DictationController->>Composer: apply active transcript
Loading

Suggested reviewers: vasanthdev2004

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the API-key redaction changes in the streaming transcribers, which are a primary objective of the pull request.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

euxaristia and others added 12 commits July 31, 2026 15:38
Streaming dictation (Deepgram, OpenAI Realtime) injected API keys into
websocket URLs/headers and echoed unredacted transport errors and
server-side error payloads. Wrap those error endpoints with
providerio.Redact and switch the connect-error format specifier from %w
to %s so the raw error cannot leak via errors.Unwrap().

Adds regression tests asserting the API key is not present in the
returned stream error for both transcribers.

Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com>
…edaction tests

- Redact API key in OpenAI Realtime session configuration error.
- Return redacted OpenAI Realtime write error from writeErrCh instead of discarding.
- Move TestOpenAIRealtimeStreamTranscribeErrorRedaction and
  TestDeepgramStreamTranscribeErrorRedaction next to their source files.

Refs Gitlawb#710
…close

Address CodeRabbit nitpick: the server now drains the client's audio frames and
waits for CloseStream before rejecting the connection with an API-key-bearing
policy-violation close reason. This ensures StreamTranscribe observes the close
reason (and redacts it) instead of failing earlier on a write error.

Refs Gitlawb#710
…action

Formatting the read error with %s dropped the unwrap chain, so an Esc
abort of a Deepgram or OpenAI Realtime dictation no longer matched
errors.Is(msg.err, context.Canceled) in the TUI and produced a spurious
error toast on top of the cancellation notice. Return the cancellation
sentinel itself before redacting — it carries no key — and keep the
redacted flat string for genuine failures. Adds regression tests that
cancel a live stream and assert the sentinel survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Key cancellation detection on ctx.Err() instead of
errors.Is(err, context.Canceled), which was racing against
writer-goroutine transport errors that could overwrite the error
value right before the check. Applied consistently across both
transcriber implementations. Fix goroutine leaks and a possible hang
in the cancellation regression tests.
Add regression tests for Esc-abort during WebSocket dial and right after
accept (session update / first write) so those redaction sites keep
returning context.Canceled for the TUI errors.Is check.
Esc can cancel after conn.Read already has an error frame. Check ctx.Err()
after partial callbacks and on the realtimeError path so the stream still
returns context.Canceled instead of a redacted failure notice.

Refs Gitlawb#710
handleDictationTranscribed suppressed context.Canceled but then fell
through to the msg.submit branch. A delta -> Esc -> already-buffered
realtime error race returns a nonempty transcript alongside
context.Canceled, so with stt.autoSubmit on, Esc could submit the
composer's restored pre-existing text instead of doing nothing.

Treat a canceled result as terminal before the auto-submit path is
reached, and add a TUI-level test covering the race.
…fter Esc and synchronize write cancellation test
…on 0 messages

Ensure the first dictation session in a process gets stale-message protection by initializing sessionID to 1 in newDictationController and removing msg.sessionID != 0 exemptions in handleDictationTranscribed and handleDictationPartial. Clean up unreachable redaction guards in providerio.

Refs Gitlawb#710
@euxaristia
euxaristia force-pushed the fix/streaming-dictation-key-redaction branch from 2814b35 to fee2e2e Compare July 31, 2026 19:38

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the rework — the cancellation half of this is in good shape now. I mutated each of the eight ctx.Err() != nil guards in the two transcribers one at a time (dial, read, session-update write, delta, completed, error-event, writeErrCh, and the Deepgram pair) and every one of them turned a test red, so the %w%s flattening no longer costs you errors.Is(err, context.Canceled). That was the thing that broke last time and it's properly pinned.

The best part of the PR isn't in the title, though: the session-ID gate. I set up a model with sessionID=2, phase=dictRecording and live streamStop/cancel hooks, then fed it dictationTranscribedMsg{sessionID: 1}. With the gate, phase stays dictRecording and neither hook fires. With the gate disabled, phase drops to dictIdle and both streamStop() and cancel() are called — a stale completion from the previous session kills the mic on the session the user just started. That's a genuine bug on main and it's worth saying so in the description.

Some things to fix before this goes in.

The providerio.Redact change doesn't belong in this PR. I reverted internal/providers/providerio/providerio.go to main and reran all twelve new dictation tests — every one still passes, including transcriber_deepgram_test.go:154 TestDeepgramCustomHeaderErrorRedaction, which is named for the new X-Api-Key branch. It passes because the key is passed to Redact as an explicit secret, so the literal strings.ReplaceAll scrubs it and the header-word scavenging never runs. Meanwhile redact_classify_test.go wasn't touched, so Token / X-Api-Key / api-key have no coverage anywhere, and Redact sits on every provider error path in the repo. It also picks up new false positives — Redact("invalid Token gpt-4o-transcribe-2026-01-01") returns invalid Token [REDACTED]. Please drop it here; if you want the extra header names, they're a separate PR with tests in providerio's own package.

Three tests stay green when the thing they're named for is deleted.

  • transcriber_deepgram_test.go:211TestDeepgramSinglePassRedaction asserts the error doesn't contain "[REDACTED:[REDACTED", a string Redact never produces. I replaced line 122 with fmt.Errorf("Deepgram stream error: %s", err.Error()) (redaction gone entirely): TestDeepgramStreamTranscribeErrorRedaction FAIL, TestDeepgramCustomHeaderErrorRedaction FAIL, TestDeepgramSinglePassRedaction PASS. Either assert what you mean (exactly one [REDACTED] for one occurrence of the key) or delete it.

  • internal/tui/dictation_test.go:139TestDictationCanceledStreamRaceDoesNotAutoSubmit passes with dictation.go:402 disabled (if false && errors.Is(msg.err, context.Canceled)). The test builds model{} so sessionID is 0, cancelDictation bumps it to 1, and the message carries 0 — the session-ID gate at dictation.go:382 catches it before the context.Canceled branch is reached. To actually test that branch, give the message the same sessionID the controller currently holds.

  • internal/tui/dictation_stream.go:82 — the partial gate passes with if false && msg.sessionID != .... Both new stale-partial assertions are already satisfied by the pre-existing phase guard, since phase is dictIdle after cancelDictation. The gate is real, it just needs the live-session case: sessionID=2, phase=dictRecording, composer "user typed this", then handleDictationPartial(sttPartialMsg{sessionID: 1, text: "ghost text from session 1"}) — with the gate you get "user typed this", without it "user typed this ghost text from session 1".

transcriber_deepgram.go:122 dropped the //nolint:staticcheck that main carried on that line. make lint-static now reports ST1005: error strings should not be capitalized. It's advisory in CI so nothing goes red, but it was a deliberate suppression for the user-facing "Deepgram stream error:" text — please put it back.

Two smaller things, not blocking:

transcriber_openai_realtime.go:199-203 changes behaviour relative to main. Main silently swallowed a non-nil werr in the bottom writeErrCh select; you now abort the stream with a redacted error. I think that's the right call, but it isn't mentioned in the description and nothing pins it — replacing the final else with _ = werr leaves ./internal/dictation green.

And on the framing: I don't think the connect path was leaking. I stood up a real httptest server that 401s and echoes back the Authorization header it received — the server does see Token sk-probe-… and Bearer sk-probe-…, but coder/websocket v1.8.15 reports only failed to WebSocket dial: expected handshake response status code 101 but got 401, and a refused dial surfaces just the query-string URL, which carries no key. The realistic surface is server-echoed text (the Deepgram close reason, the OpenAI realtime error event), which is worth redacting as defence in depth. Could you reword the summary to say that rather than "could leak the raw API key"? It matters for anyone reading the changelog later.

Happy to re-review as soon as the providerio change is out and the three tests pin what they claim.

Everything in one pass, nothing queued behind it. Worth restating the shape of this one: the code changes here are good — the cancellation fix is properly pinned and the session-ID gate closes a real bug on main. What I'm asking for is mostly on the test side, plus dropping the providerio widening into its own PR where it can be covered properly. That's a much smaller ask than the count suggests.

Revert the Token/X-Api-Key providerio.Redact expansion (belongs in its own PR with coverage). Make Deepgram single-pass, cancel-race, and live-session partial tests pin the branches they name, and restore the ST1005 nolint on the user-facing Deepgram error string.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed review:

  1. Reverted the providerio.Redact Token/X-Api-Key/api-key widen (belongs in a separate PR with its own coverage).
  2. TestDeepgramSinglePassRedaction now asserts the key is gone and exactly one [REDACTED].
  3. TestDictationCanceledStreamRaceDoesNotAutoSubmit uses a matching sessionID so it hits the context.Canceled branch, not only the stale-session gate.
  4. Added TestStaleDictationPartialIgnoredWhileLive for the session gate while phase is still recording.
  5. Restored //nolint:staticcheck on the user-facing Deepgram stream error string.

Framing note: defence-in-depth redaction of server-echoed close/error text (not the websocket dial path itself).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
internal/dictation/transcriber_deepgram_test.go (1)

154-183: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename this test to match what it verifies.

The name TestDeepgramCustomHeaderErrorRedaction implies a custom-header feature. The transcriber sends only Authorization: Token <key>, and this test exercises the same stream-error redaction path as TestDeepgramStreamTranscribeErrorRedaction. The only difference is the shape of the close reason. Rename the test so the intent is clear.

♻️ Proposed rename
-func TestDeepgramCustomHeaderErrorRedaction(t *testing.T) {
+func TestDeepgramStreamErrorRedactsHeaderShapedKey(t *testing.T) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dictation/transcriber_deepgram_test.go` around lines 154 - 183,
Rename TestDeepgramCustomHeaderErrorRedaction to clearly describe stream-error
redaction for a close reason containing an API key, matching the intent of
TestDeepgramStreamTranscribeErrorRedaction. Do not alter the test behavior or
implementation.
internal/dictation/transcriber_openai_realtime_test.go (2)

193-199: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the review provenance from this test comment.

The comment starts with "jatmn (review, PR #710)". A reviewer handle and a pull request number age badly and add no information about the behavior under test. Keep the technical rationale and remove the attribution.

♻️ Proposed edit
-// jatmn (review, PR `#710`): Esc can race an incoming OpenAI error event.
-// conn.Read may already have the error frame in hand by the time the
-// cancellation lands. The server fires a delta immediately followed by
+// Esc can race an incoming OpenAI error event. conn.Read may already have
+// the error frame in hand by the time the cancellation lands. The server
+// fires a delta immediately followed by
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dictation/transcriber_openai_realtime_test.go` around lines 193 -
199, Remove the “jatmn (review, PR `#710`):” attribution from the comment in the
affected test, while preserving the technical explanation about cancellation
racing the incoming OpenAI error event and the expected context.Canceled result.

147-191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test does not reliably exercise the writeErrCh cancel branch.

After the writer sends the single buffered chunk, it blocks on range chunks. The cancel then unblocks conn.Read(ctx) in the main loop first. So the returned error normally comes from the read-error path in transcriber_openai_realtime.go Lines 130-140, not from the writer-error branch at Lines 199-201.

The assertion still passes, because both branches return context.Canceled. Correct the comment so it does not claim coverage the test does not provide. If you want to pin the writer branch, keep audio flowing after the cancel so a writeJSON call fails while the loop is still processing events.

♻️ Proposed comment correction
-// After a server event the client selects writeErrCh. Cancel while the writer
-// is blocked on more chunks so that path observes a cancelled write and must
-// still return context.Canceled rather than a redacted flat string.
+// Cancel while the writer is blocked on more chunks. Whichever path observes
+// the cancellation first (the blocked conn.Read or the writeErrCh drain) must
+// return context.Canceled rather than a redacted flat string.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dictation/transcriber_openai_realtime_test.go` around lines 147 -
191, Correct the comment above
TestOpenAIRealtimeStreamTranscribeWriteCancelKeepsSentinel to describe the
actual read-error cancellation path rather than claiming it exercises
writeErrCh. Do not change the test assertion or behavior; only remove the
inaccurate writer-branch coverage claim and state that cancellation must
preserve context.Canceled.
internal/dictation/transcriber_openai_realtime.go (1)

183-202: 🩺 Stability & Availability | 🔵 Trivial

Run the affected OpenAI Realtime concurrency tests with the race detector.

Go test -race requires CGO_ENABLED=1; include those tests in the PR check run, for example:

CGO_ENABLED=1 go test -race ./internal/dictation \
  -run 'TestOpenAIRealtimeStreamTranscribe.*(Cancel|Race|Redaction|WriteCancel|DialCancel|StartupCancel)$'

Add the result to the PR description per the concurrent-code guideline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dictation/transcriber_openai_realtime.go` around lines 183 - 202,
Run the specified OpenAI Realtime concurrency test subset in internal/dictation
with CGO_ENABLED=1 and the race detector enabled, then add the command and its
result to the pull request description. Do not alter the implementation unless
the race run exposes a failure.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/tui/dictation.go`:
- Line 317: Add sessionID propagation to startBatchRecordingCmd and
dictationStartedMsg, then update handleDictationStarted to ignore messages whose
session ID no longer matches the active dictation session before changing state
or calling reset. Add regression tests covering stale successful and failed
startup messages after cancellation, ensuring the current recording remains
unaffected.

---

Nitpick comments:
In `@internal/dictation/transcriber_deepgram_test.go`:
- Around line 154-183: Rename TestDeepgramCustomHeaderErrorRedaction to clearly
describe stream-error redaction for a close reason containing an API key,
matching the intent of TestDeepgramStreamTranscribeErrorRedaction. Do not alter
the test behavior or implementation.

In `@internal/dictation/transcriber_openai_realtime_test.go`:
- Around line 193-199: Remove the “jatmn (review, PR `#710`):” attribution from
the comment in the affected test, while preserving the technical explanation
about cancellation racing the incoming OpenAI error event and the expected
context.Canceled result.
- Around line 147-191: Correct the comment above
TestOpenAIRealtimeStreamTranscribeWriteCancelKeepsSentinel to describe the
actual read-error cancellation path rather than claiming it exercises
writeErrCh. Do not change the test assertion or behavior; only remove the
inaccurate writer-branch coverage claim and state that cancellation must
preserve context.Canceled.

In `@internal/dictation/transcriber_openai_realtime.go`:
- Around line 183-202: Run the specified OpenAI Realtime concurrency test subset
in internal/dictation with CGO_ENABLED=1 and the race detector enabled, then add
the command and its result to the pull request description. Do not alter the
implementation unless the race run exposes a failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c60c841f-6d6a-47e3-8f2f-2d819886213b

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and c19ceef.

📒 Files selected for processing (7)
  • internal/dictation/transcriber_deepgram.go
  • internal/dictation/transcriber_deepgram_test.go
  • internal/dictation/transcriber_openai_realtime.go
  • internal/dictation/transcriber_openai_realtime_test.go
  • internal/tui/dictation.go
  • internal/tui/dictation_stream.go
  • internal/tui/dictation_test.go

Comment thread internal/tui/dictation.go
Gate dictationStartedMsg on session ID so a late Start result after cancel cannot reset a newer recording. Rename and comment cleanups from review; race suite recorded on the PR body.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit follow-up:

  1. Stale dictationStartedMsg — batch startup carries sessionID; handleDictationStarted ignores mismatches so a late Start success/failure cannot reset a newer recording. Regression: TestStaleDictationStartedIgnoredWhileLive.
  2. Renamed TestDeepgramStreamErrorRedactsHeaderShapedKey (was CustomHeader).
  3. Dropped review provenance from the OpenAI error-race comment; corrected write-cancel coverage comment.
  4. PR body updated (defence-in-depth framing + race run: pass).

@euxaristia

Copy link
Copy Markdown
Contributor Author

Checked this against Vasanthdev2004's last review — the providerio revert, the restored nolint comment, and all three test fixes (redaction count assertion, matching sessionID in the cancel-race test, and the live-session partial gate) are already in the pushed commits, along with the coderabbit nitpicks (test rename, dropped review-provenance comment). Build and tests pass. Ready for re-review.

@euxaristia

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/dictation/transcriber_deepgram_test.go`:
- Around line 13-46: Extend internal/dictation/transcriber_deepgram_test.go at
lines 13-46 with a controllable non-cancellation WebSocket dial failure
containing the configured API key, then assert StreamTranscribe’s returned error
omits it. In internal/dictation/transcriber_openai_realtime_test.go at lines
13-43, add equivalent redaction tests for dial, session-configuration, and
asynchronous writer failures, ensuring each injected failure includes the API
key and each returned error excludes it; anchor the tests to Deepgram’s
StreamTranscribe and the OpenAI realtime transcription entry points.

In `@internal/dictation/transcriber_deepgram.go`:
- Around line 119-123: Update the audio writer loops in
internal/dictation/transcriber_deepgram.go:119-123 and
internal/dictation/transcriber_openai_realtime.go:137-140 to select between
receiving the next chunk and ctx.Done() while waiting, so cancellation exits the
writer even when chunks remains open and idle; preserve the existing chunk
processing and error behavior for non-cancelled sessions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a396d4bd-584c-4de4-9264-0d25aa6578b5

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and 03fa588.

📒 Files selected for processing (7)
  • internal/dictation/transcriber_deepgram.go
  • internal/dictation/transcriber_deepgram_test.go
  • internal/dictation/transcriber_openai_realtime.go
  • internal/dictation/transcriber_openai_realtime_test.go
  • internal/tui/dictation.go
  • internal/tui/dictation_stream.go
  • internal/tui/dictation_test.go

Comment thread internal/dictation/transcriber_deepgram_test.go
Comment thread internal/dictation/transcriber_deepgram.go
…aths

Select on ctx.Done while waiting for audio chunks so cancelled streaming
sessions do not leave writer goroutines blocked on an open idle channel.
Add non-cancellation redaction tests for Deepgram dial failure and OpenAI
Realtime dial, session-configuration, and async writer failures.

Refs Gitlawb#852
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed the two open CodeRabbit majors on f34ad758:

  1. Writer goroutine leak on cancel — Deepgram and OpenAI Realtime audio writers now select on ctx.Done() while waiting for the next chunk, so a cancelled session does not leave a goroutine blocked on an open idle chunks channel.
  2. Direct redaction tests for dial/config/writer failure boundaries
    • TestDeepgramDialFailureRedactsKey — non-cancellation dial failure with key embedded in the Upgrade header
    • TestOpenAIRealtimeDialFailureRedactsKey — same for OpenAI Realtime
    • TestOpenAIRealtimeSessionConfigFailureRedactsKey — session-update write failure with key-bearing injected error
    • TestOpenAIRealtimeWriterFailureRedactsKey — async writer / writeErrCh path with key-bearing injected error

Tests

go test ./internal/dictation/ -count=1
go test ./internal/dictation/ -count=1 -race

Both pass.

@euxaristia

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 13 minutes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/dictation/transcriber_openai_realtime.go`:
- Around line 108-119: Update the audio writer failure paths in the
transcription flow to send the write error to writeErrCh and then close the
WebSocket connection, ensuring conn.Read(ctx) unblocks. Apply this ordering to
both the commit write after channel closure and the append write failure. Add a
regression test covering a silent server after session setup with an injected
audio write failure, verifying the main goroutine observes the error.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f9c408f1-61fc-4fbd-ba7a-b4899b478139

📥 Commits

Reviewing files that changed from the base of the PR and between 03fa588 and f34ad75.

📒 Files selected for processing (4)
  • internal/dictation/transcriber_deepgram.go
  • internal/dictation/transcriber_deepgram_test.go
  • internal/dictation/transcriber_openai_realtime.go
  • internal/dictation/transcriber_openai_realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/dictation/transcriber_deepgram.go

Comment thread internal/dictation/transcriber_openai_realtime.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 7, 2026
@euxaristia

Copy link
Copy Markdown
Contributor Author

Checked head f34ad7589 against Vasanthdev2004's CHANGES_REQUESTED (2026-08-01). Every ask is already addressed in the pushed commits, so no new commit was needed:

  1. providerio.Redact widening: dropped in c19ceefc; internal/providers/providerio/providerio.go matches main (no diff), so Redact("invalid Token gpt-4o-transcribe-2026-01-01") no longer false-positives.
  2. Test pins:
    • TestDeepgramSinglePassRedaction now asserts exactly one [REDACTED] for one key occurrence.
    • TestDictationCanceledStreamRaceDoesNotAutoSubmit sends the message with the controller's current sessionID so the context.Canceled branch is exercised, not just the stale-session gate.
    • TestStaleDictationPartialIgnoredWhileLive covers the live-session partial gate (sessionID=2, dictRecording, composer unchanged by a session-1 partial).
  3. //nolint:staticcheck restored on transcriber_deepgram.go for the user-facing "Deepgram stream error:" text.

Non-blocking: the bottom writeErrCh abort behavior is pinned by TestOpenAIRealtimeWriterFailureRedactsKey (injected writer failure must return a redacted "OpenAI Realtime stream error", not be swallowed). The PR description already frames the redaction as defence-in-depth against server-echoed text.

Verification at head: git diff --check origin/main...HEAD clean, go vet on the touched packages clean, go build ./... passes, and go test ./internal/dictation/... ./internal/tui/... ./internal/providers/providerio/... -count=1 passes (dictation 0.8s, tui 18.2s, providerio 7.2s). Note: TestAltScreenTranscriptScrollKeepsFooterFixed fails only in worktrees whose cwd path is long enough to clip the title bar at width 90; it passes from a normal-length worktree at this same head and is unrelated to this PR.

…nt server cannot strand the read loop

CodeRabbit review 4885680006 flagged that the OpenAI Realtime writer goroutine pushed write errors to writeErrCh and returned without closing the connection, so conn.Read stays parked forever when the peer goes silent after session setup and the error is never drained. Close the socket on both the append and commit write failure paths (CloseNow, since the close handshake itself would block on a silent peer), and add a silent-server regression test.

Refs Gitlawb#852

Co-Authored-By: cairn-code <282421612+cairn-code@users.noreply.github.com>
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit's actionable item from review 4885680006 (the one outstanding finding at head f34ad75): the audio writer goroutine now closes the WebSocket on write failure in both the append and commit paths, so a silent peer cannot leave conn.Read parked forever with the write error undrained in writeErrCh. CloseNow is used rather than a graceful close because the close handshake itself would block against a silent peer. Regression test TestOpenAIRealtimeWriterFailureUnblocksSilentServer covers the silent-server case: it fails without the close (5s timeout) and passes with it. Full internal/dictation suite passes, go vet and gofmt are clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/dictation/transcriber_openai_realtime_test.go`:
- Around line 186-212: Update the test around StreamTranscribe to create a
cancellable context, pass it to StreamTranscribe, and defer its cancellation so
teardown unblocks even if the connection remains stuck in conn.Read. Keep the
existing writer-failure assertions and timeout behavior unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fa960052-30ae-463f-aa62-86940c256b4c

📥 Commits

Reviewing files that changed from the base of the PR and between f34ad75 and 3fa9f8e.

📒 Files selected for processing (2)
  • internal/dictation/transcriber_openai_realtime.go
  • internal/dictation/transcriber_openai_realtime_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/dictation/transcriber_openai_realtime.go

Comment thread internal/dictation/transcriber_openai_realtime_test.go
…teardown

CodeRabbit review 4909044011 identified that the silent-server regression test used context.Background(), allowing its goroutine to remain blocked in conn.Read after a timeout. Give the test a cancellable context and defer cancellation so teardown always releases the blocked read.

Refs Gitlawb#852

Co-Authored-By: cairn-code <282421612+cairn-code@users.noreply.github.com>
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit review 4909044011 in 2f14ac2. TestOpenAIRealtimeWriterFailureUnblocksSilentServer now creates a cancellable context, passes it to StreamTranscribe, and defers cancellation. The existing writer-failure assertions and timeout remain unchanged, while teardown can now release conn.Read if the regression fails. go test ./internal/dictation/... -count=1, go vet ./internal/dictation/..., and go build ./... pass.

@euxaristia

Copy link
Copy Markdown
Contributor Author

The reviewed teardown finding was already addressed at the current head 2f14ac2c: internal/dictation/transcriber_openai_realtime_test.go creates a cancellable ctx, passes it to StreamTranscribe, and defers cancel() while preserving the existing writer-failure assertions and timeout behavior. No additional code change or commit was necessary. Verified with gofmt -d, go test ./internal/dictation, and go vet ./internal/dictation.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 2f14ac2c. Checks green.

First, sorry for the wait. You answered on 2026-08-07 and my changes-requested sat on top of it until now, which is on me and not on you.

Re-verified rather than taking the summary at its word, since a stale verdict is exactly the thing I would not want held against this PR:

  • The providerio.Redact widening is genuinely gone. providerio does not appear in this PR's changed files at all, so the false positive I was worried about, Redact("invalid Token gpt-4o-transcribe-2026-01-01") eating a model name, cannot happen from here.
  • TestDeepgramSinglePassRedaction asserts strings.Count(got, "[REDACTED]") != 1, which is the strong form. A substring check would have passed against a double wrap; a count cannot.

The rest of my list reads as addressed too, and the writer-failure work that came out of the CodeRabbit round is better than what I asked for: TestOpenAIRealtimeWriterFailureUnblocksSilentServer fails without the close and passes with it, which is the property worth pinning rather than just exercising the path. Using CloseNow because the graceful handshake would itself block against a silent peer is the right call and worth the comment it has.

Nothing further from me.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kevincodex1
kevincodex1 merged commit 64a783b into Gitlawb:main Aug 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants