Real-world test triggered a Sidekiq worker hang on conv 67 after a
message was routed through Daniela: two ResponseBuilderJobs (msg 1318
and 1319) started, emitted typing_on, then never returned. Sidekiq
showed 2/12 workers stuck for 10+ minutes — indefinite.
Root cause likely: Agents::Runner evaluates the orchestrator
instructions lambda multiple times per turn, and our wrapped lambda
calls MemoryPromptInjector#append_memory_block each time. Inside,
RecallService invokes OpenAI embedding API (2s timeout) and pgvector.
Ruby's Timeout.timeout has documented holes on net/http syscalls — if
the embedding API stalls at the socket level, the worker hangs forever
even though the timeout "fired".
Two fixes:
1. Per-message cache in the injector instance: the same
message_text is embedded + queried once, not N times per turn.
Dramatic reduction in network calls + DB queries during a single
agent run. Every call after the first returns the cached block
instantly.
2. Absolute rescue at append_memory_block top level:
rescue StandardError => e; return base_prompt. Even if the whole
memory pipeline throws, the base system prompt passes through and
the agent keeps responding. Memory is NEVER allowed to block a
response — that was already the design intent but the lambda caller
path didn't honor it rigorously enough.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback revealed a fundamental design issue: the memory model was
accumulating contradictory "Prefere X" facts because a single choice was
being treated as a permanent preference. Result: 3 different
"Prefere suite X" entries coexisting, all at 90% confidence, with
reservation patterns over time (2hrs, 4hrs, pernoite) all claiming to be
the customer's "preferred" duration.
Corrections:
1. ExtractionService prompt — preferencia now requires EXPLICIT
declaration words ("prefiro", "gosto mais de", "sempre escolho",
"adoro", "favorita"). A mere choice in one conversation is NO LONGER
extracted as preferencia — instead it goes to padrao_comportamental
WITH THE DATE in the content (e.g. "Reservou Alexa para pernoite em
23/05/2026"). This makes memory temporal and auditable instead of
imposing fake consistency.
2. Reference date is passed to the LLM prompt via the latest message
timestamp, used as the anchor date the LLM must embed in every
padrao_comportamental content.
3. ContradictionCheckerService — dual threshold:
- cosine < 0.15 → auto-supersede without LLM (pure duplicate)
- 0.15 to 0.6 → ask LLM if contradicts, supersede if yes
- > 0.6 → ignore, unrelated facts
Previously only the middle band existed, so near-duplicate facts like
two "aniversário 23/05" entries or three "prefere suite X" entries
were never cleaned up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real test revealed gpt-4o-mini was still:
- Hallucinating suite names ("Aluba" doesn't exist — we only have
Alexa, Stilo, Hidromassagem)
- Extracting cadastral data as memory ("Rodrigo has a CPF", "Name is X")
despite the per-type NÃO examples
Added two sections at the top of the prompt:
1. Business canonical data — explicit whitelist of suite names (Alexa,
Stilo, Hidromassagem) and stay types. Anything else = discard, NO auto-
normalization. LLM must not guess.
2. Cadastral data absolute rule — explicit list of fields that are
profile data, not memory: name, CPF/RG/passport, email/phone/address,
birth date. Plus 5 concrete ❌ examples of what was being wrongly
extracted in the wild.
Existing 9 specs still pass (stub at call_llm; prompt change is
semantic, not structural).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-world test revealed the LLM extractor (gpt-4o-mini) was using type
labels too loosely: a customer's QUESTION about parking ("tem
estacionamento?") was classified as 'reclamacao'. Similarly cortesia
generica ("obrigado") was becoming 'feedback_positivo', and transactional
events (CPF informed, reservation made) were becoming memories when they
should be ignored.
Rewrote build_prompt with:
- Per-type strict definition (what it IS)
- YES/NO examples for each of the 9 types, with the most common pitfalls
explicitly shown as NO
- 7 absolute rules, including: questions are never complaints, generic
courtesy is never feedback, agent actions are never customer memory,
transactional events are not long-term facts
- Confidence threshold guidance (>=0.9 only if totally explicit, 0.7-0.89
for strong inference, <0.7 discard)
- "If in doubt, discard — quality > quantity. Most transactional
conversations should return empty facts list"
Existing 9 specs still pass (stub call_llm, so prompt changes don't
affect unit test assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-world observation: OpenAI embedding API takes 200-400ms typical,
plus pgvector query overhead, the 500ms budget was being exceeded
frequently, silently dropping memory recall. Agent typing delay is
already 2-15s humanized, so a 2s recall budget is well within UX
tolerance and gives ~4-5x margin over typical embedding latency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problema observado: Daniela chamou generate_pix com arguments vazios apos
cliente informar "27/4". Tool retornou missing_fields=[check_in, amount] e
LLM caiu no fallback silenciosamente.
Correcoes:
- DDMMYYYY_REGEX agora aceita "DD/MM" sem ano (assume ano corrente, empurra
pro proximo ano se a data ja passou)
- parse_date_without_year com fallback explicito
- Instruction da scenario Daniela_Reservas (DB, scenario_id=2) atualizada
para listar todos os 4 parametros obrigatorios de generate_pix e
distinguir requires_input (erro do LLM) de success=false (erro tecnico)
Backup da instruction anterior: /tmp/daniela_instruction_backup_20260418.txt
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>
Adds concierge.* and reservation.* Liquid variables to agent_instructions
so Sofia's orchestrator_prompt receives unit persona/knowledge/variables
and reservation data resolved from conversation.custom_attributes.current_unit_id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace no-op stub with full perform body: find delivery by id, skip if
blank, delegate to Captain::Lifecycle::Dispatcher#call. Add retry_on
with polynomially_longer backoff (3 attempts). Spec covers dispatcher
delegation and graceful skip for missing records.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Orchestrates guards → render (Liquid) → send pipeline for one delivery.
Handles skip, reschedule, sent, failed states and re-enqueues on reschedule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement guards following the same pass/reschedule/too_stale pattern as QuietHours.
Also fix belongs_to :conversation on Delivery to use class_name: '::Conversation' to avoid namespace resolution failure inside Captain::Lifecycle module.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add after_commit callbacks to call Captain::Lifecycle::Scheduler on
create, status change (cancelled/no_show), and check_in_at change.
Each handler wraps in rescue StandardError to preserve existing behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure function mapping reservation events to timestamps; used by Scheduler (T9) to compute fire_at.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TDD: 16 examples passing. Adds EVENTS constant, active/for_event scopes,
and matches_reservation? with unit_ids/categorias/permanencias filters.
Also adds captain_reservation factory used by the 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.
- GeneratePixTool: envia payment_link como mensagem outgoing direta (bypassa
hallucination de [Link do Pix] placeholder pela LLM)
- GeneratePixTool: extrai email das mensagens recentes via regex e persiste
em contact.email
- GenerateReservationLinkTool: mesmo padrao de envio direto do link
- Captain::Reservation: after_create_commit callback atualiza
ultima_suite/permanencia/reserva_em/total_reservas em contact.custom_attributes
(aparece no painel lateral)