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>
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>
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>
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>
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>
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>
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).
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.
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>
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>
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).
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.
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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.
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>
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>
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.
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>
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>
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>
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>
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>
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>
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>
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).
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
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>
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>
- 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>
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>
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>