From 9e4aad973f87e83b26ae4d25f269f72e04a08b8c Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Thu, 19 Feb 2026 00:46:41 +0100 Subject: [PATCH] fix: WS auto-reconnect, reduce keepalive to 15s, downgrade ChatDOM to debug Co-authored-by: Cursor --- src/bot/chatProcedure.ts | 2 +- src/bot/orchestrator.ts | 56 ++++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts index 1e8e8a4..44c44f5 100644 --- a/src/bot/chatProcedure.ts +++ b/src/bot/chatProcedure.ts @@ -134,7 +134,7 @@ export class ChatProcedure { children: number; html: string; }) => { - this._logger.info(`ChatDOM: <${info.tag} data-tid="${info.tid}"> children=${info.children}, text="${info.text.substring(0, 120)}"`); + this._logger.debug(`ChatDOM: <${info.tag} data-tid="${info.tid}"> children=${info.children}, text="${info.text.substring(0, 120)}"`); }); } catch { // Already exposed diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index a1b725c..dd47c3e 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -511,9 +511,9 @@ export class BotOrchestrator { // Connection might be closing } } - }, 30000); + }, 15000); - this._logger.info('Keepalive started (30s interval)'); + this._logger.info('Keepalive started (15s interval)'); } /** @@ -527,22 +527,32 @@ export class BotOrchestrator { } } + private _wsReconnectAttempts: number = 0; + private _wsMaxReconnectAttempts: number = 10; + private _wsReconnecting: boolean = false; + /** * Connect to the Gateway WebSocket for this session. */ private async _connectToGateway(): Promise { - // gatewayWsUrl is the full WebSocket URL provided by the Gateway - // It already includes instanceId and sessionId const wsUrl = this._options.gatewayWsUrl; this._logger.info(`Connecting to Gateway: ${wsUrl}`); - // Derive HTTP base URL from WebSocket URL for fallback this._httpBaseUrl = wsUrl .replace('wss://', 'https://') .replace('ws://', 'http://') .replace(/\/bot\/ws\/.*$/, ''); - return new Promise((resolve, reject) => { + return this._createWsConnection(wsUrl, true); + } + + /** + * Create (or recreate) the WebSocket connection. + * On initial connect, `isInitial` = true and the promise resolves/rejects. + * On reconnect, the promise resolves immediately (fire-and-forget). + */ + private _createWsConnection(wsUrl: string, isInitial: boolean): Promise { + return new Promise((resolve) => { this._gatewayWs = new WebSocket(wsUrl); const wsTimeout = setTimeout(() => { @@ -551,7 +561,7 @@ export class BotOrchestrator { this._useHttpFallback = true; this._gatewayWs?.close(); this._gatewayWs = null; - resolve(); // Continue with HTTP fallback instead of failing + resolve(); } }, 10000); @@ -559,6 +569,8 @@ export class BotOrchestrator { clearTimeout(wsTimeout); this._logger.info('Connected to Gateway via WebSocket'); this._useHttpFallback = false; + this._wsReconnectAttempts = 0; + this._wsReconnecting = false; resolve(); }); @@ -570,23 +582,45 @@ export class BotOrchestrator { this._gatewayWs.on('close', (code, reason) => { this._logger.warn(`Gateway WebSocket closed: ${code} - ${reason}`); - if (!this._isShuttingDown && !this._useHttpFallback) { - this._logger.info('Switching to HTTP fallback for transcript delivery'); + if (!this._isShuttingDown) { this._useHttpFallback = true; + this._scheduleReconnect(wsUrl); } }); this._gatewayWs.on('error', (error) => { clearTimeout(wsTimeout); this._logger.error('Gateway WebSocket error:', error); - this._logger.info('Switching to HTTP fallback for transcript delivery'); this._useHttpFallback = true; this._gatewayWs = null; - resolve(); // Continue with HTTP fallback + if (isInitial) resolve(); }); }); } + /** + * Schedule a WebSocket reconnection with exponential backoff. + */ + private _scheduleReconnect(wsUrl: string): void { + if (this._isShuttingDown || this._wsReconnecting) return; + if (this._wsReconnectAttempts >= this._wsMaxReconnectAttempts) { + this._logger.warn(`WebSocket reconnect limit reached (${this._wsMaxReconnectAttempts}), staying on HTTP fallback`); + return; + } + + this._wsReconnecting = true; + this._wsReconnectAttempts++; + const delayMs = Math.min(2000 * Math.pow(1.5, this._wsReconnectAttempts - 1), 30000); + this._logger.info(`WebSocket reconnect attempt ${this._wsReconnectAttempts}/${this._wsMaxReconnectAttempts} in ${(delayMs / 1000).toFixed(1)}s`); + + setTimeout(() => { + if (this._isShuttingDown) return; + this._createWsConnection(wsUrl, false).catch((err) => { + this._logger.error('WebSocket reconnect failed:', err); + }); + }, delayMs); + } + /** * Handle incoming messages from the Gateway. * Async operations are awaited to ensure proper error handling