motor multi-tenant que detecta conversas e contatos que pararam de responder e retoma o contato sozinho, seguindo uma régua configurável — e que agora também sabe desistir sozinho assim que o motivo de continuar deixa de existir.
Como funciona
roda em dois processos separados por cron. O detector (yafollowup-engine, a cada 2 minutos) varre as réguas ativas por tenant, chama get_idle_conversations para achar conversas paradas e agenda o passo 1. O disparador (yafollowup-sender, a cada 1 minuto) reivindica execuções agendadas (FOR UPDATE SKIP LOCKED, sem risco de dois ciclos disparando a mesma), gera a mensagem, envia e agenda o próximo passo.
Detecção de inatividade: o relógio de inatividade agora é resetado tanto por mensagem do cliente quanto por mensagem manual de um atendente humano (direction = 'outbound' AND sender_type = 'user') — antes só contava a última mensagem do cliente, o que fazia a régua disparar em cima de uma conversa que um humano acabara de responder. O filtro pode ser restrito por setor, canal (WhatsApp, Instagram ou Messenger — ver "Multi-canal" abaixo), funil, estágio do pipeline, se a conversa está com IA/humano/ambos, e por tags do contato (incluir ou excluir).
Sequência e prioridade: cada régua tem uma sequência de passos com atraso próprio (delay_minutes) e modo de mensagem — fixa, sorteio entre variações, template aprovado de WhatsApp (janela de 24h fechada) ou instrução para a IA gerar o texto lendo o histórico real. Quando duas ou mais réguas ativas poderiam capturar a mesma conversa no mesmo ciclo, vale prioridade explícita (slider 1-10 configurável por régua, editor FollowUpPriorityField) e, em empate, a régua mais específica (com filtro de estágio) vence; a conversa fica "reivindicada" pela régua de prioridade mais alta no ciclo mesmo que essa régua não chegue a agendar (por rate limit ou cooldown), então uma régua de prioridade menor não pega a sobra.
Travas antes do disparo: limite de envios por contato/mês (padrão 5, configurável de 1 a 30) e horário de silêncio — ver seção dedicada abaixo. No instante do disparo há um novo gate consolidado (should_send_followup, chamado antes de qualquer geração de mensagem) que revalida: conversa ainda aberta, conversa não "adiada" (snooze), régua ainda ativa, o responsável atual (IA/humano) ainda bate com o filtro da régua, e — quando o tenant tem tabela de consentimento — o contato não optou por sair. Qualquer falha nesse gate é tratada como decisão definitiva (cancela); erro de rede/banco é tratado como transitório (tenta de novo, nunca cancela por instabilidade).
Cancelamento automático — não é só supressão pontual. Existe uma camada de 4 gatilhos independentes no banco que cancelam TODOS os passos futuros ainda agendados de uma régua, não apenas o disparo do instante:
1. Cliente responde (trigger em messages, qualquer mensagem inbound do contato) — cancela tudo que estava scheduled/retry para aquela conversa, e ainda marca as execuções já enviadas como "respondida", registrando a latência da resposta.
2. Negócio é ganho ou perdido (trigger em deals.stage_id) — cancela a régua daquele deal, condicionado ao toggle stop_conditions.on_deal_won (ligado por padrão).
3. Conversa é transferida (trigger em conversations.inbox_id) — cancela, condicionado a stop_conditions.on_transfer (ligado por padrão).
4. Um passo falha (cascata) — se um passo intermediário falha definitivamente, os passos seguintes da mesma régua/conversa são cancelados em vez de continuar tentando uma sequência quebrada.
Além disso, no instante exato do disparo ainda existe a checagem pontual: se a resposta chegou entre o agendamento e o envio efetivo, o envio é suprimido e registrado como "suprimido — lead respondeu durante o disparo".
Réguas com filtro de pipeline/estágio também têm uma trava própria: se o negócio muda de estágio, muda de funil, é fechado ou é apagado, a sequência para (o cancelamento por deal won/lost acima cobre só ganho/perdido; este cobre qualquer saída do escopo configurado).
O ciclo da régua só reabre depois que a conversa é marcada como resolvida; sem essa resolução, o mesmo ciclo não reaciona a régua. Ao final da sequência sem resposta, a régua pode executar uma ação com atraso configurável (padrão 24h): nada, encerrar a conversa (pelo caminho oficial de encerramento, com motivo registrado), marcar deal como perdido (só em vendas), tag "frio" ou transferir para uma fila/operador específico (com cooldown próprio para não repetir a transferência).
Falhas e novas tentativas: execuções que falham (erro transiente, LLM incompleto, canal indisponível) entram em retentativa com backoff (1min, 5min, 15min) até 3 tentativas antes de ir para "falhou" definitivamente, e cada tentativa fica registrada. Um checador de saúde roda a cada ciclo do detector e alerta a equipe internamente quando a taxa de terminações anormais (falha, canal não entregável, template só-WhatsApp) de um canal passa de 20 casos e mais de 50% do volume na última hora — pensado para nunca mais deixar um travamento de régua passar despercebido por dias.
Multi-canal: a régua não é mais exclusiva de WhatsApp. O escopo de canais é configurável na tela (WhatsApp, Instagram, Messenger — pelo menos um precisa ficar marcado) e o disparo resolve o destino certo (WhatsApp via telefone, Instagram via ID do Instagram, Messenger via PSID) e o identificador correto do contato para cada canal. Quando a régua inclui um canal sem janela de 24h/template (Instagram, Messenger), a tela avisa se algum passo da sequência ultrapassa a janela de resposta gratuita.