diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt b/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt index ac742a09..ed7e7397 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt @@ -16,13 +16,18 @@ import com.correx.core.kernel.orchestration.OrchestrationRepository 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory + +private val log = LoggerFactory.getLogger(ServerModule::class.java) class ServerModule( val orchestrator: DefaultSessionOrchestrator, @@ -63,12 +68,34 @@ class ServerModule( fun start() { if (subscriptionJob != null) return preRegisterPendingApprovals() + resumeAbandonedSessions() subscriptionJob = eventStore.subscribeAll() .filter { it.payload is ApprovalRequestedEvent } .onEach { approvalCoordinator.onApprovalRequested(it.payload as ApprovalRequestedEvent) } .launchIn(moduleScope) } + private fun resumeAbandonedSessions() { + eventStore.allSessionIds().forEach { sessionId -> + val orchState = orchestrationRepository.getState(sessionId) + if (orchState.status != OrchestrationStatus.RUNNING && + orchState.status != OrchestrationStatus.PAUSED + ) 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) + } + } + } + } + private fun preRegisterPendingApprovals() { val projector = ApprovalProjector(DefaultApprovalReducer()) eventStore.allSessionIds().forEach { sessionId -> diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt index 2eabbb1a..9372150e 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultOrchestrationReducer.kt @@ -4,6 +4,7 @@ import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent import com.correx.core.events.events.RetryAttemptedEvent import com.correx.core.events.events.StoredEvent +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 @@ -44,6 +45,8 @@ class DefaultOrchestrationReducer : OrchestrationReducer { pauseReason = null, ) + is TransitionExecutedEvent -> state.copy(currentStageId = p.to) + is RetryAttemptedEvent -> state.copy( status = OrchestrationStatus.RUNNING, retryCount = p.attemptNumber, diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt index 062e996d..a79be8e7 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt @@ -136,6 +136,22 @@ class DefaultSessionOrchestrator( } + suspend fun resume( + sessionId: SessionId, + graph: WorkflowGraph, + config: OrchestrationConfig, + ): WorkflowResult { + val stageId = orchestrationRepository.getState(sessionId).currentStageId + ?: return WorkflowResult.Failed(sessionId, "resume: no currentStageId", retryExhausted = false) + log.info("[Orchestrator] resuming session={} workflow={} stage={}", sessionId.value, graph.id, stageId.value) + val base = ExecutionContext(graph, sessionId, 0, stageId, config, null, null) + val enriched = base.enrich() + return when (val result = enterStage(enriched, stageId)) { + is StepResult.Continue -> step(result.ctx.copy(currentStageId = stageId)) + is StepResult.Terminal -> result.result + } + } + override suspend fun cancel(sessionId: SessionId) { cancellations.getOrPut(sessionId) { AtomicBoolean(false) }.set(true) }