Commit Graph

141 Commits

Author SHA1 Message Date
Rodribm10
cbbfccaf42 fix(captain): resolve Hermes quoted replies by internal id
Some checks failed
Build and Push to GHCR (multi-arch) / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / merge (push) Has been cancelled
2026-05-24 17:22:05 -03:00
Rodribm10
572b9ccd10 fix(captain): send WhatsApp reply context to Hermes 2026-05-24 16:51:28 -03:00
Rodribm10
358114d04d fix(captain): respect report date filters 2026-05-17 14:00:22 -03:00
Rodribm10
e94cadbdf6 feat(captain): pix_mode manual_static pra Padova e Express
Adiciona caminho paralelo de PIX manual estático pra unidades sem
integração Inter (Padova, Express AL). Mudança 100% aditiva — todas as
outras unidades continuam no fluxo Inter inalterado (default
pix_mode=inter_dynamic aplicado pela migration).

Backend (sem SOUL/SKILL ainda — Frente 7 vem depois):
- Migration concurrent: pix_mode + 4 campos manual_pix_* em captain_units;
  provider + manual_proof_payload + manual_review_reason em captain_pix_charges
- Captain::Unit: enum pix_mode (prefix), validação condicional manual_*
- Captain::PixCharge: status estendido (awaiting_proof, pending_review),
  scope manual/inter, nota interna ramificada por modo
- GeneratePixTool MCP: branch manual_static (chave fixa, mensagem direta
  sem QR/Inter), preserva fluxo Inter intacto
- 4 tools MCP novas: verificar_comprovante_pix (vision gpt-5.3-codex),
  criar_nota_interna (genérica), confirmar_reserva_pix_manual (wrapper
  do ConfirmationService), marcar_reserva_pendente
- ConfirmationService: source_label cobre 'manual_pix_proof'

Próximos passos manuais (não inclusos neste commit):
1. Rodar migration em prod (entrypoint não roda no boot)
2. Seed Padova/Express com pix_mode=manual_static + chaves Stone
3. Deploy nova imagem via docker service update
4. Editar SOUL/SKILL Padova/Express na VPS Hermes + kill+boot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:01:01 -03:00
Codex CLI
d670c5644b fix(captain/reservations): prioritize confirmed bookings 2026-05-03 11:03:38 +00:00
Codex CLI
f4255cff97 fix(captain/hermes): improve deterministic auto reactions 2026-05-03 00:21:45 +00:00
Rodribm10
069e464ee4 feat(construtor): SKILL.md inclui regras de parsing de payload (Valentina-style)
A skill da Valentina (referência, 353 linhas) tem regras explícitas que
faltavam nas Hermes geradas pelo Construtor (107 linhas). Sem elas, o
LLM mistura turns no mesmo payload, repete info já dada, ou alucina em
texto de outro suporte.

save_agent_spec_tool.build_skill_md agora gera bloco final com:
- "Última fala manda" (responder só a mais recente quando vier rajada)
- "Burst repetido" (mesma fala 2x → 1 resposta)
- "Texto de outro fluxo" (operadora/menu externo → frase canônica)
- "Mensagem vazia" (frase canônica)
- "Loop" (mesma entrada → mesma saída, sem variação)
- "Nunca vazar bastidor" (sem [ctx]/CONTEXT COMPACTION/meta-texto)

Aplicado também manualmente nas SKILL.md das 3 Hermes existentes
(Nina/Lara/Juliana) que tinham a versão curta. Valentina já tinha a
versão completa na skill dela.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:43:21 -03:00
Rodribm10
ed99f67525 feat(captain/hermes): camada 2 — gating de saída factual sem tool call
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>
2026-05-02 17:58:17 -03:00
Rodribm10
10fec1d2cb feat(captain/hermes): auto-react ambiente — 20% de chance em msgs neutras
Cobre o gap entre saudação e despedida: quando msg do cliente não bate
nenhum dos regex determinísticos (thanks/confirm/greet/farewell), rola
dado e reage com emoji discreto (😊/💕//💯/🤗) em ~1 a cada 5 msgs.

Filtros pra não reagir em momentos errados (ambient_eligible?):
- Tamanho 6..180 chars (evita "oi" e narrativas longas).
- Sem "?" (perguntas esperam resposta de texto, não emoji).
- Sem keywords de fluxo de reserva (cpf, valor, hora, data, suite).
- Não começa com número (CPF, telefone, valor).

Determinístico continua igual — agente parece mais vivo sem virar
robozinho que reage em tudo. Atende pedido do Rodrigo de "reagir 20%
da conversa, não só nas saudações".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:20:36 -03:00
Rodribm10
42ada8100e feat(captain/mcp): tool check_suite_availability — bridge PlugPlay
Tool nova reusa as Captain::CustomTool 'custom_status_suites_<unidade>'
já cadastradas no painel (Ferramentas) que apontam pro PlugPlay
(oxpi.com.br/PlugPlay/SuitesStatus) com auth headers PLUG-PLAY-ID +
PLUG-PLAY-TOKEN específicos por unit.

Hermes consulta antes de generate_pix pra confirmar disponibilidade.
Resolve unit via Assistant.captain_unit_id; mapping unit → CustomTool é
por substring no nome (qnn/dolce/express/primeal/primevl).

Filtro opcional por suite_category. Retorno textual agrupado por classe
com contagem livre/ocupada e número das suítes livres (max 8).

Validado em Qnn01: 4 hidromassagens livres (101,102,103,108) /
15 Master livres / 5 Standard livres — bate com painel PlugPlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:10:57 -03:00
Rodribm10
bd494c424d fix(captain/mcp): generate_pix dispatcha fallback em mais cenários
Antes: fallback (link da página de reserva) só disparava quando Inter API
explodia (rescue StandardError). Erros estruturais (categoria não existe
na unit, período/dia inválido, sem credencial Inter cadastrada) caíam em
error_response e o LLM travava em " Um momento — vou verificar." sem
mandar nada pro cliente.

Agora: dispatch_no_pricing_fallback! cobre os casos onde Pix nem foi
tentado. Mesma UX do fallback existente — link pré-preenchido + label
pix_falhou_fallback pra triagem da gerência. Tool retorna isError=false
com instrução curta pro LLM ("só confirme que o link foi enviado, não
fale do problema técnico").

Caso real: Rodrigo pediu hidromassagem na inbox da Dolce Amore (unit que
não tem essa categoria). PricingTables retornou "Categoria não
reconhecida"; antes o LLM ficava no placeholder. Agora cliente recebe
link da página oficial automaticamente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:56:52 -03:00
Rodribm10
47f32b540b fix(captain/mcp): generate_pix prefere assistant.captain_unit + 3 checks runtime
resolve_unit agora prioriza Captain::Assistant.captain_unit_id sobre o
mapping legado CaptainInbox (que falha quando 2 agentes — interno e
Hermes — compartilham a mesma inbox).

Caso real: Juliana Hermes (unit Qnn01) compartilhava inbox 1 com Juliana
captain_interno (unit Recanto), mas o CaptainInbox da inbox 1 estava
mapeado pra unit Dolce Amore (id=4) por contaminação anterior. Tool
resolvia unit errada, generate_pix retornava "categoria não reconhecida"
e o agente travava em " Um momento — vou verificar." sem retomar.

bin/hermes-validate ganha 3 checks novos:
- CaptainInbox.unit == Assistant.unit (FAIL — exatamente o bug acima)
- Pricing dry-run (calcula preço da 1ª categoria sem erro)
- Credenciais Inter completas (WARN se faltar cert/key — cai no fallback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:09:04 -03:00
Rodribm10
bc87b496a4 fix(captain/hermes): SOUL.md vem da Valentina (template) com sed identity + Pix flow no skill 2026-05-02 14:39:36 -03:00
Rodribm10
f0f8322cce fix(captain/mcp): tool descriptions sem exemplos motel-flavored + brand match
Bug 1: send_suite_images_tool description mencionava 'Master, Luxo, Mini
Chalé 45' como exemplos. Quando outros agentes (ex: Juliana de Qnn01)
faziam tools/list, o LLM via essas categorias e usava como referência —
respondia oferecendo Mini Chalé 45 e Suíte Ouro pra cliente do 1001
Noites Ceilândia (que não tem essas categorias). Removidos exemplos.

Bug 2: lookup_brand fazia fuzzy match permissivo demais. 'Hoteis 1001
Noites' e 'Hotel 1001 Noites Prime' ambos contêm '1001 Noites' — quem
vinha primeiro no .find ganhava (Prime, ID menor). Juliana de Qnn01
(brand 'Hoteis 1001 Noites') saiu do Construtor com SOUL.md dizendo
'Hotel 1001 Noites Prime'. Fix: prioriza brand do parent_unit (fonte
canônica), depois exact-match casecmp, depois fuzzy.

Tudo isso vale pra Construtor entregar agente certo da próxima vez —
foram os 2 erros de provisionamento da Juliana hoje.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:07:15 -03:00
Rodribm10
88a5adb65e fix(captain/hermes): subscription_name e secret usam profile_name (slug)
Bug: Captain dispatchava pra /webhooks/captain-inbox-<inbox.id>, mas o
script hermes-provision criava subscription com nome captain-inbox-<slug>.
Mismatch → daemon retornava 404, Sidekiq retentava, AutoReact firava
N reactions sem nunca dispatchar pro LLM.

Fix:
- subscription_name_for(inbox): se o assistant tem hermes_profile_name,
  usa "captain-inbox-<slug>" (estável por agente). Fallback pra
  "captain-inbox-<inbox.id>" só se não tiver slug.
- subscription_signing_secret(inbox): lê de
  assistant.hermes_subscription_secret primeiro (DB-driven, gravado pelo
  script). Fallback pra env var legacy CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id>.

Resultado: admin pode apontar Angelina (inbox 1) pra qualquer agente
Hermes (Valentina · Hermes / Nina · Hermes / Lara.H / Juliana · Hermes)
e o roteamento funciona — não depende mais de inbox.id no path.

Renomeei manualmente as subscriptions de Valentina e Nina nos profiles
da VPS (eram captain-inbox-1 e captain-inbox-5 legado) pra
captain-inbox-valentina e captain-inbox-nina.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:42:22 -03:00
Rodribm10
955cb824b3 fix(captain/mcp): save_agent_spec — caminho de sucesso retorna tupla [hash, errors]
Bug: expand_spec retornava [{}, errors] no erro mas só {hash} no sucesso.
Destruturação 'expanded, errors = expand_spec(...)' deixava errors=nil
→ errors.any? → undefined method any? for nil.

Fix: extraído build_expanded_hash + sucesso retorna [hash, []].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:26:25 -03:00
Rodribm10
828e2e6af3 fix(captain/mcp): save_agent_spec — passa categorias já serializadas pra build_skill_md
Bug: build_skill_md recebia AR Captain::PricingCategory objects mas o
format_pricing_block fazia cat['amounts'] (índice de hash). AR respondem
a .amounts, não a ['amounts'] → nil → undefined method 'group_by' for nil.

Fix: serializa primeiro, reusa em ambos os lugares.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:17:38 -03:00
Rodribm10
5ef59ecc12 fix(captain): captain_unit_id em captain_assistants — fim do lookup quebrado
Captain::Assistant agora aponta direto pra Captain::Unit. Antes a relação
ia via CaptainInbox, mas isso quebrou quando re-apontamos as inboxes pros
Hermes — assistants captain_interno (Juliana, Bianca, Lara, Nina,
Valentina) ficaram SEM CaptainInbox associada e o lookup
unit_for(assistant) retornava nil.

Resultado: get_assistant_pricing(3) (Lara) caía no fallback de scenario
text. Construtor reportava "veio cenário/prompt, não tabela estruturada".

Migration adiciona captain_unit_id (FK opcional). Backfill explícito:
- 1 Juliana → unit 3 (Qnn01)
- 2 Bianca → unit 2 (PrimeAL)
- 3 Lara → unit 2 (PrimeAL — mesmo brand)
- 4 Nina → unit 5 (Express)
- 6 Valentina → unit 4 (Dolce Amore)
- 9 Lara.H → unit 2 (via parent_assistant_id=3)

Tools get_assistant_pricing_tool e save_agent_spec_tool atualizados pra
usar assistant.captain_unit primeiro (nova relação direta), com fallback
pro CaptainInbox se nulo (pra retrocompatibilidade).

Validado live: tool retorna grid markdown com Stilo/Alexa/Hidromassagem
em Seg-Qua + Qui-Dom corretamente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:45:53 -03:00
Rodribm10
22eab86302 fix(captain/mcp): get_assistant_pricing lê do DB + save_agent_spec expande
ROOT FIX (não paliativo) das 3 lacunas que travavam o Construtor:

1. get_assistant_pricing_tool: lia de Captain::Mcp::PricingTables::TABLES
   (hash Ruby) que NÃO EXISTE MAIS desde a migração pra DB. Caía no
   fallback de scenario raw. Refactor: lê de Captain::PricingCategory +
   Captain::PricingAmount, formata grid markdown agrupado por day_bucket.

2. save_agent_spec_tool: Construtor salvava REFERÊNCIAS
   (pricing_source.copied_from_assistant_id) mas hermes-provision script
   espera DADOS EXPANDIDOS (categories[] com amounts, soul_md+skill_md).
   Refactor: tool agora EXPANDE server-side — busca PricingCategory do
   parent, monta categories array, gera SOUL.md (template + identity +
   disclosure_policy) e SKILL.md (template + pricing + rules + identity).
   Output já é spec consumível pelo script.

3. Captain::PricingAmount::PERIODS: adicionado '1h' (Prime tem 1h).

4. Seed pras 3 units faltando: Hotel Recanto (1) + PrimeAL (2) + Qnn01
   (3). Agora os 6 units existentes têm pricing em DB.

Hot-patched ambos tools + USR1 no Puma. Construtor pronto pra criar
Bianca/Juliana etc end-to-end sem intervenção manual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:37:53 -03:00
Rodribm10
0f39945f43 feat(captain/mcp): get_assistant_scenario tool — Construtor copia identidade de outro agente
Construtor atende 'copiar maps/endereço/telefone/wifi da Lara' sem o admin
redigitar. Tool retorna o markdown bruto do scenario (default
Daniela_Reservas) do assistant fonte; LLM extrai os campos relevantes.

Cobre o gap entre get_assistant_pricing (preços estruturados) e
get_assistant_faqs (Q&As): essa retorna prompt CRU pra LLM interpretar
campos não estruturados (contatos, links, wifi, persona).

Hot-patched + USR1 no Puma e Construtor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:55:13 -03:00
Rodribm10
dd9e11da14 fix(captain/mcp): get_assistant_faqs — não filtrar por documentable_type
Filtro original só retornava FAQs com documentable_type=NULL, mas a maior
parte das FAQs aprovadas das Jasmines (Juliana, Bianca, Lara, Nina) tem
documentable_type='User' ou 'Conversation' (origem: histórico de
conversas). Resultado: tool retornava "0 FAQs aprovados" pra todos exceto
Valentina (única com FAQs criadas direto sem origem documentável).

Removido o filtro. Status='approved' já é suficiente — admin reviewou.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:49:54 -03:00
Rodribm10
59747e5400 fix(captain/mcp): tools do Construtor — typo _context: → context:
Tool registry chama tool.call(args, context: foo) mas as 3 tools read-only
do Construtor (get_assistant_faqs, get_assistant_pricing, save_agent_spec)
estavam declaradas como def call(args, _context:), com underscore. No
Ruby isso muda o nome do parâmetro keyword — ArgumentError:
'missing keyword: :_context' quando o Construtor tentava copiar FAQs/
pricing de um assistant existente.

Corrigido pra context: (sem underscore) com rubocop disable de
Lint/UnusedMethodArgument já que essas tools não usam o context.

Hot-patched via docker cp + Puma USR1 antes do deploy pro Rodrigo seguir
testando o Construtor sem esperar build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:41:23 -03:00
Rodribm10
7995bc6fe6 feat(captain): pricing em DB + colunas de provisionamento Hermes
Migra a tabela de preços do PricingTables.rb hardcoded pras tabelas
captain_pricing_categories + captain_pricing_amounts no DB. Mantém a
mesma API pública Captain::Mcp::PricingTables.calculate(...) — código
chama o banco via novos modelos Captain::PricingCategory e
Captain::PricingAmount.

Seed db/seed_pricing_tables.rb faz backfill idempotente pra Dolce Amore
(unit 4) e Express (unit 5) com a mesma estrutura que tava no Ruby.

Adiciona em captain_assistants:
- hermes_subscription_secret (gerado pelo script de provisionamento)
- hermes_port (alocado no range 8650-8699)
- parent_assistant_id (link informativo Hermes → captain_interno parent
  pra sombrear FAQs/scenarios via header X-Captain-Assistant-Id)

Adiciona em captain_units: extra_person_fee + currency.

Primeiro milestone do roadmap arquitetural pro Construtor autônomo
(decisões em memory/project_construtor_autonomo_decisions.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:04:50 -03:00
Rodribm10
a2bb613e68 feat(captain/hermes): debounce — inbox.typing_delay como buffer + agrupar msgs
Antes: inbox.typing_delay funcionava só pro Captain interno
(schedule_internal_response). Hermes ignorava o campo e disparava
OutgoingJob na hora — campo da UI era cosmético pra inboxes Hermes.

Agora:
- schedule_hermes_response cancela jobs OutgoingJob pendentes pra mesma
  conversa e enfileira com wait=inbox.typing_delay (debounce window).
- OutgoingJob agrupa todas msgs incoming entre a última resposta real
  do agente (ignora reactions) e a msg âncora; dispatch envia o texto
  concatenado pro Hermes via novo content_override no Client#dispatch.

Resultado: cliente que digita "Oi" + "quero pernoite Master" em segundos
vê o agente esperar até o buffer vencer e responder UMA vez cobrindo
ambas as falas, em vez de 2 respostas atropelando o pensamento.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:46:24 -03:00
Rodribm10
e662913b21 fix(captain/hermes): auto-react idempotente — bloqueia retry duplicate
OutgoingJob faz retry no DispatchError (até 3x ActiveJob + Sidekiq).
Cada retry chamava AutoReactService.maybe_react! e criava uma reaction
nova — observado em prod 02/05 quando o env var
CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_5 estava faltando, gerando
401 → 6 reações duplicadas no inbox EXPRESS.

Adiciona guard already_reacted? que checa se já existe Message outgoing
com external_source='hermes_auto_react' e in_reply_to=msg.id antes de
criar uma nova. Defesa contra futuro 5xx/timeout do Hermes daemon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:32:47 -03:00
Rodribm10
3182002bd9 feat(captain): engine column + DB-driven Hermes routing + Express pricing
Marca cada Captain::Assistant com engine ('captain_interno' | 'hermes')
e move o roteamento Hermes do env var pro banco — admin troca engine
re-apontando a inbox no painel, sem deploy. Mantém fallback pras env
vars antigas (CAPTAIN_HERMES_INBOX_IDS etc) durante a migração gradual,
pra não quebrar Valentina antes da re-associação.

Frontend: badge "Hermes" (âmbar) ou "Interno" (cinza) ao lado de cada
assistant no dropdown switcher e no card da listagem, com chaves i18n
em en + pt_BR.

Tabela de preço (pricing_tables.rb): adiciona unit Express (id=5) e
estende a estrutura pra aceitar preço por dia da semana
(mon_wed/thu_sun) — necessário pro Express, retrocompatível com Dolce
Amore (preço único).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 07:54:01 -03:00
Rodribm10
cb067e1f17 feat(captain/hermes): auto-react cobre saudacoes e despedidas
Estende AutoReactService com 2 padroes novos:
- GREETING_REGEX (bom dia, boa noite, oi, ola, hello) -> 👋
  apenas na PRIMEIRA mensagem da conversa pra nao ficar forcado
- FAREWELL_REGEX (tchau, ate logo, abraço, bjs, flw) -> ❤️

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:21:43 -03:00
Rodribm10
bfa06597f2 feat(captain/hermes): auto-react deterministico antes do LLM
Pattern matching no Captain dispara reaction <1s sem esperar Codex
processar. Resposta de texto continua vindo do Hermes normalmente —
auto-react eh COMPLEMENTAR.

Padroes detectados (no incoming message):
- "obrigado/valeu/vlw/thanks" -> 🙏
- "ok/fechado/perfeito/blz/combinado" -> 👍
- attachment image (msg sem texto) -> 😍
- attachment audio (msg curta) -> 🙏

Conservative: só dispara em casos CLAROS. Em duvida, deixa pro LLM
decidir via react_to_message tool.

Plugado em Captain::Hermes::OutgoingJob#perform — chamado antes do
dispatch pro Hermes. Falha silenciosa (rescue) — nao bloqueia fluxo.

SOUL.md atualizado: regra de frequencia (~30% das msgs) + nota sobre
auto-react pra LLM nao duplicar reacao em casos cobertos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:59:28 -03:00
Rodribm10
d35084334c feat(captain/mcp): tools read-only pro Skill Construtor
4 tools novas (admin scope, read-only) usadas pelo profile Hermes
"construtor" durante criacao guiada de novos agentes:

- list_assistants: lista todos os assistants da conta com badge engine
  (Captain interno vs Hermes), counts de scenarios/FAQs, marca/unidade
- get_assistant_pricing: retorna tabela de preços parsed (Captain::Mcp::
  PricingTables se Hermes, scenario fallback se Captain interno)
- get_assistant_faqs: retorna FAQs aprovados em markdown (até 50)
- save_agent_spec: salva JSON da especificacao em /tmp/agent-specs/<slug>.json
  (NÃO cria filesystem do profile nem registros DB — só persiste spec
  pra revisao/provisionamento separado depois)

Construtor coleta perguntas socraticas, oferece "copiar de existente" pra
acelerar quando marca for igual (Prime VL/AL/ADE preços iguais), e ao
final salva spec.json pra admin revisar antes de provisionar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:24:02 -03:00
Rodribm10
48fad2977b feat(captain/hermes): payload enriquecido + humanizadores + notif Pix proativa
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>
2026-05-01 20:15:50 -03:00
Rodribm10
9ed3491d55 feat(captain/mcp): suite de 9 tools MCP + pricing tables Dolce Amore
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>
2026-05-01 20:13:16 -03:00
Rodribm10
60759b955c fix(captain): força provider :openai quando há config dedicada de embedding
RubyLLM auto-detecta provider pelo prefixo do nome do modelo (ex:
`gemini-*` → provider Gemini → exige `gemini_api_key`). Quando temos
config dedicada de embedding (CAPTAIN_EMBEDDING_API_KEY) apontando pra
endpoint OpenAI-compatible (ex: Gemini OpenAI-compat em
generativelanguage.googleapis.com/v1beta/openai), queremos que o RubyLLM
mande a request via OpenAI client mesmo que o nome do modelo bata com
outro provider.

Solução: passar provider: :openai e assume_model_exists: true ao chamar
embed quando dedicated_embedding_config? retornar true. Sem isso, o
RubyLLM falha com `Missing configuration for Gemini: gemini_api_key`
mesmo com a key correta setada.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:18:50 -03:00
Rodribm10
2ade066468 feat(captain): EmbeddingService aceita provider de embedding dedicado
Permite trocar provider de embedding sem afetar o provider de chat. Útil
quando OpenAI key tradicional está fora (ban, billing, etc) mas você
quer usar outro provider OpenAI-compatible só pra embeddings — exemplo
clássico: Gemini OpenAI-compatible em
https://generativelanguage.googleapis.com/v1beta/openai com modelo
gemini-embedding-001 + dimensions=1536 (pra bater com schema pgvector).

Env vars novas (com fallback pro legacy_openai_settings se não setadas):

  CAPTAIN_EMBEDDING_API_KEY     — API key dedicada pra embeddings
  CAPTAIN_EMBEDDING_ENDPOINT    — base URL sem /v1 (default herda OpenAI)
  CAPTAIN_EMBEDDING_DIMENSIONS  — força redução do vector (ex: 1536)

Quando CAPTAIN_EMBEDDING_API_KEY está vazia, comportamento é idêntico ao
de antes (legacy_openai_settings). Backward-compatible.

Também aceita as variáveis via InstallationConfig (UI) ou ENV — ENV tem
precedência (padrão Chatwoot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:08:18 -03:00
Rodribm10
23911ea878 feat(captain): MCP server (HTTP) expondo tools pro Hermes Agent
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>
2026-05-01 15:32:38 -03:00
Rodribm10
35de8b7fde feat(captain): cliente Captain ↔ Hermes (outgoing job + callback endpoint)
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>
2026-05-01 13:22:22 -03:00
Rodribm10
7700afd508 feat(captain): adiciona Hermes Gateway como 3ª opção de LLM provider
Acrescenta valor 'openai_hermes_gateway' ao CAPTAIN_LLM_PROVIDER, sem mexer
nas opções existentes (openai_api e openai_codex_oauth continuam intactos).

Quando ativado, o Captain chama o Hermes Agent rodando em modo gateway HTTP
local (CAPTAIN_HERMES_GATEWAY_URL, default http://host.docker.internal:9877).
O Hermes faz o roteamento multi-modelo (Codex/Anthropic/Gemini) usando o
OAuth dele em ~/.hermes/auth.json — o Captain não precisa fazer OAuth direto.

Configs novas em installation_config.yml:
- CAPTAIN_HERMES_GATEWAY_URL — URL do gateway (default host.docker.internal:9877)
- CAPTAIN_HERMES_GATEWAY_MODEL — modelo no formato <provider>/<model>
- CAPTAIN_HERMES_GATEWAY_API_KEY — opcional, dummy se gateway local não exige

Embeddings e Files API continuam apontando pra OpenAI tradicional via
legacy_openai_settings — Hermes Gateway não expõe esses endpoints.

Specs cobrem: dummy key, custom api_key override, custom model, defaults,
trailing slash strip, light_model por provider, hermes_gateway? predicate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:24:31 -03:00
Rodribm10
4e798944cf fix(captain): provision unit via RPC SECURITY DEFINER (RLS bypass)
Anon key não tinha permissão de INSERT em reserva_hotel.unidades — RLS
exige authenticated + tenant_member, não atendido. POST direto falhava
sem feedback útil.

Solução: RPC reserva_hotel.provision_unidade(...) com SECURITY DEFINER
que faz upsert idempotente bypassando RLS, com validações de tenant +
marca dentro da função. EXECUTE granted to anon.

Service agora chama /rpc/provision_unidade em vez de POST /unidades.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:11:57 -03:00
Rodribm10
c5cd15665e feat(captain): provisionamento automático de Captain::Unit em reserva_hotel.unidades
Hook after_commit on:create no Captain::Unit dispara
ProvisionUnitInSupabaseJob, que upserta a unit em reserva_hotel.unidades
via Supabase REST (UNIQUE on tenant_id+chatwoot_unit_id) e grava IDs no
Captain::Unit (supabase_unit_id, supabase_tenant_id, supabase_marca_id).

Sem isso, criar nova unidade no painel Pix não habilitava roleta — a row
no Supabase ficava ausente e OfferService caía em "tenant não resolvido".

Inclui rake captain:reprovision_unit_in_supabase[id] + provision_all
pra reconciliação manual e migration retroativa.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:21:20 -03:00
Rodribm10
e7f3723938 fix(captain): Accept-Encoding identity nos clients Supabase (gzip silencioso)
Supabase REST manda response gzip por default. Faraday default não tem
middleware de descompressão, então JSON.parse(gzip_bytes) explodia em
JSON::ParserError, capturado pelo rescue → array vazio silencioso.

OfferService#fetch_unidade retornava [] mesmo com row presente,
caindo em "Sem unidade vinculada — tenant não resolvido".

Fix em offer_service, weekly_report_service, notify_revealed_job e
notify_revealed_scheduler_job. (get_reserva_preco_tool já tinha o fix.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:21:10 -03:00
Rodribm10
f0db8b0361 fix(captain): captain_inbox.unit -> captain_unit (bug bloqueante roleta + tools)
CaptainInbox.belongs_to :captain_unit (não existe método .unit).
OfferService quebrava antes de criar draw — roleta nunca disparava
em prod mesmo após pix confirmado. Mesmo bug em get_reserva_preco_tool
e create_reservation_intent_tool (silenciosamente caíam em fallback nil).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:11:00 -03:00
Rodribm10
21f5fcce6a fix(retention): cohort endpoint 500 — Pundit policy + SQL binding
Dois bugs que faziam o cohort retornar 500 e a página de Retenção mostrar
"Falha ao carregar cohort":

1. `Captain::AssistantPolicy` não tinha `cohort?` → Pundit batia em
   NoMethodError no `check_authorization`. Adicionado como leitura pública
   da assistente, igual `show?`/`playground?`.

2. `RetentionCohortService#cohort_activity` chamava `exec_query(sql, name, [@account.id])`
   passando array de valores onde a API espera bind objects. A SQL ainda
   interpolava `account_id = $1` direto na string (sem placeholder ligado).
   Migrado pra `ActiveRecord::Base.sanitize_sql_array` com `?`, igual ao
   resto da base. Mantém parametrização e remove acoplamento com posicional.

Validado em prod via hot-patch (USR2): GET retention/cohort agora 200 com
3 cohorts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:46:20 -03:00
Rodribm10
0ecfce5c27 fix(captain): translate response_format to text.format on Codex proxy
Sem isso o Codex devolvia texto puro e o reaction_emoji do JSON
estruturado nunca chegava ao ResponseBuilderJob — quebrava a
ferramenta de reagir mensagens com emoji.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:47:09 -03:00
Rodribm10
9e8550dd45 feat(captain): CAPTAIN_CODEX_MODEL_OVERRIDE pra usar modelos fora do catalog do RubyLLM
Adiciona sobrescrita de modelo no proxy. Motivação: o RubyLLM valida o modelo
contra um catalog interno antes de enviar a call. Modelos novos (gpt-5.4,
gpt-5.3-codex) ainda não estão nesse catalog e geram RubyLLM::ModelNotFoundError.

Com CAPTAIN_CODEX_MODEL_OVERRIDE definida, o Translator substitui o modelo do
body antes de enviar ao Codex. Captain continua passando um modelo reconhecido
(gpt-5.2), mas o Codex recebe o modelo real (gpt-5.4).

Exemplo:
  InstallationConfig.find_or_initialize_by(name: "CAPTAIN_CODEX_MODEL_OVERRIDE")
    .update!(value: "gpt-5.4", locked: false)

Validado: curl → proxy → Codex retorna "model":"gpt-5.4" no response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:55:22 -03:00
Rodribm10
b457e84c2f fix(captain): route embeddings to legacy OpenAI + retry transient errors
Resolve duas camadas de problema identificadas em teste end-to-end:

1. Embeddings falhavam com HTTP 404 (/codex/v1/embeddings não existe).
   Solução: Captain::Llm::EmbeddingService sempre usa OpenAI tradicional
   via Llm::Config.with_api_key(legacy_settings). ProviderConfig expõe
   legacy_openai_settings pra isso.

2. Servidor Codex ocasionalmente responde com response.failed +
   code=server_error (instabilidade transitória). Client agora retenta
   até 2x com backoff exponencial (0.5s, 1.5s) em erros retryable:
   HTTP 5xx, server_error no response.failed, ou stream inacabado.

Outras correções nesta etapa:
- Scenario#agent_model: em modo Codex, ignora CAPTAIN_OPEN_AI_MODEL_SCENARIO
  (que pode ter gpt-4o legado) e usa ProviderConfig.model.
- ExtractionService/ContradictionCheckerService/TranslateQueryService:
  trocam constantes hardcoded gpt-4o-mini/gpt-4.1-nano por
  ProviderConfig.light_model (respeitando o provider ativo).
- ProviderConfig.DEFAULT_CODEX_MODEL agora é gpt-5.2 (reconhecido pelo
  RubyLLM; gpt-5.4 não está no catalog do gem).

Validado ponta-a-ponta: WhatsApp → Chatwoot → Jasmine → handoff Daniela
→ faq_lookup com embedding OK → resposta com preços corretos.

Docs em docs/captain-codex-oauth.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:42:31 -03:00
Rodribm10
26290c34a7 feat(captain): feature flag CAPTAIN_LLM_PROVIDER + ProviderConfig central
Adiciona o toggle openai_api | openai_codex_oauth. Por padrão mantém
comportamento legado (API key OpenAI tradicional). Quando mudamos pra
openai_codex_oauth, os clientes (RubyLLM + Agents gem) passam a
apontar para o proxy interno em http://localhost:3000/codex,
configurável via CAPTAIN_CODEX_PROXY_URL.

- Captain::Llm::ProviderConfig: single source of truth de api_key,
  api_base e model, baseado em CAPTAIN_LLM_PROVIDER
- config/initializers/ai_agents.rb refatorado
- lib/llm/config.rb refatorado
- 8 specs do ProviderConfig passando
- Fallback seguro: api_key dummy ('codex-oauth') quando usando proxy
  (o proxy ignora Authorization e usa OAuth interno)

NÃO mexe no Llm::LegacyBaseOpenAiService (PDF/Files API). Esse
continua sempre na API tradicional porque o endpoint Codex não
expõe Files API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:29:52 -03:00
Rodribm10
d53c86df94 fix(captain): always include instructions in Codex responses body
Codex endpoint retorna HTTP 400 "Instructions are required" quando o
campo vem ausente. Agora sempre incluímos o campo — string com espaço
quando não há system message no request.

Validado end-to-end: curl → /codex/v1/chat/completions → proxy traduz
→ Codex devolve streaming SSE → proxy agrega → JSON Chat Completions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:27:37 -03:00
Rodribm10
928b1ec6b9 feat(captain): Codex OAuth auth module + proxy controller
Implementa Fases 1+2 do plano Captain Codex OAuth.

Fase 1 — Auth módulo:
- Migration captain_codex_credentials (tokens AR-encrypted)
- Model Captain::CodexCredential (singleton-ish com .current)
- Captain::Codex::AuthService com device flow completo:
  start_device_login, poll_once, exchange_for_credential,
  valid_access_token (auto-refresh), refresh!
- Rake task captain:codex:{login,status,refresh}
- Sidekiq job Captain::Codex::RefreshTokensJob rodando a cada 30min

Fase 2 — Proxy Chat Completions → Responses:
- Captain::Codex::Translator (chat ↔ responses, tools, tool_calls)
- Captain::Codex::Client (streaming SSE → agregado)
- Api::Internal::CodexProxyController expondo
  POST /codex/v1/chat/completions
- 10 specs do Translator passando

Próximo: Fase 3 (feature flag + fallback) e reconfiguração dos
clientes RubyLLM/Agents/ruby-openai pra apontarem pro proxy quando
CAPTAIN_LLM_PROVIDER=openai_codex_oauth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:07:01 -03:00
Rodribm10
aed6d62640 feat(retention): summary KPIs + cohort endpoints
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.
2026-04-22 09:59:21 -03:00
Rodribm10
f6488ce2de feat(retention): foundation for customer retention metrics
Lays the data + job foundation for tracking customer interactions,
recurrence, and Pix conversion on Contact. Design decisions negotiated
with Rodrigo (see docs to come):

Rules:
- Gap of 30h from last message defines separate interactions
- Qualified interaction = >=2 customer msgs + >=2 attendant msgs,
  both with textual content (>= 2 letters)
- One-shot consultation = >=1+1 but below the qualified threshold
  (tracked as secondary KPI)
- Excludes contacts labeled `equipe_interna`
- is_recurring = interactions_count >= 2
- pix_generated_count counts all PixCharges; reservations_paid_count
  only counts those with status = paid

Surface area:
- Migration adds denormalized stats to contacts + indexes for fast filtering
- Captain::ContactStats::InteractionCalculatorService computes the stats
  for a single contact (pure, no persistence)
- Captain::Retention::RecalculateContactStatsJob persists them for one
  contact (idempotent)
- Captain::Retention::RecalculateAllContactStatsJob runs daily at 3am BRT,
  enqueues per-contact jobs for everyone active in the last 120 days
- Event-driven refresh: CaptainListener#conversation_resolved enqueues
  recalc; Captain::PixCharge after_create/after_update enqueues recalc
  on status change

No UI yet — that's the next layer.
2026-04-22 09:50:23 -03:00
Rodribm10
08a06c6528 fix(captain): memory allows 'Solicitou Pix ..., aguardando pagamento'
Some checks failed
Build and Push to GHCR (multi-arch) / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / merge (push) Has been cancelled
Previous commit made the extractor reject any reservation-shaped fact
without a literal payment confirmation in the conversation. That
killed the useful middle ground: a customer who requests a Pix and
hasn't paid yet is still a concrete signal worth remembering (for
follow-up, interest mapping, CRM). We were going from "hallucinated
reservation" to "nothing remembered".

Add the intermediate pattern:
- Payment confirmed → "Reservou X para Y em DD/MM/AAAA"
- Pix generated, no payment yet → "Solicitou Pix para X em DD/MM/AAAA, aguardando pagamento"
- Just a price quote → nothing

The "aguardando pagamento" suffix is required so the downstream recall
never confuses it with a closed reservation.
2026-04-22 05:01:24 -03:00