fix: address P0-4 chat zombie, P0-5 double SessionStarted, P0-6 steering wrong content

This commit is contained in:
2026-05-28 23:10:29 +04:00
parent cdee5f2245
commit eed557fe9c
8 changed files with 47 additions and 83 deletions
@@ -5,6 +5,7 @@ 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.events.ChatSessionStartedEvent
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.events.InferenceStartedEvent
@@ -45,6 +46,13 @@ suspend fun domainEventToServerMessage(
): ServerMessage? {
val seq = event.sequence
return when (val p = event.payload) {
is ChatSessionStartedEvent -> ServerMessage.SessionStarted(
sessionId = p.sessionId,
workflowId = "chat",
sequence = seq,
sessionSequence = sessionSequence,
)
is WorkflowStartedEvent -> ServerMessage.SessionStarted(
sessionId = p.sessionId,
workflowId = p.workflowId,
@@ -14,10 +14,10 @@ import com.correx.apps.server.protocol.ToolDecl
import com.correx.core.approvals.GrantScope
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ChatSessionStartedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.types.GrantId
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
@@ -275,8 +275,9 @@ class GlobalStreamHandler(private val module: ServerModule) {
val sessionId = msg.sessionId
log.info("starting chat session={}", sessionId.value)
// Emit WorkflowStartedEvent for the new session (no graph, no orchestrator).
val event = NewEvent(
// Emit ChatSessionStartedEvent for the new session (no graph, no orchestrator).
// streamGlobal picks this up and maps it to SessionStarted with the real sequence.
module.eventStore.append(NewEvent(
metadata = EventMetadata(
eventId = EventId(UUID.randomUUID().toString()),
sessionId = sessionId,
@@ -285,22 +286,8 @@ class GlobalStreamHandler(private val module: ServerModule) {
causationId = null,
correlationId = null,
),
payload = WorkflowStartedEvent(
sessionId = sessionId,
workflowId = "chat",
startStageId = StageId("none"),
),
)
module.eventStore.append(event)
// Send SessionStarted so the TUI creates the session entry immediately.
val started = ServerMessage.SessionStarted(
sessionId = sessionId,
workflowId = "chat",
sequence = 0L,
sessionSequence = 0L,
)
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(started)))
payload = ChatSessionStartedEvent(sessionId = sessionId),
))
// Call router and send response.
runCatching {
@@ -340,17 +327,12 @@ class GlobalStreamHandler(private val module: ServerModule) {
val sessionId: SessionId = TypeId(UUID.randomUUID().toString())
log.info("starting session={} workflow={}", sessionId.value, msg.workflowId)
// Send protocol messages FIRST. If the WS is already closed, these throw and the
// Send StageToolManifest FIRST. If the WS is already closed, this throws and the
// orchestrator never launches — no silent data loss. The exception propagates to the
// runCatching in handleClientMessage (caller) and is logged there.
val started = ServerMessage.SessionStarted(
sessionId = sessionId,
workflowId = msg.workflowId,
sequence = 0L,
sessionSequence = 0L,
)
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(started)))
// SessionStarted is not sent here — it is derived from WorkflowStartedEvent
// by streamGlobal via DomainEventMapper, ensuring exactly one SessionStarted
// per session with the correct sequence number (P0-5).
val stages = graph.stages.map { (stageId, stageConfig) ->
val tools = stageConfig.allowedTools.mapNotNull { toolName ->
module.toolRegistry.resolve(toolName)?.let { tool ->