diff --git a/tests/04a-realtime-oneshot-cs/Program.cs b/tests/04a-realtime-oneshot-cs/Program.cs index eca0d35..1f5e426 100644 --- a/tests/04a-realtime-oneshot-cs/Program.cs +++ b/tests/04a-realtime-oneshot-cs/Program.cs @@ -323,10 +323,12 @@ static Stream OpenOutputStream(int device, Channel downstream, SharedSt for (int i = fill; i < (int)frameCount; i++) dst[i] = 0; } - // Completion = working chunk drained AND channel empty AND writer completed. + // Completion = working chunk drained AND channel empty AND no more deltas expected. + // _noMoreDeltas is the spec's "redundant fast path" — set immediately on response.done, + // avoids touching downstream.Reader.Completion (a Task allocation per callback). if (state.CurrentChunk == null && downstream.Reader.Count == 0 - && downstream.Reader.Completion.IsCompleted) + && state.NoMoreDeltas) { doneFlag.Set(); return StreamCallbackResult.Complete; @@ -343,7 +345,13 @@ static async Task UpstreamSendLoop(ClientWebSocket ws, Channel upstream { await foreach (var frame in upstream.Reader.ReadAllAsync(cts.Token)) { - if (state.StopSending) continue; + if (state.StopSending) + { + // After server VAD said the user is done, drop in-flight mic frames and exit + // once the channel is drained. Matches the spec's "_stopSending && empty" exit. + if (upstream.Reader.Count == 0) break; + continue; + } ReadOnlySpan bytes = MemoryMarshal.AsBytes(frame.AsSpan()); string base64 = Convert.ToBase64String(bytes); var msg = new { type = "input_audio_buffer.append", audio = base64 };