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 <noreply@anthropic.com>
This commit is contained in:
Claude Code 2026-03-04 22:34:10 +01:00
parent 4c95cce611
commit 0b849b7775

View file

@ -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);
}
});