chat / agent / coder, e — opcionalmente — aciona o agent automaticamente via um rules engine com três modos de operação.
sse(HTTP+SSE clássico) — listener persistente no GET/sse.http(Streamable HTTP da spec 2025-03-26) — listener opt-in via GET no endpoint configurado.stdio— qualquer mensagem JSON-RPC semidque o processo filho emita em stdout é tratada como notification.
confirm ou auto em ~/.chatcli/mcp/triggers.json).Arquitetura em uma figura
cli/mcp/channels.go, cli/mcp/triggers/triggers.go, cli/channel_triggers.go, cli/agent_system_prompt.go). A separação é proposital: o ChannelManager é process-local e independente do CLI; o engine de triggers é um pacote separado sem dependência reversa; o CLI pluga via OnMessage.
Como uma mensagem chega ao agent
Server emite notification
id (notification, no jargão do spec). Pode ser via SSE stream, GET de Streamable HTTP, ou linha de stdout em stdio.Transport extrai e roteia
ChannelManager.ProcessSSENotification. O método da notification vira o channel: notifications/ci-pipeline → channel ci-pipeline; channel/message com params.channel → channel params.channel.Filtro de subscription
channels: [...] e o channel não está na lista, a mensagem é descartada (log debug). Lista vazia significa “aceita tudo”.Persist + ring + unread
seq monotônico, vai pro ring em memória (FIFO, default 200), é gravada append-only em ~/.chatcli/mcp/channels.jsonl, e o contador de unread incrementa.Fan-out para subscribers
OnMessage registrado recebe uma cópia. O CLI registra dois: o engine de triggers (que avalia rules) e o renderer de banner (que prepara a UI).System prompt — turno seguinte
Persistência
Garantias
- Durável entre sessões: mensagens recebidas enquanto o ChatCLI estava fechado ficam visíveis no próximo boot (até o limite de load).
- Append-only: nunca reescrevemos o arquivo. Crash do processo no meio de uma write deixa, no pior caso, uma linha truncada — o loader skip por linha, então uma linha corrompida nunca contamina as outras.
- Rotação automática: ao chegar em 10 MiB, o arquivo é renomeado para
.1e um novo é aberto. Só mantemos um histórico — channels são telemetria, não log de auditoria forense. - Best-effort: se a escrita falhar (disco cheio, permissão), o ChatCLI loga um warning uma vez, marca persistence como desabilitada só nessa sessão e continua servindo o ring em memória.
Boot — replay
Ao iniciar, o ChatCLI lê as últimas 200 linhas combinadas dechannels.jsonl.1 + channels.jsonl em ordem cronológica. Mensagens recarregadas:
- entram no ring;
- não contam como unread (já foram vistas);
- preservam seu
seqoriginal (o contador interno é avançado pro máximo observado).
Como configurar
Persistência é ligada por padrão quandoHOME é resolvível. Em ambientes containerizados sem HOME (raro), o ChannelManager cai para in-memory-only e loga um info no startup. Não há flag de configuração para “desligar persistence” — o caminho é deletar manualmente o arquivo se necessário (rm ~/.chatcli/mcp/channels.jsonl*).
Filtro por servidor — channels
O campo opcional channels na config do servidor MCP é um allow-list que decide quais channels desse servidor são aceitos pelo ring.
- Vazio/omitido → aceita todos os channels que o servidor emitir.
"*"explícito → equivalente a vazio.- Lista → só os channels listados literalmente entram no ring; outros são descartados (log debug).
- Whitespace nos entries é trimmado, então
" alerts "casa com"alerts".
enabledTools/disabledTools), aqui não há suporte a glob no channels da ServerConfig — é match literal. Globs (alerts/*, *) só existem nas rules de trigger, que rodam depois do filtro.Auto-injection no system prompt
Em todo turn (chat, agent, coder), as 5 mensagens mais recentes do ring são adicionadas ao system prompt como um bloco extra:
Garantias importantes
- Sem cache hint: esse bloco é volátil e propositalmente fica fora do prefixo cacheado da Anthropic. Inserir um bloco que muda toda turn dentro do cache trasharia o KV cache inteiro — pior do que não cachear nada.
- Mesmo conteúdo em chat / agent / coder: o builder de system prompt em todos os três modos foi unificado pra incluir o bloco.
- Ring vazio → bloco omitido: zero overhead quando você nunca recebeu nada.
Por que apenas 5?
Calibrado pra equilíbrio: o suficiente pra dar contexto recente (CI rodando + alert mais quente) sem ocupar tokens demais quando o usuário tem muitos servers conversando. Se você precisar de mais histórico em uma turn específica, use/channel inject que injeta as últimas 10.
Reactive Triggers (rules engine)
Esta é a parte opt-in. Quando você quer que o ChatCLI reaja a eventos (ex: “se o CI falhou, peça pra investigar”; “se houver alerta crítico, rode o agent”), defina rules em~/.chatcli/mcp/triggers.json.
Schema da rule
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string | ✅ | Identificador único da rule. Aparece em logs e em /channel rules |
server | string | ❌ | Match exato pelo nome do servidor MCP. Vazio = qualquer servidor. "*" = qualquer servidor (explícito) |
channel | string | ❌ | Match. Suporta literal ("alerts"), wildcard ("*") e prefix-glob ("alerts/*") |
contentRegex | string | ❌ | Regex (Go regexp) aplicada sobre Content da mensagem. Vazio = qualquer conteúdo |
mode | string | ❌ | "notify" (default), "confirm", "auto" |
prompt | string | obrigatório em confirm/auto | Template do prompt enviado ao agent quando a rule dispara. Variáveis: {{content}}, {{channel}}, {{server}}, {{seq}}, {{timestamp}} |
tools | string[] | obrigatório em auto | Whitelist de tools que o agent pode invocar. Em auto é exigida pra prevenir um trigger que abre acesso irrestrito |
rateLimit | string (duração Go) | ❌ | Cap por rule: depois de uma fire, ignora matches dentro dessa janela. Ex: "5m", "30s", "1h" |
dedupWindow | string (duração Go) | ❌ | Dedup por (rule, content prefix): mesma rule + mesmo prefixo de content dentro da janela vira no-op |
Match — exemplos práticos
alerts/critical, qualquer servidor.
alerts/critical, alerts/warning, alerts/info — qualquer sub-channel de alerts/. Não pega errors/critical (prefixo diferente).
prom-alerts, channel alerts/*, e content casando o regex. Todas precisam ser satisfeitas (AND).
Modos
mode: notify (default — zero surpresa)
Quando a rule casa:
- Imediatamente: um toast aparece no stderr com o nome da rule e um preview do content.
- Na próxima vez que você for digitar (próximo prompt tick), aparece um banner acima do prompt com o item na inbox.
- Nada roda. Você decide se quer agir.
mode: confirm (user-in-the-loop)
Quando a rule casa:
- Toast no stderr + prompt no banner: “run
/channel confirm <id> yesou/channel confirm <id> no”. - Nada roda até o usuário responder.
/channel confirm <id> yes(ou só/channel confirm <id>, default é yes) dispara o agent no template da rule./channel confirm <id> nodescarta a action.
mode: auto (autônomo — opt-in forte)
Requer tools whitelist obrigatória (validação rejeita rules auto sem tools). Quando casa:
- Toast no stderr informando que o trigger entrou na fila.
- No próximo prompt tick (depois de drainar park resumes e antes de processar input do usuário), o agent é acionado automaticamente no template da rule.
- A execução é renderizada dentro de uma
AUTO-AGENT envelope box— visualmente distinta de uma resposta a turno normal. Escaborta como em qualquer execução de agent.
Guard-rails do auto
toolswhitelist obrigatória — validação no startup rejeita ruleautosemtools.rateLimitededupWindowrecomendados — sem eles, um servidor barulhento pode disparar dezenas de turns por minuto.Escsempre aborta, não importa se o agent foi triggado por user ou por rule./channel pausedesliga tudo globalmente — usar antes de uma operação sensível para evitar interferência.
Configuração de rules
CI watcher (notify only — safe default)
Prod alerts → confirm
Canary deploys → auto (com whitelist apertada)
Validação
Falhas de schema rejeitam o arquivo inteiro (atomic apply — ou tudo entra, ou nada muda). Erros comuns:| Sintoma | Causa | Fix |
|---|---|---|
mode "auto" requires a non-empty tools whitelist | mode: "auto" sem campo tools | Adicione "tools": [...] ou troque pra mode: "confirm" |
invalid contentRegex: error parsing regexp | Regex inválido | Teste em regex101.com com flavor “Golang”; observe que metachars precisam de escape JSON duplo (\\b, \\d) |
invalid mode "Notify" | Case-sensitive | Use lowercase: notify, confirm, auto |
duplicate rule name "x" | Duas rules com mesmo name | Nomes têm que ser únicos no arquivo |
invalid rateLimit "5min" | Formato Go time.Duration | Use 5m, 30s, 1h (não 5min, 30sec) |
/channel rules reload.
Comandos /channel
Todos os subcomandos suportam autocomplete (Tab após /channel ).
| Subcomando | Argumento | Descrição |
|---|---|---|
/channel ou /channel list | — | Lista até 20 mensagens mais recentes do ring, com seq, timestamp, servidor, channel e preview do content |
/channel <name> | nome de channel | Filtra a listagem pelo channel especificado |
/channel ack | — | Marca todas as mensagens como lidas e limpa o banner de notify pendentes |
/channel inject | — | Splica as últimas 10 mensagens no histórico como uma mensagem de system pro próximo turn — útil quando você quer dar contexto explícito ao LLM sem esperar a auto-injection das 5 mais recentes |
/channel pause | — | Pausa o trigger engine. Mensagens continuam entrando no ring/persistence, mas nenhuma action é emitida (sem banner, sem auto, sem confirm) |
/channel resume | — | Reativa o trigger engine |
/channel rules | — | Lista as rules ativas com seus modos, filtros e prompts |
/channel rules reload | — | Re-lê ~/.chatcli/mcp/triggers.json sem reiniciar o ChatCLI. Em caso de erro de validação, mantém as rules anteriores |
/channel confirm <id> | id obrigatório | Aceita um action confirm pendente; default é yes |
/channel confirm <id> no | id obrigatório | Recusa um action confirm pendente sem rodar nada |
/channel run <seq> | seq obrigatório | Roda o agent manualmente em cima de uma mensagem do ring (use o seq mostrado em /channel list) — útil pra investigar algo que entrou só como notify |
Exemplos
Listando
Filtrando
Rodando manualmente uma mensagem
Inspecionando rules
Auto-injection vs /channel inject — quando usar cada um
| Você quer | Comando | Custo de tokens |
|---|---|---|
| Que o agent sempre saiba das últimas 5 mensagens | nada — é automático em todo turn | ~5 linhas no system prompt |
| Forçar contexto explícito (e mais profundo) pro próximo turn | /channel inject | ~10 linhas como system message permanente no histórico |
| Investigar uma mensagem específica do passado | /channel run <seq> | dispara um agent run completo |
/channel inject é uma ação explícita que adiciona um system message ao histórico (permanece até a próxima compaction). /channel run é a opção “investigar agora” sem regra cadastrada.
Reconexão e fault tolerance
| Cenário | Comportamento |
|---|---|
| Servidor SSE cai mid-session | Supervisor reconecta com backoff exponencial cheio + jitter (500ms → 30s cap). Pending RPCs aguardam, sem timeout extra |
| Servidor Streamable HTTP cai mid-session | Mesma estratégia de backoff, mas a listener (GET aberto) é o canal que reconecta — POSTs continuam funcionando normalmente quando o servidor volta |
| Servidor stdio crasha | onClose propaga, o manager marca como disconnected em /mcp status. Notifications param porque o processo morreu |
| Servidor HTTP retorna 405 / 404 / 501 no GET | Servidor não suporta push listener. Stop limpo, sem retry storms — log info “server does not support push” |
| Persistence file corrompido | Linhas inválidas são puladas individualmente. Arquivo continua usável |
| Disco cheio no append | Warning uma vez, persistence desligada nessa sessão, ring em memória continua |
| Notification chega durante shutdown | Push é no-op quando Close() já foi chamado — sem race com close do arquivo |
Mcp-Session-Id no initialize, o transport ecoa o header em todas as requests (incluindo o GET do listener). Reconexão preserva a sessão automaticamente.Casos de uso
CI/CD
rule notify em ci-pipeline. Você vê falhas no banner; /channel run <seq> lança o agent pra investigar com tools.Prod alerts (Prometheus/Datadog)
rule confirm em alerts/critical com prompt template. Cada alerta vira uma pergunta “investigate?” que vc responde sob demanda.Canary deploys
rule auto com whitelist apertada (kubectl_*, http_request). Toda canary inicia automaticamente as smoke checks.Webhooks externos (GitHub/Jira/Slack)
Limites e trade-offs
| Item | Valor | Por quê |
|---|---|---|
| Ring em memória | 200 mensagens | Cobre uso normal (várias horas de CI/alertas) sem inchar memória |
| Auto-injection | 5 mensagens | Equilíbrio entre contexto útil e tokens consumidos por turn |
/channel inject | 10 mensagens | Mais profundidade pra investigação explícita |
| Persistence file | 10 MiB antes de rotacionar | Suficiente pra ~50k mensagens (curtas) |
| Load no boot | 200 mensagens | Garante ring “quente” sem ler arquivo gigante |
| Backoff de reconnect | 500ms → 30s, jitter cheio | Recupera rápido de blip, evita thundering herd |
| Confirm expiration | 30 min | Bound em memória; user que sumiu por horas não acumula confirms |
| Buffer de actions no engine | 64 | Defesa contra “tempestade” — drop com warn em vez de back-pressure |
Troubleshooting
Recebo a notificação no /channel list mas o agent não reage automaticamente
Recebo a notificação no /channel list mas o agent não reage automaticamente
notify ou nenhuma rule. notify é passivo por desenho. Para ação:- Adicione uma rule
confirmouautoem~/.chatcli/mcp/triggers.jsone rode/channel rules reload. - Ou rode
/channel run <seq>manualmente em cima da mensagem.
O servidor HTTP envia push, mas nada chega no /channel list
O servidor HTTP envia push, mas nada chega no /channel list
/mcp logs <nome>— se aparecerserver returned 405 on GET <url>ou404ou501, o servidor não implementa push e o listener parou limpamente.- Caso contrário, verifica se o servidor está emitindo notifications no formato JSON-RPC sem
id. Conteúdodata:não-JSON também é capturado (cai no channelraw), mas não dispara rules específicas de outros channels.
A mensagem aparece em /channel list mas nenhuma rule dispara
A mensagem aparece em /channel list mas nenhuma rule dispara
/channel rules— confirma que a rule existe e está ativa./channel pausefoi rodado?/channel resumereativa.- Match: o
server/channel/contentRegexrealmente bate? Lembre quechannel: "alerts"não pega"alerts/critical"— use"alerts/*". rateLimitoudedupWindow— uma fire recente pode estar suprimindo.
O JSONL ficou enorme. Posso apagar?
O JSONL ficou enorme. Posso apagar?
rm ~/.chatcli/mcp/channels.jsonl*. Na próxima boot, o ring começa vazio.Em runtime, a rotação corta no limite de 10 MiB automaticamente — você pode chegar até ~20 MiB no total (ativo + .1) antes do próximo corte.Rules paradas depois de editar triggers.json
Rules paradas depois de editar triggers.json
/channel rules reload. Se a recarga falhar (schema inválido), o erro aparece na resposta e o ChatCLI mantém as rules anteriores ativas — sem fica num estado “sem rules”.Se o erro for de regex, copie-cole o regex pra https://regex101.com flavor “Golang” para isolar.Rule auto dispara em loop infinito
Rule auto dispara em loop infinito
- Bump
rateLimitsignificativamente (ex:"15m"). - Refine o
contentRegexpra só pegar o caso patológico. /channel pauseenquanto você investiga a config offline.
Quero ver TUDO que chegou (não só 5 últimas) no próximo turn
Quero ver TUDO que chegou (não só 5 últimas) no próximo turn
/channel inject — coloca as últimas 10 como system message no histórico. Persiste no histórico até a próxima compaction.Para visualizar sem injetar no LLM: /channel list mostra até 20.O agent é triggado em auto mode e abre uma tool que eu não autorizei
O agent é triggado em auto mode e abre uma tool que eu não autorizei
tools whitelist da rule. Em auto, só as tools listadas podem ser invocadas sem aprovação extra; tool fora da whitelist cai no fluxo normal de aprovação do agent — o que pode ser sua intenção ou não. Refine a whitelist no triggers.json.Próximos passos
MCP Integration
MCP Config
mcp_servers.json (campo channels incluído).Sistema de Hooks
Command Reference
/channel.