mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
fix: expose WhatsApp Baileys socket timing
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
49
extensions/whatsapp/src/socket-timing.test.ts
Normal file
49
extensions/whatsapp/src/socket-timing.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
38
extensions/whatsapp/src/socket-timing.ts
Normal file
38
extensions/whatsapp/src/socket-timing.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user