import { PrismaClient } from '@b2bcall/database'; import { logger } from './logger'; // Reconciliação de estados órfãos (agente.md seção 51): tentativas que // ficaram presas em estados transitórios (ORIGINATING/RINGING/ANSWERED/ // QUEUED/AGENT_CONNECTED) por tempo maior do que qualquer timeout normal // explica — geralmente por queda do worker no meio de uma chamada. Nunca // deixa uma tentativa presa para sempre, o que travaria o lead // indefinidamente (nunca mais elegível para nova tentativa). const STALE_ATTEMPT_MINUTES = 10; export async function reconcileOrphanedAttempts(prisma: PrismaClient): Promise { const staleSince = new Date(Date.now() - STALE_ATTEMPT_MINUTES * 60_000); const orphaned = await prisma.dialAttempt.findMany({ where: { state: { in: ['ORIGINATING', 'RINGING', 'ANSWERED', 'QUEUED', 'AGENT_CONNECTED'] }, endedAt: null, startedAt: { lt: staleSince }, }, }); if (orphaned.length === 0) return 0; for (const attempt of orphaned) { await prisma.$transaction([ prisma.dialAttempt.update({ where: { id: attempt.id }, data: { state: 'FAILED', hangupCause: 'RECONCILED_ORPHAN', endedAt: new Date() }, }), prisma.lead.updateMany({ where: { id: attempt.leadId, status: { in: ['DIALING', 'RINGING', 'ANSWERED'] } }, data: { status: 'READY', nextAttemptAt: new Date() }, }), ]); } logger.warn({ count: orphaned.length }, 'Reconciliação: tentativas órfãs corrigidas após possível queda de worker'); return orphaned.length; }