Edit history

Earlier versions of [Self-hosted] PATCH /guilds/{id}/members/{id} channel_id move disconnects user — phase-2 (dest LiveKit token / VOICE_SERVER_UPDATE) never fires server-side (still present after #500), newest first.

Current version | Edited by Rex
Changes
### SummaryRemoved: Follow-up to [#500](https://feedback.fluxer.com/p/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.Added: Follow-up to [#296](https://feedback.fluxer.com/p/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.150855` - `fluxer-gateway` `BUILD_VERSION=2026.630.20736` - `livekit/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 reproduce1. Non-owner user joins voice channel A.2. Bot opens a gateway WS, reads the user's live `connection_id` from `VOICE_STATE_UPDATE` (e.g. `lleyn-alioth`).3. Bot calls `PATCH /guilds/{gid}/members/{uid}` with body `{"channel_id":"<B>","connection_id":"<live connection_id>"}` → HTTP `200`.4. Observe gateway events + LiveKit/api logs.### ExpectedUser moves seamlessly A → B (as on hosted `api.fluxer.app` with `connection_id`).### ActualOnly 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)Removed: - **Not the API contract / our client:** the exact same PATCH body (with live `connection_id`) holds the move on hosted `api.fluxer.app`; [#500](https://feedback.fluxer.com/p/296) was also reproduced by an unrelated bot stack on hosted.Added: - **Not the API contract / our client:** the exact same PATCH body (with live `connection_id`) holds the move on hosted `api.fluxer.app`; [#296](https://feedback.fluxer.com/p/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.
Show

[Self-hosted] PATCH /guilds/{id}/members/{id} channel_id move disconnects user — phase-2 (dest LiveKit token / VOICE_SERVER_UPDATE) never fires server-side (still present after #500)

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.150855
    • fluxer-gateway BUILD_VERSION=2026.630.20736
    • livekit/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

  1. Non-owner user joins voice channel A.
  2. Bot opens a gateway WS, reads the user's live connection_id from VOICE_STATE_UPDATE (e.g. lleyn-alioth).
  3. Bot calls PATCH /guilds/{gid}/members/{uid} with body {"channel_id":"<B>","connection_id":"<live connection_id>"} → HTTP 200.
  4. 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.
Original by Rex
Show

[Self-hosted] PATCH /guilds/{id}/members/{id} channel_id move disconnects user — phase-2 (dest LiveKit token / VOICE_SERVER_UPDATE) never fires server-side (still present after #500)

Summary

Follow-up to #500 (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.150855
    • fluxer-gateway BUILD_VERSION=2026.630.20736
    • livekit/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

  1. Non-owner user joins voice channel A.
  2. Bot opens a gateway WS, reads the user's live connection_id from VOICE_STATE_UPDATE (e.g. lleyn-alioth).
  3. Bot calls PATCH /guilds/{gid}/members/{uid} with body {"channel_id":"<B>","connection_id":"<live connection_id>"} → HTTP 200.
  4. 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; #500 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.