diff --git a/server/src/http/ws/mod.rs b/server/src/http/ws/mod.rs index 079f4714..b4476295 100644 --- a/server/src/http/ws/mod.rs +++ b/server/src/http/ws/mod.rs @@ -147,13 +147,34 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc>) -> impl poem ); tokio::pin!(chat_fut); - let mut perm_rx = ctx.services.perm_rx.lock().await; + // Take perm_rx for local permission forwarding — but never + // block on it. Since story 884 the Matrix permission + // listener (and the sled uplink, when configured) hold this + // lock for the process lifetime, so a blocking + // `lock().await` here parks the entire WS connection loop + // forever: chat_fut is never polled, RPC frames on this + // socket are never answered, and everything queues behind + // the dispatcher's serial session lock. If another task + // already owns permission routing, proceed without the + // local forwarding arm — requests are handled there. + let mut perm_rx = ctx.services.perm_rx.try_lock().ok(); + if perm_rx.is_none() { + crate::slog!( + "[ws] perm_rx held by another listener; \ + skipping local permission forwarding for this chat" + ); + } let chat_result = loop { tokio::select! { result = &mut chat_fut => break result, - Some(perm_fwd) = perm_rx.recv() => { + Some(perm_fwd) = async { + match perm_rx.as_mut() { + Some(rx) => rx.recv().await, + None => std::future::pending().await, + } + } => { let _ = tx.send(ws::permission_request_response( &perm_fwd.request_id, &perm_fwd.tool_name,