From 87c7b8345ead3ddb1e2626acaa02d990683048b3 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 27 Feb 2026 22:27:53 -0800 Subject: [PATCH 1/4] Fix Whatsapp Connection issue --- src/cli/commands/auth.ts | 147 +++++++++++++++++--------------------- src/platforms/whatsapp.ts | 82 +++++++++++++-------- 2 files changed, 118 insertions(+), 111 deletions(-) diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 5c566f6..30f834a 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -80,7 +80,6 @@ function authenticateWhatsApp(): Promise { try { fs.rmSync(WA_AUTH_DIR, { recursive: true, force: true }); - await new Promise((r) => setTimeout(r, 1000)); fs.mkdirSync(WA_AUTH_DIR, { recursive: true }); } catch { console.log(chalk.yellow("Warning: Could not clear old session")); @@ -93,28 +92,7 @@ function authenticateWhatsApp(): Promise { console.log(chalk.gray("Initializing WhatsApp connection...")); console.log(); - const { state, saveCreds: _saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR); - const saveCreds = async () => { - const credsPath = path.join(WA_AUTH_DIR, "creds.json"); - const tmpPath = credsPath + "." + Date.now() + ".tmp"; - for (let attempt = 0; attempt < 10; attempt++) { - try { - fs.writeFileSync(tmpPath, JSON.stringify(state.creds, null, 2)); - try { - fs.unlinkSync(credsPath); - } catch {} - fs.renameSync(tmpPath, credsPath); - return; - } catch { - try { - fs.unlinkSync(tmpPath); - } catch {} - if (attempt < 9) await new Promise((r) => setTimeout(r, 500 * (attempt + 1))); - } - } - // Last resort: try the original saveCreds - await _saveCreds(); - }; + const { state, saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR); const { version } = await fetchLatestBaileysVersion(); connectionTimeout = setTimeout(() => { @@ -177,10 +155,11 @@ function authenticateWhatsApp(): Promise { pairingComplete = true; console.log(chalk.green("\n[OK] WhatsApp authenticated successfully!")); + // Keep socket alive so the phone can finalize the handshake setTimeout(() => { closeSock(sock, true); resolvePromise(); - }, 500); + }, 3000); } if (connection === "close" && !pairingComplete) { @@ -214,69 +193,71 @@ function authenticateWhatsApp(): Promise { ); // 515 = stream replaced, normal after QR pairing. - // Close websocket but DON'T remove listeners yet (let creds.update finish saving) - closeSock(sock, false); - await new Promise((r) => setTimeout(r, 3000)); - - // Now remove old listeners and reconnect using the SAME in-memory state - // (don't re-read from disk — cached keys may not be flushed yet) + // Creds are already saved. Close old socket and create a fresh one from disk. closeSock(sock, true); - const retryVersion = await fetchLatestBaileysVersion(); - const retrySock = makeWASocket({ - auth: { - creds: state.creds, - keys: makeCacheableSignalKeyStore(state.keys, silentLogger), - }, - version: retryVersion.version, - printQRInTerminal: false, - browser: ["TxtCode", "CLI", "1.0.0"], - syncFullHistory: false, - markOnlineOnConnect: false, - logger: silentLogger, - }); - - retrySock.ev.on("creds.update", saveCreds); - - const retryTimeout = setTimeout(() => { - if (!pairingComplete) { - closeSock(retrySock, true); - rejectPromise( - new Error("WhatsApp linking timed out after restart. Please try again."), - ); - } - }, 30000); - - retrySock.ev.on("connection.update", (retryUpdate) => { - if (retryUpdate.connection === "open") { - clearTimeout(retryTimeout); - pairingComplete = true; - console.log(chalk.green("[OK] WhatsApp linked successfully!\n")); - - setTimeout(() => { + + try { + const { state: freshState, saveCreds: freshSaveCreds } = + await useMultiFileAuthState(WA_AUTH_DIR); + const retrySock = makeWASocket({ + auth: { + creds: freshState.creds, + keys: makeCacheableSignalKeyStore(freshState.keys, silentLogger), + }, + version, + printQRInTerminal: false, + browser: ["TxtCode", "CLI", "1.0.0"], + syncFullHistory: false, + markOnlineOnConnect: false, + logger: silentLogger, + }); + + retrySock.ev.on("creds.update", freshSaveCreds); + + const retryTimeout = setTimeout(() => { + if (!pairingComplete) { closeSock(retrySock, true); - resolvePromise(); - }, 1000); - } - - if (retryUpdate.connection === "close" && !pairingComplete) { - const retryStatusCode = ( - retryUpdate as { - lastDisconnect?: { error?: { output?: { statusCode?: number } } }; + rejectPromise( + new Error("WhatsApp linking timed out after restart. Please try again."), + ); + } + }, 30000); + + retrySock.ev.on("connection.update", (retryUpdate) => { + if (retryUpdate.connection === "open") { + clearTimeout(retryTimeout); + pairingComplete = true; + console.log(chalk.green("[OK] WhatsApp linked successfully!\n")); + // Keep socket alive briefly so the phone can finalize the handshake + setTimeout(() => { + closeSock(retrySock, true); + resolvePromise(); + }, 3000); + } + + if (retryUpdate.connection === "close" && !pairingComplete) { + const retryStatusCode = ( + retryUpdate as { + lastDisconnect?: { error?: { output?: { statusCode?: number } } }; + } + )?.lastDisconnect?.error?.output?.statusCode; + if (retryStatusCode === 515) { + return; } - )?.lastDisconnect?.error?.output?.statusCode; - // If we get another 515, keep retrying - if (retryStatusCode === 515) { - return; + clearTimeout(retryTimeout); + closeSock(retrySock, true); + rejectPromise( + new Error( + `WhatsApp authentication failed after restart (code: ${retryStatusCode})`, + ), + ); } - clearTimeout(retryTimeout); - closeSock(retrySock, true); - rejectPromise( - new Error( - `WhatsApp authentication failed after restart (code: ${retryStatusCode})`, - ), - ); - } - }); + }); + } catch (err) { + rejectPromise( + err instanceof Error ? err : new Error("Failed to restart WhatsApp connection"), + ); + } } else if (hasShownQR && statusCode !== 515) { closeSock(sock, true); rejectPromise(new Error(`WhatsApp authentication failed (code: ${statusCode})`)); diff --git a/src/platforms/whatsapp.ts b/src/platforms/whatsapp.ts index 09e71a7..a3246cf 100644 --- a/src/platforms/whatsapp.ts +++ b/src/platforms/whatsapp.ts @@ -17,6 +17,10 @@ import { WhatsAppTypingSignaler } from "../shared/typing-signaler"; const WA_AUTH_DIR = path.join(os.homedir(), ".txtcode", ".wacli_auth"); const MAX_WA_LENGTH = 4096; +const MAX_RECONNECT_ATTEMPTS = 12; +const RECONNECT_BASE_MS = 2000; +const RECONNECT_MAX_MS = 30000; +const RECONNECT_FACTOR = 1.8; const noop = () => {}; const silentLogger = { @@ -35,11 +39,27 @@ interface ActiveRequest { aborted: boolean; } +function computeBackoff(attempt: number): number { + const delay = Math.min(RECONNECT_BASE_MS * Math.pow(RECONNECT_FACTOR, attempt), RECONNECT_MAX_MS); + const jitter = delay * 0.25 * Math.random(); + return delay + jitter; +} + +// Serialized credential save queue (prevents concurrent writes racing on Windows) +function createCredsSaver(saveCreds: () => Promise): () => void { + let queue: Promise = Promise.resolve(); + return () => { + queue = queue.then(() => saveCreds()).catch(() => {}); + }; +} + export class WhatsAppBot { private agent: AgentCore; private sock!: WASocket; private lastProcessedTimestamp: number = 0; private activeRequests: Map = new Map(); + private reconnectAttempts = 0; + private connectedAt = 0; constructor(agent: AgentCore) { this.agent = agent; @@ -65,27 +85,9 @@ export class WhatsAppBot { process.exit(1); } - const { state, saveCreds: _saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR); - const saveCreds = async () => { - const credsPath = path.join(WA_AUTH_DIR, "creds.json"); - const tmpPath = credsPath + "." + Date.now() + ".tmp"; - for (let attempt = 0; attempt < 10; attempt++) { - try { - fs.writeFileSync(tmpPath, JSON.stringify(state.creds, null, 2)); - try { - fs.unlinkSync(credsPath); - } catch {} - fs.renameSync(tmpPath, credsPath); - return; - } catch { - try { - fs.unlinkSync(tmpPath); - } catch {} - if (attempt < 9) await new Promise((r) => setTimeout(r, 500 * (attempt + 1))); - } - } - await _saveCreds(); - }; + const { state, saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR); + const enqueueSaveCreds = createCredsSaver(saveCreds); + const { version } = await fetchLatestBaileysVersion().catch(() => ({ version: undefined as unknown as [number, number, number], })); @@ -98,33 +100,57 @@ export class WhatsAppBot { version, printQRInTerminal: false, browser: ["TxtCode", "CLI", "1.0.0"], + syncFullHistory: false, + markOnlineOnConnect: false, logger: silentLogger, }); + // Handle WebSocket errors to prevent unhandled crashes + if (this.sock.ws && typeof (this.sock.ws as { on?: Function }).on === "function") { + (this.sock.ws as { on: Function }).on("error", (err: Error) => { + logger.error("WebSocket error:", err.message); + }); + } + this.sock.ev.on("connection.update", async (update: Partial) => { const { connection, lastDisconnect } = update; if (connection === "open") { logger.info("WhatsApp connected!"); logger.info("Waiting for messages..."); + this.connectedAt = Date.now(); + this.reconnectAttempts = 0; this.sock.sendPresenceUpdate("available").catch(() => {}); } if (connection === "close") { - const shouldReconnect = - (lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; + const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; + const shouldReconnect = statusCode !== DisconnectReason.loggedOut; - if (shouldReconnect) { - logger.error("Connection closed. Reconnecting..."); - await this.start(); - } else { + if (!shouldReconnect) { logger.error("WhatsApp logged out. Run txtcode auth again."); process.exit(1); } + + // Reset backoff if connection was healthy for > 60s + if (this.connectedAt && Date.now() - this.connectedAt > 60000) { + this.reconnectAttempts = 0; + } + + if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + logger.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Exiting.`); + process.exit(1); + } + + const delay = computeBackoff(this.reconnectAttempts); + this.reconnectAttempts++; + logger.error(`Connection closed (code: ${statusCode}). Reconnecting in ${Math.round(delay / 1000)}s...`); + await new Promise((r) => setTimeout(r, delay)); + await this.start(); } }); - this.sock.ev.on("creds.update", saveCreds); + this.sock.ev.on("creds.update", enqueueSaveCreds); this.sock.ev.on( "messages.upsert", From 8e0a324013764e6adefc2f3c61c043c514ad7b1e Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 27 Feb 2026 22:28:15 -0800 Subject: [PATCH 2/4] format --- src/platforms/whatsapp.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/platforms/whatsapp.ts b/src/platforms/whatsapp.ts index a3246cf..5b35fb6 100644 --- a/src/platforms/whatsapp.ts +++ b/src/platforms/whatsapp.ts @@ -144,7 +144,9 @@ export class WhatsAppBot { const delay = computeBackoff(this.reconnectAttempts); this.reconnectAttempts++; - logger.error(`Connection closed (code: ${statusCode}). Reconnecting in ${Math.round(delay / 1000)}s...`); + logger.error( + `Connection closed (code: ${statusCode}). Reconnecting in ${Math.round(delay / 1000)}s...`, + ); await new Promise((r) => setTimeout(r, delay)); await this.start(); } From 85d5c4cc74b4b1f59f8414744691aae3889c4e43 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 27 Feb 2026 22:28:36 -0800 Subject: [PATCH 3/4] fix lint --- src/cli/commands/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 30f834a..0b4d2e2 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -57,7 +57,7 @@ function authenticateWhatsApp(): Promise { const closeSock = (s: unknown, removeListeners = false) => { try { const socket = s as { ev?: { removeAllListeners: () => void }; ws?: { close: () => void } }; - if (removeListeners) socket?.ev?.removeAllListeners(); + if (removeListeners) {socket?.ev?.removeAllListeners();} socket?.ws?.close(); } catch { // Ignore From 206451b2b542a5994b94c762a6b1f9508028b411 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 27 Feb 2026 22:32:59 -0800 Subject: [PATCH 4/4] format --- src/cli/commands/auth.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 0b4d2e2..bc838ee 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -57,7 +57,9 @@ function authenticateWhatsApp(): Promise { const closeSock = (s: unknown, removeListeners = false) => { try { const socket = s as { ev?: { removeAllListeners: () => void }; ws?: { close: () => void } }; - if (removeListeners) {socket?.ev?.removeAllListeners();} + if (removeListeners) { + socket?.ev?.removeAllListeners(); + } socket?.ws?.close(); } catch { // Ignore