610 Commits
Author SHA1 Message Date
melgarafaelandClaude Opus 5.5 55def8727c Merge da main no PR 1: a 0501 passa a carregar os avisos do Jev (0500)
A main recebeu a 0500 (avisos do Jev na Central), que acrescenta
jev_pedido_de_humano e jev_parar_de_receber ao CHECK de agent_inbox_items.kind.
A 0501 reconstrói esse CHECK com a lista inteira e roda DEPOIS da 0500 na
cadeia do CLI: sem os dois tipos, ela apagaria o vocabulário da 0500 (ou
falharia com avisos do Jev já gravados). A lista da 0501 e o bloco único do
baseline levam os três tipos. Nos testes, os dois lados ficam.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:32:29 -03:00
melgarafaelandClaude Opus 5.5 099fac569c fix(followup): o turno que rodava na hora da suspensão não mata mais o follow-up na volta
A suspensão descarta o turno de envio `pending` e grava `turn_discarded`,
que faz o motor enfileirar um turno novo na reativação. O turno que JÁ
rodava naquele segundo seguia, tinha o envio barrado (OrgNaoOperanteError)
e era cancelado sem o evento: na reativação os rechecks esgotavam o
dead-man e a inscrição morria com `action_turn_never_completed` — um
follow-up morto na Central com motivo falso, e o lead sem a mensagem.

Uma regra só para "o turno saiu sem enviar": `fn_followup_turno_descartado
(org, job)` (seção C0a da 0501, invoker, só service_role). A C0 passa a
chamá-la em vez de repetir o INSERT, e os dois donos de turno em voo a
chamam antes de devolver o job: o agent-worker (createFollowupTurnHandler,
só purpose send_message) e o envio inline (enviarTextoFixoPendente). Erro
que não é suspensão não grava nada — o dead-man segue valendo.

Prova: tests/invariants/followup-org-suspensa.test.ts reproduz a sequência
(turno running → suspende → cancela → reativa) com controle que mostra a
morte sem o evento; tests/unit/turno-rodando-na-suspensao.test.ts e
lib/followup/enviar-texto-fixo.test.ts cobrem as duas chamadas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:59:03 -03:00
melgarafael 15a1a30ec0 Merge remote-tracking branch 'origin/main' into feat/jev-onda-4
# Conflicts:
#	.github/workflows/e2e.yml
#	docs/architecture/escalacao-ciclo-humano.architecture.json
#	docs/white-label.en.md
#	docs/white-label.es.md
2026-09-30 14:23:42 -03:00
melgarafael 3d4c2842a1 Merge remote-tracking branch 'origin/main' into fix/followup-classificar-espera-a-resposta 2026-09-30 13:16:35 -03:00
Rafael Melgaço b46e320c7f Merge pull request #1747 from melgarafael/feat/jev-onda-3
feat(ia): o Jev percebe pedidos de pessoa e de parar de receber que a regra deixa passar, e pode avisar a equipe (onda 3)
2026-09-30 13:16:13 -03:00
melgarafaelandClaude Opus 5.5 b095c7969e chore(suspensao): renumera a migration para 0501, acima do que está na main e em voo
A main recebeu 0497–0499 (carimbos até 20260930160000) e dois PRs abertos
usam 0500@20260930170000. A 0496@20260930130000 ficaria abaixo de migrations
já aplicadas pela cadeia do Supabase CLI. Passa a 20260930180000_0501; o
apêndice do baseline, o COMMENT da coluna, o MANIFEST e a prosa acompanham.
Os UUIDs de fixture c0de0496 são identificadores e ficam.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:11:47 -03:00
melgarafael 390cc9e884 Merge remote-tracking branch 'origin/main' into feat/org-operante 2026-09-30 13:09:33 -03:00
melgarafael 203ecef5a4 Merge remote-tracking branch 'origin/main' into fix/followup-classificar-espera-a-resposta 2026-09-30 12:48:29 -03:00
Codex eb33f7d39b fix(agent): respeita escopo de funis no espelho de etapa 2026-09-30 14:40:01 +00:00
melgarafael d74d4457e3 Merge remote-tracking branch 'origin/main' into feat/jev-onda-3
Destrava o #1747 (706 commits atras), a pedido do dono.

- A migration da onda 3 sai de 0433 (20260926210000) para 0500
  (20260930170000): estava fora de ordem, com a main ate a 0499. Arquivo,
  rotulos do baseline, linha do MANIFEST (depois da 0499), testes e
  comentarios.
- A lista do CHECK de agent_inbox_items.kind na migration ganha os cinco
  kinds da proposta (0464/0466/0475): rodando depois delas, a lista antiga os
  apagaria. Igual a do baseline.
- Conflitos: baseline partiu da main com o delta do PR reaplicado (kinds na
  lista unica; apendice antes da VARREDURA anon); InboxKind, rotulos e destino
  da Central ficam com os kinds do Jev e os da proposta; mapa vivo com as
  arestas da main (e121-e123) e as do Jev renumeradas e124-e129; selo das
  traducoes do white-label regravado sobre o original mesclado; e2e.yml com os
  dois comentarios.

O caminho da migration dentro de tests/invariants/jev-aviso-fecha-sozinho-e-e-unico.test.ts
muda no commit seguinte, separado: o guard freeze-invariants barra edicao de
invariante dentro da resolucao.
2026-09-30 11:34:09 -03:00
melgarafael 671a2e6f8b Merge remote-tracking branch 'origin/main' into fix/followup-classificar-espera-a-resposta 2026-09-30 11:04:17 -03:00
melgarafael ccb0b5141b Merge remote-tracking branch 'origin/main' into triagem/1997-debounce 2026-09-30 11:01:02 -03:00
melgarafaelandClaude Opus 5.5 d645fe8fb9 fix(ia): a janela de rajada do agente sobrevive a salvar, reverter e criar pela tela
Os três INSERTs de versão da server action (rascunho novo de agente
publicado, revert pelo Histórico e criação pela tela) não levavam
inbound_debounce_ms: só o PATCH de rascunho existente gravava o campo, e a
janela voltava calada para a env. A coluna entra nos três, e
agent-version-columns-drift ganha a cerca que lê cada .insert({...}) de
ai_agent_versions na action (sabotada: tirar a chave de um INSERT deixa o
caso vermelho).

Ajustes de acabamento:
- drain.ts: o comentário dizia "a MESMA resolução que o turno"; a
  resolução é a de loadConversationAgentConfig (dono ou agente da sessão),
  sem router/campanha. Falha da consulta degrada para a env com log.warn,
  como a checagem de elegibilidade, em vez de mandar o evento para retry.
- AgentForm: o campo sai do meio do par "Mensagens anteriores que ele lê"
  / "Tamanho máximo desse histórico"; o valor em ms é arredondado.
- database.types.ts e o comparador do Histórico (VersionDiff) conhecem a
  coluna. Chave i18n junto das irmãs; fragmento com o crédito do PR.

Refs: #1997, #1856

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:42:37 -03:00
webtecnica 7592f332c7 feat(agent): atraso humano configurável por conexão (0499)
Os quatro números que governam o atraso humano ANTES da primeira bolha
(atraso-humano.ts: NOTAR 900ms, POR_CARACTERE 22ms, MINIMO 1200ms, MAXIMO
7500ms) eram literais no módulo, sem superfície. Uma clínica e um e-commerce
querem ritmos diferentes e recebiam sempre o mesmo valor cravado (issue #653).

Os quatro saem do literal e passam a ser 4 colunas NULL em channel_knobs — a
MESMA tabela dos knobs de pacing por conexão (0010/0495); NULL cai no default
conservador em defaults.ts que espelha os valores de antes (regressão zero).
Exposição na ficha Anti-ban via /api/v1/ai/pacing (POST/GET), com CHECK de
sanidade (max >= min, teto intervalMaxMs).

O jitter entre bolhas (literal 1200 + rand*800 em inbound-turn.ts) NÃO ganha
coluna: ele já é throttle_ms + jitter_max_ms (defaults 1200/800) e o call site
passa a LER os knobs da conexão em vez do literal.

DoD: migration 0499 + apêndice idempotente no baseline + linha no MANIFEST;
leitor testado (default, null -> default, knobs da conexão), regressão zero em
calcularAtrasoHumano; teste novo vermelho sem a mudança (sabotagem: 4 falharam).
2026-09-30 10:18:29 -03:00
webtecnica 4a8a65bcdd feat(agent): janela de rajada do WhatsApp configurável por agente (0498)
O debounce de rajada (mensagens do MESMO contato dentro da janela viram uma
resposta só) era só a env do worker INBOUND_DEBOUNCE_MS (default 8000, global,
invisível na tela, exige VPS). Agora vira campo opcional por agente em
'Freios de segurança': vazio = usa a env da instalação (regressão zero),
0 desliga a coalescência, teto de 60s clampa pelo leitor (debounceEfetivo).

O drain lê a config do agente da conversa (loadConversationAgentConfig, mesma
resolução do turno) e usa o debounce do agente no lugar do literal.

DoD: migration 0498 + apêndice idempotente no baseline + linha no MANIFEST;
leitor testado (default/env, campo, teto no limite); gate de consumidor de
knob atendido.
2026-09-30 10:03:31 -03:00
melgarafaelandClaude Opus 5.5 c3c5672e52 chore(db): renumera a migration da suspensão de 0495 para 0496
A main recebeu a 0495 (janela de resposta separada) enquanto a branch ainda
usava o mesmo número. Renomeia para 0496 com o carimbo 20260930130000 (depois
do da 0495), e acompanha o número no apêndice do baseline, no MANIFEST, na
prosa do código e nos fixtures dos invariantes novos (que ainda carregavam
o 0492 da renumeração anterior).

Invariante alterado sem afrouxar invariante da main — checar-invariantes-so-crescem.sh verde.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 02:56:48 -03:00
melgarafael 606cb83033 Merge remote-tracking branch 'origin/main' into feat/org-operante 2026-09-30 02:55:55 -03:00
melgarafaelandClaude Opus 5.5 4a57262564 merge: origin/main em feat/org-operante (migration renumerada 0492 -> 0495)
A main ganhou a 0494 (cascata de LGPD), então a migration desta PR passa a
0495 / 20260930020000: arquivo, apêndice do baseline, MANIFEST e referências
em prosa atualizados. Conflito em baseline.sql: os dois apêndices ficam, ambos
antes da VARREDURA anon. A referência do invariante org-suspensa.test.ts segue
no commit seguinte, próprio, como o hook manda.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 02:16:54 -03:00
b740def16f fix(pacing): a janela de resposta do #1983 compila e se chama resposta_*
- app/api/v1/ai/pacing/route.ts: um "\n" escrito como texto punha o
  comentario e a lista de colunas numa linha so de comentario, e
  KNOB_COLUMNS engolia a funcao lerFusoDaOrganizacao (GET/PUT quebrados).
  A validacao do par de resposta perde o desvio redundante de 0/24.
- reengajar_* vira resposta_*: a coluna guarda a janela de RESPOSTA, e
  reengajar e justamente o que ela nao rege. Migration e apendice ganham
  um bloco do guardado que renomeia onde o nome antigo ja foi aplicado.
- PacingKnobs ganhou 2 campos obrigatorios: janela-do-canal.ts e um teste
  montavam o literal sem eles.
- A pergunta do roteiro que sai no mesmo turno le a janela de resposta
  (eTurnoDeResposta, agora exportada e testada).
- Sai a guarda morta de janelaDoPacing: o fallback real e coluna a coluna
  na leitura (store/effectiveKnobs), e os testes passam a exercitar esse
  caminho em vez de undefined forjado.
- Comentarios: 0381 -> 0495; some o "DEFAULT 9h-21h / cutucar" que
  descrevia o inverso do codigo.

Co-authored-by: suporteubere99-coder <267519567+suporteubere99-coder@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 02:08:12 -03:00
Rafael Melgaço b71cea9626 feat(pacing): a janela de RESPOSTA sai da janela de DISPARO (0495)
Um cliente escreveu as 22h e ficou sem resposta ate as 7h da manha. O log
diz por que:

    turno adiado - fora da janela anti-ban de envio
    janela: 7h-22h  abertura: 2026-09-30T10:00:00Z

A janela era UM par de horas (`channel_knobs.window_start_hour`/
`window_end_hour`, da 0010) e ela atendia a tres coisas: a resposta do
agente, o disparo em massa e a cutucar de conversa parada. Abrir o
atendimento para 24h abria o disparo junto - e `followup_turn`, que e a
cutucar, e o tipo de envio que mais incomoda quando sai de hora: "e ai,
tudo certo?" as 4h para quem dormiu.

Agora sao duas janelas, por numero (migration 0495,
`channel_knobs.reengajar_start_hour`/`reengajar_end_hour`):

    RESPOSTA a quem escreveu .... 0h-24h
    DISPARO em massa ............ 7h-22h  (inalterado)
    Cutucar conversa parada .... 7h-22h  (inalterado)

O que separa as duas e o TIPO do envio. `inbound_turn` e `case_reply_turn`
sao resposta e leem `reengajar*`; `followup_turn` continua na janela
comercial. O `PacingInput.resposta` default `false` (disparo) mantem todo
chamador anterior a 0495 no comportamento velho, e e a direcao que fecha o
numero: quem esquece o campo nao abre nada.

## O anti-ban continua inteiro

Abrir o horario nao abre o limite. Cap diario, degraus de warm-up por idade
do numero e throttle rodam para os dois lados, e o veto por cap as 3h
continua sendo "cap" e nao "fora da janela". Um numero novo segue com 20
mensagens por dia ate completar o aquecimento.

`nextDayOpen` fica na janela de DISPARO de proposito: cap diario e
protecao anti-ban, e o "amanha" que se anuncia a quem pagou as 3h e o
numero de novo as 7h.

## Migration

Colunas soltas, nao jsonb: `window_*_hour` e coluna desde a 0010 e a ficha
Anti-ban ja os edita. Default NULL = a RESPOSTA herda a janela de DISPARO,
que e o comportamento de antes - um clone que aplicou a 0495 sem gravar as
colunas nao muda de operacao por omissao. `drop ... if exists` antes do
`add constraint` nos dois arquivos porque o `update.sh` reaplica o
apendice inteiro e `add constraint` sem guarda quebra com 'already exists'
no segundo clone que atualizar.

Par pela metade e tratado como ausente: misturar `reengajar_start_hour=0`
com `window_end_hour=22` daria `0h-22h`, que abre a madrugada pelo caminho
que parece conservador. O editor tambem recusa `start >= end` no par
resultante, senão o motor traduz em "nunca responde" e o operador so
descobre quando o cliente para de receber resposta.

## Numeracao

A 0381 ja era da captura de UTM (`20260921080000`) e o MANIFEST ia ate
0494 na 1.65.0 - por isso 0495, e com a linha do MANIFEST, que e o que o
gate `manifest-x-migrations` exige.

## Verificacao

Medido nesta VPS, com o numero real: "Boa noite" recebido 23:25, resposta
"Boa noite! Tou por aqui. O que tu precisa?" 00:02; "Quero uber" recebido
22:27, "Boa, mano! Conta nova Uber fica R$ 380." 00:03 - com o preco vindo
do RAG, sem passar pelo prompt.

- `lib/agent-engine/pacing/janela-de-resposta.test.ts` - 12 casos: as 3h a
  resposta passa e o disparo nao; `resposta` omitido barra; par incompleto
  nao abre; cap diario continua barrando; domingo continua calando a
  resposta; o veto da resposta atrasa para a abertura DA resposta, nao 7h
- 10/10 nos gates de migration/baseline contra a 1.65.0, incluindo
  `baseline-reaplicavel`, que reprovou a primeira versao deste apendice
  por `add constraint` sem guarda

`tests/unit/janela-24h-recusa-quem-envia-por-token` e
`lib/ai/dispatcher/rate-limit` falham tambem no `main` limpo - provado
rodando contra `git show fork/main:` sem nenhuma mudanca deste PR.
2026-09-30 04:07:02 +00:00
melgarafaelandClaude Opus 5.5 401c018fc9 fix(envio): suspensão não vira opt-out no ledger e encerra o texto fixo
O 403 da organização parada passava pelo ramo do contato bloqueado: o
agent-engine cancelaria todos os follow-ups do contato em definitivo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 00:02:06 -03:00
melgarafaelandClaude Opus 5.5 cbe32a39e7 fix(agent-engine): resgate de fila não reenvia mensagem de organização parada
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 23:57:44 -03:00
melgarafaelandClaude Opus 5.5 fc094270dd fix(agent-engine): agendador avança o cron de organização parada sem enfileirar
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 23:56:19 -03:00
melgarafaelandClaude Opus 5.5 9811bc613b fix(agent-engine): mensagem de organização parada não vira turno
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 23:54:43 -03:00
melgarafaelandClaude Opus 5.5 a05ac8fcd1 fix(ia): organização parada é o primeiro veto do gate de elegibilidade
orgStatus vira campo obrigatório do estado; os quatro montadores leem a org
na mesma consulta (pg, supabase-js, varredura de silêncio) e o de reunião
herda a garantia de fn_meet_delivery_current. O preflight do CLI
(scripts/lib/gate-ativacao.ts) também monta o estado e passa a dizer a org.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 23:46:33 -03:00
melgarafaelandClaude Opus 5.5 c913e4c8f5 test(ia): a fiação do #1943 exige a marca de descarte dentro da guarda
O teste de fiação conferia só que a primeira atribuição de
`turnoDescartado = true` vinha antes do `code: 'resposta_obsoleta'`.
Ligar a flag logo na declaração (o roteiro calado em TODO turno)
passava nos 22 casos. Agora exige exatamente uma atribuição, e depois
de `await respostaFicouObsoleta(`. Sabotagem medida: 1 failed.

No fragmento, "segue feita" vira "fica pendente" ("feita" lia como já
perguntada) e o arquivo ganha o \n final.

Refs: #1943, #1968

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 22:14:38 -03:00
webtecnicaandClaude Opus 5.5 313391c20b fix(ia): turno descartado como resposta obsoleta não emite a pergunta do roteiro
O #1940 recusa (resposta_obsoleta) o send_message de um turno quando o cliente
escreve de novo enquanto o modelo pensa. Mas garantirPerguntaDoRoteiro enviava
pelo runBeforeSend a pergunta pendente do roteiro — esse envio não passa na
régua, e num turno descartado corposEnviados fica vazio, então a pergunta saía
e o turno da mensagem nova respondia em seguida (resposta dupla).

A guarda agora marca o turno como descartado (turnoDescartado no closure do
run); com a flag ligada, o `enviar` passado a garantirPerguntaDoRoteiro devolve
false (perguntaDoRoteiroPodeSair). Sem envio, o evento roteiro_pergunta_feita
não é registrado e a pergunta segue pendente para o turno seguinte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:04:07 -03:00
melgarafaelandClaude Opus 5.5 91ef9e886a feat(central): aviso org_reativada leva ao Inbox depois de uma suspensão (0492)
O kind entra no bloco único de agent_inbox_items_kind_check (no lugar, no
baseline) e na lista completa da 0492, que passa a ser a última reconstrutora.
InboxKind, rótulo, política de destino (/app/inbox, agent+, sem referência) e
as três frases em espanhol vêm juntos.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:24:54 -03:00
melgarafaelandClaude Opus 5.5 ac4c0e896b fix(ia): a resposta descartada tem quem responda depois, e a reentregue com atraso não silencia
Dois complementos ao #1940 (commit 66c48ba, de @automatikpg-ux):

1. O teste que faltava. Descartada a resposta obsoleta, a mensagem nova
   percorre o caminho de produção (ai_agent.dispatch_requested -> drain ->
   job inbound_turn próprio, atrás do turno em curso) e o turno DELA
   responde, com uma resposta no total. O teste da porta do PR gravava a
   mensagem direto em `messages`, então nada vigiava o silêncio.

2. A régua passa a olhar a inbound mais nova pela MESMA ordem do
   anti-backlog do drain (coalesce(sent_at, created_at)). Uma mensagem
   reentregue com atraso chega depois da anotação, mas com relógio do
   aparelho anterior ao da última lida. A régua antiga descartava a
   resposta, o drain pulava o evento dela como superado, e ninguém
   respondia. Medido com a SQL da régua num Postgres 17 descartável, nos
   7 casos da régua: antes, 1 falha (a reentregue dava obsoleta); depois,
   0. Nos mesmos dados, a consulta do drain elege a última lida, não a
   reentregue.

Os casos novos moram num arquivo NOVO
(tests/invariants/resposta-descartada-tem-quem-responda.test.ts) porque
turno-nao-responde-duas-vezes.test.ts é invariante congelado.

Fragmento: uma frase sobre a reentregue e a linha de crédito.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 14:49:00 -03:00
melgarafael a609f1a808 Merge remote-tracking branch 'origin/main' into triagem/1940-resposta-obsoleta 2026-09-29 14:37:43 -03:00
automatikpg-uxandClaude Opus 5.5 66c48ba9eb fix(agente): a resposta desatualizada não sai quando o cliente escreve de novo durante o turno
O turno lê a conversa, leva 10–40 s no modelo e envia. Se a pergunta real do
cliente chegou nesse intervalo ("Oi" / "Boa tarde" / ... / a pergunta), a
resposta sai desatualizada ("Como posso te ajudar?") e o turno seguinte, que
viu a pergunta, responde de novo. Medido numa instalação: 232 de 589 respostas
(39%) a menos de 3 min de outra ao mesmo cliente.

`respostaFicouObsoleta` usa a anotação `ultima_inbound_vista_em` que o turno já
grava antes de ler a conversa: havendo inbound mais nova, o primeiro
`send_message` do turno é recusado com `resposta_obsoleta` e o job da mensagem
nova responde a tudo. Teto (RESPOSTA_OBSOLETA_TETO_MS, 120 s; 0 desliga) pela
mensagem mais antiga sem resposta, para cliente que escreve sem parar não
ficar sem resposta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:18:05 -03:00
Rafael MelgaçoandClaude Opus 5.5 4792748467 fix(ia): custo sem arredondar para cima, sentimento parado sem agente no ar, frase "no credits remaining" da OpenAI
- lib/ai/cost.ts: grava centavos fracionados. O Math.ceil transformava cada
  classificação de sentimento (~0,01 centavo) em 1 centavo — >90% do gasto
  que o teto de orçamento enxergava numa instalação real.
- workers/ai-sentiment-worker.ts: sem nenhum agente no ar (todos pausados ou
  despublicados), não classifica. O único efeito do worker é alimentar o
  handoff por sentimento, que mandava "não há atendente disponível" ao
  cliente enquanto a equipe respondia por fora.
- espera-de-saldo / normalizarErro: reconhece "You have no credits
  remaining" da OpenAI, que não era coberta e fazia a fila gastar as
  tentativas (~102 mil chamadas recusadas, 17 mil job_dead medidos).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:32:35 -03:00
Rafael Melgaço 5dd8e492ad Merge pull request #1929 from webtecnica/fix/1880-custo-agente-openrouter
fix(custo): id de modelo com prefixo provider/ do OpenRouter deixa de dar custo nulo (#1880)
2026-09-29 10:53:16 -03:00
melgarafaelandClaude Opus 5.5 b99699c4e0 Merge origin/main (3f39446b6) em fix/followup-classificar-espera-a-resposta
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:52:36 -03:00
webtecnica dc22d17332 fix(custo): id de modelo com prefixo provider/ do OpenRouter deixa de dar custo nulo (#1880)
O OpenRouter devolve o id do modelo com o prefixo `anthropic/…` e a tabela de
preços do agente (indexada sem o prefixo) não achava a linha → llm_calls.cost_cents
nulo em toda chamada de atendimento, guarda e classificação, tela de Uso com gasto
zero e teto de gasto de IA cego.

precoDoModelo agora recorta o prefixo até a primeira barra na MESMA ordem da
tolerância do sufixo de data (o id completo segue tentado primeiro), e um id com
prefixo+sufixo (`anthropic/claude-sonnet-5-20250929`) também casa. O recorte não
inventa preço: modelo que não casa em nenhum formato volta NULL (armadilha 3).

test: unit (53/53) — bloqueio novo: prefixo provider/ custa como o id sem prefixo,
sufixo de data tolerado mesmo com prefixo, TTL do cache respeita o knob, a conversa
real deixa de ser null. Crédito: @webtecnica
2026-09-29 10:03:33 -03:00
webtecnica 8e0f31158e fix(central): o aviso de evento morto não abre em dobro (#880)
Dois drenos (cron event-log-drain e drain-loop do worker) podem processar
o mesmíssimo aviso event_dead no mesmo instante. O dedupe era uma PERGUNTA
e uma ESCRITA separadas: o insert com where-not-exists não tinha índice —
os dois leem 'não existe' antes de qualquer escrita e os dois inserem, e a
Central repetida é alerta que ninguém abre.

O banco passa a fechar a corrida: índice único parcial
agent_inbox_event_dead_aberto_unico (organization_id, kind, title)
where status='open' and kind='event_dead' — escopo só event_dead, os
outros kinds querem várias linhas abertas com o mesmo título. O
insertInboxItem e o dreno tratam 23505 como 'já havia um aviso aberto'
(desfecho normal, devolve null). A reabertura (PATCH /ai/inbox/[id]) para
um slot que já tem aberto igual volta 409 em vez de 500. Prévia resolve as
cópias abertas (nunca delete).

Gates: migration 0491 + baseline + MANIFEST; teste de corrida em
tests/unit/aviso-event-dead-concorrente-abre-uma-vez.test.ts (7 casos).
2026-09-29 09:07:24 -03:00
melgarafaelandClaude Opus 5.5 a8bf6f52b9 Merge origin/main (61077371b) em fix/followup-classificar-espera-a-resposta
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:28:15 -03:00
Rafael Melgaço f98fca0fc1 Merge pull request #1864 from melgarafael/feat/base-com-chave-google-1130
feat(conhecimento): a base pode ser preparada pelo Google (Gemini), com troca que refaz a base
2026-09-28 16:57:38 -03:00
melgarafaelandClaude Opus 5.5 544eb2bebd Merge origin/main (a842e9579) em fix/followup-classificar-espera-a-resposta
Conflito unico: docs/testing/user-journey-map.md. O numero J34 foi usado
nos dois lados; na main ele e "Achar uma mensagem dentro da conversa
aberta" (#1795, ja mergeado e citado por run de CI). A jornada deste PR
("Enviar e classificar a resposta") vira J35, o proximo numero livre na
main e em todos os PRs abertos, com as linhas J34.1..J34.8 renumeradas
para J35.1..J35.8. Nenhum outro arquivo do PR citava J34.

Resolucao partindo da main: o bloco da main fica inteiro no lugar e a
secao do PR entra depois do J34, na ordem numerica. O delta do PR no
arquivo e o mesmo conjunto de 55 linhas, modulo a renumeracao (conferido
com diff das linhas adicionadas contra os dois pais).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:13:01 -03:00
214ef1f13d feat(roteiro): roteiro concluído não recomeça para o mesmo cliente, a não ser que o roteiro permita
Decisão do dono (doc 69, pergunta 2 = b): cada roteiro escolhe se pode
recomeçar para quem já o concluiu; o padrão é NÃO. Antes, repetir a
palavra-gatilho reabria um cadastro já respondido.

- settings.pode_recomecar (jsonb do grafo, sem migration), ausente = não.
- podeComecarParaOContato: a regra pura, usada nos dois lugares.
- iniciarFluxoDeAtendimento: guarda de TODAS as entradas (gatilho,
  roteador, encadeamento) — "já concluiu" = enrollment 'completed' do
  mesmo roteiro (pointer) para o mesmo contato. Encerrado por prazo
  ('cancelled') não conta.
- escolherFluxoPeloGatilho: o concluído sai da DISPUTA, para a palavra ir
  ao próximo roteiro em vez de ser ganha por um que não começaria (ideia
  do #1130).
- Tela: "Pode recomeçar para quem já concluiu" nas configurações do
  roteiro, com t() e es.

Contribuição de @vgamkt (#1130).

Co-authored-by: VANDER GUSTAVO ALVES <62725454+vgamkt@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:11:32 -03:00
melgarafael 19e1648455 Merge remote-tracking branch 'origin/main' into feat/base-com-chave-google-1130 2026-09-28 14:05:20 -03:00
8e91b5c8f3 feat(conhecimento): a base pode ser preparada pelo Google (Gemini), com troca que refaz a base
Decisão do dono (doc 69, pergunta 1 = a): cada empresa escolhe OpenAI ou
Google para os embeddings da base; trocar refaz a base, e a tela avisa
ANTES de trocar.

- Dimensão: gemini-embedding-001 com outputDimensionality 1536, então a
  coluna ai_chunks.embedding vector(1536) serve aos dois sem migration. A
  busca é por cosseno (<=>), então o vetor não normalizado dessa dimensão
  não distorce a nota. taskType RETRIEVAL_DOCUMENT/RETRIEVAL_QUERY.
- O modelo é função do provedor (modeloDeEmbedding): a versão de índice
  grava o modelo DA CHAVE, a busca filtra pelo modelo da pergunta, e o
  indexador reindexa a fonte cujo modelo mudou. É isso que faz a troca
  refazer a base.
- Escolha: bindings dos DOIS pontos de embedding (PUT
  /api/v1/ai/knowledge/provedor, admin), que já enfileira a reindexação
  no mesmo pedido. Credencial Google sem escolha entra só no fim da escada.
- Legado (kbVersionId, só vetores OpenAI) não é consultado com pergunta
  do Google.
- Tela: Google no cadastro de chave; "Quem prepara a base" com troca e
  diálogo de aviso antes.

Contribuição de @vgamkt (#1130), reescrita sobre a main.

Co-authored-by: VANDER GUSTAVO ALVES <62725454+vgamkt@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:05:19 -03:00
webtecnica 5b976d292d merge main into recorte 1 branch (a CI mede a branch; base velha engana) 2026-09-28 12:58:18 -03:00
webtecnica 172d2254e5 refactor(agent): split buildOpening/checkpoint out of inbound-turn (#636)
Recorte 1 of #636: extraction only, no behaviour change.

- lib/agent-engine/agent/abertura/checkpoint.ts keeps the checkpoint
  schema, the ROW type, the closing instruction, the parse and the write,
  moved verbatim (the previous commit git-mv'd inbound-turn.ts onto this
  path so `git log --follow` keeps the file's history).
- lib/agent-engine/agent/abertura/ritual.ts keeps `ritualBlocks` and
  `buildOpeningMessage`.
- inbound-turn.ts is reduced to orchestration and RE-EXPORTS every symbol
  of the recorte, so the tests' grep and the siblings (follow-up, case
  reply) keep resolving them from the legacy path.
- tests/unit/recorte-1-build-opening.test.ts pins that: the grep, symbol
  identity (same object, not a copy), the types, the absence of an import
  cycle, and the fronteira (followup-turn and case-reply-turn never talk
  about the "inbound atual").

Recortes 2 (before_send chain) and 3 (outcomes/traces persistence) are
declared as next in the PR; this one closes only the first slice.
2026-09-28 12:00:53 -03:00
webtecnica 9b5ec86bbb refactor(agent): git mv inbound-turn to the new abertura module dir (#636)
First step of extraction 1 of #636. The rename is a pure `git mv` (R100):
it is what gives the extracted module the history of the file the code
lived in (git log --follow on lib/agent-engine/agent/abertura/checkpoint.ts
walks back through inbound-turn.ts).

The next commit does the split: inbound-turn.ts comes back reduced, and the
opening/checkpoint symbols are re-exported from it so the tests' grep and
the siblings (follow-up, case reply) keep resolving them.
2026-09-28 11:35:55 -03:00
melgarafael 021561e4b0 Merge remote-tracking branch 'origin/main' into triagem/1832-proposta 2026-09-28 10:53:57 -03:00
Paulo Lima JrandClaude Opus 5.5 6e582ff866 feat(propostas): proposta comercial, do briefing da IA ao PDF que o cliente recebe
Nova capacidade "Propostas", desligada por padrão (Configurações › Propostas):

- tabelas crm_proposals / crm_proposal_items / modelos de proposta, com RLS por
  organização, numeração por organização e ano alocada só no envio, versão, e
  trava de organização nos vínculos (0462–0477, com apêndice no baseline);
- o assistente que conversa levanta um briefing universal de sete categorias,
  pede confirmação e só então rascunha (crm_preparar_proposta + recusa
  em crm_draft_proposal quando falta categoria ou confirmação);
- modelos da empresa: editar, criar, importar PDF/texto, desligar os da
  plataforma;
- editor com "Preencher com a conversa", seções editáveis e prévia fiel do PDF;
- envio exige modelo confirmado e preço, salva antes de enviar e manda o PDF
  pelo WhatsApp da empresa, com a marca dela;
- aviso na Central, em destaque na tela e no Radar; LGPD alcança a proposta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:00:12 -03:00
webtecnica d4379e7942 1082: SUPABASE_SERVER_URL opcional para o servidor falar com o Supabase por endereço próprio 2026-09-27 01:48:00 -03:00
melgarafael ff7f23f718 Merge branch 'feat/jev-onda-3' into feat/jev-onda-4 2026-09-26 22:47:18 -03:00
melgarafael f1f32a5d81 Merge remote-tracking branch 'origin/main' into feat/jev-onda-3 2026-09-26 22:44:54 -03:00