From a0da220e8e10d91f7449a359adedafbaa5dcd9d2 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 12 Jun 2026 12:26:04 +0400 Subject: [PATCH] fix(server): boot/event-driven resume paths get rehydrate + freestyle handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resumeAbandonedSessions (boot pickup of RUNNING sessions) and the restart-while-approval-pending subscription still used the bare launchSessionResume: no artifact-cache rehydrate and no freestyle plan-lock handoff. A freestyle session killed mid-planning was grabbed by the boot path before the operator could POST /resume, completed the planning graph, and stranded — WorkflowCompleted but never locked (live repro: session b36b0e45, 2026-06-12). Both paths now use launchSessionResumeWithRehydrate; the bare launcher is deleted. --- .../com/correx/apps/server/ServerModule.kt | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) 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 62f564c8..d005f0e6 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 @@ -168,7 +168,7 @@ class ServerModule( val orchState = orchestrationRepository.getState(sessionId) if (orchState.status != OrchestrationStatus.RUNNING) return@onEach val graph = workflowRegistry.find(orchState.workflowId) ?: return@onEach - launchSessionResume(sessionId, graph) + launchSessionResumeWithRehydrate(sessionId, graph) } .launchIn(moduleScope) @@ -309,23 +309,12 @@ class ServerModule( ) } - private fun launchSessionResume(sessionId: SessionId, graph: com.correx.core.transitions.graph.WorkflowGraph) { - activeSessionJobs.computeIfAbsent(sessionId) { - moduleScope.launch { - runCatching { - orchestrator.resume(sessionId, graph, orchestrationConfig()) - }.onFailure { e -> - log.error("resumeSession: session={} failed", sessionId.value, e) - } - activeSessionJobs.remove(sessionId) - } - } - } - /** * Rehydrates [artifactContentCache] from durable events and then resumes the session. - * Called by [POST /sessions/{id}/resume] to restart a session after a server restart - * without re-running already-completed stages. + * Used by [POST /sessions/{id}/resume], boot-time abandoned-session pickup, and the + * restart-while-approval-pending subscription — every resume needs the rehydrate + * (pre-restart `needs`-artifacts live only in events/CAS) and the freestyle handoff + * (a planning graph that completes on resume must still lock and run phase 2). */ fun launchSessionResumeWithRehydrate(sessionId: SessionId, graph: com.correx.core.transitions.graph.WorkflowGraph) { activeSessionJobs.computeIfAbsent(sessionId) { @@ -365,7 +354,7 @@ class ServerModule( return@forEach } log.info("resumeAbandoned: launching session={} workflow={}", sessionId.value, orchState.workflowId) - launchSessionResume(sessionId, graph) + launchSessionResumeWithRehydrate(sessionId, graph) } }