fix: expose WhatsApp Baileys socket timing

This commit is contained in:
Radek Sienkiewicz
2026-04-28 15:17:18 +02:00
parent 16906780fd
commit ee516f4745
21 changed files with 281 additions and 5 deletions

View File

@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
- 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.
- 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.
- 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.
- 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. Thanks @velvet-shark.
## 2026.4.27

View File

@@ -1,4 +1,4 @@
85842690af24b21a5e074d722930af95faaf6e91a918061bdc1b5c956860a7a0 config-baseline.json
86ad0927d992bc873affb3e20a31c6e3c95b2185a91f46cc8e6262a723a78f7d config-baseline.core.json
39c5c0620611f355f20d5e9d2ddd74e198c344c63d5551a987e4b7538833ceac config-baseline.json
805bd3f63ff7327da45c01b78dbc990ed53bd13b89e0cbf50f319aa99334ba92 config-baseline.core.json
323a9fd49a669951ca5b3442d95aad243bd1330083f9857e83a8dcfae2bbc9d0 config-baseline.channel.json
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json

View File

@@ -147,6 +147,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- Gateway owns the WhatsApp socket and reconnect loop.
- 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.
- 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.
- Outbound sends require an active WhatsApp listener for the target account.
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
@@ -520,6 +521,23 @@ Behavior notes:
restarts when WhatsApp Web transport activity stops, the socket closes, or
application-level activity stays silent beyond the longer safety window.
If logs show repeated `status=408 Request Time-out Connection was lost`, tune
Baileys socket timings under `web.whatsapp`. Start by shortening
`keepAliveIntervalMs` below your network's idle timeout and increasing
`connectTimeoutMs` on slow or lossy links:
```json5
{
web: {
whatsapp: {
keepAliveIntervalMs: 15000,
connectTimeoutMs: 60000,
defaultQueryTimeoutMs: 60000,
},
},
}
```
Fix:
```bash
@@ -643,7 +661,7 @@ High-signal WhatsApp fields:
- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`
- multi-account: `accounts.<id>.enabled`, `accounts.<id>.authDir`, account-level overrides
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*`
- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit`
- prompts: `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt`

View File

@@ -96,6 +96,13 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
```json5
{
web: {
whatsapp: {
keepAliveIntervalMs: 25000,
connectTimeoutMs: 60000,
defaultQueryTimeoutMs: 60000,
},
},
channels: {
whatsapp: {
dmPolicy: "pairing", // pairing | allowlist | open | disabled

View File

@@ -28,6 +28,7 @@ import {
sleepWithAbort,
} from "../reconnect.js";
import { formatError, getWebAuthAgeMs, logoutWeb, readWebSelfId } from "../session.js";
import { resolveWhatsAppSocketTiming } from "../socket-timing.js";
import { getRuntimeConfig, getRuntimeConfigSourceSnapshot } from "./config.runtime.js";
import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
import { buildMentionConfig } from "./mentions.js";
@@ -181,6 +182,7 @@ export async function monitorWebChannel(
const maxMediaBytes = resolveWhatsAppMediaMaxBytes(account);
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
const socketTiming = resolveWhatsAppSocketTiming(cfg, tuning.socketTiming);
const baseMentionConfig = buildMentionConfig(cfg);
const groupHistoryLimit =
account.historyLimit ??
@@ -229,6 +231,7 @@ export async function monitorWebChannel(
messageTimeoutMs,
watchdogCheckMs,
reconnectPolicy,
socketTiming,
abortSignal,
sleep,
isNonRetryableStatus: isNonRetryableWebCloseStatus,

View File

@@ -1,5 +1,6 @@
import type { WebInboundMessage } from "../inbound/types.js";
import type { ReconnectPolicy } from "../reconnect.js";
import type { WhatsAppSocketTimingOptions } from "../socket-timing.js";
export type WebChannelHealthState =
| "starting"
@@ -32,6 +33,7 @@ export type WebChannelStatus = {
export type WebMonitorTuning = {
reconnect?: Partial<ReconnectPolicy>;
socketTiming?: WhatsAppSocketTimingOptions;
heartbeatSeconds?: number;
messageTimeoutMs?: number;
watchdogCheckMs?: number;

View File

@@ -14,6 +14,7 @@ import {
logoutWeb,
waitForWaConnection,
} from "./session.js";
import type { WhatsAppSocketTimingOptions } from "./socket-timing.js";
const LOGGED_OUT_STATUS = DisconnectReason?.loggedOut ?? 401;
const WHATSAPP_LOGIN_RESTART_MESSAGE =
@@ -171,6 +172,7 @@ export async function waitForWhatsAppLoginResult(params: {
runtime: RuntimeEnv;
waitForConnection?: typeof waitForWaConnection;
createSocket?: typeof createWaSocket;
socketTiming?: WhatsAppSocketTimingOptions;
onQr?: (qr: string) => void;
onSocketReplaced?: (sock: WaSocket) => void;
}): Promise<WhatsAppLoginWaitResult> {
@@ -196,6 +198,7 @@ export async function waitForWhatsAppLoginResult(params: {
try {
currentSock = await createSocket(false, params.verbose, {
authDir: params.authDir,
...params.socketTiming,
onQr: params.onQr,
});
params.onSocketReplaced?.(currentSock);
@@ -249,6 +252,7 @@ export class WhatsAppConnectionController {
private readonly abortSignal?: AbortSignal;
private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
private readonly isNonRetryableStatus: (statusCode: unknown) => boolean;
private readonly socketTiming: WhatsAppSocketTimingOptions;
private readonly abortPromise?: Promise<"aborted">;
private readonly disconnectRetryController = new AbortController();
@@ -267,6 +271,7 @@ export class WhatsAppConnectionController {
abortSignal?: AbortSignal;
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
isNonRetryableStatus?: (statusCode: unknown) => boolean;
socketTiming?: WhatsAppSocketTimingOptions;
}) {
this.accountId = params.accountId;
this.authDir = params.authDir;
@@ -280,6 +285,7 @@ export class WhatsAppConnectionController {
this.abortSignal = params.abortSignal;
this.sleep = params.sleep ?? ((ms: number, signal?: AbortSignal) => sleepWithAbort(ms, signal));
this.isNonRetryableStatus = params.isNonRetryableStatus ?? (() => false);
this.socketTiming = params.socketTiming ?? {};
this.socketRef = { current: null };
this.abortPromise =
params.abortSignal &&
@@ -378,6 +384,7 @@ export class WhatsAppConnectionController {
try {
sock = await createWaSocket(false, this.verbose, {
authDir: this.authDir,
...this.socketTiming,
});
await waitForWaConnection(sock);

View File

@@ -17,6 +17,7 @@ import { cacheInboundMessageMeta } from "../quoted-message.js";
import { DEFAULT_RECONNECT_POLICY, computeBackoff, sleepWithAbort } from "../reconnect.js";
import type { OpenClawConfig } from "../runtime-api.js";
import { createWaSocket, formatError, getStatusCode, waitForWaConnection } from "../session.js";
import { resolveWhatsAppSocketTiming } from "../socket-timing.js";
import { resolveJidToE164 } from "../text-runtime.js";
import { checkInboundAccessControl } from "./access-control.js";
import {
@@ -774,6 +775,7 @@ export async function attachWebInboxToSocket(
export async function monitorWebInbox(options: MonitorWebInboxOptions) {
const sock = await createWaSocket(false, options.verbose, {
authDir: options.authDir,
...resolveWhatsAppSocketTiming(options.cfg),
});
await waitForWaConnection(sock);
return attachWebInboxToSocket({

View File

@@ -16,6 +16,7 @@ import {
readWebSelfId,
WHATSAPP_AUTH_UNSTABLE_CODE,
} from "./session.js";
import { resolveWhatsAppSocketTiming, type WhatsAppSocketTimingOptions } from "./socket-timing.js";
type WaSocket = Awaited<ReturnType<typeof createWaSocket>>;
export type StartWebLoginWithQrResult = {
@@ -45,6 +46,7 @@ type ActiveLogin = {
qrRenderPromise: Promise<string> | null;
verbose: boolean;
runtime: RuntimeEnv;
socketTiming: WhatsAppSocketTimingOptions;
};
type LoginQrRaceResult =
@@ -178,6 +180,7 @@ function attachLoginWaiter(accountId: string, login: ActiveLogin) {
isLegacyAuthDir: login.isLegacyAuthDir,
verbose: login.verbose,
runtime: login.runtime,
socketTiming: login.socketTiming,
onQr: (qr) => {
const current = activeLogins.get(accountId);
if (!current || current.id !== login.id) {
@@ -282,6 +285,7 @@ export async function startWebLoginWithQr(
const runtime = opts.runtime ?? defaultRuntime;
const cfg = getRuntimeConfig();
const account = resolveWhatsAppAccount({ cfg, accountId: opts.accountId });
const socketTiming = resolveWhatsAppSocketTiming(cfg);
const authState = await readWebAuthExistsForDecision(account.authDir);
if (authState.outcome === "unstable") {
return {
@@ -327,6 +331,7 @@ export async function startWebLoginWithQr(
try {
sock = await createWaSocket(false, Boolean(opts.verbose), {
authDir: account.authDir,
...socketTiming,
onQr: (qr: string) => {
pendingQr = qr;
const current = activeLogins.get(account.accountId);
@@ -370,6 +375,7 @@ export async function startWebLoginWithQr(
qrRenderPromise: null,
verbose: Boolean(opts.verbose),
runtime,
socketTiming,
};
resetQrUpdateSignal(login);
activeLogins.set(account.accountId, login);

View File

@@ -7,6 +7,7 @@ import { resolveWhatsAppAccount } from "./accounts.js";
import { restoreCredsFromBackupIfNeeded } from "./auth-store.js";
import { closeWaSocketSoon, waitForWhatsAppLoginResult } from "./connection-controller.js";
import { createWaSocket, waitForWaConnection } from "./session.js";
import { resolveWhatsAppSocketTiming } from "./socket-timing.js";
export async function loginWeb(
verbose: boolean,
@@ -16,9 +17,11 @@ export async function loginWeb(
) {
const cfg = getRuntimeConfig();
const account = resolveWhatsAppAccount({ cfg, accountId });
const socketTiming = resolveWhatsAppSocketTiming(cfg);
const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir);
let sock = await createWaSocket(true, verbose, {
authDir: account.authDir,
...socketTiming,
});
logInfo("Waiting for WhatsApp connection...", runtime);
try {
@@ -29,6 +32,7 @@ export async function loginWeb(
verbose,
runtime,
waitForConnection,
socketTiming,
onSocketReplaced: (replacementSock) => {
sock = replacementSock;
},

View File

@@ -15,6 +15,7 @@ let logWebSelfId: typeof import("./session.js").logWebSelfId;
let waitForWaConnection: typeof import("./session.js").waitForWaConnection;
let waitForCredsSaveQueue: typeof import("./session.js").waitForCredsSaveQueue;
let writeCredsJsonAtomically: typeof import("./session.js").writeCredsJsonAtomically;
let DEFAULT_WHATSAPP_SOCKET_TIMING: typeof import("./socket-timing.js").DEFAULT_WHATSAPP_SOCKET_TIMING;
async function flushCredsUpdate() {
await new Promise<void>((resolve) => setImmediate(resolve));
@@ -148,6 +149,7 @@ describe("web session", () => {
waitForCredsSaveQueue,
writeCredsJsonAtomically,
} = await import("./session.js"));
({ DEFAULT_WHATSAPP_SOCKET_TIMING } = await import("./socket-timing.js"));
});
beforeEach(() => {
@@ -171,7 +173,10 @@ describe("web session", () => {
await createWaSocket(true, false, { authDir });
const makeWASocket = baileys.makeWASocket as ReturnType<typeof vi.fn>;
expect(makeWASocket).toHaveBeenCalledWith(
expect.objectContaining({ printQRInTerminal: false }),
expect.objectContaining({
printQRInTerminal: false,
...DEFAULT_WHATSAPP_SOCKET_TIMING,
}),
);
const passed = makeWASocket.mock.calls[0][0];
const passedLogger = (passed as { logger?: { level?: string; trace?: unknown } }).logger;
@@ -187,6 +192,22 @@ describe("web session", () => {
openMock.restore();
});
it("passes explicit Baileys socket timing overrides", async () => {
await createWaSocket(false, false, {
keepAliveIntervalMs: 10_000,
connectTimeoutMs: 90_000,
defaultQueryTimeoutMs: 120_000,
});
expect(baileys.makeWASocket).toHaveBeenCalledWith(
expect.objectContaining({
keepAliveIntervalMs: 10_000,
connectTimeoutMs: 90_000,
defaultQueryTimeoutMs: 120_000,
}),
);
});
it("uses ambient env proxy agent when HTTPS_PROXY is configured", async () => {
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");

View File

@@ -34,6 +34,10 @@ import {
makeWASocket,
useMultiFileAuthState,
} from "./session.runtime.js";
import {
DEFAULT_WHATSAPP_SOCKET_TIMING,
type WhatsAppSocketTimingOptions,
} from "./socket-timing.js";
export { formatError, getStatusCode } from "./session-errors.js";
export {
@@ -126,7 +130,7 @@ async function printTerminalQr(qr: string): Promise<void> {
export async function createWaSocket(
printQr: boolean,
verbose: boolean,
opts: { authDir?: string; onQr?: (qr: string) => void } = {},
opts: { authDir?: string; onQr?: (qr: string) => void } & WhatsAppSocketTimingOptions = {},
): Promise<ReturnType<typeof makeWASocket>> {
const baseLogger = getChildLogger(
{ module: "baileys" },
@@ -151,6 +155,13 @@ export async function createWaSocket(
const { version } = await fetchLatestBaileysVersion();
const agent = await resolveEnvProxyAgent(sessionLogger);
const fetchAgent = await resolveEnvFetchDispatcher(sessionLogger, agent);
const socketTiming = {
keepAliveIntervalMs:
opts.keepAliveIntervalMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs,
connectTimeoutMs: opts.connectTimeoutMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs,
defaultQueryTimeoutMs:
opts.defaultQueryTimeoutMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
};
const sock = makeWASocket({
auth: {
creds: state.creds,
@@ -162,6 +173,7 @@ export async function createWaSocket(
browser: ["openclaw", "cli", VERSION],
syncFullHistory: false,
markOnlineOnConnect: false,
...socketTiming,
agent,
// Baileys types still model `fetchAgent` as a Node agent even though the
// runtime path accepts an undici dispatcher for upload fetches.

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_WHATSAPP_SOCKET_TIMING, resolveWhatsAppSocketTiming } from "./socket-timing.js";
describe("resolveWhatsAppSocketTiming", () => {
it("uses OpenClaw's explicit WhatsApp Web socket defaults", () => {
expect(resolveWhatsAppSocketTiming({})).toEqual(DEFAULT_WHATSAPP_SOCKET_TIMING);
});
it("reads Baileys timing values from web.whatsapp config", () => {
expect(
resolveWhatsAppSocketTiming({
web: {
whatsapp: {
keepAliveIntervalMs: 10_000,
connectTimeoutMs: 90_000,
defaultQueryTimeoutMs: 120_000,
},
},
}),
).toEqual({
keepAliveIntervalMs: 10_000,
connectTimeoutMs: 90_000,
defaultQueryTimeoutMs: 120_000,
});
});
it("lets call-site overrides take precedence over config", () => {
expect(
resolveWhatsAppSocketTiming(
{
web: {
whatsapp: {
keepAliveIntervalMs: 10_000,
connectTimeoutMs: 90_000,
defaultQueryTimeoutMs: 120_000,
},
},
},
{
keepAliveIntervalMs: 20_000,
},
),
).toEqual({
keepAliveIntervalMs: 20_000,
connectTimeoutMs: 90_000,
defaultQueryTimeoutMs: 120_000,
});
});
});

View File

@@ -0,0 +1,38 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
export type WhatsAppSocketTimingOptions = {
keepAliveIntervalMs?: number;
connectTimeoutMs?: number;
defaultQueryTimeoutMs?: number;
};
export const DEFAULT_WHATSAPP_SOCKET_TIMING: Required<WhatsAppSocketTimingOptions> = {
keepAliveIntervalMs: 25_000,
connectTimeoutMs: 60_000,
defaultQueryTimeoutMs: 60_000,
};
function positiveInteger(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
}
export function resolveWhatsAppSocketTiming(
cfg: OpenClawConfig,
overrides?: WhatsAppSocketTimingOptions,
): Required<WhatsAppSocketTimingOptions> {
const configured = cfg.web?.whatsapp;
return {
keepAliveIntervalMs:
positiveInteger(overrides?.keepAliveIntervalMs) ??
positiveInteger(configured?.keepAliveIntervalMs) ??
DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs,
connectTimeoutMs:
positiveInteger(overrides?.connectTimeoutMs) ??
positiveInteger(configured?.connectTimeoutMs) ??
DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs,
defaultQueryTimeoutMs:
positiveInteger(overrides?.defaultQueryTimeoutMs) ??
positiveInteger(configured?.defaultQueryTimeoutMs) ??
DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
};
}

View File

@@ -21678,6 +21678,39 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
description:
"Reconnect backoff policy for web channel reconnect attempts after transport failure. Keep bounded retries and jitter tuned to avoid thundering-herd reconnect behavior.",
},
whatsapp: {
type: "object",
properties: {
keepAliveIntervalMs: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
title: "WhatsApp Web Keepalive Interval (ms)",
description:
"Baileys WhatsApp Web application ping interval in milliseconds. Lower values detect and refresh idle links sooner; keep this comfortably below your network's idle-flow timeout.",
},
connectTimeoutMs: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
title: "WhatsApp Web Connect Timeout (ms)",
description:
"Maximum time in milliseconds Baileys waits for the WhatsApp WebSocket opening handshake. Use a higher value on slow or lossy networks that report opening handshake 408 timeouts.",
},
defaultQueryTimeoutMs: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
title: "WhatsApp Web Query Timeout (ms)",
description:
"Default Baileys query timeout in milliseconds for WhatsApp Web requests. Keep aligned with upstream unless a network-specific investigation shows queries need longer.",
},
},
additionalProperties: false,
title: "WhatsApp Web Socket Timing",
description:
"WhatsApp Web socket timing controls passed directly to Baileys. Tune these when network edges, proxies, or NATs are closing otherwise healthy WhatsApp Web sessions.",
},
},
additionalProperties: false,
title: "Web Channel",
@@ -28020,6 +28053,26 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
help: "Maximum reconnect attempts before giving up for the current failure sequence (0 means no retries). Use finite caps for controlled failure handling in automation-sensitive environments.",
tags: ["performance"],
},
"web.whatsapp": {
label: "WhatsApp Web Socket Timing",
help: "WhatsApp Web socket timing controls passed directly to Baileys. Tune these when network edges, proxies, or NATs are closing otherwise healthy WhatsApp Web sessions.",
tags: ["advanced"],
},
"web.whatsapp.keepAliveIntervalMs": {
label: "WhatsApp Web Keepalive Interval (ms)",
help: "Baileys WhatsApp Web application ping interval in milliseconds. Lower values detect and refresh idle links sooner; keep this comfortably below your network's idle-flow timeout.",
tags: ["performance"],
},
"web.whatsapp.connectTimeoutMs": {
label: "WhatsApp Web Connect Timeout (ms)",
help: "Maximum time in milliseconds Baileys waits for the WhatsApp WebSocket opening handshake. Use a higher value on slow or lossy networks that report opening handshake 408 timeouts.",
tags: ["performance"],
},
"web.whatsapp.defaultQueryTimeoutMs": {
label: "WhatsApp Web Query Timeout (ms)",
help: "Default Baileys query timeout in milliseconds for WhatsApp Web requests. Keep aligned with upstream unless a network-specific investigation shows queries need longer.",
tags: ["performance"],
},
"discovery.wideArea": {
label: "Wide-area Discovery",
help: "Wide-area discovery configuration group for exposing discovery signals beyond local-link scopes. Enable only in deployments that intentionally aggregate gateway presence across sites.",

View File

@@ -308,6 +308,10 @@ const TARGET_KEYS = [
"web.reconnect.factor",
"web.reconnect.jitter",
"web.reconnect.maxAttempts",
"web.whatsapp",
"web.whatsapp.keepAliveIntervalMs",
"web.whatsapp.connectTimeoutMs",
"web.whatsapp.defaultQueryTimeoutMs",
"discovery",
"discovery.wideArea.domain",
"discovery.wideArea.enabled",

View File

@@ -401,6 +401,14 @@ export const FIELD_HELP: Record<string, string> = {
"Randomization factor (0-1) applied to reconnect delays to desynchronize clients after outage events. Keep non-zero jitter in multi-client deployments to reduce synchronized spikes.",
"web.reconnect.maxAttempts":
"Maximum reconnect attempts before giving up for the current failure sequence (0 means no retries). Use finite caps for controlled failure handling in automation-sensitive environments.",
"web.whatsapp":
"WhatsApp Web socket timing controls passed directly to Baileys. Tune these when network edges, proxies, or NATs are closing otherwise healthy WhatsApp Web sessions.",
"web.whatsapp.keepAliveIntervalMs":
"Baileys WhatsApp Web application ping interval in milliseconds. Lower values detect and refresh idle links sooner; keep this comfortably below your network's idle-flow timeout.",
"web.whatsapp.connectTimeoutMs":
"Maximum time in milliseconds Baileys waits for the WhatsApp WebSocket opening handshake. Use a higher value on slow or lossy networks that report opening handshake 408 timeouts.",
"web.whatsapp.defaultQueryTimeoutMs":
"Default Baileys query timeout in milliseconds for WhatsApp Web requests. Keep aligned with upstream unless a network-specific investigation shows queries need longer.",
canvasHost:
"Canvas host settings for serving canvas assets and local live-reload behavior used by canvas-enabled workflows. Keep disabled unless canvas-hosted assets are actively used.",
"canvasHost.enabled":

View File

@@ -802,6 +802,10 @@ export const FIELD_LABELS: Record<string, string> = {
"web.reconnect.factor": "Web Reconnect Backoff Factor",
"web.reconnect.jitter": "Web Reconnect Jitter",
"web.reconnect.maxAttempts": "Web Reconnect Max Attempts",
"web.whatsapp": "WhatsApp Web Socket Timing",
"web.whatsapp.keepAliveIntervalMs": "WhatsApp Web Keepalive Interval (ms)",
"web.whatsapp.connectTimeoutMs": "WhatsApp Web Connect Timeout (ms)",
"web.whatsapp.defaultQueryTimeoutMs": "WhatsApp Web Query Timeout (ms)",
discovery: "Discovery",
"discovery.wideArea": "Wide-area Discovery",
"discovery.wideArea.enabled": "Wide-area Discovery Enabled",

View File

@@ -3,6 +3,7 @@ import { SENSITIVE_URL_HINT_TAG } from "../shared/net/redact-sensitive-url.js";
import { buildConfigSchema, lookupConfigSchema } from "./schema.js";
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
import { OpenClawSchema } from "./zod-schema.js";
describe("config schema", () => {
type SchemaInput = NonNullable<Parameters<typeof buildConfigSchema>[0]>;
@@ -290,6 +291,24 @@ describe("config schema", () => {
expect(parsed?.web?.fetch?.maxResponseBytes).toBe(2_000_000);
});
it("accepts WhatsApp Web Baileys socket timing in the runtime zod schema", () => {
const parsed = OpenClawSchema.parse({
web: {
whatsapp: {
keepAliveIntervalMs: 15_000,
connectTimeoutMs: 60_000,
defaultQueryTimeoutMs: 90_000,
},
},
});
expect(parsed.web?.whatsapp).toEqual({
keepAliveIntervalMs: 15_000,
connectTimeoutMs: 60_000,
defaultQueryTimeoutMs: 90_000,
});
});
it("accepts web fetch ssrfPolicy in the runtime zod schema", () => {
const parsed = ToolsSchema.parse({
web: {

View File

@@ -290,11 +290,21 @@ export type WebReconnectConfig = {
maxAttempts?: number; // 0 = unlimited
};
export type WebWhatsAppConfig = {
/** Baileys application ping interval in milliseconds. Default: 25000. */
keepAliveIntervalMs?: number;
/** WebSocket opening handshake timeout in milliseconds. Default: 60000. */
connectTimeoutMs?: number;
/** Baileys query timeout in milliseconds. Default: 60000. */
defaultQueryTimeoutMs?: number;
};
export type WebConfig = {
/** If false, do not start the WhatsApp web provider. Default: true. */
enabled?: boolean;
heartbeatSeconds?: number;
reconnect?: WebReconnectConfig;
whatsapp?: WebWhatsAppConfig;
};
// Provider docking: allowlists keyed by provider id (and internal "webchat").

View File

@@ -678,6 +678,14 @@ export const OpenClawSchema = z
})
.strict()
.optional(),
whatsapp: z
.object({
keepAliveIntervalMs: z.number().int().positive().optional(),
connectTimeoutMs: z.number().int().positive().optional(),
defaultQueryTimeoutMs: z.number().int().positive().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),