fix: end WhatsApp sockets during teardown

This commit is contained in:
Peter Steinberger
2026-05-02 02:28:56 +01:00
parent f8e16be711
commit 6922500382
6 changed files with 46 additions and 6 deletions

View File

@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- WhatsApp: close long-lived web sockets through Baileys `end(error)` before falling back to raw websocket close, so listener teardown runs Baileys cleanup instead of leaving zombie sockets. Fixes #52442. Thanks @essendigitalgroup-cyber.
- Gateway/sessions: move hot transcript reads and mirror appends onto async bounded IO with serialized parent-linked writes, keeping large session histories from stalling Gateway requests and channel replies. Fixes #75656. Thanks @DerFlash.
- macOS/Voice Wake: accept trigger-only phrases in the built-in Voice Wake test, matching the settings UI and runtime trigger-only path instead of requiring extra command text after the wake word. Fixes #64986. Thanks @zoiks65.
- Cron/TTS: run cron announce payloads through the normal TTS directive transform before outbound delivery, so scheduled `[[tts]]` replies generate voice payloads instead of leaking raw tags. Fixes #52125. Thanks @kenchen3000.

View File

@@ -1,7 +1,7 @@
import { EventEmitter } from "node:events";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js";
import { WhatsAppConnectionController } from "./connection-controller.js";
import { closeWaSocket, WhatsAppConnectionController } from "./connection-controller.js";
import type { WhatsAppSendKind, WhatsAppSendResult } from "./inbound/send-result.js";
import { createWaSocket, waitForWaConnection } from "./session.js";
@@ -40,6 +40,7 @@ function createSocketWithTransportEmitter() {
const ws = new EventEmitter() as EventEmitter & { close: ReturnType<typeof vi.fn> };
ws.close = vi.fn();
return {
end: vi.fn(),
ws,
};
}
@@ -74,6 +75,7 @@ describe("WhatsAppConnectionController", () => {
it("closes the socket when open fails before listener creation", async () => {
const sock = {
end: vi.fn(),
ws: {
close: vi.fn(),
},
@@ -91,11 +93,21 @@ describe("WhatsAppConnectionController", () => {
).rejects.toThrow("handshake failed");
expect(createListener).not.toHaveBeenCalled();
expect(sock.ws.close).toHaveBeenCalledOnce();
expect(sock.end).toHaveBeenCalledOnce();
expect(sock.end).toHaveBeenCalledWith(expect.any(Error));
expect(sock.ws.close).not.toHaveBeenCalled();
expect(controller.socketRef.current).toBeNull();
expect(controller.getActiveListener()).toBeNull();
});
it("falls back to raw websocket close when Baileys end is unavailable", () => {
const sock = { ws: { close: vi.fn() } };
closeWaSocket(sock);
expect(sock.ws.close).toHaveBeenCalledOnce();
});
it("lets createWaSocket own the auth barrier before opening a socket", async () => {
const callOrder: string[] = [];
createWaSocketMock.mockImplementationOnce(async () => {

View File

@@ -131,8 +131,20 @@ function createLiveConnection(params: {
};
}
export function closeWaSocket(sock: { ws?: { close?: () => void } } | null | undefined): void {
export function closeWaSocket(
sock:
| {
end?: (error: Error | undefined) => void;
ws?: { close?: () => void };
}
| null
| undefined,
): void {
try {
if (typeof sock?.end === "function") {
sock.end(new Error("OpenClaw WhatsApp socket close"));
return;
}
sock?.ws?.close?.();
} catch {
// ignore best-effort shutdown failures
@@ -140,7 +152,13 @@ export function closeWaSocket(sock: { ws?: { close?: () => void } } | null | und
}
export function closeWaSocketSoon(
sock: { ws?: { close?: () => void } } | null | undefined,
sock:
| {
end?: (error: Error | undefined) => void;
ws?: { close?: () => void };
}
| null
| undefined,
delayMs = 500,
): void {
setTimeout(() => {

View File

@@ -7,6 +7,7 @@ type OffCapableEmitter = {
};
type ClosableSocket = {
end?: (error: Error | undefined) => void;
ws?: {
close?: () => void;
};
@@ -30,5 +31,9 @@ export function attachEmitterListener(
}
export function closeInboundMonitorSocket(sock: ClosableSocket): void {
if (typeof sock.end === "function") {
sock.end(new Error("OpenClaw WhatsApp listener close"));
return;
}
sock.ws?.close?.();
}

View File

@@ -124,7 +124,7 @@ describe("web monitor inbox", () => {
await listener.close();
});
it("detaches inbound listeners and closes the socket on close()", async () => {
it("detaches inbound listeners and ends the socket on close()", async () => {
const listener = await openMonitor(vi.fn());
const sock = getSock();
@@ -135,7 +135,9 @@ describe("web monitor inbox", () => {
expect(sock.ev.listenerCount("messages.upsert")).toBe(0);
expect(sock.ev.listenerCount("connection.update")).toBe(0);
expect(sock.ws.close).toHaveBeenCalledTimes(1);
expect(sock.end).toHaveBeenCalledTimes(1);
expect(sock.end).toHaveBeenCalledWith(expect.any(Error));
expect(sock.ws.close).not.toHaveBeenCalled();
});
it("logs inbound bodies through the inbound child logger", async () => {

View File

@@ -34,6 +34,7 @@ export const upsertPairingRequestMock = pairingUpsertPairingRequestMock;
export type MockSock = {
ev: EventEmitter;
end: AnyMockFn;
ws: { close: AnyMockFn };
sendPresenceUpdate: AnyMockFn;
sendMessage: AnyMockFn;
@@ -107,6 +108,7 @@ function createMockSock(): MockSock {
const ev = new EventEmitter();
return {
ev,
end: vi.fn(),
ws: { close: vi.fn() },
sendPresenceUpdate: createResolvedMock(),
sendMessage: createResolvedMock(),