Test 4a: wire NoMoreDeltas + add UpstreamSendLoop stop-and-empty exit

Code-review feedback from Task 2:
- Use state.NoMoreDeltas in the output callback completion check instead
  of downstream.Reader.Completion.IsCompleted (spec called for it as the
  redundant fast path; it was dead code before).
- UpstreamSendLoop now break's when StopSending && upstream channel empty,
  matching the spec's stated exit shape.
This commit is contained in:
2026-06-12 19:15:29 +00:00
parent a1a3b5c981
commit a590c96d07
+11 -3
View File
@@ -323,10 +323,12 @@ static Stream OpenOutputStream(int device, Channel<short[]> 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<short[]> 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<byte> bytes = MemoryMarshal.AsBytes(frame.AsSpan());
string base64 = Convert.ToBase64String(bytes);
var msg = new { type = "input_audio_buffer.append", audio = base64 };