Skip to content

[bug] #490 added three paths that discard captured audio with no report — the idle recycle eats the clip and the live words, and a backgrounded tab discards every clip #510

Description

@serge-ivo

#490 added three paths that throw audio away with no report — the invariant #377/#420/#425 established

The owner: "sometimes it wasn't capturing or transcribing my voice even." These are the paths that can do that and leave no trace, so neither he nor the error log can tell it happened.

1. The idle recycle discards the clip AND the live words, silently

packages/sdk/src/voice/use-voice.ts:468-475

} else if (decision === "idle") {
	vadStateRef.current = initVad();
	idleRecycleRef.current = true;
	clearVoiceText();
	sttRef.current?.stopDiscard();
}

No gate consultation, no reportClientError, no bubble left behind. Compare the "end" branch thirteen lines above (use-voice.ts:455-458), which asks endOfTurnAction(gateSnapshot()) before discarding. The two paths disagree about whether a proven-alive Web Speech gate may vouch for the turn — and the idle path is the one that throws away both the audio and the words the user is watching appear on screen.

vadStep returns "idle" after 15 s when onset never fires (vad.ts:196-198; the caller passes no idleMs, so the 15 000 default applies — use-voice.ts:446).

User-visible signature: the pill pulses "Listening", live words appear, and 15 s later everything vanishes and the mic silently reopens. Saying it again usually works, because the VAD state is fresh. That is verbatim the #377 failure shape ("watched their own words appear and then vanish"), live again on this branch.

It also cannot produce a watchdog row: clearVoiceText() runs before stopDiscard(), so no turn is left in transcribing and the 50 s watchdog (use-voice.ts:70, :1803) never arms. There are zero (watchdog) rows in the last 83 error rows — consistent with this path, and by construction it could never appear there.

2. _peakLevel === 0 now discards — and a backgrounded tab guarantees it

packages/sdk/src/voice/stt.ts:364-372

if (this._peakLevel === 0) {
	// No energy data at all. Discard rather than uploading an unknown clip.
	this.onEnd();
	return;
}

The comment #490 deleted stated the old contract: "Skipped when nothing ever called noteLevel(), so a caller with no analyser keeps working rather than going silently deaf." #490 inverted exactly that.

Two problems:

Inferred, not observed: no live row proves a hidden-tab discard occurred — and by construction there cannot be one. That is the point.

Why this belongs with #291

#377, #420 and #425 are all closed, and together they established the invariant that every drop path reports itself. #490 broke it in three places. #291 ("Stop swallowing runtime errors across voice, coder, and worker paths") is open and is exactly this subject — but both paths above postdate its last pass (50bea16, "the last 29 swallowed failures"), so they are uncounted by it.

Acceptance criteria

  1. The idle-recycle path consults the gate the same way the "end" path does, or states in code why it must not.
  2. Every path that discards captured audio reports itself to the durable log — including the idle recycle and the zero-peak discard.
  3. The zero-peak branch either implements the gate check its comment describes, or the comment is corrected to match the code.
  4. A backgrounded/minimised tab does not silently discard every clip. Either noteLevel gets a source that runs while hidden, or the zero-peak case is treated as "no analyser data" (the pre-[bug] STT transcribes silence in hands-free mode — Whisper hallucinations posted as phantom user turns ("Pottery Barn", "Thank you for watching") — gate fails on iOS Safari and when SpeechRecognition stalls #490 contract) rather than "no speech".
  5. Voice error rows carry enough context to diagnose from the log alone: at minimum peakLevel, noiseFloor, onsetFloor and whether the gate heard speech. Today context is only {code} / {transcript,path} / {sttWhisper}, which is why this took a code read rather than a query.
  6. A test for the idle path asserting it reports, and one for a zero-peak clip with a live gate.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingvoiceVoice / STT / TTS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions