From 573e1b40d20400a490f8ac09329ad9e14c228e2c Mon Sep 17 00:00:00 2001 From: Lukas May Date: Thu, 5 Mar 2026 21:04:39 +0100 Subject: [PATCH] fix: Re-queue approved phases before dispatch to survive server restarts The in-memory phaseQueue (Map) in DefaultPhaseDispatchManager is lost on server restart. After approving a phase review, dispatchNextPhase() found nothing in the empty queue, so the next unblocked phase never started. Now the orchestrator re-queues all approved phases for the initiative from the DB before attempting to dispatch, making the queue self-healing. --- apps/server/execution/orchestrator.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/server/execution/orchestrator.ts b/apps/server/execution/orchestrator.ts index dfa0483..aefce3b 100644 --- a/apps/server/execution/orchestrator.ts +++ b/apps/server/execution/orchestrator.ts @@ -234,6 +234,9 @@ export class ExecutionOrchestrator { await this.mergePhaseIntoInitiative(phaseId); await this.phaseDispatchManager.completePhase(phaseId); + // Re-queue approved phases (self-healing: survives server restarts that wipe in-memory queue) + await this.requeueApprovedPhases(phase.initiativeId); + // Check if this was the last phase — if so, trigger initiative review const dispatched = await this.phaseDispatchManager.dispatchNextPhase(); if (!dispatched.success) { @@ -305,6 +308,9 @@ export class ExecutionOrchestrator { await this.mergePhaseIntoInitiative(phaseId); await this.phaseDispatchManager.completePhase(phaseId); + // Re-queue approved phases (self-healing: survives server restarts that wipe in-memory queue) + await this.requeueApprovedPhases(phase.initiativeId); + // Check if this was the last phase — if so, trigger initiative review const dispatched = await this.phaseDispatchManager.dispatchNextPhase(); if (!dispatched.success) { @@ -395,6 +401,25 @@ export class ExecutionOrchestrator { return { taskId: task.id }; } + /** + * Re-queue approved phases for an initiative into the in-memory dispatch queue. + * Self-healing: ensures phases aren't lost if the server restarted since the + * initial queueAllPhases() call. + */ + private async requeueApprovedPhases(initiativeId: string): Promise { + const phases = await this.phaseRepository.findByInitiativeId(initiativeId); + for (const p of phases) { + if (p.status === 'approved') { + try { + await this.phaseDispatchManager.queuePhase(p.id); + log.info({ phaseId: p.id }, 're-queued approved phase'); + } catch { + // Already queued or status changed — safe to ignore + } + } + } + } + /** * Check if all phases for an initiative are completed. * If so, set initiative to pending_review and emit event.