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