fix(server): boot/event-driven resume paths get rehydrate + freestyle handoff

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.
This commit is contained in:
2026-06-12 12:26:04 +04:00
parent 35e921c6d1
commit a0da220e8e
@@ -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)
}
}