mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 09:01:33 +00:00
fix(openai): discard preconnect audio on close
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user