feat(server+tui): event bridge — live streaming and historical replay
- SessionEventBridge: replaySnapshot() sends all historical events on connect;
streamLive(sessionId) subscribes to live event flow per session
- DomainEventMapper: maps 14 domain event types to ServerMessage; reads
InferenceCompleted response text from ArtifactStore with empty-string fallback
- GlobalStreamHandler: wires bridge on connect for replay, launches per-session
live-stream job on StartSession, cancels all jobs on disconnect; removes
ProtocolError("ping") heartbeat hack
- SessionStreamHandler: fixes replay to use DomainEventMapper instead of
emitting SessionStarted for every event; injects real artifactStore;
removes ProtocolError("ping") heartbeat; adds structured logging throughout
- Main: startup log block showing port, model config, sandbox, stores, tools,
providers
- SessionsReducer: StageCompleted/StageFailed now clears currentStage; adds
5 missing tests (InferenceCompleted, StageStarted, ToolCompleted, SessionPaused)
- RouterPanel: new TUI widget rendering active session's lastResponseText
- TuiWsClient: remove println that was polluting the TUI on WS connect failure
This commit is contained in:
@@ -35,8 +35,11 @@ import com.correx.infrastructure.tools.ToolConfig
|
||||
import com.correx.core.events.EventDispatcher
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.netty.Netty
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Paths
|
||||
|
||||
private val log = LoggerFactory.getLogger("com.correx.apps.server.Main")
|
||||
|
||||
fun main() {
|
||||
val artifactStore = InfrastructureModule.createArtifactStore()
|
||||
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))
|
||||
@@ -47,8 +50,6 @@ fun main() {
|
||||
val toolRegistry = InfrastructureModule.createToolRegistry(buildToolConfig(artifactStore, sandboxRoot))
|
||||
val toolExecutor = InfrastructureModule.createToolExecutor(
|
||||
registry = toolRegistry,
|
||||
approvalEngine = approvalEngine,
|
||||
eventStore = eventStore,
|
||||
eventDispatcher = EventDispatcher(eventStore),
|
||||
workDir = sandboxRoot,
|
||||
)
|
||||
@@ -72,10 +73,26 @@ fun main() {
|
||||
val module = ServerModule(
|
||||
orchestrator = orchestrator,
|
||||
eventStore = eventStore,
|
||||
artifactStore = artifactStore,
|
||||
sessionRepository = repositories.sessionRepository,
|
||||
workflowRegistry = FileSystemWorkflowRegistry(InfrastructureModule.createWorkflowLoader()),
|
||||
providerRegistry = infraRegistry.asServerRegistry(),
|
||||
)
|
||||
val modelId = System.getenv("CORREX_MODEL_ID") ?: "default"
|
||||
val modelPath = System.getenv("CORREX_MODEL_PATH") ?: "(not set)"
|
||||
val llamaUrl = System.getenv("CORREX_LLAMA_URL") ?: "http://127.0.0.1:10000"
|
||||
log.info("=== correx server starting ===")
|
||||
log.info(" port : 8080")
|
||||
log.info(" model id : {}", modelId)
|
||||
log.info(" model path : {}", modelPath)
|
||||
log.info(" llama url : {}", llamaUrl)
|
||||
log.info(" sandbox root : {}", sandboxRoot)
|
||||
log.info(" event store : {}", eventStore::class.simpleName)
|
||||
log.info(" artifact store: {}", artifactStore::class.simpleName)
|
||||
log.info(" tools : {}", toolRegistry.all().map { it.name })
|
||||
log.info(" providers : {} registered", infraRegistry.listAll().size)
|
||||
log.info("==============================")
|
||||
|
||||
embeddedServer(Netty, port = 8080) { configureServer(module) }.start(wait = true)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.correx.apps.server
|
||||
|
||||
import com.correx.apps.server.registry.ProviderRegistry
|
||||
import com.correx.apps.server.registry.WorkflowRegistry
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
||||
import com.correx.core.sessions.DefaultSessionRepository
|
||||
@@ -9,6 +10,7 @@ import com.correx.core.sessions.DefaultSessionRepository
|
||||
data class ServerModule(
|
||||
val orchestrator: DefaultSessionOrchestrator,
|
||||
val eventStore: EventStore,
|
||||
val artifactStore: ArtifactStore,
|
||||
val sessionRepository: DefaultSessionRepository,
|
||||
val workflowRegistry: WorkflowRegistry,
|
||||
val providerRegistry: ProviderRegistry,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.correx.apps.server.bridge
|
||||
|
||||
import com.correx.apps.server.protocol.PauseReason
|
||||
import com.correx.apps.server.protocol.RiskSummaryDto
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.types.ArtifactId
|
||||
import com.correx.core.events.events.InferenceCompletedEvent
|
||||
import com.correx.core.events.events.InferenceStartedEvent
|
||||
import com.correx.core.events.events.InferenceTimeoutEvent
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.StageFailedEvent
|
||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||
import com.correx.core.events.events.ToolExecutionFailedEvent
|
||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||
import com.correx.core.events.events.WorkflowFailedEvent
|
||||
import com.correx.core.events.events.WorkflowStartedEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
|
||||
class DomainEventMapper(private val artifactStore: ArtifactStore = NoopArtifactStore) {
|
||||
suspend fun map(event: StoredEvent): ServerMessage? =
|
||||
domainEventToServerMessage(event, artifactStore)
|
||||
}
|
||||
|
||||
private object NoopArtifactStore : ArtifactStore {
|
||||
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("noop")
|
||||
override suspend fun get(id: ArtifactId): ByteArray? = null
|
||||
override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
suspend fun domainEventToServerMessage(event: StoredEvent, artifactStore: ArtifactStore): ServerMessage? =
|
||||
when (val p = event.payload) {
|
||||
is WorkflowStartedEvent -> mapWorkflowStarted(p)
|
||||
is WorkflowCompletedEvent -> ServerMessage.SessionCompleted(sessionId = p.sessionId)
|
||||
is WorkflowFailedEvent -> ServerMessage.SessionFailed(sessionId = p.sessionId, reason = p.reason)
|
||||
is TransitionExecutedEvent -> ServerMessage.StageStarted(sessionId = p.sessionId, stageId = p.to)
|
||||
is StageCompletedEvent -> ServerMessage.StageCompleted(sessionId = p.sessionId, stageId = p.stageId)
|
||||
is StageFailedEvent -> ServerMessage.StageFailed(
|
||||
sessionId = p.sessionId, stageId = p.stageId, reason = p.reason,
|
||||
)
|
||||
is OrchestrationPausedEvent -> mapOrchestrationPaused(p)
|
||||
is InferenceStartedEvent -> ServerMessage.InferenceStarted(sessionId = p.sessionId, stageId = p.stageId)
|
||||
is InferenceCompletedEvent -> mapInferenceCompleted(p, artifactStore)
|
||||
is InferenceTimeoutEvent -> ServerMessage.InferenceTimedOut(
|
||||
sessionId = p.sessionId, stageId = p.stageId, elapsedMs = p.timeoutMs,
|
||||
)
|
||||
is ToolInvocationRequestedEvent -> ServerMessage.ToolStarted(
|
||||
sessionId = p.sessionId, toolName = p.toolName, tier = p.tier,
|
||||
)
|
||||
is ToolExecutionCompletedEvent -> ServerMessage.ToolCompleted(
|
||||
sessionId = p.sessionId, toolName = p.toolName, outputSummary = p.receipt.outputSummary,
|
||||
)
|
||||
is ToolExecutionFailedEvent -> ServerMessage.ToolFailed(
|
||||
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
|
||||
)
|
||||
is ToolExecutionRejectedEvent -> ServerMessage.ToolRejected(
|
||||
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
|
||||
)
|
||||
is ApprovalRequestedEvent -> mapApprovalRequested(p)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun mapWorkflowStarted(p: WorkflowStartedEvent): ServerMessage =
|
||||
ServerMessage.SessionStarted(sessionId = p.sessionId, workflowId = p.startStageId.value)
|
||||
|
||||
private fun mapOrchestrationPaused(p: OrchestrationPausedEvent): ServerMessage {
|
||||
val reason = if (p.reason == "APPROVAL_PENDING") PauseReason.APPROVAL_PENDING else PauseReason.USER_REQUESTED
|
||||
return ServerMessage.SessionPaused(sessionId = p.sessionId, reason = reason)
|
||||
}
|
||||
|
||||
private suspend fun mapInferenceCompleted(p: InferenceCompletedEvent, artifactStore: ArtifactStore): ServerMessage {
|
||||
val text = runCatching {
|
||||
artifactStore.get(p.responseArtifactId)?.toString(Charsets.UTF_8) ?: ""
|
||||
}.getOrElse { "" }
|
||||
return ServerMessage.InferenceCompleted(
|
||||
sessionId = p.sessionId, stageId = p.stageId, outputSummary = text, responseText = text,
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapApprovalRequested(p: ApprovalRequestedEvent): ServerMessage =
|
||||
ServerMessage.ApprovalRequired(
|
||||
sessionId = p.sessionId,
|
||||
requestId = p.requestId,
|
||||
tier = p.tier,
|
||||
riskSummary = RiskSummaryDto("unknown", emptyList(), ""),
|
||||
toolName = null,
|
||||
preview = null,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.correx.apps.server.bridge
|
||||
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.SessionId
|
||||
|
||||
class SessionEventBridge(
|
||||
private val eventStore: EventStore,
|
||||
private val artifactStore: ArtifactStore,
|
||||
private val send: suspend (ServerMessage) -> Unit,
|
||||
) {
|
||||
suspend fun replaySnapshot() {
|
||||
eventStore.allEvents().toList()
|
||||
.mapNotNull { domainEventToServerMessage(it, artifactStore) }
|
||||
.forEach { send(it) }
|
||||
}
|
||||
|
||||
suspend fun streamLive(sessionId: SessionId) {
|
||||
eventStore.subscribe(sessionId).collect { event ->
|
||||
domainEventToServerMessage(event, artifactStore)?.let { send(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.correx.apps.server.ws
|
||||
|
||||
import com.correx.apps.server.ServerModule
|
||||
import com.correx.apps.server.bridge.SessionEventBridge
|
||||
import com.correx.apps.server.protocol.ClientMessage
|
||||
import com.correx.apps.server.protocol.ProtocolSerializer
|
||||
import com.correx.apps.server.protocol.ProviderHealthDto
|
||||
@@ -16,31 +17,34 @@ import com.correx.core.utils.TypeId
|
||||
import io.ktor.server.websocket.DefaultWebSocketServerSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
private const val HEARTBEAT_INTERVAL_MS = 30_000L
|
||||
private val log = LoggerFactory.getLogger(GlobalStreamHandler::class.java)
|
||||
|
||||
class GlobalStreamHandler(private val module: ServerModule) {
|
||||
|
||||
suspend fun handle(session: DefaultWebSocketServerSession) {
|
||||
log.info("client connected")
|
||||
sendInitialSnapshot(session)
|
||||
|
||||
val heartbeatJob = session.launch {
|
||||
while (isActive) {
|
||||
delay(HEARTBEAT_INTERVAL_MS)
|
||||
val ping = ServerMessage.ProtocolError("ping")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(ping)))
|
||||
}
|
||||
val sendFrame: suspend (ServerMessage) -> Unit = {
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(it)))
|
||||
}
|
||||
|
||||
val bridge = SessionEventBridge(
|
||||
eventStore = module.eventStore,
|
||||
artifactStore = module.artifactStore,
|
||||
send = sendFrame,
|
||||
)
|
||||
sendInitialSnapshot(session)
|
||||
bridge.replaySnapshot()
|
||||
|
||||
val liveStreamJobs = mutableMapOf<SessionId, Job>()
|
||||
|
||||
try {
|
||||
for (frame in session.incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
@@ -49,7 +53,7 @@ class GlobalStreamHandler(private val module: ServerModule) {
|
||||
.onSuccess { msg ->
|
||||
log.debug("recv: {}", msg::class.simpleName)
|
||||
runCatching {
|
||||
handleClientMessage(session, msg)
|
||||
handleClientMessage(session, msg, liveStreamJobs, sendFrame)
|
||||
}.onFailure {
|
||||
log.error("handle failed", it)
|
||||
}
|
||||
@@ -64,7 +68,7 @@ class GlobalStreamHandler(private val module: ServerModule) {
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
log.info("client disconnected")
|
||||
} finally {
|
||||
heartbeatJob.cancel()
|
||||
liveStreamJobs.values.forEach { it.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,64 +92,76 @@ class GlobalStreamHandler(private val module: ServerModule) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleClientMessage(session: DefaultWebSocketServerSession, msg: ClientMessage) {
|
||||
private suspend fun handleClientMessage(
|
||||
session: DefaultWebSocketServerSession,
|
||||
msg: ClientMessage,
|
||||
liveStreamJobs: MutableMap<SessionId, Job>,
|
||||
sendFrame: suspend (ServerMessage) -> Unit,
|
||||
) {
|
||||
when (msg) {
|
||||
is ClientMessage.Ping -> Unit
|
||||
|
||||
is ClientMessage.StartSession -> {
|
||||
val graph = module.workflowRegistry.find(msg.workflowId)
|
||||
log.info("find returned: {}", graph)
|
||||
if (graph == null) {
|
||||
val error = ServerMessage.ProtocolError("Unknown workflowId: ${msg.workflowId}")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||
return
|
||||
}
|
||||
val sessionId: SessionId = TypeId(UUID.randomUUID().toString())
|
||||
log.info("starting session={} workflow={}", sessionId.value, msg.workflowId)
|
||||
session.launch {
|
||||
val config = OrchestrationConfig(
|
||||
defaultSystemPromptPath = "~/.config/correx/prompts/default_system.md",
|
||||
)
|
||||
runCatching { module.orchestrator.run(sessionId, graph, config) }
|
||||
.onFailure { ex ->
|
||||
log.error("run failed for session={}: {}", sessionId.value, ex.message, ex)
|
||||
module.eventStore.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId(UUID.randomUUID().toString()),
|
||||
sessionId = sessionId,
|
||||
timestamp = Clock.System.now(),
|
||||
schemaVersion = 1,
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
payload = WorkflowFailedEvent(
|
||||
sessionId = sessionId,
|
||||
stageId = graph.start,
|
||||
reason = ex.message ?: "unexpected orchestrator failure",
|
||||
retryExhausted = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val started = ServerMessage.SessionStarted(sessionId = sessionId, workflowId = msg.workflowId)
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(started)))
|
||||
}
|
||||
|
||||
is ClientMessage.CancelSession -> {
|
||||
module.orchestrator.cancel(msg.sessionId)
|
||||
}
|
||||
|
||||
is ClientMessage.ResumeSession -> {
|
||||
val error = ServerMessage.ProtocolError("ResumeSession not supported")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||
}
|
||||
|
||||
is ClientMessage.ApprovalResponse -> {
|
||||
val error = ServerMessage.ProtocolError("ApprovalResponse not supported on global stream")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||
}
|
||||
is ClientMessage.StartSession -> handleStartSession(session, msg, liveStreamJobs, 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 not supported on global stream"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeError(message: String): Frame.Text =
|
||||
Frame.Text(ProtocolSerializer.encodeServerMessage(ServerMessage.ProtocolError(message)))
|
||||
|
||||
private suspend fun handleStartSession(
|
||||
session: DefaultWebSocketServerSession,
|
||||
msg: ClientMessage.StartSession,
|
||||
liveStreamJobs: MutableMap<SessionId, Job>,
|
||||
sendFrame: suspend (ServerMessage) -> Unit,
|
||||
) {
|
||||
val graph = module.workflowRegistry.find(msg.workflowId)
|
||||
log.info("find returned: {}", graph)
|
||||
if (graph == null) {
|
||||
val error = ServerMessage.ProtocolError("Unknown workflowId: ${msg.workflowId}")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||
return
|
||||
}
|
||||
val sessionId: SessionId = TypeId(UUID.randomUUID().toString())
|
||||
log.info("starting session={} workflow={}", sessionId.value, msg.workflowId)
|
||||
session.launch {
|
||||
val config = OrchestrationConfig(
|
||||
defaultSystemPromptPath = "~/.config/correx/prompts/default_system.md",
|
||||
)
|
||||
runCatching { module.orchestrator.run(sessionId, graph, config) }
|
||||
.onFailure { ex ->
|
||||
log.error("run failed for session={}: {}", sessionId.value, ex.message, ex)
|
||||
module.eventStore.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId(UUID.randomUUID().toString()),
|
||||
sessionId = sessionId,
|
||||
timestamp = Clock.System.now(),
|
||||
schemaVersion = 1,
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
payload = WorkflowFailedEvent(
|
||||
sessionId = sessionId,
|
||||
stageId = graph.start,
|
||||
reason = ex.message ?: "unexpected orchestrator failure",
|
||||
retryExhausted = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
liveStreamJobs.put(sessionId, session.launch {
|
||||
val bridge = SessionEventBridge(
|
||||
eventStore = module.eventStore,
|
||||
artifactStore = module.artifactStore,
|
||||
send = sendFrame,
|
||||
)
|
||||
bridge.streamLive(sessionId)
|
||||
})?.cancel()
|
||||
val started = ServerMessage.SessionStarted(sessionId = sessionId, workflowId = msg.workflowId)
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(started)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.correx.apps.server.ws
|
||||
|
||||
import com.correx.apps.server.ServerModule
|
||||
import com.correx.apps.server.bridge.DomainEventMapper
|
||||
import com.correx.apps.server.protocol.ApprovalDecision
|
||||
import com.correx.apps.server.protocol.ClientMessage
|
||||
import com.correx.apps.server.protocol.ProtocolSerializer
|
||||
@@ -19,37 +20,28 @@ import io.ktor.server.websocket.DefaultWebSocketServerSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
private const val HEARTBEAT_INTERVAL_MS = 30_000L
|
||||
private val log = LoggerFactory.getLogger(SessionStreamHandler::class.java)
|
||||
|
||||
class SessionStreamHandler(private val module: ServerModule) {
|
||||
|
||||
private val mapper = DomainEventMapper(module.artifactStore)
|
||||
|
||||
suspend fun handle(session: DefaultWebSocketServerSession, sessionId: SessionId, lastEventId: Long?) {
|
||||
log.info("client connected session={} lastEventId={}", sessionId.value, lastEventId)
|
||||
|
||||
val replayEvents = if (lastEventId != null) {
|
||||
module.eventStore.readFrom(sessionId, lastEventId)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
runCatching { module.sessionRepository.getSession(sessionId) }.onSuccess {
|
||||
val snapshot = ServerMessage.SessionStarted(sessionId, sessionId.value)
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(snapshot)))
|
||||
}
|
||||
log.debug("replaying {} event(s) for session={}", replayEvents.size, sessionId.value)
|
||||
|
||||
for (event in replayEvents) {
|
||||
val msg = ServerMessage.SessionStarted(sessionId, event.metadata.eventId.value)
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(msg)))
|
||||
}
|
||||
|
||||
val heartbeatJob = session.launch {
|
||||
while (isActive) {
|
||||
delay(HEARTBEAT_INTERVAL_MS)
|
||||
val ping = ServerMessage.ProtocolError("ping")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(ping)))
|
||||
mapper.map(event)?.let { msg ->
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(msg)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +50,19 @@ class SessionStreamHandler(private val module: ServerModule) {
|
||||
if (frame is Frame.Text) {
|
||||
val text = frame.readText()
|
||||
runCatching { ProtocolSerializer.decodeClientMessage(text) }
|
||||
.onSuccess { msg -> handleClientMessage(session, msg) }
|
||||
.onSuccess { msg ->
|
||||
log.debug("recv: {}", msg::class.simpleName)
|
||||
handleClientMessage(session, msg)
|
||||
}
|
||||
.onFailure {
|
||||
log.warn("decode error: {}", it.message)
|
||||
val error = ServerMessage.ProtocolError("Unknown message: ${it.message}")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
// client disconnected
|
||||
} finally {
|
||||
heartbeatJob.cancel()
|
||||
log.info("client disconnected session={}", sessionId.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,13 +70,16 @@ class SessionStreamHandler(private val module: ServerModule) {
|
||||
when (msg) {
|
||||
is ClientMessage.Ping -> Unit
|
||||
is ClientMessage.CancelSession -> {
|
||||
module.orchestrator.cancel(msg.sessionId)
|
||||
runCatching { module.orchestrator.cancel(msg.sessionId) }
|
||||
.onFailure { log.error("cancel failed for session={}: {}", msg.sessionId.value, it.message, it) }
|
||||
}
|
||||
is ClientMessage.ApprovalResponse -> {
|
||||
val domainDecision = msg.toDomainDecision(msg.requestId.value)
|
||||
runCatching { module.orchestrator.submitApprovalDecision(msg.requestId, domainDecision) }
|
||||
.onFailure { log.error("submitApprovalDecision failed for request={}: {}", msg.requestId.value, it.message, it) }
|
||||
}
|
||||
else -> {
|
||||
log.warn("unexpected message type={} in session stream", msg::class.simpleName)
|
||||
val error = ServerMessage.ProtocolError("Unexpected message type in session stream")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user