fix(whatsapp): auto-reconnect after manual login in recovery mode

## Problem
After WhatsApp Web disconnects with status codes 428, 503, or 408, Clawdbot
does not automatically reconnect. When the reconnect loop reaches maxAttempts
(default 12), it exits completely and stops monitoring. A subsequent manual
`clawdbot channels login` successfully re-authenticates, but the internal
listener is not restarted, requiring a gateway restart to restore functionality.

## Root Cause
The `monitorWebChannel` function in `src/web/auto-reply/monitor.ts` breaks
out of its main loop when `maxAttempts` is reached, setting `running=false`.
The CLI `channels login` command uses `loginWeb()` which only handles
authentication - it does not signal the gateway to restart the channel
monitoring loop.

## Solution
Instead of completely exiting when max attempts is reached, the monitor now
enters a 'recovery mode' that:
1. Periodically checks (every 30 seconds) if valid auth exists
2. When auth is detected (e.g., after `clawdbot channels login`), resets
   reconnect attempts and resumes the monitoring loop automatically
3. Properly handles abort signals during recovery for clean shutdown

This makes the system self-healing without requiring gateway restarts after
manual re-authentication.

## Testing
To verify the fix:
1. Start the gateway with WhatsApp connected
2. Simulate disconnects until max attempts is reached
3. Run `clawdbot channels login` to re-authenticate
4. Verify the gateway automatically resumes monitoring within 30 seconds

Fixes: WhatsApp Web auto-reconnect after disconnect
This commit is contained in:
Clawdbot Fix 2026-01-27 06:08:35 +00:00
parent 9daa846457
commit 6ec0a6a9a2

View File

@ -22,7 +22,7 @@ import {
resolveReconnectPolicy,
sleepWithAbort,
} from "../reconnect.js";
import { formatError, getWebAuthAgeMs, readWebSelfId } from "../session.js";
import { formatError, getWebAuthAgeMs, readWebSelfId, webAuthExists } from "../session.js";
import { DEFAULT_WEB_MEDIA_BYTES } from "./constants.js";
import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
import { buildMentionConfig } from "./mentions.js";
@ -385,6 +385,12 @@ export async function monitorWebChannel(
reconnectAttempts += 1;
status.reconnectAttempts = reconnectAttempts;
emitStatus();
// Recovery mode: when max attempts is reached, don't exit completely.
// Instead, wait longer and periodically check if auth has been refreshed
// (e.g., via manual `clawdbot channels login`). This enables self-healing
// without requiring a gateway restart.
const RECOVERY_CHECK_INTERVAL_MS = 30_000; // Check every 30 seconds in recovery mode
if (reconnectPolicy.maxAttempts > 0 && reconnectAttempts >= reconnectPolicy.maxAttempts) {
reconnectLogger.warn(
{
@ -393,13 +399,45 @@ export async function monitorWebChannel(
reconnectAttempts,
maxAttempts: reconnectPolicy.maxAttempts,
},
"web reconnect: max attempts reached; continuing in degraded mode",
"web reconnect: max attempts reached; entering recovery mode",
);
runtime.error(
`WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`,
`WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). ` +
`Entering recovery mode - will auto-resume when auth is refreshed via 'clawdbot channels login'.`,
);
await closeListener();
break;
// Recovery loop: wait and check for valid auth periodically
while (!stopRequested() && !sigintStop) {
try {
await sleep(RECOVERY_CHECK_INTERVAL_MS, abortSignal);
} catch {
break;
}
if (stopRequested() || sigintStop) break;
// Check if auth has been refreshed (e.g., user ran `clawdbot channels login`)
const authValid = await webAuthExists(account.authDir);
if (authValid) {
reconnectLogger.info(
{ connectionId, reconnectAttempts },
"web reconnect: auth detected in recovery mode; resetting attempts and resuming",
);
runtime.error(
`WhatsApp Web: Valid auth detected. Resetting reconnect attempts and resuming monitoring.`,
);
// Reset attempts and continue the main loop to reconnect
reconnectAttempts = 0;
status.reconnectAttempts = 0;
status.lastError = null;
emitStatus();
break;
}
}
// If we exited recovery due to stop request, break out of main loop
if (stopRequested() || sigintStop) break;
// Otherwise, continue to the next iteration of the main loop to reconnect
continue;
}
const delay = computeBackoff(reconnectPolicy, reconnectAttempts);