Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Latest commit

 

History

History
166 lines (130 loc) · 8.87 KB

File metadata and controls

166 lines (130 loc) · 8.87 KB

W1-GATILHOS — relatório

Worktree: /Users/rafaelmelgaco/fv-gatilhos · Branch: fv/gatilhos (de feat/followup-vivo = 4f89a0da) Autor: DevGatilhos · Data: 2026-08-10


1. O que já era emitido em event_log (a lista levantada)

Trinta e cinco tipos, extraídos dos p_event_type de toda chamada a emit_event em lib/, app/ e workers/ no SHA 4f89a0da (excluindo *.test.ts):

ai.handoff_resolved            lead.created                   message.failed
ai.handoff_triggered           lead.risk_backlog_seeded       message.received
ai.responded                   lead.stage_changed             message.send_requested
ai.sentiment_alert             lead.tag_added                 message.sent
ai_agent.dispatch_requested    lead.updated                   org.updated
ai_agent.run_started           lgpd.data_request_received     user.profile_updated
contact.anonymized             lgpd.export_delivered          whatsapp.chat_id_not_recognized
contact.created                lgpd.export_generated          whatsapp.conversation_mark_failed
contact.tag_added              lgpd.redact_applied            knowledge_source.updated
contact.updated                lgpd.redact_failed             media.derive_requested
conversation.claimed           lgpd.redact_received           media.persist_requested
conversation.transferred       crm.activity_write_failed

Mais três emitidos por trigger Postgres (fn_emit_event_on_lead_change, via fn_log_event — nenhum faz HTTP): lead.won, lead.lost, lead.reopened, e lead.assigned na troca de dono.

Consumidores registrados hoje (lib/event-log/register-handlers.ts): resposta de IA, sentimento, handoff por sentimento, indexador RAG, export e redact LGPD, regras de automação, reatividade do follow-up, persistência e derivação de mídia — e, agora, o gatilho de etapa.

O achado que muda o desenho

lead.stage_changed sai de exatamente três rotas HTTP:

Emissor Linha Quando
app/api/v1/leads/[id]/move/route.ts 171 arrasto do card no quadro
app/api/v1/leads/_handler.ts (moveLeadHandler) 619 MCP e regras de automação
app/api/v1/leads/bulk/route.ts 200 movimento em lote

lib/leads/agent-stage-sync.ts não é um deles. O briefing afirmava que a linha 220 desse arquivo emite stage_changed em event_log; medido, o arquivo não tem uma única ocorrência de event_log. Ele escreve crm_leads.stage_id direto (linha 238) e grava uma atividade em crm_lead_activities via emitLeadActivity (linha 264). O maestro confirmou por medição independente e corrigiu o briefing na fonte.

Consequência que fica aberta, e não é só minha: card movido pelo assistente de IA não arma este gatilho — e também não dispara as regras de automação, que consomem o mesmo lead.stage_changed (lib/automation/engine.handler.ts:9). Não consertei porque o conserto muda comportamento de automação existente, fora da minha fronteira de arquivos. Reportado ao maestro; o lugar do conserto é o emissor, nunca um segundo produtor lendo a atividade — isso duplicaria a verdade e passaria a disparar dois enrollments no dia em que o primeiro fosse corrigido.


2. "Caso" e "demanda": não são a mesma coisa, e o gatilho pedido colide

São duas entidades distintas, com donos e ciclos de vida diferentes:

demandas (migrations 0136–0138) agent_cases (migration 0050)
O que é o problema do cliente o caso de escalada — a IA travou e passou ao humano
Como abre automática, no primeiro inbound de todo contato sem demanda aberta (trigger trg_demanda_abre_no_inbound em messages) quando o agente aciona handoff
Vocabulário estado, origem, proximo_passo, desfecho status, blocker, followup_attempts
Liga as duas demandas.agent_case_id — ponteiro, não cópia

(O briefing citava a migration 0119; a de demandas é a 0136.)

Daí saem dois problemas com "caso aberto" como gatilho:

  1. Sobre demandas, o gatilho é vazio de significado. Como toda primeira mensagem de todo contato abre demanda, "quando abrir um caso" equivale a "quando alguém escrever pela primeira vez" — o follow-up dispararia em cima de quem acabou de falar com a gente.
  2. Sobre agent_cases, o gatilho colide de frente com o que já existe. A abertura do caso emite ai.handoff_triggered, e lib/followup/reactivity.ts já consome esse evento para pausar ou cancelar todos os follow-ups vivos do contato — porque tem um humano atendendo. Um gatilho que criasse um enrollment no mesmo evento estaria brigando com a política de handoff dentro do mesmo tick.

Proposta: o gatilho coerente é o simétricocaso encerrado, que já existe no schema como conversation_end e tem evento (ai.handoff_resolved, e o fechamento de conversa que fn_demanda_fecha_com_conversa já observa). É o momento em que um follow-up de pós-atendimento faz sentido. Parei aqui e aguardo a decisão do Rafael antes de construir — construir "caso aberto" como pedido literalmente entregaria um controle que briga com outro.


3. "Proposta feita": não existe tabela, e não precisa de uma

Varri lib/, app/ e supabase/: não há entidade de proposta comercial. O que existe é:

  • crm_stages.agent_stage_hint (migration 0084) com o valor negotiating, definido no próprio classificador como "há proposta/preço/condições na mesa e o lead está discutindo valor, desconto, parcelamento" (lib/agent-engine/agent/stage-classifier.ts:55);
  • lib/leads/next-action.ts, onde "proposta" significa outra coisa — a próxima ação proposta pela IA ao humano. Homônimo, não sinônimo.

Ou seja: "proposta feita" já é uma etapa do funil. No DeskcommCRM o negócio que teve proposta é o negócio que está na coluna "Proposta enviada" (ou o nome que o clone deu a ela). Aplicando DIRC, é caso de Calcular/Referenciar, não de Duplicar: modelar uma tabela propostas criaria uma segunda verdade sobre um estado que o funil já guarda, e cujo movimento já é auditado.

Portanto o gatilho 3 está entregue pelo gatilho 1 — o usuário escolhe a etapa "Proposta enviada" no mesmo controle. Zero tabela nova, zero campo novo.


4. O que foi construído

Peça Arquivo Papel
Produtor lib/followup/gatilho-etapa.ts consome lead.stage_changed, aplica o gate, cria o enrollment, grava a proveniência
Adapter lib/followup/gatilho-etapa.handler.ts pluga no dispatcher genérico do event_log
Registro lib/event-log/register-handlers.ts +1 linha
Publish app/api/v1/ai/followup-flows/[id]/publish/route.ts libera stage_change; recusa etapa ausente, apagada ou arquivada
Tela .../TriggerConfigControl.tsx terceiro tipo de gatilho, seletor de etapa por nome
Leitura hooks/followup/useEtapasDeGatilho.ts id → nome da etapa, agrupada por funil

Event-driven, não time-driven, porque "o lead entrou na etapa X" é um evento com hora e autor — ao contrário de silêncio, que é ausência de evento e por isso continua sendo varredura no tick do cron.

As três regras do motor, respeitadas e provadas: o índice org-wide idx_followup_enrollments_one_live barra o segundo enrollment (23505 é caminho normal, vira skipped_existing); resolveAgentForAutomaticTrigger gateia e devolve o agent_id pinado; nada roda dentro da transação do banco.

Sem migration. A 0143 continua livre: não há mudança de schema. A proveniência do enrollment (de que negócio, de qual etapa para qual, e por qual linha do event_log) entra como enrolled_by_stage_change em followup_enrollment_events, que já existe e já é a timeline que a fila lê. Uma coluna origem em followup_enrollments seria uma segunda verdade sobre o mesmo fato.

Duas decisões de tela que não são cosméticas

  • A etapa é escolhida pelo nome, agrupada por funil. Duas etapas podem se chamar "Proposta" em funis diferentes; sem o cabeçalho do funil, o operador escolheria a errada sem ter como saber.
  • O rótulo diz quando. O disparo depende de dois crons de um minuto (o dreno do event_log e o tick do motor), então a tela promete "poucos minutos", não "na hora". Prometer instantâneo seria o controle mentindo sobre a própria função — que é exatamente o defeito que esta missão existe para consertar.
  • conversation_end continua escondido e continua recusado no publish, porque continua sem produtor. A honestidade do arquivo foi preservada.

Um efeito colateral em teste alheio

tests/e2e/followup-builder.spec.ts congelava "o seletor oferece exatamente 2 opções e nenhuma de stage_change". Ela reprovava, de propósito, a ausência do produtor que esta entrega criou — atualizei para 3 opções, mantendo a asserção de que conversation_end não aparece.