fix(voice-call): settle cleared tts queue

This commit is contained in:
Peter Steinberger
2026-04-25 05:38:34 +01:00
parent 52267a6b75
commit b8239be46b
3 changed files with 39 additions and 3 deletions

View File

@@ -106,6 +106,7 @@ Docs: https://docs.openclaw.ai
- Providers/OpenRouter: add an OpenRouter TTS provider using the OpenAI-compatible `/audio/speech` endpoint and `OPENROUTER_API_KEY`. Fixes #71268.
- macOS Talk Mode: retry failed local ElevenLabs stream playback through gateway `talk.speak` before falling back to the system voice, so configured ElevenLabs voices still play when streaming playback fails. Fixes #65662.
- Plugins/Voice Call: reap stale pre-answer calls by default, honor configured TTS timeouts for Twilio media-stream playback, and fail empty telephony audio instead of completing as silence. Fixes #42071; supersedes #60957. Thanks @Ryce and @sliekens.
- Plugins/Voice Call: resolve queued-but-not-yet-playing Twilio TTS entries when barge-in or stream teardown clears the playback queue, so callers awaiting `queueTts()` do not hang. Thanks @kevinWangSheng.
- Plugins/Voice Call: terminate expired restored call sessions with the provider and restart restored max-duration timers with only the remaining duration, preventing stale outbound retry loops after Gateway restarts. Fixes #48739. Thanks @mira-solari.
- Plugins/Voice Call: start provider STT after Telnyx outbound conversation greetings and pass configured Telnyx voice IDs through to the speak action. Fixes #56091. Thanks @Roshan.
- Skills: honor legacy `metadata.clawdbot` requirements and installer hints when `metadata.openclaw` is absent, so older skills no longer appear ready when required binaries are missing. Fixes #71323. Thanks @chen-zhang-cs-code.

View File

@@ -103,7 +103,7 @@ describe("MediaStreamHandler TTS queue", () => {
started.push("active");
await waitForAbort(signal);
});
void handler.queueTts("stream-1", async () => {
const queued = handler.queueTts("stream-1", async () => {
queuedRan = true;
});
@@ -112,10 +112,37 @@ describe("MediaStreamHandler TTS queue", () => {
handler.clearTtsQueue("stream-1");
await active;
await withTimeout(queued);
await flush();
expect(queuedRan).toBe(false);
});
it("resolves pending queued playback during stream teardown", async () => {
const handler = new MediaStreamHandler({
transcriptionProvider: createStubSttProvider(),
providerConfig: {},
});
let queuedRan = false;
const active = handler.queueTts("stream-1", async (signal) => {
await waitForAbort(signal);
});
const queued = handler.queueTts("stream-1", async () => {
queuedRan = true;
});
await flush();
(
handler as unknown as {
clearTtsState(streamSid: string): void;
}
).clearTtsState("stream-1");
await withTimeout(active);
await withTimeout(queued);
expect(queuedRan).toBe(false);
});
});
describe("MediaStreamHandler security hardening", () => {

View File

@@ -561,7 +561,7 @@ export class MediaStreamHandler {
*/
clearTtsQueue(streamSid: string, _reason = "unspecified"): void {
const queue = this.getTtsQueue(streamSid);
queue.length = 0;
this.resolveQueuedTtsEntries(queue);
this.ttsActiveControllers.get(streamSid)?.abort();
this.clearAudio(streamSid);
}
@@ -634,13 +634,21 @@ export class MediaStreamHandler {
private clearTtsState(streamSid: string): void {
const queue = this.ttsQueues.get(streamSid);
if (queue) {
queue.length = 0;
this.resolveQueuedTtsEntries(queue);
}
this.ttsActiveControllers.get(streamSid)?.abort();
this.ttsActiveControllers.delete(streamSid);
this.ttsPlaying.delete(streamSid);
this.ttsQueues.delete(streamSid);
}
private resolveQueuedTtsEntries(queue: TtsQueueEntry[]): void {
const pending = queue.splice(0);
for (const entry of pending) {
entry.controller.abort();
entry.resolve();
}
}
}
/**