Summary
Follow-up to
#296 (bulk-closed for Fluxer V2 with "reopen if still present"). The bot-driven voice
move via
PATCH /guilds/{guild_id}/members/{user_id} with a
channel_id still disconnects the user instead of moving them —
on the current self-hosted :v1 images. Server-side logs show the move only ever performs phase 1 (leave/broadcast
channel:null); the destination LiveKit room is
never created and no destination token /
VOICE_SERVER_UPDATE is ever emitted, so the client leaves the old room and is stranded.
Note: on
hosted api.fluxer.app the same call
works when the PATCH body includes the target's live
connection_id (the seamless move holds across multiple channels). On
self-hosted, the move fails
even with the correct live connection_id in the body — so this appears specific to the self-hosted build/config path, not to the API contract.
Environment
- Deployment: self-hosted (
deploy/self-hosting, single-community mode) - Images (
:v1, pulled 2026-07-08):fluxer-api BUILD_VERSION=2026.707.150855fluxer-gateway BUILD_VERSION=2026.630.20736livekit/livekit-server:v1.12.0
livekit.yaml: identical to upstream deploy/self-hosting/livekit.yaml (use_external_ip: true, tcp 7881 / udp 7882 forwarded, STUN google)- Bot has
MOVE_MEMBERS, MANAGE_CHANNELS, CONNECT. Target is a normal (non-owner) member. - Voice join works perfectly (room created, media flows for minutes, presence heartbeats 200).
Steps to reproduce
- Non-owner user joins voice channel A.
- Bot opens a gateway WS, reads the user's live
connection_id from VOICE_STATE_UPDATE (e.g. lleyn-alioth). - Bot calls
PATCH /guilds/{gid}/members/{uid} with body {"channel_id":"<B>","connection_id":"<live connection_id>"} → HTTP 200. - Observe gateway events + LiveKit/api logs.
Expected
User moves seamlessly A → B (as on hosted
api.fluxer.app with
connection_id).
Actual
Only a single
VOICE_STATE_UPDATE channel=null is emitted for the user; the join-to-B phase never happens; the user is disconnected from voice entirely.
LiveKit log (self-hosted) — destination room B is never created; only A ever exists:
# join to A works:
starting RTC session room=guild_{gid}_channel_{A} participant=user_{uid}_lleyn-alioth
... DestinationRoom: "" Region: ""
# on the move (PATCH -> B, status 200):
participant closing room=guild_{gid}_channel_{A}
reason: "CLIENT_REQUEST_LEAVE" isExpectedToResume: false
# ...no "starting RTC session" for channel_{B} ever appears...
closing room room=guild_{gid}_channel_{A}
API log (self-hosted) — no destination token mint, only a disconnect:
"LiveKit participant_left - disconnecting voice user" guildId=... userId=... channelId={A} connectionId=lleyn-alioth
Notably
isExpectedToResume: false and the empty
DestinationRoom in the join grant indicate LiveKit's native participant-move / destination-room path is never invoked for the move.
What we've ruled out (self-hosted side)
- Not the API contract / our client: the exact same PATCH body (with live
connection_id) holds the move on hosted api.fluxer.app; #296 was also reproduced by an unrelated bot stack on hosted. - Not our LiveKit networking: join works end-to-end (room created, media flows, correct region resolution, presence heartbeats OK).
- Not our compose voice config: we removed our only non-upstream env (
FLUXER_LIVEKIT_URL) so voice config matches upstream defaults — join still works, move still fails identically. livekit.yaml is byte-identical to upstream.
Question
Is there a self-hosted build/config gap in the voice-move token migration path (gateway
move_member → request dest voice token →
VOICE_SERVER_UPDATE)? Phase 1 (leave +
channel:null broadcast) runs on both hosted and self-hosted; phase 2 (mint destination token, create/route the destination LiveKit room, emit
VOICE_SERVER_UPDATE) runs on hosted but never fires on the self-hosted
:v1 build. Happy to provide full logs / test against a newer build.
1 comment
Comment by @x-JR
Deleted comment