- Reconciliação de tentativas órfãs após restart (reconciliation.ts), rodando a cada 60s. - Vínculo real agente<->chamada (agent-call-binding.ts): claim atômico de agente disponível via FOR UPDATE SKIP LOCKED, DialAttempt.agentId populado no connect, ciclo AVAILABLE -> IN_CALL -> WRAP_UP -> AVAILABLE. - Corrige abandonRate (EWMA) nunca atualizado pelo campaign-worker real — agora o fluxo QUEUED -> connect-or-abandon atualiza as estatísticas de fato usadas pelo predictive engine. - Novo módulo de relatórios: /api/reports/calls (+export CSV), /metrics (TME/TMA/abandono), /agents/:id. - Novo módulo de dashboard: /api/dashboard, /calls-by-hour, /campaigns/:id (Postgres + snapshot EWMA do Redis). - Novo módulo de compliance: ComplianceSettings configurável + /api/compliance/settings e /indicators com contadores reais. - Validado end-to-end contra containers reais (campanha de teste em modo simulação): agentId no connect, ciclo de estado do agente e abandonRate todos confirmados corrigidos com dados reais, não só no harness isolado. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoVkLx1KsvtT1C88dRS3QW
41 lines
1.5 KiB
TypeScript
41 lines
1.5 KiB
TypeScript
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<number> {
|
|
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;
|
|
}
|