fix(events): eventos CUSTOM do ESL nunca eram entregues

Achado ao iniciar a fase de Realtime Monitoring: nenhum evento CUSTOM
(sofia::register, sofia::gateway_state, callcenter::info) jamais chegou em
b2bcall-fs-events nesta sessao, apesar do estado real do FreeSWITCH mudar de
verdade (confirmado originando uma chamada de teste pra dentro de uma fila
real com agente logado). Isso deixava dois gaps documentados como "nao
verificado" em docs/TRUNKS.md e docs/AGENTS.md.

Causa raiz: `event_json(...SUBSCRIBED_EVENTS)` mandava "CUSTOM" como ultimo
token do comando `event json`, sem nenhum subclass depois. O
mod_event_socket do FreeSWITCH exige que os subclasses (callcenter::info,
sofia::register, ...) venham imediatamente depois do token CUSTOM no mesmo
comando — sem isso, zero eventos CUSTOM sao entregues, de qualquer
subclass.

Corrigido separando PLAIN_EVENTS (nomes normais, cada um vira um listener
.on()) de CUSTOM_SUBCLASSES (so compoe o comando de subscricao — o client
ESL sempre emite "CUSTOM" como nome de evento, com o subclass real no
header Event-Subclass). Corrigido tambem um bug de nome de campo:
normalizeCustomEvent lia CC-Agent-Status, que nao existe; o campo real e
CC-Agent-State.

Com o pipeline corrigido, chegam eventos ricos de callcenter::info nunca
antes observados: agent-offering, bridge-agent-fail, members-count (fila em
tempo real) e member-queue-end (com CC-Cause/CC-Cancel-Reason e timestamps
de entrada/saida — atendida vs. abandonada). Adicionados como novos tipos
normalizados: AGENT_OFFERED_CALL, AGENT_BRIDGE_FAILED, QUEUE_MEMBER_COUNT,
QUEUE_MEMBER_LEFT.

De quebra, achado e corrigido um segundo bug real ao reverificar Trunks com
o pipeline de eventos funcionando: `sofia profile external rescan` nunca
descarregava um gateway cujo arquivo .xml foi apagado (fica fantasma na
memoria do Sofia indefinidamente). trunk-sync.ts agora roda `sofia profile
external killgw <nome>` pra cada gateway removido, antes do rescan.

Reverificado ponta a ponta pra ambos os bugs:
- Trunk com register:true apontando pra host inexistente: Trunk.status no
  banco passa de UNKNOWN pra FAILED sozinho, via evento, sem polling.
- Trunk apagado via API: gateway some imediatamente de `sofia status
  gateway`, sem esperar reinicio de profile.
- Chamada de teste real numa fila com agente logado: members-count,
  agent-offering, agent-state-change (CC-Agent-State correto: Waiting/
  Receiving), bridge-agent-fail, todos chegando certos no canal Redis
  b2bcall:events.

typecheck do workspace inteiro limpo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
This commit is contained in:
2026-08-28 11:41:09 -03:00
parent a05104a05f
commit b940ce3e63
7 changed files with 174 additions and 36 deletions

View File

@@ -22,9 +22,9 @@ reagir a `connect`/`reconnecting`/`error` e resubscrever a cada `connect`
`mod_callcenter` mas não foram exercitados contra uma fila real ainda —
não existe nenhuma (fase Queues).
- `normalizeEslEvent()`: traduz eventos ESL crus pro vocabulário interno
(agente.md secao 24). Mapeamento de `callcenter::info``AGENT_STATUS_CHANGED`
é best-effort (nomes de campo inferidos da documentação, não testados
revisar na fase Queues/Agents).
(agente.md secao 24). Mapeamento de `callcenter::info`/`sofia::*` com nomes
de campo confirmados contra uma fila real na fase Realtime Monitoring
ver "Achados" abaixo e docs/AGENTS.md.
## Achados durante os testes
@@ -41,6 +41,19 @@ reagir a `connect`/`reconnecting`/`error` e resubscrever a cada `connect`
3. O logger JSON de `packages/shared` quebrava (`TypeError: Do not know how
to serialize a BigInt`) porque a lib `esl` usa `bigint` nos campos de
estatística de erro. Corrigido com um `replacer` no `JSON.stringify`.
4. **Bug real, só descoberto na fase Realtime Monitoring**: nenhum evento
CUSTOM (`sofia::gateway_state`, `sofia::register`, `callcenter::info`)
nunca chegou nesta sessão até então, apesar da subscrição incluir
`"CUSTOM"` na lista de `SUBSCRIBED_EVENTS`. Causa: `event_json(...events)`
manda `"CUSTOM"` como último token do comando `event json`, sem nenhum
subclass depois — o mod_event_socket do FreeSWITCH exige que os
subclasses venham imediatamente depois do token `CUSTOM` no mesmo
comando pra serem entregues. Corrigido: `SUBSCRIBED_EVENTS` agora termina
em `"CUSTOM", "sofia::register", "sofia::unregister", "sofia::expire",
"sofia::gateway_state", "callcenter::info"`, e o loop que registra
listeners `.on()` só usa os nomes "planos" (`PLAIN_EVENTS`) mais um único
`.on("CUSTOM", ...)` — os nomes de subclass nunca viram listener, só
compõem o comando de subscrição.
## Eventos consumidos e publicados