A causa raiz dos bugs de "info repetida em turns anteriores" era o
default_scope ASC do Message conflitando com .order(desc) no debounce
(ver commit f1d3a124d). Como já corrigi com .reorder, as Camadas 2, 3 e
4 viraram peso morto que adicionava latência/false positive sem ganho.
Removido:
- Camada 2 (factual sem tool → retrigger force_factual_tool)
- Camada 3 (strip de linhas repetidas com pool de outgoings anteriores)
- Camada 4 (topic gating: bloqueio quando resposta tem tópico não pedido)
- Tracker de tool calls em McpController (suportava Camada 2)
- Snapshot baseline em OutgoingJob (suportava Camada 2)
- Regra "🚨 NÃO CONFIE NA SUA MEMÓRIA" das 4 SOUL.md Hermes
Mantido:
- Camada 1: handoff intencional ("Um momento — vou verificar") +
loop detection (Jaccard >= 0.50 ou pergunta reformulada com 3+
keywords). Genuíno pra bot externo (Claro/Vivo) e loops óbvios.
- Label-guard em OutgoingJob (não dispatch se conv tem triagem_humana).
- Auto-react ambient (feature original).
- Reorder fix no combined_incoming_content (causa raiz).
Memory + user_profile reabilitados nos 4 Hermes (config.yaml) e no
template do hermes-provision pra futuros agentes. Sem memória, cliente
precisa repetir nome/CPF/contexto a cada turn — UX horrível.
Contaminação cross-unit que justificava desligar vinha de outro bug
(X-Captain-Assistant-Id apontando pro parent), já corrigido.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Camada 3 (strip de linhas repetidas) não cobre quando LLM mistura tópico
antigo + atual numa LINHA SÓ (caso real: cliente pergunta sobre pet,
Hermes responde "A senha é Prime2025 e pode levar animais sim" —
overlap baixo, strip não dispara).
Camada 4 detecta tópicos factuais (Wi-Fi/senha/pet/estacionamento/preço/
cancelamento) presentes NA RESPOSTA mas AUSENTES da última pergunta do
cliente. Quando detectado, bloqueia entrega + dispara
[SISTEMA: force_topic_focus] no Hermes mandando responder
EXCLUSIVAMENTE sobre o tópico atual. 1 retry; persistindo, entrega.
Validado: cliente "Posso levar animais?" + resposta "Senha + pet" →
detecta [:wifi] como off-topic. Cliente "Qual senha wifi?" + resposta
"Senha é X" → vazio (passa normal).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LLM tende a "resumir" info de turns anteriores em toda nova resposta.
Camada 3 strip linhas onde >=70% das palavras significativas já apareceram
nas últimas 3 outgoings (filtra reactions). Saudações curtas preservadas.
Caso real Juliana 2026-05-02 (turn 3 ela ia repetir "Senha Prime2025 +
pet" mesmo cliente só dizendo "valeu"). Após strip: vira só "Imagina,
Rodrigo 😊".
Validator UI: novo check "FAQs próprias aprovadas > 0" — alerta quando
zero (faq_lookup cai no parent, risco de info desatualizada igual ao
bug do X-Captain-Assistant-Id que vazou Wi-Fi do parent hoje cedo).
Filtro SQL `content_attributes ->> 'external_source'` não casava (coluna
json, não jsonb); migrado pra filtro Ruby.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detecta alucinação de memória: se resposta do Hermes contém info
factual (preço/senha/horário/regra/política) E o LLM NÃO chamou
nenhuma tool MCP entre dispatch e callback, bloqueia entrega + dispara
system_message forçando consulta a tool. 1 retry; persistindo, escala.
Implementação:
- McpController: incrementa Rails.cache hermes_tool_calls:<conv_id>
em cada tools/call.
- OutgoingJob: snapshot do contador como hermes_tool_calls_baseline
ANTES de despachar pro Hermes.
- HermesCallbackController.gate_factual_no_tool!: compara baseline vs
current; se igual + FACTUAL_PATTERNS bate, intercepta. Patterns
cobrem R$, %, "senha", check-in/out + horário, política de
cancelamento, "permitido", "pode levar pet/animal".
Caso real: cliente pede senha do Wi-Fi → Hermes responde de cabeça
"é passada presencialmente" sem chamar faq_lookup → callback intercepta,
não entrega pro cliente, manda [SISTEMA: force_factual_tool] pro Hermes
com instrução de chamar faq_lookup. Se faq_lookup vier vazio → frase-
âncora handoff.
Auto-react ambient: removido filtro de "?" que barrava em prod.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substitui o interceptor agressivo de "Um momento — vou verificar"
(que bloqueava silenciosamente) por handoff explícito + loop detection:
- HANDOFF_PATTERNS: detecta a frase-âncora ("Um momento — vou
verificar", "Aguarde um instante") e ENTREGA pro cliente,
marcando conv com label triagem_humana.
- looped_response?: detecta 2 outgoing similares (Jaccard >= 0.50)
OU pergunta reformulada sobre mesmo tópico (>= 3 keywords em comum
+ ambas inquisitivas via "?" / "me confirma" / "qual prefere"
etc). 1ª resposta passa, 2ª escala. Cobre o caso "endereço ou
link?" → "apenas link ou link + endereço?".
- OutgoingJob: guard que pula dispatch se conv tem label
triagem_humana ou hermes_placeholder. Hermes não responde mais →
não gasta token + não gera loop.
Cobre 2 casos do Rodrigo:
1. Bot da Claro insistindo em menu → 2ª resposta similar escala.
2. Hermes pedindo confirmação 2x sem entregar → escala.
Tokenize normaliza acentos (transliterate) pra stopwords baterem
"voce/você", "endereco/endereço", etc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quando o LLM emite "⏳ Um momento — vou verificar." (ou variantes:
"deixa eu verificar", "aguarde um instante") sem chamar nenhuma tool,
o callback agora:
1. NÃO entrega a msg pro cliente (UX terrível ver "vou verificar" e
ficar esperando indefinidamente).
2. Dispara notify_event pro Hermes com [SISTEMA: force_tool_call]
instruindo a retomar e chamar a tool relevante (generate_pix,
send_suite_images, faq_lookup) com base na última msg do cliente.
3. Limita 2 retries por conversation via Rails.cache (TTL 5min). Após
esgotar, marca labels hermes_placeholder + triagem_humana e descarta.
Caso real do Rodrigo: cliente confirmou reserva ("Para hoje 23h por 4h")
e o LLM respondeu apenas o placeholder (api_calls=1 no daemon, sem
tool). Cliente ficava esperando sem resposta. Agora Captain força o
LLM a chamar a tool, ou cai pra triagem humana após 2 tentativas.
PLACEHOLDER_PATTERNS cobre as variações observadas. SKILL.md já proibia
"Um momento", mas o LLM ignorava — defesa em camadas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hermes daemon faz POST /webhooks/captain/hermes_callback?slug=<profile>
mas controller só conhecia ?inbox_id. 404 → resposta do LLM nunca chegava
ao Captain. Cliente via só auto-react.
Fix: fetch_inbox resolve via Captain::Assistant.find_by(hermes_profile_name)
quando slug está presente. Inbox é a primeira CaptainInbox associada a
esse assistant. Suporta o pattern admin de re-apontar uma inbox de teste
(ex: Angelina) entre vários agentes Hermes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: Hermes nao propaga chat_id no metadata do callback. Callback
controller nao conseguia resolver qual session armazenar a resposta.
WARN "no session_key resolvable — ignorando" descartava todas as
respostas do Construtor.
Fix: HermesBuilder::Storage.remember_last_session() grava ultima
session por account quando admin chama /start ou /create. Callback
le essa key via last_session_for(account_id). MVP-safe pra 1 admin
por conta.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tela "Construtor" no painel Captain (acessivel em sidebar pra admins) que
permite criar novo agente Hermes via chat guiado com agente Construtor
(profile Hermes separado).
Backend (admin scope):
- POST /api/v1/accounts/:id/captain/hermes_builder — manda mensagem do
admin pro gateway do Construtor (Hermes na porta 8646)
- GET — retorna historico da sessao (Rails.cache, TTL 4h)
- DELETE /reset — limpa sessao
- POST /webhooks/captain/builder_callback — recebe respostas async do
Construtor via plugin captain-http-callback do Hermes
- HermesBuilder::Storage (Rails.cache) — persiste msgs por session_key
(account_id + user_id) com role/content/created_at
- HermesBuilder::Dispatcher — encaminha pro webhook do Construtor com
HMAC opcional via ENV HERMES_BUILDER_WEBHOOK_SECRET
Frontend:
- Pagina Vue HermesBuilder/Index.vue — chat simples com:
* Lista de mensagens com bubbles user/construtor
* Indicador "digitando..." enquanto aguarda resposta
* Input com Enter pra enviar / Shift+Enter pra nova linha
* Polling 2s pra novas msgs
* Botao Limpar conversa
- API client em api/captain/hermesBuilder.js
- Rota captain_hermes_builder_index (admin only)
- Item no sidebar Captain "Construtor (Hermes)"
- i18n keys CAPTAIN.HERMES_BUILDER em pt_BR + en
UX flow:
Admin abre tela → digita "olá" → Construtor pergunta nome → admin
responde → marca, persona, tabela (com opcao copiar de existente),
regras, FAQs, identidade → resumo → confirmar → Construtor chama
save_agent_spec → JSON salvo em /tmp/agent-specs/<slug>.json pra
revisao posterior. NAO cria filesystem do profile nem registros DB
(etapa SEPARADA, prox sessao).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captain::Hermes::Client (enterprise/app/services/captain/hermes/client.rb):
- text_for_hermes: transcreve audio via Whisper antes de enviar pro Hermes
(reusa Captain::OpenAiMessageBuilderService)
- image_urls_for_hermes: URLs publicas de imagens da message; plugin
captain-webhook do Hermes baixa em /tmp/ e popula event.media_urls pra
vision multimodal (gpt-4o-mini auxiliary)
- contact_history_snapshot: dados eager pro [ctx] (last_reservation_*,
total_conversations, ultima_suite, etc) — memoria do contato direto no
prompt sem precisar tool call
- notify_event + build_event_payload: dispara webhook sintetico pro Hermes
pra eventos do sistema (Pix pago etc) — Valentina manda mensagem
espontanea sem cliente perguntar
Captain::Payments::ConfirmationService:
- Hook notify_hermes_proactively! enfileira NotifyPaymentConfirmedJob
apos confirmacao de Pix, somente se inbox estiver no fluxo Hermes
(Captain interno continua igual sem mudanca)
Captain::Hermes::NotifyPaymentConfirmedJob (NOVO):
- Monta system_message "[SISTEMA: pagamento_confirmado]\n..." e dispara
webhook pro Hermes Valentina
- Valentina (via SOUL.md) interpreta como evento do Captain e manda
mensagem celebrativa pro cliente
Captain::Hermes::DelayedReplyJob (NOVO) — humanizadores:
- Liga indicador "digitando..." (composing) via wuzapi
- Aguarda delay configuravel via Captain::Assistant.config['response_delay']
(modos: none, fixed, typing_simulation com chars_per_second + min/max)
- Posta msg outgoing
- Desliga typing
- Fallback no HermesCallbackController posta direto se class nao carregada
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tools novas em enterprise/app/services/captain/mcp/tools/:
- generate_pix: Pix Inter via PricingTable + fallback link reserva-1001
- check_pix_payment: consulta Inter + dispara ConfirmationService
- send_suite_images: fotos da galeria (Captain::GalleryItem) via wuzapi
- reschedule_reservation: remarca reserva (regra 3h antecedência Dolce)
- update_contact: persiste nome/CPF/email/notas no Contact
- get_contact_history: markdown do histórico do cliente on-demand
- react_to_message: reage com emoji via wuzapi (is_reaction=true)
Captain::Mcp::PricingTables: tabela hardcoded Dolce Amore (8 categorias x 4
periodos + regras de pessoa extra). Backend e fonte de verdade — LLM nao
inventa preco.
add_label_tool: cria Label oficial automaticamente se nao existir, aceita
conversation_id em arguments.
mcp_controller: aceita X-Captain-Account-Id/Assistant-Id/Inbox-Id como
fallback de contexto.
tool_registry: 9 tools ativas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hermes Agent (cliente MCP) usa `--auth header` que envia
`Authorization: Bearer <token>` — padrão MCP. Antes o Captain MCP server
só aceitava HMAC-SHA256 (X-Hub-Signature-256), incompatível com o que
Hermes manda nativamente.
Agora aceita qualquer um dos 2 modos:
- Bearer token (recomendado, padrão MCP) — Hermes envia automaticamente
- HMAC-SHA256 do body — pra clientes que preferem assinar payload
Ambos validados com ActiveSupport::SecurityUtils.secure_compare contra
o mesmo CAPTAIN_MCP_SECRET. Sem secret = validação desabilitada (PoC/dev).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementa servidor MCP (Model Context Protocol) HTTP no Captain pra
o Hermes Agent invocar tools do Captain via `hermes mcp add`. Substrato
pra integração de Nível 2 onde o agente consulta tools quando precisa
executar ações reais (buscar FAQ, adicionar label, futuramente Pix etc).
Arquivos:
- app/controllers/webhooks/captain/mcp_controller.rb
Endpoint POST /webhooks/captain/mcp. Valida HMAC (CAPTAIN_MCP_SECRET),
parseia JSON-RPC, despacha pro Server. Extrai params._captain_context
com multi-tenant ids (conversation_id, inbox_id, account_id, etc).
- enterprise/app/services/captain/mcp/server.rb
Subset MCP suficiente: initialize, tools/list, tools/call, ping,
notifications/initialized. JSON-RPC síncrono (sem SSE).
- enterprise/app/services/captain/mcp/tool_registry.rb
Lista centralizada de tools.
- enterprise/app/services/captain/mcp/tools/base_tool.rb
Interface base pras tools (.name, .description, .input_schema, #call).
- enterprise/app/services/captain/mcp/tools/add_label_tool.rb
Tool 1: aplica label na conversation atual.
- enterprise/app/services/captain/mcp/tools/faq_lookup_tool.rb
Tool 2: busca semântica em FAQs (Captain::AssistantResponse via pgvector
cosine). Reaproveita SearchReplyDocumentationService pra paridade com
o caminho legado do Captain.
- config/routes.rb
Rota POST /webhooks/captain/mcp.
Conexão pelo Hermes:
hermes mcp add captain-tools --url http://CAPTAIN_HOST/webhooks/captain/mcp
Auth: HMAC X-Hub-Signature-256 quando CAPTAIN_MCP_SECRET setado.
TODO próxima sprint: generate_pix_tool, send_suite_images_tool. Handoff
fica via automation hoje (UI Chatwoot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hermes (e LLMs default em geral) emitem **negrito** no formato markdown
padrão. WhatsApp usa formato próprio: *negrito* (single asterisk). Sem
conversão, o cliente vê asteriscos literais no WhatsApp, parecendo bug.
Defesa em camadas:
1. SOUL.md da Valentina foi atualizado com regra explícita de formato
WhatsApp (single asterisk pra bold, underscore pra itálico, etc).
2. Este controller faz normalização defensiva no callback recebido do
Hermes: regex `**texto**` -> `*texto*` antes de criar a mensagem
outgoing. Não afeta o resto do conteúdo.
normalize_for_whatsapp() é trivialmente reversível e idempotente
(executar 2x é igual a 1x).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementa o lado Captain da integração Nível 2 (Hermes como cérebro).
Ativação por inbox via env var CAPTAIN_HERMES_INBOX_IDS — inboxes não
listadas seguem usando o orquestrador interno do Captain (Daniela_Reservas
etc) sem mudança alguma. Princípio "só adiciona, não retira".
Componentes:
- enterprise/app/services/captain/hermes.rb
Módulo helper de config (env vars, URLs, secrets per-inbox).
- enterprise/app/services/captain/hermes/client.rb
Service que monta payload (msg + contexto da conversa/inbox/contato) e
faz POST autenticado via HMAC-SHA256 (X-Hub-Signature-256) no webhook
do Hermes Agent (porta 8644). DispatchError em falha de rede/HTTP.
- enterprise/app/jobs/captain/hermes/outgoing_job.rb
Wrapper Sidekiq do Client. Retry 3x em DispatchError.
- app/controllers/webhooks/captain/hermes_callback_controller.rb
Recebe callback do plugin captain-http-callback do Hermes. Valida HMAC
se CAPTAIN_HERMES_CALLBACK_SECRET setado, identifica conversation pela
última pending da inbox (janela 5min) e cria mensagem outgoing.
- config/routes.rb
Rota POST /webhooks/captain/hermes_callback (fora de /api/v1/accounts).
- enterprise/app/services/enterprise/message_templates/hook_execution_service.rb
Branch novo no schedule_captain_response: se Hermes habilitado pra inbox,
dispara HermesOutgoingJob; senão, fluxo Captain interno como antes.
Env vars (todas opcionais; sem set = Hermes desabilitado em todas inboxes):
- CAPTAIN_HERMES_INBOX_IDS (CSV de inbox.id)
- CAPTAIN_HERMES_WEBHOOK_BASE_URL (default http://172.17.0.1:8644)
- CAPTAIN_HERMES_CALLBACK_SECRET (HMAC validar callbacks de entrada)
- CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id> (HMAC assinar saídas)
Limitação: identificação da conversation no callback usa última pending
da inbox dentro de 5min. OK pra PoC com 1 conversa de teste por vez. Em
produção, melhorar mapeando delivery_id ↔ conversation_id em Redis.
Próximo passo manual (admin VPS): criar subscription no Hermes:
hermes webhook subscribe captain-inbox-1 \\
--prompt 'Cliente disse: {message}. Responda como Daniela ...' \\
--deliver http_callback \\
--deliver-chat-id 'http://CAPTAIN_HOST/webhooks/captain/hermes_callback?inbox_id=1'
Depois set CAPTAIN_HERMES_INBOX_IDS=1 + CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_1
no stack do Captain e testar pela inbox Angelina.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Primeira onda do roadmap de indicadores executivos do Grupo Nova. Mede
ADOÇÃO DO CANAL DIGITAL, não a operação total — banner explícito alerta
que reservas fechadas manualmente na recepção ainda não estão capturadas
(Onda 1B vai adicionar marcação manual via botão na conversa).
Backend:
- V2::Reports::ConversionFunnelBuilder — leads (novo/retorno/total),
reservas (criadas != draft, pagas in active/completed/confirmed),
taxas de conversão. Filtro opcional por inbox.
- V2::Reports::InboxBenchmarkingBuilder — uma linha por inbox com
brand_name (via Captain::UnitInbox -> Unit -> Brand)
- Endpoints GET /reports/conversion_funnel e /reports/inbox_benchmarking
- RSpec do ConversionFunnelBuilder
Frontend:
- Rota top-level Reports → Painel Diretoria
- DirectoryDashboard.vue: banner de adoção + filtros + cards + funil + tabela
benchmarking agrupada por marca com variação vs média
- API client getConversionFunnel + getInboxBenchmarking
- i18n EN + PT
Memórias suporte: feedback_metricas_adocao_canal.md + project_painel_diretoria_roadmap.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoje as métricas e séries do BotReports agregam toda a conta — não dá pra ver
"a Jasmine da PrimeAL está errando mais que a do Qnn01". Cada unidade tem
prompt próprio, então um sintoma localizado precisa de medição localizada.
Backend:
- Inbox#has_many :reporting_events (relação inversa que faltava)
- BotMetricsBuilder aceita inbox_id e filtra bot_conversations + base_reporting_events
- bot_metrics endpoint passa inbox_id pelos params permitidos
- count_report_builder já suporta scope=inbox; agora funciona pra
bot_resolutions_count e bot_handoffs_count graças à relação acima
Frontend:
- BotReports.vue: ReportFilters com filter-type='inboxes' e dropdown ativo
- Quando uma inbox é escolhida, requestPayload inclui inboxId/type/id e os
fetches (BotMetrics + ReportContainer) passam o filtro
- API client getBotMetrics aceita inboxId; getBotSummary aceita type+id
- Sem inbox selecionada: comportamento antigo (agregação da conta)
Bonus na rake task de retroativo:
- rebuild_bot_resolved.rake: Message.unscope(:order) pra evitar conflito
PG::InvalidColumnReference (DISTINCT + ORDER BY default scope)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mede por inbox/período: leads novos (1ª conversa do contato em qualquer
inbox da rede), retorno (conversa anterior resolved há >24h) e outras
(conversa anterior open ou resolved <24h). Categorias somadas batem com
o conversations_count nativo do report — bucket "outras" garante o
fechamento.
- Novo builder V2::Reports::InboxLeadsSummaryBuilder com CTE única
- Endpoint GET /api/v2/accounts/:id/reports/inbox_leads_summary
- Tabs no InboxReportsShow (Visão Geral | Novas × Retorno)
- Componente InboxLeadsReport com 3 metric cards + barras empilhadas
- API client + Pinia (state/getters/actions/mutations)
- i18n en + pt_BR
- RSpec do builder cobrindo classificação e isolamento por inbox
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ActionController::Parameters não tem .any?; chamar sem to_h quebra
com NoMethodError (500) ao salvar agente. Bug introduzido em b69fa21e5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admin configura em Account Settings → Agents → Editar quais inboxes
disparam o banner agressivo (reopened + inatividade) pra cada agente.
- user.ui_settings.aggressive_alert_inbox_ids: null (todas) | [] (nenhuma) | [1,2,3]
- Filtro aplicado no actionCable.feedInactivityTracker, maybeTriggerAggressiveAlert
e no hydrate do AggressiveConversationBanner
- Backend aceita ui_settings no agents#update e serializa em _agent.json.jbuilder
- UI no EditAgent com toggle "todas inboxes" + multiselect
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Banner agressivo passa de uma notificação só ("status→open") pra
um sistema de escalada baseado em inatividade quando o cliente é
o último a falar.
Níveis:
- 5 min sem resposta → AMARELO, sem som
- 15 min sem resposta → LARANJA, beep 1x + notificação do SO
- 28 min sem resposta → VERMELHO pulsante + som em loop infinito
- status→open (reabertura) → VERMELHO imediato
Por conversa, o banner mostra um item com nome do contato, inbox
e contexto ("reabriu agora" / "15 min sem resposta"). Headline
grande e explicação clara sobre como limpa.
Comportamento do × dismiss:
- Antes: apagava o alerta de vez. Agente podia "fingir que viu".
- Agora: esconde temporariamente. Volta quando escalar (próximo
threshold) ou nova mensagem. A única forma de LIMPAR de vez é
responder o cliente (tracker detecta msg outgoing do User ou
AgentBot e chama dismissForReply).
Permissões:
- account.settings.aggressive_alert_enabled (master switch admin)
- user.ui_settings.aggressive_alert_enabled (toggle do próprio agente)
- Default true pros dois; um false em qualquer bloqueia alertas.
Settings UI:
- Conta → General: novo card "Alerta agressivo (master switch)"
- Perfil do usuário: novo card "Receber alertas agressivos"
Arquivos:
- helper/aggressiveAlert.js: multi-level state, hide vs dismiss-for-reply
- helper/inactivityAlertTracker.js: timer único, thresholds declarativos
- helper/actionCable.js: hook em onMessageCreated (feed tracker) +
isAggressiveAlertEnabled() + limpa tracker em status_changed != open
- components/app/AggressiveConversationBanner.vue: variantes de cor,
headline grande, explanation, × temp-hide
- account.rb + accounts_controller.rb: store_accessor + permitted
- settings UI components (account + profile): switches auto-persist
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes two JSON endpoints under /api/v1/accounts/:id/captain/reports:
- GET /retention — aggregate KPIs (active/recurring/sleeping/at-risk/
churned, new vs returned in period, Pix generated/paid/conversion,
retention rates at 30d and 90d)
- GET /retention/cohort — monthly cohort matrix, 12 months lookback,
12 months of offset. Each cell is % of the cohort that interacted in
month M+N. SQL-aggregated with DATE_TRUNC + DISTINCT so it is a
single query even on large histories.
Consolida o trabalho desta branch de abril/2026 em um bloco pronto pra
testar em staging antes do merge pra main.
## Correções de memória semântica
- ExtractionService: Princípio Zero + Regra de Ouro (ação consumada vs intenção).
- Cenário Daniela_Reservas: Passo 0 de classificação (consulta/intenção/fora).
## Roleta da Sorte (end-to-end)
- Schema Supabase + 7 RPCs atômicas (server-side, idempotentes).
- Services: Offer, Redeem, WeeklyReport.
- Jobs: OfferRouletteJob (hook em ConfirmationService após Pix pago),
NotifyRevealed + Scheduler de fallback.
- Tool manual GenerateRoletaLinkTool + endpoint público /roleta/notify.
- Dashboard /captain/roleta com Resgate + Relatório + anomaly detection.
## Cenário Reclamacoes_Ouvidoria
- Triagem P1-P4, framework LAST, Three-level listening, Self-check.
- Sem compensação material, detecção de cliente frustrado eleva prioridade.
## Analytics
- Funil de conversão /captain/funnel: 5 etapas via regex, zero LLM.
- Detector de churn via ChurnOutreach* (cron dias úteis 10h-17h BRT).
## Trabalho pré-existente incluído
- Captain Executive Reports (ceo_digest, mattermost_delivery).
- get_reserva_preco_tool, Lifecycle ajustes, Reservations UI polimentos.
## Outros
- .gitignore: patterns pra credenciais.
- Migrations de scenarios idempotentes.
- i18n completa pt_BR+en pra roleta/funnel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Os stubs de lifecycle criados na task anterior estavam em app/controllers/
causando futura colisão de redefinição de classe quando os controllers reais
forem implementados em enterprise/app/controllers/ (tasks 4-6). Move os 3
stubs para o enterprise path onde vivem todos os controllers Captain.
Routing spec: 7 examples, 0 failures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires 3 new captain namespace resources (lifecycle_rules, lifecycle_config,
lifecycle_deliveries) and a member action `patch :concierge` on units.
Includes stub controllers (to be expanded in Tasks 4-7) and passing routing spec.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cobre ambos os caminhos (generate_pix_tool e PublicReservationsController):
toda reserva criada recebe um after_create_commit que posta uma mensagem
privada na conversa com os detalhes (suite, check-in, valores, ID).
Remove a criacao duplicada do PublicReservationsController.
- Controller grava cpf/ultima_suite/ultima_permanencia/ultima_reserva_em/total_reservas
em contact.custom_attributes (aparece no painel lateral do Chatwoot)
- GenerateReservationLinkTool exige marca/unidade/categoria/permanencia/checkin_at;
retorna erro se Jasmine chamar sem esses dados
Smoke test revelou que o inbox do tipo whatsapp valida source_id com
regex ^\d{1,15}\z. Trocar UUID por telefone em digitos (phone_digits)
e normalizar phone_number pra +phone_digits antes de criar o contato.
- Cria modelo LeadClick para registrar cliques das landing pages
- Cria modelo LandingHost para mapear hostname → inbox_id
- Endpoint público POST /track/click para receber eventos de clique
- Leads::AttributionMatcherService para correlacionar clique com conversa
- Integração com IncomingMessageWuzapiService para atribuição automática
- API REST para gerenciar LandingHosts por inbox (index/create/destroy)
- UI: nova aba 'Landing Pages' nas configurações da caixa de entrada
- Dashboard API client dedicado (landingHosts.js)
- RuboCop: refatora shift_signature_name, TrackingController, AttributionMatcherService e WuzapiService
Implementa a página Relatórios IA com geração de análises semanais
por IA baseadas nas conversas de cada unidade/caixa de entrada.
Funcionalidades:
- Página /settings/captain/reports com dois tabs (Insights IA / Operacional)
- Botão "Gerar Análise" que enfileira job Sidekiq
- Filtro por unidade ou caixa de entrada
- Exibe insights com status (pendente/processando/concluído/falhou)
- Mostra top_topics, ai_failures e period_summary
- Estado vazio com CTA para gerar primeiro relatório
Backend:
- InsightsController com endpoints index/show/generate
- GenerateInsightsJob que processa conversas com LLM
- ConversationInsightService com chunking e merge inteligente
- Migração para adicionar inbox_id à tabela captain_conversation_insights
- Link sidebar "Relatórios IA" em /settings/captain/reports
Frontend:
- Vuex store captainReports com actions/mutations/getters
- API client CaptainReportsAPI (getInsights, generateInsight)
- i18n en e pt_BR para CAPTAIN_REPORTS.*
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Melhorias na ferramenta send_suite_images para resolver confusão entre
categoria e número de suíte:
1. **Descrições de parâmetros mais claras**
- suite_category: exemplos específicos (Hidromassagem, ALEXA, STILO)
- suite_number: apenas números (101, 102, 103) - remove exemplos confusos
2. **Instruções explícitas no system prompt**
- Seção [Galeria de Fotos] com regras claras
- Prioriza suite_category quando ambíguo
- Evita confirmações desnecessárias com cliente
3. **Mensagens de erro melhoradas**
- Sugere buscar por categoria quando busca por número falha
- Feedback mais útil para a IA
Resultado esperado:
- Cliente: "Me manda foto da suite Alexa"
- IA: busca por suite_category="Alexa" ✓ (sem pedir confirmação)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>