Skip to content

Commit dd643c8

Browse files
authored
fix(whatsapp): expose Baileys socket timing (#73580)
Merged via squash. Prepared head SHA: d347552 Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com> Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com> Reviewed-by: @velvet-shark
1 parent 1690678 commit dd643c8

21 files changed

Lines changed: 281 additions & 5 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424
- Memory-core/dreaming: retry managed dreaming cron registration after startup when the cron service is not reachable yet, so the scheduled Memory Dreaming Promotion sweep recovers without waiting for heartbeat traffic. Fixes #72841. Thanks @amknight.
2525
- Acpx/runtime: validate the runtime session mode at the `AcpxRuntime.ensureSession` wrapper boundary so callers that pass anything other than `persistent` or `oneshot` get a clear `ACP_INVALID_RUNTIME_OPTION` error instead of silently round-tripping through the encoded handle as a default `persistent` mode and later throwing `SessionResumeRequiredError`. Investigation context: #73071. (#73548) Thanks @amknight.
2626
- CLI/infer: keep web-search fallback on missing provider API keys, preserve structured validation errors from the selected provider, and let per-request image describe prompts override configured media-entry prompts. (#63263) Thanks @Spolen23.
27+
- WhatsApp/Web: pass explicit Baileys socket timings into every WhatsApp Web socket and expose `web.whatsapp.*` keepalive, connect, and query timeout settings so unstable networks can avoid repeated 408 disconnect and opening-handshake timeout loops. Fixes #56365. (#73580) Thanks @velvet-shark.
2728

2829
## 2026.4.27
2930

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
85842690af24b21a5e074d722930af95faaf6e91a918061bdc1b5c956860a7a0 config-baseline.json
2-
86ad0927d992bc873affb3e20a31c6e3c95b2185a91f46cc8e6262a723a78f7d config-baseline.core.json
1+
39c5c0620611f355f20d5e9d2ddd74e198c344c63d5551a987e4b7538833ceac config-baseline.json
2+
805bd3f63ff7327da45c01b78dbc990ed53bd13b89e0cbf50f319aa99334ba92 config-baseline.core.json
33
323a9fd49a669951ca5b3442d95aad243bd1330083f9857e83a8dcfae2bbc9d0 config-baseline.channel.json
44
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json

docs/channels/whatsapp.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
147147

148148
- Gateway owns the WhatsApp socket and reconnect loop.
149149
- The reconnect watchdog uses WhatsApp Web transport activity, not only inbound app-message volume, so a quiet linked-device session is not restarted solely because nobody has sent a message recently. A longer application-silence cap still forces a reconnect if transport frames keep arriving but no application messages are handled for the watchdog window.
150+
- Baileys socket timings are explicit under `web.whatsapp.*`: `keepAliveIntervalMs` controls WhatsApp Web application pings, `connectTimeoutMs` controls the opening handshake timeout, and `defaultQueryTimeoutMs` controls Baileys query timeouts.
150151
- Outbound sends require an active WhatsApp listener for the target account.
151152
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
152153
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
@@ -520,6 +521,23 @@ Behavior notes:
520521
restarts when WhatsApp Web transport activity stops, the socket closes, or
521522
application-level activity stays silent beyond the longer safety window.
522523

524+
If logs show repeated `status=408 Request Time-out Connection was lost`, tune
525+
Baileys socket timings under `web.whatsapp`. Start by shortening
526+
`keepAliveIntervalMs` below your network's idle timeout and increasing
527+
`connectTimeoutMs` on slow or lossy links:
528+
529+
```json5
530+
{
531+
web: {
532+
whatsapp: {
533+
keepAliveIntervalMs: 15000,
534+
connectTimeoutMs: 60000,
535+
defaultQueryTimeoutMs: 60000,
536+
},
537+
},
538+
}
539+
```
540+
523541
Fix:
524542

525543
```bash
@@ -643,7 +661,7 @@ High-signal WhatsApp fields:
643661
- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`
644662
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`
645663
- multi-account: `accounts.<id>.enabled`, `accounts.<id>.authDir`, account-level overrides
646-
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`
664+
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*`
647665
- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit`
648666
- prompts: `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt`
649667

docs/gateway/config-channels.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
9696

9797
```json5
9898
{
99+
web: {
100+
whatsapp: {
101+
keepAliveIntervalMs: 25000,
102+
connectTimeoutMs: 60000,
103+
defaultQueryTimeoutMs: 60000,
104+
},
105+
},
99106
channels: {
100107
whatsapp: {
101108
dmPolicy: "pairing", // pairing | allowlist | open | disabled

extensions/whatsapp/src/auto-reply/monitor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
sleepWithAbort,
2929
} from "../reconnect.js";
3030
import { formatError, getWebAuthAgeMs, logoutWeb, readWebSelfId } from "../session.js";
31+
import { resolveWhatsAppSocketTiming } from "../socket-timing.js";
3132
import { getRuntimeConfig, getRuntimeConfigSourceSnapshot } from "./config.runtime.js";
3233
import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
3334
import { buildMentionConfig } from "./mentions.js";
@@ -181,6 +182,7 @@ export async function monitorWebChannel(
181182
const maxMediaBytes = resolveWhatsAppMediaMaxBytes(account);
182183
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
183184
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
185+
const socketTiming = resolveWhatsAppSocketTiming(cfg, tuning.socketTiming);
184186
const baseMentionConfig = buildMentionConfig(cfg);
185187
const groupHistoryLimit =
186188
account.historyLimit ??
@@ -229,6 +231,7 @@ export async function monitorWebChannel(
229231
messageTimeoutMs,
230232
watchdogCheckMs,
231233
reconnectPolicy,
234+
socketTiming,
232235
abortSignal,
233236
sleep,
234237
isNonRetryableStatus: isNonRetryableWebCloseStatus,

extensions/whatsapp/src/auto-reply/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { WebInboundMessage } from "../inbound/types.js";
22
import type { ReconnectPolicy } from "../reconnect.js";
3+
import type { WhatsAppSocketTimingOptions } from "../socket-timing.js";
34

45
export type WebChannelHealthState =
56
| "starting"
@@ -32,6 +33,7 @@ export type WebChannelStatus = {
3233

3334
export type WebMonitorTuning = {
3435
reconnect?: Partial<ReconnectPolicy>;
36+
socketTiming?: WhatsAppSocketTimingOptions;
3537
heartbeatSeconds?: number;
3638
messageTimeoutMs?: number;
3739
watchdogCheckMs?: number;

extensions/whatsapp/src/connection-controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
logoutWeb,
1515
waitForWaConnection,
1616
} from "./session.js";
17+
import type { WhatsAppSocketTimingOptions } from "./socket-timing.js";
1718

1819
const LOGGED_OUT_STATUS = DisconnectReason?.loggedOut ?? 401;
1920
const WHATSAPP_LOGIN_RESTART_MESSAGE =
@@ -171,6 +172,7 @@ export async function waitForWhatsAppLoginResult(params: {
171172
runtime: RuntimeEnv;
172173
waitForConnection?: typeof waitForWaConnection;
173174
createSocket?: typeof createWaSocket;
175+
socketTiming?: WhatsAppSocketTimingOptions;
174176
onQr?: (qr: string) => void;
175177
onSocketReplaced?: (sock: WaSocket) => void;
176178
}): Promise<WhatsAppLoginWaitResult> {
@@ -196,6 +198,7 @@ export async function waitForWhatsAppLoginResult(params: {
196198
try {
197199
currentSock = await createSocket(false, params.verbose, {
198200
authDir: params.authDir,
201+
...params.socketTiming,
199202
onQr: params.onQr,
200203
});
201204
params.onSocketReplaced?.(currentSock);
@@ -249,6 +252,7 @@ export class WhatsAppConnectionController {
249252
private readonly abortSignal?: AbortSignal;
250253
private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
251254
private readonly isNonRetryableStatus: (statusCode: unknown) => boolean;
255+
private readonly socketTiming: WhatsAppSocketTimingOptions;
252256
private readonly abortPromise?: Promise<"aborted">;
253257
private readonly disconnectRetryController = new AbortController();
254258

@@ -267,6 +271,7 @@ export class WhatsAppConnectionController {
267271
abortSignal?: AbortSignal;
268272
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
269273
isNonRetryableStatus?: (statusCode: unknown) => boolean;
274+
socketTiming?: WhatsAppSocketTimingOptions;
270275
}) {
271276
this.accountId = params.accountId;
272277
this.authDir = params.authDir;
@@ -280,6 +285,7 @@ export class WhatsAppConnectionController {
280285
this.abortSignal = params.abortSignal;
281286
this.sleep = params.sleep ?? ((ms: number, signal?: AbortSignal) => sleepWithAbort(ms, signal));
282287
this.isNonRetryableStatus = params.isNonRetryableStatus ?? (() => false);
288+
this.socketTiming = params.socketTiming ?? {};
283289
this.socketRef = { current: null };
284290
this.abortPromise =
285291
params.abortSignal &&
@@ -378,6 +384,7 @@ export class WhatsAppConnectionController {
378384
try {
379385
sock = await createWaSocket(false, this.verbose, {
380386
authDir: this.authDir,
387+
...this.socketTiming,
381388
});
382389
await waitForWaConnection(sock);
383390

extensions/whatsapp/src/inbound/monitor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { cacheInboundMessageMeta } from "../quoted-message.js";
1717
import { DEFAULT_RECONNECT_POLICY, computeBackoff, sleepWithAbort } from "../reconnect.js";
1818
import type { OpenClawConfig } from "../runtime-api.js";
1919
import { createWaSocket, formatError, getStatusCode, waitForWaConnection } from "../session.js";
20+
import { resolveWhatsAppSocketTiming } from "../socket-timing.js";
2021
import { resolveJidToE164 } from "../text-runtime.js";
2122
import { checkInboundAccessControl } from "./access-control.js";
2223
import {
@@ -774,6 +775,7 @@ export async function attachWebInboxToSocket(
774775
export async function monitorWebInbox(options: MonitorWebInboxOptions) {
775776
const sock = await createWaSocket(false, options.verbose, {
776777
authDir: options.authDir,
778+
...resolveWhatsAppSocketTiming(options.cfg),
777779
});
778780
await waitForWaConnection(sock);
779781
return attachWebInboxToSocket({

extensions/whatsapp/src/login-qr.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
readWebSelfId,
1717
WHATSAPP_AUTH_UNSTABLE_CODE,
1818
} from "./session.js";
19+
import { resolveWhatsAppSocketTiming, type WhatsAppSocketTimingOptions } from "./socket-timing.js";
1920

2021
type WaSocket = Awaited<ReturnType<typeof createWaSocket>>;
2122
export type StartWebLoginWithQrResult = {
@@ -45,6 +46,7 @@ type ActiveLogin = {
4546
qrRenderPromise: Promise<string> | null;
4647
verbose: boolean;
4748
runtime: RuntimeEnv;
49+
socketTiming: WhatsAppSocketTimingOptions;
4850
};
4951

5052
type LoginQrRaceResult =
@@ -178,6 +180,7 @@ function attachLoginWaiter(accountId: string, login: ActiveLogin) {
178180
isLegacyAuthDir: login.isLegacyAuthDir,
179181
verbose: login.verbose,
180182
runtime: login.runtime,
183+
socketTiming: login.socketTiming,
181184
onQr: (qr) => {
182185
const current = activeLogins.get(accountId);
183186
if (!current || current.id !== login.id) {
@@ -282,6 +285,7 @@ export async function startWebLoginWithQr(
282285
const runtime = opts.runtime ?? defaultRuntime;
283286
const cfg = getRuntimeConfig();
284287
const account = resolveWhatsAppAccount({ cfg, accountId: opts.accountId });
288+
const socketTiming = resolveWhatsAppSocketTiming(cfg);
285289
const authState = await readWebAuthExistsForDecision(account.authDir);
286290
if (authState.outcome === "unstable") {
287291
return {
@@ -327,6 +331,7 @@ export async function startWebLoginWithQr(
327331
try {
328332
sock = await createWaSocket(false, Boolean(opts.verbose), {
329333
authDir: account.authDir,
334+
...socketTiming,
330335
onQr: (qr: string) => {
331336
pendingQr = qr;
332337
const current = activeLogins.get(account.accountId);
@@ -370,6 +375,7 @@ export async function startWebLoginWithQr(
370375
qrRenderPromise: null,
371376
verbose: Boolean(opts.verbose),
372377
runtime,
378+
socketTiming,
373379
};
374380
resetQrUpdateSignal(login);
375381
activeLogins.set(account.accountId, login);

extensions/whatsapp/src/login.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { resolveWhatsAppAccount } from "./accounts.js";
77
import { restoreCredsFromBackupIfNeeded } from "./auth-store.js";
88
import { closeWaSocketSoon, waitForWhatsAppLoginResult } from "./connection-controller.js";
99
import { createWaSocket, waitForWaConnection } from "./session.js";
10+
import { resolveWhatsAppSocketTiming } from "./socket-timing.js";
1011

1112
export async function loginWeb(
1213
verbose: boolean,
@@ -16,9 +17,11 @@ export async function loginWeb(
1617
) {
1718
const cfg = getRuntimeConfig();
1819
const account = resolveWhatsAppAccount({ cfg, accountId });
20+
const socketTiming = resolveWhatsAppSocketTiming(cfg);
1921
const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir);
2022
let sock = await createWaSocket(true, verbose, {
2123
authDir: account.authDir,
24+
...socketTiming,
2225
});
2326
logInfo("Waiting for WhatsApp connection...", runtime);
2427
try {
@@ -29,6 +32,7 @@ export async function loginWeb(
2932
verbose,
3033
runtime,
3134
waitForConnection,
35+
socketTiming,
3236
onSocketReplaced: (replacementSock) => {
3337
sock = replacementSock;
3438
},

0 commit comments

Comments
 (0)