fix: PAUSED resume after server restart, diff viewer only shows last tool

This commit is contained in:
2026-05-26 22:26:56 +04:00
parent 1af42befca
commit 92bea6c2c4
3 changed files with 56 additions and 14 deletions
@@ -9,7 +9,9 @@ import com.correx.core.approvals.DefaultApprovalRepository
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.config.ApprovalConfig
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.SessionId
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
import com.correx.core.kernel.orchestration.OrchestrationConfig
import com.correx.core.kernel.orchestration.OrchestrationRepository
@@ -17,6 +19,7 @@ import com.correx.core.router.RouterFacade
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.tools.registry.ToolRegistry
import com.correx.core.events.orchestration.OrchestrationStatus
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -55,6 +58,7 @@ class ServerModule(
)
private var subscriptionJob: Job? = null
private val activeSessionJobs = ConcurrentHashMap<SessionId, Job>()
/**
* Subscribe to the event store and forward [ApprovalRequestedEvent] payloads to
@@ -73,26 +77,50 @@ class ServerModule(
.filter { it.payload is ApprovalRequestedEvent }
.onEach { approvalCoordinator.onApprovalRequested(it.payload as ApprovalRequestedEvent) }
.launchIn(moduleScope)
// When a PAUSED session's approval is resolved without a live orchestrator coroutine
// (server was restarted while approval was pending), emit OrchestrationResumedEvent
// triggers this subscription to continue the session.
eventStore.subscribeAll()
.filter { it.payload is OrchestrationResumedEvent }
.onEach { event ->
val sessionId = (event.payload as OrchestrationResumedEvent).sessionId
if (activeSessionJobs.containsKey(sessionId)) return@onEach
val orchState = orchestrationRepository.getState(sessionId)
if (orchState.status != OrchestrationStatus.RUNNING) return@onEach
val graph = workflowRegistry.find(orchState.workflowId) ?: return@onEach
launchSessionResume(sessionId, graph)
}
.launchIn(moduleScope)
}
private fun launchSessionResume(sessionId: SessionId, graph: com.correx.core.transitions.graph.WorkflowGraph) {
if (activeSessionJobs.containsKey(sessionId)) return
val job = moduleScope.launch {
runCatching {
orchestrator.resume(sessionId, graph, defaultOrchestrationConfig)
}.onFailure { e ->
log.error("resumeSession: session={} failed", sessionId.value, e)
}
activeSessionJobs.remove(sessionId)
}
activeSessionJobs[sessionId] = job
}
private fun resumeAbandonedSessions() {
eventStore.allSessionIds().forEach { sessionId ->
val orchState = orchestrationRepository.getState(sessionId)
if (orchState.status != OrchestrationStatus.RUNNING &&
orchState.status != OrchestrationStatus.PAUSED
) return@forEach
// Skip PAUSED sessions — they are waiting for user approval, not for computation.
// preRegisterPendingApprovals() already routes their responses. When the user
// approves, submitApprovalDecision emits OrchestrationResumedEvent which triggers
// the subscription above to continue the session.
if (orchState.status != OrchestrationStatus.RUNNING) return@forEach
val graph = workflowRegistry.find(orchState.workflowId) ?: run {
log.warn("resumeAbandoned: no graph for session={} workflowId={}", sessionId.value, orchState.workflowId)
return@forEach
}
log.info("resumeAbandoned: launching session={} workflow={}", sessionId.value, orchState.workflowId)
moduleScope.launch {
runCatching {
orchestrator.resume(sessionId, graph, defaultOrchestrationConfig)
}.onFailure { e ->
log.error("resumeAbandoned: session={} failed", sessionId.value, e)
}
}
launchSessionResume(sessionId, graph)
}
}