From 979e2c3a16c892143407411fe7ebeb5c2e9acb85 Mon Sep 17 00:00:00 2001 From: kami Date: Sun, 12 Jul 2026 12:19:16 +0400 Subject: [PATCH] fix(approval): log + evict on failed approval broadcast (S4) broadcast() wrapped each client.send in a bare runCatching with no logging or dead-client eviction, so an approval prompt could vanish silently and the session hang until reconnect. Now log+evict failed clients from both registries and error when a prompt reaches 0 live clients. Note: :apps:server:test not run (core:kernel temporarily broken by concurrent work); change is isolated, compiled clean before the breakage, detekt passes. --- .../server/approval/ApprovalCoordinator.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt b/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt index 4c2c98cf..f0192ea5 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt @@ -13,6 +13,7 @@ import com.correx.core.events.types.SessionId import com.correx.core.kernel.orchestration.ApprovalGateway import io.ktor.server.websocket.DefaultWebSocketServerSession import io.ktor.websocket.Frame +import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap open class ApprovalCoordinator( @@ -117,8 +118,29 @@ open class ApprovalCoordinator( val encoded = ProtocolSerializer.encodeServerMessage(msg) val sessionSubs = sessionClients[sessionId].orEmpty() val recipients: Set = sessionSubs + globalClients + var delivered = 0 recipients.forEach { client -> runCatching { client.send(Frame.Text(encoded)) } + .onSuccess { delivered++ } + .onFailure { err -> + // A dead socket must not silently swallow an approval prompt. Log it and evict + // the client from both registries so we don't keep sending into a closed socket + // (the client re-registers and gets the pending approval from the snapshot on + // reconnect). + log.warn("approval broadcast to a client failed, evicting: {}", err.message) + sessionClients[sessionId]?.remove(client) + globalClients.remove(client) + } + } + if (delivered == 0) { + log.error( + "approval prompt for session={} reached 0 live clients; session stays paused until a client reconnects", + sessionId.value, + ) } } + + private companion object { + private val log = LoggerFactory.getLogger(ApprovalCoordinator::class.java) + } }