fix(openai): discard preconnect audio on close

This commit is contained in:
Vincent Koc
2026-07-31 16:42:54 +08:00
parent 42f1e8dd41
commit 337a2b3fc7
5 changed files with 68 additions and 2 deletions

View File

@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- **OpenAI realtime preconnect close:** discard queued Talk audio when a bridge closes before its first connection, keep repeated closes idempotent, and require an explicit fresh connect before audio can flow again.
- **Control UI session refreshes:** preserve explicitly queued list filters and background hydration across later Gateway event invalidation, while keeping append pagination followed by a canonical refresh. Fixes #116697. Thanks @shakkernerd.
- **Control UI dynamic deep links:** reuse the initial route loader result when publishing real agent, session, dashboard, Workboard, Memory, and Plugins paths, avoiding redundant route-loader work during startup. Thanks @shakkernerd.
- **Linux gateway service ownership:** refuse user-scope systemd publication and activation when the same gateway unit name is already owned or cannot be verified in the system scope, including `--force`, with actionable recovery guidance instead of creating restart-looping dual managers. Fixes #116129.

View File

@@ -209,6 +209,28 @@ describe("OpenAIQuicksilverVoiceBridge", () => {
harness.bridge.close();
});
it("discards audio closed before the first connection and reconnects fresh", async () => {
const harness = createHarness();
harness.bridge.sendAudio(Buffer.from("queued-before-connect"));
harness.bridge.close();
harness.bridge.close();
harness.bridge.sendAudio(Buffer.from("sent-after-close"));
expect(harness.connections).toHaveLength(0);
expect(harness.onClose).not.toHaveBeenCalled();
await harness.bridge.connect();
expect(
sentEvents(harness.socket).filter((event) => event.type === "input_audio.append"),
).toHaveLength(0);
harness.bridge.close();
expect(harness.onClose).toHaveBeenCalledOnce();
expect(harness.onClose).toHaveBeenCalledWith("completed");
});
it("does not carry queued audio across terminal close and explicit reconnect", async () => {
const sockets: FakeSocket[] = [];
const bridge = new OpenAIQuicksilverVoiceBridge({

View File

@@ -361,10 +361,13 @@ export class OpenAIQuicksilverVoiceBridge implements RealtimeVoiceBridge {
close(): void {
const connection = this.connection;
if (!connection || !this.lifecycle.cancel()) {
if (!this.lifecycle.cancel()) {
return;
}
this.resetTerminalState();
if (!connection) {
return;
}
if (this.socket?.readyState === WEBSOCKET_OPEN) {
this.sendEvent({ type: "session.close" });
}

View File

@@ -1549,6 +1549,43 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
bridge.close();
});
it("discards audio closed before the first connection and reconnects fresh", async () => {
const provider = buildOpenAIRealtimeVoiceProvider();
const onClose = vi.fn();
const bridge = provider.createBridge({
providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
onAudio: vi.fn(),
onClearAudio: vi.fn(),
onClose,
});
bridge.sendAudio(Buffer.from("queued-before-connect"));
bridge.close();
bridge.close();
bridge.sendAudio(Buffer.from("sent-after-close"));
expect(FakeWebSocket.instances).toHaveLength(0);
expect(onClose).not.toHaveBeenCalled();
const connecting = bridge.connect();
const socket = FakeWebSocket.instances[0];
if (!socket) {
throw new Error("expected bridge to connect");
}
socket.readyState = FakeWebSocket.OPEN;
socket.emit("open");
socket.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
await connecting;
expect(
parseSent(socket).filter((event) => event.type === "input_audio_buffer.append"),
).toHaveLength(0);
bridge.close();
expect(onClose).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledWith("completed");
});
it("does not carry queued audio across terminal close and explicit reconnect", async () => {
const provider = buildOpenAIRealtimeVoiceProvider();
const bridge = provider.createBridge({

View File

@@ -734,10 +734,13 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
close(): void {
const connection = this.connection;
if (!connection || !this.lifecycle.cancel()) {
if (!this.lifecycle.cancel()) {
return;
}
this.resetTerminalState();
if (!connection) {
return;
}
const ws = this.ws;
this.ws = null;
ws?.close(1000, "Bridge closed");