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.