Skip to content

Duas duplicações que encontrei mexendo no atendimento de um self-host #196

Description

@Gervanno

Fala, Rafa!

Tudo bem?

Investigando uma reclamação de duplicação de resposta numa instalação minha, encontrei duas causas distintas — sem relação com ai_dispatch_mode, isso eu já descartei. São as duas em lib/waha/ingest.ts e lib/agent-engine/edge/crm/drain.ts. Anonimizei os dados de cliente antes de escrever aqui.

1. Eco do próprio envio duplica a linha em messages

Em handleOutboundFromUserPhone (~linhas 561-629), o comentário que já está no código ("ECO DO PRÓPRIO ENVIO — não duplicar") descreve certo o problema, mas a solução não fecha a janela: o dedup é SELECT ... WHERE external_id IN (fullId, bareId) e só depois INSERT — não atômico. Quando o eco do WAHA chega perto o suficiente do insert que o próprio envio (composer/IA) já fez, o SELECT roda antes de esse insert commitar, e as duas linhas nascem. O unique(organization_id, external_id) não pega isso como rede de segurança porque os dois lados gravam formas diferentes do mesmo id — o envio grava o bare (3EB0…), o eco grava o composto (true_<chat>_3EB0…) — strings literalmente diferentes, então o 23505 nunca dispara.

Vi isso em produção: dois pares de linhas duplicadas, mesmo corpo, mesma conversa, ~1.3s e ~2s de diferença entre a linha sent_via='ai' (external_id bare) e a sent_via='external_device' (external_id composto terminando no mesmo bare). Não é envio duplicado pro cliente — é o mesmo WhatsApp físico — mas duplica na timeline do CRM, e infla qualquer métrica que conte linhas de messages.

Sugestão: gravar bareWaMessageId(p.id) como external_id neste insert também (em vez do p.id cru). As duas trilhas de escrita passam a usar a mesma string de identidade, e o unique(organization_id, external_id) + captura de 23505 — o padrão que o próprio arquivo já usa no handler de inbound — fecha o dedup sem precisar do SELECT. O matching de ack (~linhas 667-677) já busca as duas formas, então acho que continua funcionando sem ajuste. Não mexe em schema.

2. Debounce de rajada inbound não é deslizante

Em drain.ts (coalescência ~linhas 252-264) e o comentário de intenção em lib/agent-engine/env.ts:69-70:

"Coalescência de rajada inbound: mensagens do MESMO contato dentro desta janela viram UM job (responder em rajada é gatilho de ban)."

A intenção é clara — e é anti-ban, o mesmo motivo do throttle de envio — mas a implementação não entrega isso. A checagem é status='pending' AND run_after > now() contra o job da PRIMEIRA mensagem. run_after = created_at + INBOUND_DEBOUNCE_MS (default 8000ms) é fixado na criação e nunca estendido quando chega mensagem nova. Numa rajada humana típica — bolhas de WhatsApp com 10-20s de intervalo, bem comum — cada mensagem que chega depois que o run_after do job anterior já passou não acha nada pra pegar carona e cria um job novo. O agente responde cada bolha separadamente.

Vi isso também em produção: 3 mensagens do mesmo contato em ~30s (gaps de ~12.1s e ~17.5s), nenhuma coalesceu — 3 jobs inbound_turn, 3 respostas separadas em menos de 2 minutos, todas no mesmo assunto. É exatamente o cenário que o debounce existe pra evitar, só que escapando pela borda de fora da janela. Do lado do dono, parece "a IA respondendo em duplicidade".

Sugestão: quando uma mensagem encontra um job pendente pra coalescer (hoje só faz return 'processado'), também UPDATE job_queue SET run_after = now() + debounceMs WHERE id = ... — estendendo a janela a cada mensagem nova. Talvez valha um teto (uns 30-45s desde a primeira mensagem da rajada) pra não atrasar indefinidamente se o contato não parar de mandar mensagem. Mexe em roteamento/debounce, então por doutrina do próprio repo precisa de pnpm test:db local antes de mergear.

Sem pressa nas duas — só sinalizando o que achei. Se fizer sentido eu mando um PR pra alguma delas, me avisa. Valeu! 🙏

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions