From 0b849b7775b479e602127655387c1ab94e87bfca Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 4 Mar 2026 22:34:10 +0100 Subject: [PATCH] Fix: Voice reconnect Endlosschleife verhindern - Re-Entranz-Guard (isReconnecting) verhindert parallele Handler - Max 3 Reconnect-Versuche bevor fresh join - Exponentieller Backoff (2s, 4s, 6s) zwischen Retries - Ready-State setzt Counter zurueck Co-Authored-By: Claude Opus 4.6 --- server/src/index.ts | 65 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 2016f8a..2366aa1 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -556,8 +556,25 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { // Mehrfach-Registrierung verhindern if ((connection as any).__lifecycleAttached) return; try { (connection as any).setMaxListeners?.(0); } catch {} + + // Retry-Tracking um Endlosschleife zu verhindern + let reconnectAttempts = 0; + const MAX_RECONNECT_ATTEMPTS = 3; + let isReconnecting = false; + connection.on('stateChange', async (oldS: any, newS: any) => { console.log(`${new Date().toISOString()} | VoiceConnection: ${oldS.status} -> ${newS.status}`); + + // Ready zurückgesetzt -> Retry-Counter reset + if (newS.status === VoiceConnectionStatus.Ready) { + reconnectAttempts = 0; + isReconnecting = false; + return; + } + + // Re-Entranz verhindern: wenn schon ein Reconnect läuft, nicht nochmal starten + if (isReconnecting) return; + try { if (newS.status === VoiceConnectionStatus.Disconnected) { // Versuche, die Verbindung kurzfristig neu auszuhandeln, sonst rejoin @@ -567,7 +584,25 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { entersState(connection, VoiceConnectionStatus.Connecting, 5_000) ]); } catch { - connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + reconnectAttempts++; + console.warn(`${new Date().toISOString()} | Disconnected rejoin attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); + connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); + } else { + console.error(`${new Date().toISOString()} | Max reconnect attempts reached from Disconnected, destroying and rejoining fresh`); + reconnectAttempts = 0; + try { connection.destroy(); } catch {} + const newConn = joinVoiceChannel({ + channelId: state.channelId, + guildId: state.guildId, + adapterCreator: guild.voiceAdapterCreator as any, + selfMute: false, + selfDeaf: false + }); + state.connection = newConn; + newConn.subscribe(state.player); + attachVoiceLifecycle(state, guild); + } } } else if (newS.status === VoiceConnectionStatus.Destroyed) { // Komplett neu beitreten @@ -582,14 +617,38 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { newConn.subscribe(state.player); attachVoiceLifecycle(state, guild); } else if (newS.status === VoiceConnectionStatus.Connecting || newS.status === VoiceConnectionStatus.Signalling) { + isReconnecting = true; try { await entersState(connection, VoiceConnectionStatus.Ready, 15_000); + // Ready wird oben im Handler behandelt } catch (e) { - console.warn(`${new Date().toISOString()} | Voice not ready from ${newS.status}, rejoin`, e); - connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); + reconnectAttempts++; + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + const backoffMs = reconnectAttempts * 2_000; + console.warn(`${new Date().toISOString()} | Voice not ready from ${newS.status}, attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}, backoff ${backoffMs}ms`); + await new Promise(r => setTimeout(r, backoffMs)); + isReconnecting = false; + connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); + } else { + console.error(`${new Date().toISOString()} | Max reconnect attempts reached, destroying and rejoining fresh`); + reconnectAttempts = 0; + isReconnecting = false; + try { connection.destroy(); } catch {} + const newConn = joinVoiceChannel({ + channelId: state.channelId, + guildId: state.guildId, + adapterCreator: guild.voiceAdapterCreator as any, + selfMute: false, + selfDeaf: false + }); + state.connection = newConn; + newConn.subscribe(state.player); + attachVoiceLifecycle(state, guild); + } } } } catch (e) { + isReconnecting = false; console.error(`${new Date().toISOString()} | Voice lifecycle handler error`, e); } });