fix(tui-02): route ApprovalResponse through global socket
Complete the tui-02 spec acceptance criteria: delete the orphan `connectSession`, `disconnectSession`, `sessionStreamJob`, and `sessionWsSession` members from TuiWsClient, and remove the `send()` fallback that previously routed ApprovalResponse over a per-session socket. All ClientMessage types now travel the single global socket. Server side: GlobalStreamHandler.handleClientMessage now actually handles ApprovalResponse (was returning a protocol error), converting to a domain ApprovalDecision and dispatching to the orchestrator — mirrors SessionStreamHandler's prior logic. Adds ApprovalResponseGlobalSocketRoundTripTest covering APPROVE and STEER encode/decode through ProtocolSerializer.
This commit is contained in:
@@ -3,10 +3,17 @@ package com.correx.apps.server.ws
|
||||
import com.correx.apps.server.ServerModule
|
||||
import com.correx.apps.server.bridge.DomainEventMapper
|
||||
import com.correx.apps.server.bridge.SessionEventBridge
|
||||
import com.correx.apps.server.protocol.ApprovalDecision
|
||||
import com.correx.apps.server.protocol.ClientMessage
|
||||
import com.correx.apps.server.protocol.ProtocolSerializer
|
||||
import com.correx.apps.server.protocol.ProviderHealthDto
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
import com.correx.core.approvals.ApprovalOutcome
|
||||
import com.correx.core.approvals.ApprovalStatus
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.approvals.UserSteering
|
||||
import com.correx.core.approvals.model.ApprovalContext
|
||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
@@ -15,7 +22,9 @@ import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.inference.ProviderHealth
|
||||
import com.correx.core.sessions.ApprovalMode
|
||||
import com.correx.core.utils.TypeId
|
||||
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
|
||||
import io.ktor.server.websocket.DefaultWebSocketServerSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
@@ -121,12 +130,48 @@ class GlobalStreamHandler(private val module: ServerModule) {
|
||||
is ClientMessage.StartSession -> handleStartSession(session, msg, sendFrame)
|
||||
is ClientMessage.CancelSession -> module.orchestrator.cancel(msg.sessionId)
|
||||
is ClientMessage.ResumeSession -> session.send(encodeError("ResumeSession not supported"))
|
||||
is ClientMessage.ApprovalResponse ->
|
||||
session.send(encodeError("ApprovalResponse must be sent to /sessions/{id}/stream"))
|
||||
is ClientMessage.ApprovalResponse -> handleApprovalResponse(msg)
|
||||
is ClientMessage.ChatInput -> Unit // handler deferred to subsequent task
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleApprovalResponse(msg: ClientMessage.ApprovalResponse) {
|
||||
val domainDecision = msg.toDomainDecision(msg.requestId.value)
|
||||
runCatching { module.orchestrator.submitApprovalDecision(msg.requestId, domainDecision) }
|
||||
.onFailure {
|
||||
log.warn(
|
||||
"submitApprovalDecision failed for request={}: {}",
|
||||
msg.requestId.value,
|
||||
it.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClientMessage.ApprovalResponse.toDomainDecision(sessionIdValue: String): DomainApprovalDecision {
|
||||
val outcome = when (decision) {
|
||||
ApprovalDecision.APPROVE -> ApprovalOutcome.APPROVED
|
||||
ApprovalDecision.REJECT -> ApprovalOutcome.REJECTED
|
||||
ApprovalDecision.STEER -> ApprovalOutcome.REJECTED
|
||||
}
|
||||
val scopeSessionId: SessionId = TypeId(sessionIdValue)
|
||||
val identity = ApprovalScopeIdentity(sessionId = scopeSessionId, stageId = null, projectId = null)
|
||||
val context = ApprovalContext(identity = identity, mode = ApprovalMode.PROMPT)
|
||||
val steering = steeringNote?.let {
|
||||
UserSteering(text = it, sessionId = scopeSessionId, timestamp = Clock.System.now())
|
||||
}
|
||||
return DomainApprovalDecision(
|
||||
id = null,
|
||||
requestId = requestId,
|
||||
outcome = outcome,
|
||||
state = ApprovalStatus.COMPLETED,
|
||||
tier = Tier.T2,
|
||||
contextSnapshot = context,
|
||||
resolutionTimestamp = Clock.System.now(),
|
||||
reason = steeringNote,
|
||||
userSteering = steering,
|
||||
)
|
||||
}
|
||||
|
||||
private fun encodeError(message: String): Frame.Text =
|
||||
Frame.Text(
|
||||
ProtocolSerializer.encodeServerMessage(
|
||||
|
||||
Reference in New Issue
Block a user