Commit Graph
988 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 358e73ffe0 chore(db): o apêndice da 0501 volta a ser cópia byte a byte da migration
O cabeçalho do bloco no baseline afirma "cópia byte a byte das seções A, B,
C0a, C0, C, E, F e G"; havia três linhas em branco antes da seção B no
baseline (uma na migration) e duas antes da D na migration (uma no
baseline, onde a D não entra). Sem efeito no SQL; agora a frase é exata:
da seção A ao fim, tirando a D, as 441 linhas dos dois lados são iguais.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:21:12 -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
melgarafaelandClaude Opus 5.5 f9df965345 fix(suspensao): a data de anonimização da empresa entra na trava do banco
O gatilho que impede a sessão (tela/PostgREST) de mexer no estado da
organização cobria status, suspensão e autoria, mas não `redacted_at`: um
platform admin só-leitura ainda gravava uma data de anonimização falsa. A
coluna entra na comparação da migration 0501 e, byte a byte, no apêndice do
baseline; o único escritor legítimo (lgpd-redact-worker, service_role)
segue passando. Caso novo em org-suspensa.test.ts para os dois scopes de
platform admin, e o controle do service_role passa a gravar a data como o
worker grava.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:58:46 -03:00
Rafael Melgaço 47a25b2ed0 Merge pull request #2008 from webtecnica/fix/1998-backfill-0068-duplicate-pricing
fix(baseline): deduplica backfill 0068 por model_id
2026-09-30 13:26:24 -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
melgarafaelandClaude Opus 5.5 627050fcfb fix(baseline): desempate total no backfill 0068 (#1998)
O order by do distinct on desempatava só pelo preço de entrada: dois
provedores com a mesma entrada e saída diferente escolhiam uma linha
arbitrária. Agora desempata por saída e depois por provedor.
Acrescenta a quebra de linha final do fragmento.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 12:47:17 -03:00
webtecnica 4f7b896b7e fix(baseline): deduplica backfill 0068 por model_id
O bloco "backfill 0068 a partir de ai_models" do supabase/baseline.sql faz
`insert into public.ai_pricing (...)` a partir de um `select` de ai_models
embrulhado em `not exists`. ai_models é único por (provider, model_id), então
um mesmo model_id cadastrado sob dois provedores (ex.: openrouter e requesty)
gera DUAS linhas iguais DENTRO do mesmo select — o `not exists` não resolve,
porque os duplicados ainda não foram inseridos — e a PK `ai_pricing_pkey`
(só `model`) recusa com `duplicate key ... ai_pricing_pkey`. O update.sh para
na etapa "Atualizando o banco de dados" e o rollback `--force` falha no mesmo
ponto (issue #1998, instâncias v1.63.4→v1.66.1 e v1.64.1→v1.66.1).

Correção: `select distinct on (m.model_id) ... order by m.model_id,
m.input_price_per_million_cents asc`. Devolve UMA linha por model_id e escolhe
deterministicamente o provedor de MENOR preço quando há empate; o `not exists`
existente preserva a idempotência/re-aplicação. Não mexe em schema nem em
migration — é só o INSERT do apêndice do baseline que o self-host aplica.

Teste de invariante sobre o texto do baseline fica VERMELHO sem o distinct on
e verde com ele; roda na malha unitária (sem Postgres/Docker).
2026-09-30 12:35:03 -03: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 ccb0b5141b Merge remote-tracking branch 'origin/main' into triagem/1997-debounce 2026-09-30 11:01:02 -03:00
webtecnicaandClaude Opus 5.5 444b7ea1da fix(db): 0498 com carimbo em ordem e apêndice no fim do baseline
A 0498 levava o carimbo 20260930121224, anterior ao da 0497 já na main
(20260930140000): a ordem número x carimbo quebrava. Passa a
20260930150000, entre a 0497 e a 0499 do #1996; MANIFEST acompanha.

No baseline, a coluna e a CHECK saem do CREATE TABLE do dump e o bloco
sai do meio do arquivo: o mesmo DDL da migration vira o apêndice rotulado
"(migration 0498)" no fim. Não cria função, então não conflita com a
varredura de anon.

Refs: #1997, #1856

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:35:44 -03:00
melgarafaelandClaude Opus 5.5 43d6d424c0 fix(ia): o PR do atraso humano passa nas cercas e recusa minimo acima do maximo com 422
Por cima do trabalho de @webtecnica (#1996), quatro ajustes da triagem:

- baseline.sql e migration 0499: o `drop constraint if exists` fica logo
  antes do `add constraint`. Mesmo efeito; entra na janela de 10 linhas que
  tests/unit/baseline-reaplicavel.test.ts procura.
- dicionario.ts: "Minimo" e "Maximo" ganham espanhol (i18n-espanhol-cobre-a-tela),
  e "aquecido demais parece robo" vira "rapido demais parece robo".
- PUT /api/v1/ai/pacing: minimo acima do maximo efetivo devolve 422 com
  mensagem clara. Antes, com os dois gravados, o CHECK do banco recusava e a
  tela lia um 500 generico; com so o minimo acima do teto padrao (7500), o
  clamp o ignorava sem aviso.
- .changes/atraso-humano-por-conexao.md, com o credito.

Refs: #1996, #653

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:29:36 -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 816e59a627 fix(lgpd): a cascata do banco apaga a transcrição da mídia (0497)
messages.media_derived_text guarda a transcrição do áudio e o OCR da
imagem que o media-derive-worker extrai. Nenhum caminho de anonimização
a zerava: o body virava '[mensagem anonimizada]' e o texto do áudio
seguia legível, inclusive para o agente.

Uma linha `media_derived_text = null` em cada UPDATE de messages de
fn_redigir_conversas_ao_anonimizar (corpo da 0494, conversas do contato
e mensagens de grupo dele) e de fn_lgpd_cascade_redact_contact (corpo
da 0483, passo 3). Os corpos foram gerados a partir do atual por script
e o diff contra a origem só tem essas três linhas a mais. Cura
idempotente pelo marcador do body: alcança quem já foi anonimizado e
poupa a mensagem nova de quem voltou a escrever.

--no-verify: o hook local check-migration-triple.sh não termina neste
clone (defeito conhecido, conserto no #1978). O triplo foi conferido
com o hook do #1978 (exit 0; controle positivo com NNNN=0495 recusado).

Achado na triagem do #1988 (@AlecLimaDev).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 07:48:59 -03:00
melgarafaelandClaude Opus 5.5 60ccab5bb0 docs(suspensao): cabeçalho do apêndice, MANIFEST, comentário do dreno e fragmento dizem o que existe
- baseline.sql: o cabeçalho do apêndice 0496 lista as seções copiadas de
  fato (A, B, C0, C, E, F e G), não só A, B, C e E.
- MANIFEST.md: a linha da 0496 descreve C0 (descarte da fila que assenta
  rascunho, Meet e turn_discarded), F (claim do follow-up sem org parada,
  retomada na reativação) e G (turn_discarded só pelo servidor), e cita o
  invariante followup-org-suspensa.
- dispatcher-org-parada.test.ts: o comentário do caso da org operante diz
  o que a sabotagem mediu — `!ehOperante` já avermelhava; só "toda org
  parada" escapava sem o caso novo.
- .changes/suspensao-que-suspende.md: os follow-ups ficam parados na
  suspensão e retomam na reativação; o passo com envio descartado ganha
  envio novo, no ritmo normal da fila.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 04:29:09 -03:00
melgarafaelandClaude Opus 5.5 86923ded00 fix(db): só o servidor grava o evento turn_discarded do follow-up
A policy followup_enrollment_events_insert (0490) deixa manager inserir
pela sessão, e a chave `…:descartado` não termina em `:<n>`, então a
guarda do passo interno não a pegava: um manager forjava turn_discarded
pelo PostgREST e o motor enfileirava um 2º turno de envio.

Seção G da 0496 (migration e apêndice idênticos): a definição vigente
da 0488 de fn_followup_generation_write, com o ramo da trilha recusando
também event_type = 'turn_discarded' quando há auth.uid() (42501
followup_step_internal). service_role e as funções de estado seguem
gravando.

Invariante novo em followup-org-suspensa.test.ts: manager pela sessão
recusado com 42501/followup_step_internal (não a recusa genérica da
RLS); service_role grava.

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 04:26:39 -03:00
melgarafaelandClaude Opus 5.5 3f908c23ce fix(db): o COMMENT de suspended_kind no apêndice do baseline cita a 0496
O apêndice dizia "(migration 0495)" — na main a 0495 é outra
(janela_de_resposta_separada) — e todo self-hoster grava esse texto no
catálogo. Era a única divergência entre o apêndice e a migration.

O comentário em tests/invariants/vocabulario-banco-x-typescript.test.ts:429
também diz 0495, mas o arquivo existe na main e a troca remove uma linha:
fica para o dono (a autorização é só acréscimo).

Prova: caso novo em org-suspensa.test.ts lendo col_description.

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 03:44:15 -03:00
melgarafaelandClaude Opus 5.5 5cae8c7fa6 fix(central): o aviso de reativação manda procurar nas abas Fila e Automático
Numa empresa com IA, a conversa sem dono e sem silêncio é classificada
como `automatico` (comando-da-conversa.ts) e só cai na Fila quando a
empresa não tem atendimento automático — o aviso org_reativada mandava
revisar só a Fila. E o texto se repetia: o corpo gravado pelo banco e a
orientação da tela diziam a mesma instrução.

- fn_reativar_organizacao (migration e apêndice do baseline, idênticos):
  o corpo diz só o fato, e a contagem deixa de fora as conversas de grupo.
- Orientação em inbox-destino.ts nomeia as duas abas; chave e `es` do
  dicionário trocados (a velha sai); o comentário órfão do agent_case sai.
- Invariante: as duas strings novas e o caso em que o grupo não conta.
  Mapa de arquitetura e J37 acompanham.

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 03:40:01 -03:00
melgarafaelandClaude Opus 5.5 8b723e6b33 fix(followup): o motor não roda para a org suspensa e retoma na reativação
O claim (fn_claim_due_followup_enrollments) não olhava organizations.status:
a org suspensa seguia avançando fluxos, enfileirando turnos e pagando o
LLM de classificação. E o anti-backlog falhava o turno `pending` de uma
inscrição parada num nó `action`; na reativação os rechecks retomavam até
MAX_ACTION_RECHECKS, que marcava `dead` com action_turn_never_completed e
abria followup_dead na Central com motivo falso.

- 0496 seção F (migration e apêndice do baseline, texto idêntico): o claim
  vigente da 0308 com UMA mudança — a CTE `orgs` só aceita org `active`.
  Revoke e grant iguais.
- C0 grava `turn_discarded` para o turno de envio descartado de uma
  inscrição viva no mesmo nó. O motor lê o evento: a estadia RETOMA com um
  turno novo (decisão do controlador: o claim faz rodízio por org com
  p_limit e o envio tem throttle, então não há rajada), e o dead-man
  recomeça no descarte.
- aplicarTextoNosFollowups sai cedo para a org parada (o handler de
  reatividade segue rodando por causa do opt-out e do handoff), e o
  relógio tira as orgs paradas da varredura de waiting_reply.
- O dossiê descreve o descarte (com `es` no dicionário), e o comentário da
  cerca dos crons deixa de afirmar que o gate filtra o tick.

Provas: followup-org-suspensa.test.ts (claim com controle da org ativa;
suspender e reativar com a inscrição no action), unitários de
aplicar-inbound, relogio/executar e node-handlers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:37:34 -03:00
melgarafaelandClaude Opus 5.5 ebaa1b0684 fix(db): suspensão assenta o rascunho aprovado e o link do Meet cujo job descarta
O anti-backlog de fn_suspender_organizacao e fn_reativar_organizacao
falhava o job `pending` por fora, e o estado que dependia dele só é
assentado pelo acerto normal (fn_reply_settle, fn_meet_delivery_settle),
que nunca roda para um job que não saiu da fila: o rascunho aprovado
ficava 'aguardando envio' e o link do Meet nunca saía, sem aviso.

A seção C0 da 0496 (fn_org_parada_descarta_fila, interna, sem EXECUTE
para nenhum papel) falha a fila numa CTE e, no mesmo passo, vira o
rascunho `approved` do job em `failed|org_suspensa` e o compromisso cuja
entrega estava na fila em state `failed` com fn_meet_notice. Precedente:
fn_reply_redact. Migration e apêndice do baseline com o mesmo texto.

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 03:30:04 -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 d169ad4b3c fix(lgpd): a cerca da exportacao enxerga lead_notes, lead_state e ai_agent_runs (#1973)
tests/unit/lgpd-exporta-o-que-redige.test.ts reprovava o #1973 com
lead_notes e lead_state: o gatilho do banco passou a redigi-las, e o
coletor da exportacao (#1969) as lia por `.from(query.tabela)`, que a
cerca, que reconhece so `.from("<nome>")`, nao enxerga.

- lib/lgpd/export-collector.ts: lePaginado recebe a consulta pronta e cada
  tabela aparece com `.from` literal. Mesmas colunas, filtros, ordem e
  paginacao.
- migration 0494 e apendice do baseline: sem o alias `r` no update de
  ai_agent_runs, que deixava a tabela fora da varredura da cerca.
- tests/invariants/lgpd-cascata-alcanca-quem-guarda-pessoa.test.ts: a
  razao de lead_notes em DIVIDA_LGPD_CONHECIDA dizia que o gatilho ainda
  nao a alcancava; agora alcanca (0494). A entrada fica so porque o
  instrumento le uma funcao e nao enxerga trigger, como crm_tasks.
- tests/invariants/lgpd-cascata-do-banco-alcanca-notas-itens.test.ts:
  caso novo que compara fn_lgpd_redigir_tool_calls com redigirToolCalls
  da app em tres entradas bem formadas.

Valvula DESKCOMM_GOV_INVARIANTS_EDIT=1: nenhuma assercao existente muda.
Uma edicao troca o texto de uma `razao` (a catraca so mede comprimento
>= 40); a outra acrescenta um caso ao arquivo que o proprio #1973 cria.

Medido: vitest de 13 arquivos de LGPD em tests/unit, lib/lgpd e workers
(79 casos verdes), pnpm typecheck exit 0, eslint dos tres arquivos exit 0.
Os invariantes precisam de Postgres e ficam para o job invariants do CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 22:57:06 -03:00
webtecnicaandClaude Opus 5.5 a6f69a7aeb chore(lgpd): renumera a migration da cascata do banco para 0494 (#1964)
O 0493 ja estava reservado ao #1967; aqui fica o numero de quem mescla
primeiro. Muda o nome do arquivo, a linha 1, o rotulo do bloco no
baseline.sql, a linha do MANIFEST e as tres mencoes no catalogo de LGPD.
O carimbo 20260930010300 continua o do autor.

Valvula DESKCOMM_GOV_INVARIANTS_EDIT=1: o arquivo
tests/invariants/lgpd-redact-unificado-alcanca-pelo-catalogo.test.ts ja
e editado por este PR (#1973); a mudanca aqui e so o numero 0493->0494
dentro de tres strings `razao` (git diff --cached: 3 linhas -/+ que diferem
apenas no numero). Nenhuma asserção muda. Medido: `git grep 0493 --
supabase tests lib app .changes` vazio depois da troca.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 22:50:09 -03:00
webtecnica 7ed1ebf094 fix(lgpd): jsonb_typeof recebe jsonb (s.step -> campo), não text (#1964) 2026-09-29 22:33:02 -03:00
webtecnica 05d1ca4961 fix(lgpd): parentização correta em fn_lgpd_redigir_tool_calls (#1964) 2026-09-29 22:32:36 -03:00
webtecnica 500d33769b fix(lgpd): reescreve fn_lgpd_redigir_tool_calls em forma de subquery válida (#1964) 2026-09-29 22:28:29 -03:00
webtecnica 8524e55304 fix(lgpd): ai_agent_runs não tem updated_at — não o tocar no gatilho (#1964) 2026-09-29 22:27:38 -03:00
webtecnica 66e1c17f68 fix(lgpd): cascata do banco alcança lead_notes, tool_calls, lead_state e social_identity
Issue #1964. O gatilho da virada de is_anonymized (desenho da 0391,
fn_redigir_conversas_ao_anonimizar) passa a redigir, na mesma transação,
as quatro fontes que só a camada de app (lib/lgpd/cascata.ts, #1958)
alcançava:
- lead_notes.headline/body → '(anonimizado)', embedding → null
- ai_agent_runs.tool_calls → redação que preserva o nome da ferramenta
  (fn_lgpd_redigir_tool_calls, redacted=true em todo passo)
- lead_state.next_action → null, qualification → '{}'
- contacts.social_identity → null

Filtra organização e contato em todo passo; idempotente (guard repete o
marcador de já redigido). Migration 0493 + apêndice no baseline.sql +
linha no MANIFEST. Catálogo: lead_notes/ai_agent_runs/lead_state passam
de manter para redigir (gatilho). Gate:
tests/invariants/lgpd-cascata-do-banco-alcanca-notas-itens.test.ts.
2026-09-29 22:15:21 -03:00
melgarafaelandClaude Opus 5.5 f2ad44b9d0 fix(org): reativar exige o tipo e chama o humano em vez de reprocessar (0492)
fn_reativar_organizacao recusa desfazer uma suspensão de outro tipo, zera os
campos de suspensão, falha os jobs pending que sobraram e abre um único item
org_reativada (sem referência) com a contagem de conversas que receberam
mensagem durante a suspensão. Suspensão legada sem tipo vale como administrativa.

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-29 21:28:11 -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 a13e988378 fix(org): suspender para a fila e grava o evento na mesma transação (0492)
fn_suspender_organizacao trava a linha, grava tipo, motivo e autor, falha os
jobs pending (org_nao_operante) e as mensagens queued (org_suspensa) e insere
tenant.suspended no event_log na mesma transação. A suspensão administrativa
prevalece sobre a de cobrança; redacted/archived ficam intocadas.

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-29 21:20:24 -03:00
melgarafaelandClaude Opus 5.5 969f2e0f00 fix(org): status e suspensão da organização só mudam pelo servidor (0492)
O gatilho trg_organizacao_estado_so_pelo_servidor recusa com 42501 o INSERT de
organização e a troca de status, tipo e campos de suspensão ou created_by vinda
de authenticated/anon. Antes, qualquer platform admin, support_readonly
inclusive, reativava uma org suspensa por um PATCH no PostgREST.

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-29 21:16:34 -03:00
melgarafaelandClaude Opus 5.5 9a3382025d feat(org): suspended_kind e fn_org_operante, a régua única de org operante (0492)
A organização ganha o tipo da suspensão (administrativa ou cobranca), com
backfill antes do CHECK, e fn_org_operante(uuid) espelha em SQL o predicado
status = 'active' com falha fechada. O par suspended_kind x TIPOS_DE_SUSPENSAO
entra no invariante de vocabulário no mesmo commit.

Invariante existente alterado só por acréscimo (+12 −0), não afrouxamento — checar-invariantes-so-crescem.sh verde.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:13:04 -03:00
melgarafaelandClaude Opus 5.5 c514fa440a merge: traz a main para o #1928 (baseline e MANIFEST)
Conflito no apêndice do baseline.sql: a main acrescentou o bloco da 0486
(playbook agendamento v3) no mesmo ponto em que o PR acrescentou o da 0491.
Resolvido mantendo os dois, na ordem da main, com o bloco da 0491 no fim
(ambos depois da VARREDURA anon, e nenhum cria função nem devolve grant a anon).
Conferido por conjunto: nenhuma linha da main nem do PR ficou de fora.

MANIFEST.md: o union pôs a linha da 0491 antes da 0485; movida para o fim.
Cada uma de 0485, 0486, 0488, 0489, 0490 e 0491 aparece uma vez.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:14:15 -03:00
dfed38e5a0 fix(agenda): o playbook só manda chamar a ferramenta que o agente tem (#1922)
Duas linhas novas do corpo v3 mandavam chamar `crm_list_event_types` e
`crm_book_appointment` sem condicionar à posse da ferramenta. As ferramentas
de agenda são ligadas uma a uma, e o playbook entra por palavra-chave sem
saber quais o agente tem: um agente só de consulta lia a ordem de marcar.

- passo 1 e passo 4 ganham "e `X` está na sua mão";
- md5 recalculado (73c66800...) na migration 0486 e no apêndice do baseline,
  corpos byte a byte iguais;
- dois casos novos no teste da #1019, vermelhos contra o corpo anterior;
- o fragmento diz que a VPS recebe pelo baseline.sql do update.sh, e que
  cópia própria da organização continua como está.

Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:25:49 -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
webtecnica bc7cd7015c fix(agenda): o playbook semeado ensina os dois passos da cadeia (#1019)
O bloco residente do turno ganhou a cadeia no #1038; o OUTRO texto residente — o
playbook `agendamento`, servido por palavra-chave — continuava começando no meio
dela: mandava consultar `crm_find_free_slots` sem dizer de onde vem o
`event_type_slug` que ela exige, e `crm_list_event_types` não aparecia em nenhuma
linha do corpo semeado (medido: zero ocorrências em `supabase/`).

A migration 0486 publica o v3: a lista de tipos (daí sai o `slug`), os horários
DESSE tipo no mesmo turno, e a marcação com o `starts_at` que a consulta devolveu.
Todo nome de ferramenta novo entra DENTRO de uma condição — o playbook é org-wide
e não sabe quais capacidades o agente tem ligadas; quem não tem a ferramenta
continua mandado para a equipe, sem inventar horário.

Testes: a catraca do playbook passa a cobrar `crm_list_event_types`, e a nova
suíte na mesma seção cobra a ORDEM da cadeia (4 RED medidos antes, GREEN depois).
Mais o teste da prévia cobrindo a cadeia inteira na política de teste, com os
controles negativos: sem capacidade a porta não arma e nada roda de verdade na
prévia.

Refs #1019
2026-09-29 08:59:59 -03:00
melgarafaelandClaude Opus 5.5 4961e6a87c fix(followup): RLS por operação na trilha de eventos e nas versões de fluxo
followup_enrollment_events e followup_flow_versions tinham policy `for all`,
USING = membro e sem papel mínimo: pelo PostgREST um viewer apagava ou
reescrevia a trilha de uma inscrição e as versões publicadas de um fluxo.

Migration 0490, medida contra quem escreve pela SESSÃO:
- trilha: SELECT membro; INSERT manager (cancel/pause/resume/skip/snooze
  gravam o evento manual com requireRole manager); UPDATE/DELETE sem
  policy — append-only para a equipe, só o motor reescreve;
- versões: SELECT membro; DELETE manager (DELETE do fluxo); INSERT/UPDATE
  sem policy — a versão nasce só por fn_publish_followup_flow_version
  (security definer, chamada com service_role).
Apêndice idêntico no baseline antes da VARREDURA anon; as duas policies
`for all` saem do corpo do dump. Invariante novo no molde do #1914.

Invariantes existentes reforçados (DESKCOMM_GOV_INVARIANTS_EDIT=1):
- contato-com-turno (b) e agenda-presenca-recuperacao esperavam o 42501
  do gatilho no DELETE de evento pela sessão; agora a RLS recusa antes e
  o DELETE apaga zero. A asserção vira contagem explícita de zero (mais o
  controle de que a linha existia), e o 42501 da guarda segue provado no
  INSERT de manager com chave de passo do motor, que a policy alcança.
- rbac-config-ia-canais: as duas tabelas saem de DIVIDA_RBAC_CONHECIDA e
  entram na lista das corrigidas (a lista só encolhe).

O hook check-migration-triple passou de 20 min sem terminar e foi
encerrado; commit com DESKCOMM_GOV_MIGRATION_EDIT=1 depois de conferir à
mão: baseline.sql e MANIFEST.md no mesmo commit; 0490 e 20260929080000
ausentes da origin/main, de todas as refs (git log --all) e dos arquivos
de migration dos 10 PRs abertos (o #1907 usa 0487).

Refs #1915

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 07:54:57 -03:00
melgarafael 2326518bcb Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	supabase/baseline.sql
2026-09-29 04:53:55 -03:00
melgarafaelandClaude Opus 5.5 768c768593 fix(followup): RLS por operação em inscrições e fluxos de follow-up
A policy de followup_enrollments e followup_flow_pointers era `for all`,
USING = membro da organização, sem papel mínimo: pelo PostgREST qualquer
membro, viewer inclusive, apagava ou reescrevia inscrição e fluxo, e a
cascata levava a trilha e os turnos agendados. A guarda de geração só
barrava isso por acidente, e o #1912 a libera em cascata.

Migration 0489: SELECT = membro (inalterado); INSERT/UPDATE/DELETE =
manager, que é o que toda rota de escrita de follow-up exige. Apêndice
idêntico no baseline antes da VARREDURA anon, e a criação da policy
antiga sai do corpo do dump (cerca baseline-nao-constroi-o-que-derruba).
Invariante followup-rls-por-operacao prova viewer/agent barrados,
manager passando e nenhuma escrita cruzando organização.

Refs #1913

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 04:15:56 -03:00
3a8eb07f68 test(ia): seed do invariante da #1862 com arbitro explicito; escopo da guarda por cascata
- tests/invariants/contato-com-turno-de-followup-sai-inteiro.test.ts: os
  INSERTs do seed passam a usar `on conflict (id) do nothing`. `messages` tem
  `messages_org_external_id_unique` DEFERRABLE, e o Postgres recusa constraint
  adiavel como arbitro implicito ("ON CONFLICT does not support deferrable
  unique constraints"): o invariante parava no seed nas majors 15 e 17, antes
  de testar qualquer caso.
- Comentario da guarda (migration 0488, bloco da 0224 no baseline.sql e linha
  0488 do MANIFEST): `pg_trigger_depth() > 1` libera QUALQUER cascata de FK
  (contato, inscricao, fluxo, organizacao), nao so a da ficha. O turno orfao
  falha fechado em fn_followup_job_current; o DELETE direto segue 42501. Nenhum
  corpo executavel mudou.

Por que DESKCOMM_GOV_INVARIANTS_EDIT=1 (TRIAGEM.md 8-quater): o invariante e
NOVO deste PR e nao existe na main (`git cat-file -e origin/main:<arquivo>`
falha). O diff nele troca so as 16 clausulas `on conflict do nothing` por
`on conflict (id) do nothing` (linhas +/- fora dessa clausula: 0). Nenhuma
asserção mudou; o invariante nunca rodou alem do seed, entao nao ha lei
afrouxada.

Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 03:21:13 -03:00
webtecnica cfcd90ded5 fix: excluir contato com turno de follow-up apaga a ficha inteira (#1862)
A exclusão falhava com 42501 e apagava o histórico antes de falhar:
as mensagens e conversas saíam em três chamadas separadas e, quando a
ficha era recusada, as anteriores já estavam gravadas — a auditoria
registrava apagados: [messages, conversations] como rastro do estrago.

Duas causas, dois consertos, um gate de invariante:

1. A guarda do gatilho (fn_followup_generation_write) recusava com 42501
   o DELETE em cascata quando a ficha era apagada, porque não distinguia
   cascata de escrita direta. Agora distingue pela profundidade do
   gatilho (pg_trigger_depth() > 1): a cascata passa, o DELETE direto do
   turno de follow-up continua recusado com a MESMA 42501.

2. A rota (deleteContactHandler) passa a chamar UMA função
   security-invoker (fn_apagar_contato_com_historico) que apaga mensagens,
   conversas e a ficha numa transação só — ou sai tudo, ou não sai nada.
   A RLS de quem chama continua valendo e o organization_id é filtro do
   banco. apagados fica vazio de propósito na auditoria de falha.

Gates: tests/invariants/contato-com-turno-de-followup-sai-inteiro.test.ts
(ficha inteira some junto; recusa direta do turno preservada; ficha com
compromisso na agenda intacta; organization_id fecha a linha) e
tests/unit/contato-delete.test.ts (rota chama UMA função, não apaga
tabela por conta própria).

Co-authored-by: webtecnica <webtecnica@gmail.com>
2026-09-29 03:02:52 -03:00
melgarafaelandClaude Opus 5.5 dfa4ba1fa7 fix(baseline): regra de honorários em duas linhas para o update.sh antigo não cobrá-la (#1906)
O conserto do update.sh deste PR não alcança a atualização que o traz: o bash
segue executando o corpo do update.sh que já estava no disco depois do
`git checkout` da tag nova, e todas as releases de v1.39.0 a v1.63.0 leem as
regras do TEXTO do baseline com a regex de linha única
`create policy X on public.Y`, inclusive dentro do corpo de função. Sozinha,
uma 1.63.1 ainda pararia em `⛔ REGRAS DE ISOLAMENTO AUSENTES` para toda
instalação sem o módulo de honorários.

- baseline.sql: as 8 linhas `create policy honorarios_* on public.honorarios_*`
  do corpo de fn_honorarios_provisionar() passam a ocupar duas linhas cada.
  Sem comentário e sem espaço, o arquivo é idêntico ao da main (medido); a
  migration 0480 não muda. A conferência antiga passa de 154 para 146
  regras esperadas, e a diferença são exatamente as 8 de honorários.
- Guarda em adr-0002-funcao-provisionadora.test.ts: nenhuma linha de corpo de
  provisionadora casa a regex antiga. Sabotado (uma linha desfeita): vermelho.
- Fragmento renomeado ("parma" → "para"), um parágrafo por linha, e com a
  saída para quem ficou preso na 1.61.0–1.63.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:54:47 -03:00