RexSystem1 voteMerged from #404originally by @cproudlock on GitHub
I dug into the gateway code — the root cause is that session.erl's terminate/2 callback is a no-op:
terminate(_Reason,_State)->ok.
When a WebSocket drops ungracefully (PC poweroff, network loss), the session flow is:
handle_socket_down marks socket as undefined, starts 10s resume timer
No reconnection → resume_timeout fires → {stop, normal, State}
terminate/2 is called but does nothing
Guild processes are never notified → voice states persist in KeyDB until TTL (up to 1 hour)
Graceful disconnect works fine — session_voice.erl:handle_voice_disconnect/1 calls dispatch_guild_voice_disconnects/2 which notifies all guilds. But that code path is never reached on ungraceful death.
Suggested fix: implement cleanup in terminate/2:
terminate(_Reason,State)->casemaps:get(voice_channel_id,State,undefined)ofundefined->ok;_ChannelId->%% Reuse the same disconnect logic as graceful disconnectGuilds=maps:get(guilds,State,#{}),UserId=maps:get(user_id,State),SessionId=maps:get(id,State),ConnectionId=maps:get(connection_id,State,null),Request=#{user_id => UserId,session_id => SessionId,connection_id => ConnectionId},dispatch_guild_voice_disconnects(Guilds,Request),dm_voice:disconnect_voice_user(UserId,State)end,ok.
This ensures the same cleanup runs whether the user explicitly disconnects or their connection drops.
Thread
Comment by @cproudlock
session.erl'sterminate/2callback is a no-op:- No reconnection →
- Guild processes are never notified → voice states persist in KeyDB until TTL (up to 1 hour)
Graceful disconnect works fine —handle_socket_downmarks socket as undefined, starts 10s resume timerresume_timeoutfires →{stop, normal, State}terminate/2is called but does nothingsession_voice.erl:handle_voice_disconnect/1callsdispatch_guild_voice_disconnects/2which notifies all guilds. But that code path is never reached on ungraceful death. Suggested fix: implement cleanup interminate/2: