Arquitetura
O aipager é um programa asyncio de um processo só que roda em segundo plano. Ele liga três mundos: o seu chat do Telegram, os processos do Claude Code que você roda sob o dtach, e o sistema de hooks do Claude Code. Tudo passa por um único event loop: sem pools de workers, sem banco de dados, sem filas entre processos.
O aipager é um programa asyncio de um processo só que roda em segundo plano. Ele liga três mundos: o seu chat do Telegram, os processos do Claude Code que você roda sob o dtach, e o sistema de hooks do Claude Code. Tudo passa por um único event loop: sem pools de workers, sem banco de dados, sem filas entre processos.
Diagrama das partes#
Tudo o que está no diagrama, menos o Telegram, roda neste computador. O aipager também fala com alguns outros servidores: o túnel do Cloudflare do Mini App (e o GitHub, uma vez, para o cloudflared), o telegram.org para o script do Mini App, a verificação de versões quando você pede uma atualização, o download do modelo de voz na primeira vez que você usa áudios, e o destino dos relatórios de problemas quando você envia um. Todos estão em Modelo de segurança → conexões de rede.
Como o processo funciona#
Uma única chamada asyncio.run em aipager/cli/daemon.py toca tudo. Dentro dela:
TelegramBot(aipager/bot/): tem umApplicationdopython-telegram-bot. Recebe as atualizações do Telegram, entrega cada uma ao tratador de mensagens, botões ou áudios, e mantém em dia os cartões de atividade, a barra de status fixada e as respostas.HookReceiver(aipager/dtach/hook_receiver.py): abre um socket datagram do Unix em$XDG_RUNTIME_DIR/aipager.sock, lê o JSON que oaipager-hooke oaipager-statuslinemandam, e distribui o trabalho pelohook_event_name(outype, para as mensagens do próprio aipager; veja eventos de hook).SessionMonitor(aipager/session_monitor.py): acorda a cada 2 s para olhar/tmp/claude-dtach-*.sock, acertar as sessões que encontra com a lista de sessões, e rodar os vigias: um cartão de atividade que parou de andar, uma mensagem que o Claude Code nunca recebeu, um pedido de permissão sem notícias por 5 minutos que a conversa salva mostra como acabado (AIPAGER_INTERACTIVE_TIMEOUT), uma tarefa cujo fim não foi visto.SessionRegistry(aipager/state.py): os objetosTrackedSessionpelo nome da sessão. Salvos em~/.claude/aipager-sessions.json(de forma atômica) ao desligar e sempre que muda algo que precisa sobreviver a um reinício.- Servidor do Mini App (
aipager/miniapp/server.py): um app aiohttp em127.0.0.1na portaMINIAPP_PORT(padrão 8765), que enquanto está ligado (o padrão) chega ao Telegram por um túnel do Cloudflare gerenciado. Serve o painel do/app.
Tudo isso roda como tarefas no mesmo loop. O que travaria o loop roda em threads à parte: transformar áudio em texto, montar e enviar um relatório de problemas, verificar atualizações e planejar reinícios, baixar o script do Mini App e o cloudflared, e a verificação de login ao iniciar.
Ordem de inicialização#
De aipager/cli/daemon.py:
- Passa uma configuração antiga (
config.env,team.yaml) paraaipager.yaml, uma vez só, e tira os arquivos velhos do caminho. - Não inicia se outro aipager responder no socket; depois pega a trava do aipager (
~/.local/share/aipager/daemon.lock). - Confere o token do bot com o Telegram (
getMe). - Acerta os arquivos do próprio Claude Code: adiciona qualquer hook do aipager ou a linha de status que falte em
~/.claude/settings.json, e responde às perguntas da primeira execução que um usuário do Telegram não conseguiria responder (veja Modelo de segurança). SessionRegistry.load(): lê~/.claude/aipager-sessions.json. Se o arquivo não existir ou não puder ser lido, começa com a lista vazia.- Descobre o endereço do Mini App (sua própria URL, ou o Tailscale), depois
bot.start(): começa a receber as atualizações. HookReceiver.start(): apaga um arquivo de socket que tenha sobrado, abre e escuta.bot.recover_sessions(): recupera os cartões e os pedidos das sessões que estavam trabalhando ou esperando quando o aipager parou (veja comandos → respostas).SessionMonitor.start(): começa o tique de 2 s.- O servidor do Mini App inicia, depois o túnel (quando você não definiu uma URL própria), e os botões
📱 Appsão publicados. - O aipager espera sinais: SIGINT ou SIGTERM o param, SIGUSR1 recarrega os chats e as regras (uso em equipe → recarregar a configuração sem reiniciar).
Ordem de desligamento#
- SIGINT ou SIGTERM ligam o evento
stop. - Um
/updateem andamento ganha um pouco de tempo e depois o instalador dele é parado (veja comandos → atualizar). registry.save().- O túnel para, depois o servidor do Mini App.
session_monitor.stop(),hook_receiver.stop()(fecha o socket e apaga o arquivo dele).bot.stop(): cancela as animações e para oApplication.registry.save()de novo, para um cartão cujo envio ainda estava em andamento.
O reinício feito pelo Telegram depende de todo esse caminho terminar antes de o substituto abrir o socket. O que você vê está em comandos do bot → reiniciar.
Onde ficam os arquivos e os sockets#
| Caminho | Para quê | Quem |
|---|---|---|
$XDG_RUNTIME_DIR/aipager.sock | Socket datagram do Unix para os eventos de hook (/tmp/aipager.sock sem $XDG_RUNTIME_DIR; $AIPAGER_SOCKET_PATH vale mais que os dois) | aipager (abre) |
aipager-reply-<id>.sock (ao lado do socket de controle) | Um socket de resposta de vida curta para cada pedido de permissão que o hook espera | aipager-hook |
aipager-flood-mute.json, aipager-flood-backoff.json (ao lado do socket de controle) | O estado atual dos limites de velocidade do Telegram, para o aipager status e o doctor | aipager |
aipager-modelswitch-<name>.json (ao lado do socket de controle) | Marca uma troca de modelo que o aipager digitou, por 30 s (hooks) | aipager (escreve), aipager-hook (pega) |
/tmp/claude-dtach-<name>.sock | Socket de controle do dtach de cada sessão | dtach |
/tmp/claude-status-<name>.json | Dados da linha de status de cada sessão | aipager-statusline |
/tmp/claude-notes-<name>/ | Uma nota de permissões para cada mensagem do Telegram que ainda não foi pega, e uma marca turn-open que o hook mantém enquanto há uma tarefa em andamento (uma mensagem que entra na tarefa só pode deixar as regras dela mais rígidas) | aipager (escreve as notas), aipager-hook (consome as notas; escreve turn-open, e os dois a apagam) |
/tmp/claude-policy-<name>.json | A cópia de referência das permissões da tarefa em andamento | aipager-hook (escreve ao pegar a mensagem), lida pela verificação PreToolUse |
/tmp/claude-policy-.floor-<uid>.json | O piso de segurança, a lista própria do aipager mais a sua seção safety:, para os casos de reserva do hook | aipager |
/tmp/claude-reply-<name>.txt | A mensagem que você respondeu, quando você responde a uma mensagem anterior | aipager |
/tmp/aipager-files/<chat>/ | Arquivos e áudios enviados pelo Telegram, uma pasta privada por chat (0700, arquivos 0600) | aipager |
/tmp/aipager.log | A saída de um reinício próprio quando não há serviço | aipager |
~/.claude/aipager-sessions.json | O estado duradouro da lista de sessões | aipager |
~/.claude/aipager-audit.jsonl | Registro de permitir, negar e respostas | aipager (só acrescenta) |
~/.claude/aipager-flood-state.json | O histórico dos limites de velocidade do Telegram de cada chat, que sobrevive aos reinícios (solução de problemas) | aipager |
~/.claude/aipager-tunnel.json, aipager-cloudflared.log | O túnel do Mini App que o aipager mantém: o processo dele, as portas locais e um hash do endereço (nunca o endereço), para o próximo aipager poder assumi-lo depois de um reinício planejado (permissões 600); os avisos do cloudflared | aipager; o registro também é removido pelo guardião do túnel mantido e por aipager uninstall |
~/.claude/aipager-pending-users.json | Pessoas que chamaram o bot num grupo onde ele não trabalha, ou que não são membros de um grupo onde ele trabalha, para o aipager config poder adicioná-las (permissões 0600, substituído inteiro a cada escrita, as 200 mais recentes vistas nos últimos 30 dias) | aipager (escreve), aipager config (lê) |
~/aipager-users/<label>/ | As pastas raiz padrão dos membros (permissões 0700), onde os membros user e read_only criam as sessões deles; o aipager uninstall as mantém | aipager (cria no primeiro /new ou “Nova sessão” do Mini App de um membro) |
~/.claude/settings.json | A configuração de hooks do Claude Code | aipager config / aipager setup; a cada início o aipager adiciona qualquer hook ou linha de status que falte e define skipDangerousModePermissionPrompt (Modelo de segurança) |
~/.claude/settings.json.bak.* | Cópias de segurança antes de cada reescrita | aipager config, aipager setup, aipager uninstall |
~/.claude.json | O arquivo do próprio Claude Code: o aipager marca como confiável a pasta onde uma sessão começa logo antes de ela começar (inclusive a pasta pessoal, nunca / nem uma pasta acima da pasta pessoal), para a pergunta de confiança do Claude Code não travar uma sessão criada pelo Telegram. Nada é marcado quando o aipager inicia. Nunca é reescrito quando não dá para lê-lo inteiro ou quando um Claude em execução o mudou nesse meio-tempo | aipager |
~/.claude/.credentials.json → .credentials.json.stale | O arquivo de login do Claude Code, tirado do caminho quando uma sessão inicia com CLAUDE_CODE_OAUTH_TOKEN definido e o arquivo só tem um login vencido ou vazio (senão ele ganharia do token); coloque de volta para desfazer | aipager |
~/.config/aipager/aipager.yaml | Token do bot, chats, membros e funções, configurações do Mini App | aipager config (permissões 600); o aipager move a entrada de um grupo quando o Telegram o transforma em supergrupo, e guarda uma cópia aipager.yaml.bak.<time> quando passa para lá configurações antigas do Mini App |
~/.config/aipager/policy.yaml | Regras por função e regras de segurança adicionais | você (conferido com aipager policy validate); um exemplo comentado é escrito uma vez quando uma instalação antiga é migrada |
~/.config/aipager/preferences.json | O que foi escolhido em /settings para cada chat e cada sessão | aipager |
~/.config/aipager/daemon.env | Credencial do Claude para as sessões iniciadas | você / aipager service install / aipager doctor --fix (permissões 600) |
~/.config/aipager/config.env, team.yaml → *.bak.<time>, *.retired.<time> | A configuração de uma instalação antiga: copiada e depois tirada do caminho quando o aipager.yaml já a tem | aipager, aipager config, aipager setup |
~/.config/aipager/keyboard.json | Mudanças opcionais do teclado | você |
~/.local/share/aipager/sessions/ | Uma pasta por sessão com o SESSION.md dela (quem pode falar com ela, e as regras), refeita a cada início | aipager |
~/.local/share/aipager/webapp-sdk/, cloudflared/ | O script do Mini App do Telegram e o binário cloudflared conferido | aipager |
~/.local/share/aipager/daemon.lock, update.lock, update-restart.json | Um aipager de cada vez; uma atualização de cada vez; a atualização a anunciar depois do reinício | aipager |
~/.local/share/aipager/install.json, running.json | Quando o aipager rodou aqui pela primeira vez; se está rodando agora (para diferenciar uma queda de uma parada normal) | aipager |
~/.local/state/aipager/reports.json | Relatórios de problemas: os fingerprints dos erros registrados, as contagens e o estado das sugestões (permissões 600); veja relatórios de problemas | aipager |
~/.local/state/aipager/report-sends.json | Os relatórios enviados hoje (o limite diário) | aipager, aipager report |
~/.local/state/aipager/languages.json | O estado de idioma (permissões 600): quem já usava o aipager antes de existirem idiomas, o último idioma do Telegram visto de cada pessoa, se a oferta única de idioma e a linha de primeiro contato já foram enviadas, e um hash de cada descrição do bot que o aipager definiu; veja Idiomas | aipager |
~/.config/systemd/user/aipager.service (Linux), ~/Library/LaunchAgents/com.aipager.daemon.plist (macOS), e uma cópia .bak.<time> do que uma instalação substitui (no Linux só se for diferente) | O serviço em segundo plano | aipager service install |
~/Library/Logs/aipager.log (macOS) | O log do serviço do launchd, lido pelo aipager logs | launchd, para o serviço |
Além disso, o aipager cria uma pasta nova para uma sessão quando você pede (Nova pasta no /new ou no Mini App), e os instaladores e o modelo de voz escrevem os próprios arquivos quando você atualiza ou usa a voz. Tudo fica na sua pasta pessoal, em /tmp, ou ao lado do socket de controle. O aipager nunca pega permissões a mais; veja Modelo de segurança.
Uma instância isolada (para testes)#
Defina AIPAGER_INSTANCE_DIR como uma pasta absoluta que seja sua e todos os arquivos compartilhados da tabela acima vão para lá: o socket de controle (e os sockets de resposta, os arquivos de sinal dos limites de velocidade e a marca de troca de modelo ao lado dele), os sockets claude-dtach-*, os arquivos claude-policy-*, claude-notes-*, claude-status-* e claude-reply-*, o arquivo do piso de segurança, a pasta de downloads aipager-files e o log do reinício próprio. As sessões que a instância inicia herdam a configuração, então os hooks delas só falam com essa instância, e as regras de segurança protegem do mesmo jeito os arquivos movidos. A instância não lê o .env de um checkout.
Os arquivos da sua pasta pessoal não mudam de lugar, então aponte o HOME também para outra pasta. O aipager start se recusa a iniciar quando a pasta da instância está definida e o HOME é a sua pasta pessoal de verdade, quando a pasta não é um caminho absoluto para uma pasta sua, ou quando ela é longa demais para o caminho de um socket.
O /update e o /restart não ficam isolados: eles agem sobre o pacote aipager instalado e o serviço aipager.service, então nunca os mande para uma instância de teste.
AIPAGER_TELEGRAM_API_BASE (por exemplo http://127.0.0.1:41234) manda todos os pedidos à Bot API e os downloads de arquivos para esse endereço em vez de https://api.telegram.org. Precisa ser este computador (localhost ou um endereço loopback), a não ser que você defina AIPAGER_TELEGRAM_API_ALLOW_REMOTE=1, para um erro de digitação não poder mandar o token do bot para outro lugar.
As duas configurações existem para os testes de ponta a ponta poderem rodar um segundo aipager contra um Telegram de mentira local, ao lado do seu de verdade. Sem nenhuma das duas, todos os caminhos e endereços são os da tabela acima.
Por que o dtach#
O dtach dá a cada sessão do Claude Code um PTY de verdade sem prendê-la a um terminal que precisa ficar aberto. O aipager inicia uma sessão com dtach -n e digita nela com dtach -p (mensagens, Escape, as teclas que respondem a uma caixa de diálogo); ele nunca lê a tela, porque os hooks e a conversa salva dizem o que acontece. Você pode se conectar de forma interativa de qualquer shell com aipager session <name> para ter acesso direto. Essa conexão acompanha a sessão num reinício feito pelo Telegram (/mode para o Modo Acesso total ou saindo dele, /restart, Reiniciar): quando o socket da sessão some, ela espera até 15 s pelo mesmo socket voltar e se reconecta sozinha. Uma sessão que terminou de verdade volta para o shell depois da espera (Ctrl-C encerra antes). A conexão roda com a tecla de desconexão do dtach desligada, então Ctrl-\ chega ao Claude; para sair, feche o terminal ou o painel do tmux e a sessão continua rodando.
O resultado: o Claude Code roda como se você digitasse num terminal, mas esse terminal pode ir e vir sem atrapalhar a sessão em andamento. O aipager encontra as sessões olhando /tmp/claude-dtach-*.sock a cada tique de 2 s do vigia. Uma sessão encontrada pertence ao chat para o qual o nome dela foi criado, e uma criada no terminal pertence ao chat privado do dono numa instalação com chat privado e grupo.
Veja também#
- Eventos de hook: o que chega pelo socket de controle.
- Comandos do bot: o que chega pelo Telegram.
- Modelo de segurança: limite de permissões, segredos, auditoria.
- Solução de problemas: referência do
aipager doctor.