mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 09:34:46 +08:00
main
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4792748467 |
fix(ia): custo sem arredondar para cima, sentimento parado sem agente no ar, frase "no credits remaining" da OpenAI
- lib/ai/cost.ts: grava centavos fracionados. O Math.ceil transformava cada classificação de sentimento (~0,01 centavo) em 1 centavo — >90% do gasto que o teto de orçamento enxergava numa instalação real. - workers/ai-sentiment-worker.ts: sem nenhum agente no ar (todos pausados ou despublicados), não classifica. O único efeito do worker é alimentar o handoff por sentimento, que mandava "não há atendente disponível" ao cliente enquanto a equipe respondia por fora. - espera-de-saldo / normalizarErro: reconhece "You have no credits remaining" da OpenAI, que não era coberta e fazia a fila gastar as tentativas (~102 mil chamadas recusadas, 17 mil job_dead medidos). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
52a8261058 |
fix(agente): sem saldo no provedor, a resposta espera a recarga em vez de morrer
Quando a conta do provedor de IA fica sem crédito, a fila tratava a recusa como qualquer falha: 5 tentativas em ~2,5 min e `job_dead`. Medido numa instalação real em 24/09/2026: o saldo da Anthropic acabou às 09:48 e voltou às 11:02; um cliente nunca foi respondido pela IA e a Central mostrou três "tarefa falhou" sem dizer que a causa era uma só. - `espera-de-saldo.ts`: reconhece a frase de sem saldo (Anthropic 400, OpenAI insufficient_quota), devolve o job à fila sem gastar tentativa (2 min na primeira meia hora, 10 min depois) por até 6 h, e abre UM aviso crítico na Central no idioma da organização, apontando a credencial - o aviso fecha sozinho quando a primeira resposta que esperava sai - a resposta que esperou não sai se alguém já respondeu o cliente no meio - `normalizarErro`: o 400 de saldo da Anthropic vira `limite_ou_saldo` (antes `erro_desconhecido` na tela de Execuções) Destino: núcleo — resposta ao cliente e fila são núcleo; sem nenhuma organização configurar nada, o comportamento vale para todas. (cherry picked from commit 8179d023517c5943f030c068e6114df8fd65074a; o som e o push do aviso ficaram de fora: dependem de um módulo que ainda não está na main) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
d69d708d8d |
feat: concluir jornadas da comunidade — organizações, atendimento, agenda e autonomia (#613)
* feat: complete community workflows for organizations, agents and scheduling
* docs: cite selected community evidence in the journey map
* test(automation): supply event boundary and agenda fixtures for AI sends
* fix: prevent readonly support from changing channel AI access
* test: verify integrated community journeys and secure fixture randomness
* fix(agenda): ocupação do Google sem catálogo volta a contar, e o gatilho do Meet trava na ordem das irmãs
fn_google_counts_for_conflicts exigia linha em calendar_connection_calendars
para um evento externo contar. Os três leitores (grade, semente da página e o
motor de horários livres) passaram a ler a view que a usa, então conexão sem
catálogo montado perdia a ocupação na tela E deixava de bloquear o horário —
o oposto do que os três leitores faziam antes. Passa a falhar ABERTO: só não
conta quem tem linha dizendo counts_for_conflicts=false.
fn_meet_delivery_enqueue travava job_queue segurando a linha do compromisso
sem o mutex do contato; fn_meet_redact_contact (0229) faz a ordem inversa.
Duas ordens opostas sobre os mesmos recursos = 40P01 sob concorrência.
* fix(contatos): fundir duplicado volta a funcionar no caso ordinário
A guarda `mescla_conversas_colidentes`, introduzida em 0222, abortava a fusão
sempre que os contatos do grupo tivessem mais de uma conversa no mesmo
`channel_session_id` — que é EXATAMENTE como a duplicata de WhatsApp nasce
(dois cadastros, dois números, o mesmo número de atendimento). O caminho
dominante do recurso virava 409.
Medido no Postgres da QA, fixture de dois contatos com uma conversa cada na
mesma sessão:
antes: ERROR: mescla_conversas_colidentes
depois: {"repontado": {"messages.contact_id": 1, "demandas.contact_id": 1},
"nao_repontado": {"conversations.contact_id": 1}, ...}
A colisão já tinha dono e não é perda de mensagem: `messages.contact_id` não
tem índice único por contato e passa inteira para o vencedor; quem colide é a
conversa, contra `uniq_conversations_1to1_per_contact_session`, e o passo 5 já
cai para repontamento linha a linha, deixa a conversa na lápide e a CONTA em
`nao_repontado` — que a rota devolve e a tela anuncia. É o desfecho que
`tests/e2e/juntar-contatos-duplicados.spec.ts` trava, com número.
O mutex de atendimento que 0222 trouxe (fn_service_lock + `for no key update`)
fica: o problema nunca foi a ordem de trava, foi a recusa.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ia): rascunho do assistido volta a respeitar dono humano e silêncio
O drain desliga o gate de elegibilidade antes de enfileirar quando a org tem
agente assistido publicado no canal (`canAssist`, drain.ts:307/315). Isso é
deliberado — o rascunho é o produto do modo assistido — e transfere a checagem
inteira para o turno. Só que o ramo assistido de `createInboundTurnHandler`
devolvia ANTES de `runAgentTurn`, que é onde as duas guardas moram
(isLeadInHandoff e decidirElegibilidadeDaConversa). O gêmeo de :1572 está
depois delas e por isso nunca sofreu.
O gate não é só o pré-go-live: a mesma consulta lê `contacts.force_human`,
`conversations.assignee_kind` (dono humano) e `bot_silenced_until`
(consulta-pg.ts:34-48). Na prática, uma conversa que uma PESSOA assumiu seguia
recebendo rascunho do robô.
As guardas entram DENTRO do ramo assistido, não antes dele: o caminho
automático já as refaz em `runAgentTurn`, e antecipá-las custaria duas queries
por turno sem mudar desfecho nenhum. Handoff falha fechado; falha da consulta
de elegibilidade degrada aberto, igual ao gêmeo.
tests/unit/assistido-respeita-o-gate.test.ts cobre os três motivos da regra
pura, o handoff, o controle positivo (sem ele, um `return` cedo demais deixaria
tudo verde por ausência) e a degradação aberta.
npx vitest run tests/unit/assistido-respeita-o-gate.test.ts
Test Files 1 passed (1) / Tests 6 passed (6)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(agenda): o veredito de I3 sai de cima do relógio
googlePushCandidates filtra `google_next_attempt_at <= now`, com `now` no
relógio do PROCESSO (new Date()) e a coluna no default `now()` do BANCO, que
aqui roda num container. O veredito de I3 passava a depender de os dois
relógios concordarem na casa dos milissegundos.
Medido nesta máquina, 30 amostras: o lote selecionado tem SEMPRE 1 linha (a
`healthy`), os 50 vínculos redigidos estão sempre fora, e a folga entre os dois
relógios é de 6 a 204 ms. Sabotagem de 5 ms (`now()+interval '5 milliseconds'`
na fixture) reproduz na hora o vermelho do CI — `:377:61 expected false to be
true`, 1 vermelho em 2 rodadas. A fixture passa a gravar o instante um minuto
no passado pelo relógio do próprio banco: tolera qualquer desvio abaixo de 60s
e não afrouxa nada do que I3 mede.
⚠️ freeze-invariants.sh contornado com DESKCOMM_GOV_INVARIANTS_EDIT=1, e o
motivo é que o congelamento não alcança este arquivo: ele NASCE neste PR
(ausente em
|
||
|
|
c5b45b24e7 |
Merge PR #466 — backoff exponencial no retry de failJob
Contribuição de @automatikpg-ux, de um incidente real na VPS dele em 2026-08-31: 49 jobs mortos em rajadas de poucos segundos, todos por TPM da OpenAI estourado durante um disparo em lote de follow-ups.
failJob devolvia o job a 'pending' sem tocar run_after, então a linha voltava para a fila já vencida e o claimJobs seguinte a repegava na mesma volta. Reproduzido na main num Postgres real: 12 rodadas de claim queimaram as 5 tentativas em 42 ms e abriram 1 alerta crítico. O autor estimou 'menos de 1 segundo' — foi conservador.
Com o patch: 10s / 20s / 40s / 80s / dead, matriz idêntica à que ele declarou no corpo do PR. Medido também em pg15.19 com tipos idênticos aos do DDL — a coerção double/interval não diverge entre 15 e 17, então esta linha não afeta o #422.
Convivência verificada na prévia real contra a main de agora: a main mudou o mesmo arquivo desde a base do PR (
|
||
|
|
e49e1fcfd1 |
fix(queue): backoff exponencial no retry de failJob
Job que falha voltava a 'pending' sem atraso — run_after ficava
intocado, já vencido, então o próximo claim (poll de poucos segundos)
repegava na hora. Contra um erro transitório (rate limit de tokens/min
do provedor de IA, por exemplo) isso queimava as 5 tentativas em menos
de 1 segundo, sem dar nenhum tempo pro limite ceder — o job morria
('dead' + alerta crítico em agent_inbox_items) por um incidente que um
minuto de espera teria resolvido sozinho.
Agora o retry espera 10s/20s/40s/80s (exponencial, capado em 120s)
antes da próxima tentativa — ~2,5min de tolerância total, o bastante
pra um rate limit por minuto liberar.
Caso real: 2026-08-31, 49 jobs (inbound_turn + followup_turn, 24
contatos distintos) mortos em rajadas de poucos segundos, todos por
TPM da OpenAI estourado durante o disparo em lote de followups.
|
||
|
|
3b2c16ad3b |
fix(queue): o relogio do worker deixa de depender do Postgres 17
## O defeito, medido em dois bancos reais
select 'infinity'::timestamptz - now()
pg15 -> ERROR: cannot subtract infinite timestamps
pg17 -> infinity
Subtrair timestamp infinito so funciona a partir do Postgres 16.
`faltaParaOProximoJob` (lib/agent-engine/queue/queue.ts) fazia
`min(run_after) - now()`. E `run_after = 'infinity'` nao e hipotetico: o hold
de sessao grava esse valor (edge/crm/session-watchdog.ts), e o worker usa essa
funcao como RELOGIO (workers/agent-worker/main.ts). Em Postgres 15 ou 16, uma
conexao em hold derruba o calculo do proximo job.
## A armadilha fina: a protecao existia, na ordem errada
A funcao ja tinha `least(..., 86400000)`, e o comentario dela dizia que ele
'segura o run_after = infinity'. Segurava — em pg17. O clamp estava no
RESULTADO, e em pg15 a SUBTRACAO estoura antes de ele rodar.
Conserto: clampar o TIMESTAMP.
least(min(run_after), now() + interval '1 day') - now()
Medido nas QUATRO condicoes que o proprio comentario da funcao documenta, nos
DOIS bancos, com resultados identicos:
fila vazia -> NULL | vencido -> 0 | infinity -> 86400000 | daqui a 30s -> 30000
O valor do infinity e o MESMO que a query antiga ja devolvia em pg17: compativel
para tras, nao muda comportamento de quem esta no 17.
## Por que tambem um gate
O conserto fecha a unica instancia que existe hoje — varri e confirmei que so
duas colunas recebem 'infinity' (run_after e bot_silenced_until) e que so
run_after aparecia em aritmetica. Nada impede a proxima.
tests/unit/aritmetica-de-timestamp-infinito.test.ts deriva as colunas POR
VARREDURA, nunca de lista fixa — lista escrita a mao envelhece em silencio no
dia em que aparecer a terceira.
## O gate nasceu INUTIL duas vezes, e as duas so apareceram na sabotagem
1. a regex exigia a coluna colada ao '-', e a forma real e `min(run_after) - now()`,
com parentese no meio: VERDE com o defeito na frente;
2. o detector de 'esta protegido' tambem reconhecia o `least(` da forma
INSEGURA — a que existia antes e nao funciona: VERDE de novo.
So na terceira ele reprovou, apontando arquivo, linha e trecho. Os dois enganos
estao escritos no cabecalho do teste, porque a proxima pessoa a mexer nessa
regex precisa saber por que ela e mais larga do que parece.
## Contexto
Isto e o que bloqueava o check `invariants` do #422 (baixa o piso do baseline
para pg15). O defeito e do PRODUTO, nao daquele PR: ele so tornou alcancavel o
que ja estava latente. Com isto na main, aquele PR fica so com o baseline.
Suite completa: 595 arquivos, 6622 casos, zero falhas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
00a1f2f8a8 |
fix(fila): o worker ocioso perguntava 4×/s se havia trabalho, abrindo transação
Uma instalação que não atende ninguém — zero contatos, zero conversas — gastava mais egress do que o plano free do Supabase permite, só perguntando à fila se tinha serviço. Medido pelo autor da issue #258 numa VPS real: 8,09 GB/mês contra uma cota de 5 GB, e 4,4 milhões de claims consecutivos devolvendo zero linhas. A causa é que a única forma de perguntar era CLAIMAR: `begin`, advisory lock, `count(*)`, o claim e `commit` — cinco statements e 638 B de egress por rodada, a cada 250 ms, para sempre. O laço não tinha como distinguir "a fila está vazia" de "há trabalho e eu não peguei", porque os dois chegavam como array vazio. Agora o worker pergunta QUANDO tem trabalho, não SE: um `select` que devolve quanto falta para o próximo job vencer (`null` = fila vazia). Custa 1 statement e 54 B, é servido pelo índice parcial que já existia — Index Only Scan, 1 buffer, 0,010 ms com 50 mil linhas — e deixa o laço dormir exatamente até o vencimento, limitado por um teto. A rodada ociosa deixa de abrir transação. `QUEUE_POLL_INTERVAL_MS` mantém o significado que sempre teve (o tempo que o laço espera) e muda só o default, 250 → 2000. Inverter isso — pôr o valor novo numa chave nova e mudar o sentido da antiga — teria mexido no contrato debaixo de quem já configurou a chave seguindo a issue. Quem pôs 2000 lá recebe exatamente o que pediu. O estado "havia trabalho e não peguei" (todas as vagas ocupadas, ou lane do contato em uso) ganha chave própria e segue nos 250 ms: recolher o ritmo ali custaria throughput no pico sem economizar nada no ocioso. O 2000 não é gosto: 15000 economizaria mais 3,2% da cota e cruzaria dois tetos de 10 s — o grace do Docker e o `idleTimeoutMillis` do pool, acima do qual cada rodada volta a pagar TCP+TLS e passa a gastar MAIS (medido: 499 B após 12 s de sono contra 66 B após 2 s). E 2000 cabe 4× dentro do `INBOUND_DEBOUNCE_MS`. O `sleep` também passou a receber o `AbortSignal` do desligamento, e o laço TRATA a rejeição. Sem esse tratamento a mudança seria pior que o defeito: `sleep` abortado rejeita, a rejeição chegaria ao `await workerLoop` do shutdown e mataria o processo antes do drain — os jobs já claimados ficariam presos em `running`, invisíveis pelos 10 minutos do visibility timeout, a cada `docker stop`. O laço saiu de dentro do `main.ts` para `queue/loop.ts` com as dependências injetadas porque as duas coisas que ele pode quebrar em produção são do call site, não da aritmética: um teste só da conta de espera ficaria verde com o shutdown derrubando o processo. Nenhum teste tocava esse laço até agora. Garantia do relógio, dita com a força que ela tem: ele é uma FOTO, não uma promessa. Um job que vence entre a foto e o despertar espera — mas nunca mais que um `QUEUE_POLL_INTERVAL_MS`, porque nada tira uma linha de `pending`+vencida. Medido em 1.300 estados de fronteira, em fuzz diferencial contra o claim real. Fica de fora, para não misturar assuntos: o índice parcial do `count(*) where status='running'` (178× em tempo, 238× em buffers com a tabela inchada — mas é CPU do banco, não egress, e arrastaria migration + baseline + MANIFEST para um PR que hoje não tem SQL nenhum) e a poda de `job_queue`, que nada faz hoje. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185XyGqvHsL4hjeZzRxQuwP |
||
|
|
9249e6f219 |
feat(agente): os três papéis — Conversador, Operador e Segurança (vazamento 30% → 10%) (#181)
* docs(agente): a spec dos tres papeis, e a terceira porta que a doutrina nao previa A doutrina FALAR/OPERAR ja existia e o passo 1 dela ja estava fechado com numero (30% de vazamento com prompt de operador, 0% com prompt de atendimento, 18 turnos, gate desarmado). O que faltava era a spec que rege o resto. Fecha as sete decisoes de produto. As tres que mudam o desenho: uma unidade com tres papeis (nao tres agentes), o Operador roda DEPOIS do envio (o lead nunca espera o CRM), e desligar o Operador NAO desliga o registro — a declaracao do Conversador ja e linguagem de negocio e vira estado por codigo, sem LLM. Registra a TERCEIRA porta de vazamento, que a doutrina subestimava: o vazamento medido `unsafe_url:https_required` nao veio da description nem do name da tool, veio do DADO que ela devolveu. Esconder a ferramenta nao fecha essa porta — exige camada de projecao propria. Na mesma resposta chegaram UUIDs crus de usuario a tela do cliente, que o detector nao pega por desenho. Registra tambem que o botao Testar da tela nao exercita guardrail nenhum: /versions/[vid]/test chama runAgent do runtime deprecated, que nao importa runBeforeSend. O self-hoster testa, ve limpo, publica, e producao diverge. Nao muda comportamento — tres documentos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): a declaracao do turno — a fronteira FALAR/OPERAR vira contrato Passo 2 da spec 16. O checkpoint ja era a metade de um contrato entre quem fala e quem opera; faltava o campo que diz o QUE ACONTECEU no turno, e faltava declara-lo como fronteira em vez de subproduto. A declaracao viaja na chamada de FECHAMENTO que ja existe, nao numa tool. O motivo e o que este arquivo ja escrevia sobre o checkpoint: uma tool dependeria de o modelo lembrar de chama-la, e o turno em que ele esquecesse seria um lead parado em silencio. Custo adicional zero — e a mesma chamada de modelo. AUSENTE NAO E VAZIA, e o desenho inteiro gira nisso. `declaracao: undefined` (coluna NULL) = o modelo nao declarou; `{nada_a_declarar: true}` = ele avaliou e nao havia nada. Um `.default({})` no Zod, ou um `not null default` na coluna, colapsaria os dois e faria "esqueceu" parecer "nao havia nada" — escondendo o esquecimento exatamente onde o invariante 4 do sistema vivo manda mostra-lo. LeadCheckpointRow faz Omit e redeclara o campo porque as duas pontas representam o "nao sei" diferente: o modelo omite, o Postgres guarda null. A declaracao ja NASCE com leitor, senao seria evento sem consumer: buildHandoffSummary passa a entregar ao humano o que a pessoa quer e o que foi prometido a ela, antes de compromissos e objecoes — e o prazo vai colado na promessa, porque quem assume precisa do "ate quando" na mesma linha. Um artefato, dois leitores (invariante 2). O texto que o Conversador le nao carrega vocabulario de sistema, e ha teste que reprova se carregar — e o defeito de 30% que a spec existe para matar. Schema em tripla: migration 0110 + apendice idempotente no baseline + MANIFEST. Evidencia observada: 16 testes novos; suite 2603 passed (270 arquivos); typecheck limpo; lint 0 erros e 189 warnings — o MESMO numero medido na main com as mudancas em stash, entao nenhum warning novo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): a projecao — fecha a terceira porta, a que a doutrina subestimava Passo 3 da spec 16. As duas primeiras portas de vazamento (nome e descricao da ferramenta) se fecham nao mostrando a ferramenta. A terceira nao: e o DADO que ela devolve, e foi medida em turno real — "deram erro na acao de webhook: unsafe_url:https_required", copiado do resultado para a tela do cliente. ALLOWLIST, nunca denylist: campo novo no contexto nao chega ao cliente por omissao. A linha e identificador do SISTEMA sai (lead_id, conversation_id, media_storage_path), dado da PESSOA fica (nome, telefone, e-mail) — confundir as duas produz um agente que nao sabe com quem fala. O interruptor se justifica sozinho: a projecao arma quando NENHUMA ferramenta de catalogo entrou no turno, porque e exatamente ai que os ids nao tem uso (as MCP os recebem como argumento, as nativas resolvem pelo closure). Liga-la sempre quebraria o agente que opera; por env seria mais um botao para errar. No passo 6, quando o Conversador perder as ferramentas de escrita, ela passa a valer sozinha. ACHADO NAO PREVISTO, corrigido aqui: search_knowledge devolvia chunk_id e knowledge_source_id CRUS ao modelo, dois UUIDs por resultado, em toda busca com RAG — sem uso nenhum do lado dele, ja que as citacoes sao montadas pelo codigo. Era fonte silenciosa do UUID cru que a medicao viu chegar na tela do cliente. Fecha tambem a releitura: sem isso a projecao seria decorativa, bastava o modelo chamar get_lead_context para receber tudo cru de volta. E o cabecalho do bloco de estado deixa de imprimir o nome da tabela (lead_state). UM BUG QUE QUASE ENTROU, registrado porque o teste dele fica: eu aplicava a traducao de erro ao CORPO das mensagens. "Meu site tem um webhook quebrado" — frase legitima de quem vende integracao — viraria "nao consegui concluir essa verificacao", e o agente responderia a uma pergunta que ninguem fez. Os testes medem contra os turnos REAIS de gpt-5.6-terra versionados em evidence/, com controle positivo em cada caso e falha declarada se a evidencia sumir. Fixture inventada mede a imaginacao de quem a escreveu, que e justamente a que ja falhou — senao o defeito nao teria acontecido. `projeta` ficou OPCIONAL no tipo de buildOpening porque tests/invariants/** e congelado por hook: torna-lo obrigatorio forcaria editar um invariante existente, que a catraca proibe com razao. Custo registrado no handoff. Evidencia observada: 18 testes novos; suite 2621 passed (271 arquivos); test:db verde (68 arquivos, 462 passed, 1 skipped); typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(projecao): a traducao de erro pisava no conteudo do RAG do tenant Bug MEU, introduzido no commit anterior e pego pela sabotagem — nao por leitura. `traduzirErroCru` era aplicada a TODA string do retorno de ferramenta. O detector de vazamento considera "webhook" vazamento (com razao, na SAIDA), e a base de conhecimento de qualquer empresa que venda integracao fala webhook. O `content` do search_knowledge viraria "nao consegui concluir essa verificacao" linha apos linha: a projecao destruiria o RAG do tenant para proteger contra um vazamento que o gate de saida ja cobre. E a MESMA falha que a de reescrever a fala do cliente, corrigida horas antes, numa irma que nao se parece por fora: as duas nascem de aplicar num texto de TERCEIROS um filtro desenhado para texto do SISTEMA. Procurei a classe depois de pegar a primeira instancia e nao achei esta — ela so apareceu quando a sabotagem mostrou que o teste passava pelo mecanismo errado. Agora a traducao so alcanca valor sob chave de erro (lista fechada e curta: erro/error/message/code/reason/detail e os equivalentes em pt). Dois testes novos: o conteudo do RAG sobrevive intacto falando "webhook", e o campo de erro continua traduzido — a restricao nao desarmou a defesa. COMO O TESTE ANTERIOR PASSAVA SEM MEDIR: o detector pega UUID (categoria erro_cru), entao os casos de evidencia passavam pela TRADUCAO mesmo com a remocao de chave desligada — dois mecanismos redundantes, e eu atribuia o resultado ao errado. A sabotagem S2 derrubava 1 caso de 3; era o sinal. Evidencia observada: 20 testes; typecheck limpo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): o bug do RAG e a sabotagem que o pegou Registra no handoff vivo o defeito que entrei e corrigi (traducao pisando no conteudo da base de conhecimento), COMO o teste passava sem medir (dois mecanismos redundantes, credito atribuido ao errado), e o sinal que denunciou — S2 derrubando 1 caso de 3, quando devia derrubar os 3. Depois da correcao: S2 derruba 3 de 3. Oito sabotagens, oito reprovacoes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): nasce o OPERADOR — disparo imposto pelo runtime, nunca pelo modelo Passo 4 da spec 16. O papel que mexe no sistema e nunca fala com o lead: ele nao tem send_message no toolset. A separacao e por AUSENCIA, a unica forma que nao depende de o modelo obedecer. O DISPARO E DO RUNTIME. O job `operator_turn` e enfileirado no fim do turno, logo apos o checkpoint existir, com sourceEventId = job do Conversador (retry da fila nao gera um segundo Operador). Se o Conversador o "chamasse", devolveria o problema inteiro: voltaria a depender de o modelo lembrar, e o turno em que ele nao achasse necessario seria um lead parado no funil, em silencio. Depois do checkpoint porque a declaracao E o insumo — antes, ele leria o turno anterior. O CURTO-CIRCUITO E ONDE O PASSO 2 SE PAGA. `nada_a_declarar: true` => nao chama modelo (quem avaliou estava la, com todo o contexto). Declaracao AUSENTE => roda, porque ausente significa que NINGUEM avaliou, e e ai que promessa fica orfa. Os dois estados que o passo 2 se recusou a colapsar decidem, aqui, se a chave do self-hoster e gasta. Ha teste afirmando que eles levam a decisoes OPOSTAS. `operator_enabled` nasce false: migration nao liga sozinha um papel que gasta chamada de modelo por turno. `operator_model` null = herda o do Conversador. O loader le `?? false` para o clone que ainda nao aplicou a 0111 — schema velho desliga o papel, nunca gasta modelo por engano. Promessa declarada e nao cumprida vira `promise_unfulfilled` na Central, com copy que diz o que o CLIENTE espera, nao o que o sistema deixou de gravar — do lado de la existe uma pessoa que ouviu um compromisso. DOIS ERROS MEUS QUE AS CATRACAS DO REPO PEGARAM, nao eu: - criei um SEGUNDO bloco de `job_queue_kind_check` no baseline quando ja existe um. Isso quebra o update.sh de todo clone com vocabulario posterior. Eu tinha aplicado essa licao corretamente ao agent_inbox_items minutos antes e a irma passou batido — o padrao pego numa ocorrencia da alibi as outras. Consolidado no bloco unico (tests/unit/baseline-constraint-reconstruida.test.ts); - o kind novo divergia entre banco e TypeScript (tests/invariants/vocabulario-banco-x-typescript.test.ts). Evidencia observada: 7 testes de decisao + 5 invariantes de schema; suite 2630 passed (272 arquivos); test:db verde (69 arquivos, 467 passed); typecheck limpo; lint 0 erros / 189 warnings (mesmo numero da main). Schema em tripla: migration 0111 + apendice no baseline + MANIFEST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 4 registrado, com as duas catracas que me pegaram Registra o que o passo 4 entregou e o que NAO entregou (o Operador tem canal, ainda nao tem mao), a sabotagem com contagem prevista, e os dois erros meus que foram pegos por catraca do repo e nao por leitura minha — porque a saida limpa sem essa nota creditaria ao rigor o que foi merito do mecanismo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): o Operador ganha MAO — ferramentas proprias, e nenhuma delas fala Fatia 1 do passo 5. O papel deixa de so registrar promessa e passa a operar: le `operator_tool_ids`, monta o toolset pela ponte MCP e roda um turno de modelo com briefing proprio e atribuicao de custo propria (purpose 'operator_turn' — sem isso o gasto dele entraria como se fosse conversa, e "quanto custa ligar o papel" nao teria resposta). COLUNA PROPRIA, nao reuso de tool_ids, por tres razoes que se somam: a tela nao pode mentir (lista compartilhada faria a secao Operador configurar o que o Conversador executa); o teto de 20 se resolve por DIVISAO em vez de aumento (hoje sao ate 32 tools num prompt so, e o e2e capacidades-do-agente esta fora do CI por causa disso); e o passo 6 vira migration de DADOS, mover ids entre colunas. SEM CANAL, e isso e estrutural: send_message e tool NATIVA do engine e nao existe no catalogo, entao nao ha id que a ligue; e crm_send_whatsapp_message esta em BLOCKED_TOOL_IDS, que agora e exportado para ser aussertivel — garantia que nenhum teste consegue ler e garantia que ninguem percebe quando some. O system do papel fala de CRM com todas as letras, e PODE: esse texto nao alcanca cliente. E a assimetria exata da spec — no Conversador o mesmo vocabulario e o defeito de 30% medido. Sem ferramenta configurada ele nao chama modelo: descobrir que nao tem mao nao vale a chave do self-hoster. Herdar as do Conversador daria 20 capacidades a quem nao escolheu nenhuma, entao o default e vazio. TERCEIRA CATRACA DO REPO A ME PEGAR: VERSION_COLUMNS esta copiado em 6 arquivos e eu atualizei 1. O proprio teste conta que `cases_enabled` fez isso (entrou em 2 de 7) e o sintoma nao e typecheck nem teste — e um campo que "se desmarca sozinho" na tela depois do refresh, e o save seguinte grava por cima. Repeti o padrao que o comentario descrevia. Evidencia observada: 8 testes novos; suite unit 1459 passed (154 arquivos); test:db verde (69 arquivos, 467 passed); typecheck limpo. Schema em tripla: migration 0112 + apendice no baseline + MANIFEST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(ui): a tela ganha os PAPEIS — e diz a consequencia, nao o nome da feature Fatia 2 do passo 5. A configuracao do agente deixa de ser uma lista unica e passa a ter navegacao por papel: "Conversa com o cliente" e "Organiza o sistema". Os rotulos dizem o que cada papel FAZ — Conversador/Operador e o nosso vocabulario interno; quem configura pensa em "quem fala com meu cliente" e "quem organiza minha casa". SECOES, nao abas com save proprio: o rascunho e um so. Duas telas com dois saves fariam o usuario publicar metade da configuracao e criariam dois caminhos para o mesmo ai_agent_versions. A REGUA DA DISCIPLINA DE INFORMACAO e dizer a CONSEQUENCIA. Com o papel desligado, a tela explica o que CONTINUA acontecendo (o basico e registrado sozinho — decisao da spec 16 §2.1) e o que PARA (decidir sobre a operacao). Sem a primeira frase o usuario conclui que desligar deixa o sistema cego e liga por medo, nao por escolha. Ha teste exigindo as duas. Papel desligado NAO mostra a configuracao dele: convidar alguem a configurar o que nao vai rodar produz a conclusao de que o produto quebrou. O modelo herdado tem NOME na tela ("A mesma que conversa"), em vez de aparecer como pendencia — e ganhou o caminho de VOLTA: um Select nao oferece "nenhum" como item, entao sem o botao a escolha seria de mao unica e o usuario nao teria como saber por que. QUARTA CATRACA a me pegar nesta sessao: VERSION_COLUMNS em 6 arquivos, eu atualizei 1 (corrigido na fatia anterior). Ha um teste novo que varre a tela inteira atras do nosso vocabulario (MCP, tool_ids, operator_, job, payload). Evidencia observada: 8 testes de componente; suite unit 1459 passed antes deste arquivo; typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 5 parcial, e o bloqueio que impediu a prova de tela Registra as duas fatias entregues (mao do Operador + tela dos papeis) e o motivo de a prova de jornada NAO ter sido feita: .env.local aponta para a nuvem, e o playwright sobe o app com next start, que carrega .env.local. Rodar e2e nesta maquina hoje cria agentes e conversas de teste no banco de PRODUCAO. Nao e especifico deste epico — vale para qualquer sessao que rode e2e aqui agora. Fica declarado que o que esta provado da tela e comportamento de componente, nao jornada de usuario. Sao coisas diferentes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(e2e): a suite escrevia no banco de PRODUCAO — tres camadas de conserto Medido em 2026-08-06. `.env.local` de um checkout de trabalho aponta para a nuvem, e o playwright sobe o app com `next start`, que o carrega. A suite criava organizacoes, usuarios e agentes de teste no banco real, passando verde. A org `e2e-test-org` esta na producao deste projeto desde 2026-04-29. Ela nao chegou la por acidente de uma sessao: chegou porque esse era o comportamento normal da suite. ISOLAR O webServer NAO BASTAVA, e essa foi a descoberta. Os scripts de seed leem `.env.local` DIRETO DO DISCO, ignorando process.env — entao o env injetado no servidor nunca os alcancava, e as specs chamam esses seeds sozinhas no meio do teste. O sintoma que denunciou: o factor TOTP em `.e2e-creds.json` nao existia no banco local, porque tinha sido criado na nuvem. 93 arquivos tem esse padrao. As tres camadas: 1. `.env.e2e` (gerado por `pnpm e2e:env`, nao versionado) + injecao explicita no webServer, com guard que RECUSA rodar se o arquivo faltar — cair no .env.local em silencio e o modo de falha caro; 2. `pnpm e2e:build`, porque as tres NEXT_PUBLIC_* sao embutidas no BUNDLE: buildar com o env errado e trocar so no start deixaria a URL de producao dentro do JavaScript do browser. `node --env-file` nao serve aqui — o next build cria Workers e o Node recusa propagar a flag (ERR_WORKER_INVALID_EXEC_ARGV). O script prova as DUAS direcoes: host de producao ausente do bundle E host local presente (sem o controle positivo, "nao achei producao" pode ser um grep que nao acha nada); 3. `scripts/lib/env-de-teste.ts` — process.env VENCE o arquivo, e todo seed ANUNCIA o destino ("escrevendo em LOCAL" / "⚠️ REMOTO"). Um seed que escreve em producao acha os mesmos dados de teste de sempre e termina com "✅ Seed completo": sem a linha impressa, nada distingue os dois casos. `seed-e2e-credentials.ts` migrado (e o que a suite chama sempre). Os outros ~14 seeds e as ~78 sondas ficam declarados como divida no handoff. GANHO ESTRUTURAL do worktree dedicado: aqui nao existe `.env.local`, entao um script que o leia do disco falha ALTO (ENOENT) em vez de escrever na nuvem. O isolamento deixa de depender de disciplina. Evidencia observada: spec agente-novo-e-uso 5/5 passed contra o banco LOCAL (era 0/5 antes, por MFA — o factor do banco nao batia com o do arquivo); suite unit 1467 passed (155 arquivos); typecheck limpo; guard do playwright recusa sem .env.e2e (verificado). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): o worktree dedicado, e as tres camadas do isolamento do e2e Registra o achado que destravou a prova de tela e que vale para o repo inteiro: a suite escrevia em producao, e isolar so o webServer nao bastava porque os seeds leem .env.local do disco. Registra tambem o ganho estrutural do worktree (sem .env.local, script que o leia falha alto em vez de escrever na nuvem) e as tres dividas que sobraram: os ~92 scripts nao migrados, o spec da aba nova, e a decisao sobre as fixtures que ja estao na producao. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(e2e): os 16 seeds param de ler .env.local do disco, e um gate congela isso Completa o conserto de |
||
|
|
26145fa4ea |
fix(worker): o worker parou de subir — regressao minha no alerta de job morto
Ao juntar o motivo ao alerta de job descartado, referenciei last_error dentro da CTE do reaper. Só que a CTE `expired` não devolvia essa coluna no returning, e uma CTE só enxerga o que a anterior devolve — não as colunas da tabela. A query inteira morria com "column last_error does not exist". Como esse reap roda no BOOT do worker, o worker entrava em loop de reinício: nenhum turno de agente era processado enquanto isso. O erro de método foi meu e vale registrar: eu VALIDEI a expressão nova contra linhas reais, mas isolada — não dentro da CTE onde ela ia viver. Testei a peça e não a montagem, e a peça passou. Agora last_error sai no returning, e a query INTEIRA foi executada contra o banco real (em transação com rollback) antes de publicar. Junto, um segundo erro no mesmo commit: o comentário que escrevi usava crase em volta do nome da coluna, dentro de um template literal delimitado por crase — fechava a string no meio do SQL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eDYqCWr7Zrs99vLjqGj8r |
||
|
|
054969c663 |
fix(alerta): o aviso de job morto descartava justamente o motivo
O Inbox da IA avisou certo — 16 alertas criticos "Job descartado apos esgotar
tentativas" enquanto o agente estava parado. O mecanismo anti-morte funcionou.
O problema era o conteudo: o corpo do alerta era
kind=inbound_turn; attempts=5
e mais nada. O erro que matou o job ja estava guardado em job_queue.last_error
e dizia exatamente o que fazer ("modelo LLM nao definido — configure
organizations.settings.llm.default_model"). O alerta jogava fora a unica
informacao que resolveria: existia e nao dizia nada.
Agora o motivo vai junto (400 primeiros caracteres). Conferido contra as linhas
reais desta VPS antes de publicar — a mudanca vive dentro de uma string SQL,
onde erro de sintaxe so apareceria em producao.
Vale para os dois pontos que criam o alerta (falha comum e falha por timeout).
Junto, duas correcoes de rumo no dossie de QA: eu havia escrito que as falhas
do turno vinham do meu pause do agente (nao vinham — com o agente publicado
falhava igual) e que "nada na interface avisava" (avisava; o que faltava era o
motivo).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eDYqCWr7Zrs99vLjqGj8r
|
||
|
|
474c7b9358 |
feat(casos-humanos): repositório de casos + JobKind case_reply_turn [wave 2]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
de87adc79f |
feat(fusion): porte completo dos módulos do engine ao schema canônico (Fase 0)
- queue/cron/db, pacing/spinning/health, guardrails, agent/*, edge/llm re-portados (organization_id/contact_id, agent_inbox_items, contacts.is_blocked direto) - edge/llm no AI SDK v6 do repo + BYOK via ai_provider_credentials (aes_gcm) - human-handoff sem MCP: contacts.force_human + conversations.bot_silenced_until - zod v3 em todo o engine; pnpm fixado no Dockerfile.worker Cada lane verificada com tsc pelo respectivo porte (duas rodadas repo-wide exit 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CsMA4LDfgNarXsqEoLJmyV |
||
|
|
f36540297e |
feat(fusion): raw port do daemon do Vendaval para lib/agent-engine (Fase 0, WIP)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CsMA4LDfgNarXsqEoLJmyV |