Cloud Chat Routing — Guia
Phase 70. Quando você conversa com um worker pelo CRM ou Telegram, essa conversa agora roda dentro do seu container Hetzner Cloud, não no servidor master. Feche o laptop, mude para o Telegram, continue trabalhando — a sessão, os arquivos e os repos abertos vivem todos no seu workspace Cloud.
TL;DR
- Usuários do plano Cloud: toda mensagem de chat que seu worker processa
faz spawn de
claude -pdentro de/workspace/<project>no seu container. - Usuários Free / Starter: sem mudanças — o chat continua rodando no servidor master.
- Container pausado? A primeira mensagem o desperta (~1 segundo extra) e retoma a conversa de onde você parou.
- Três novas superfícies indicam que está funcionando: um pill
Cloudno header, uma linhaclaude spawn: container/readyno log do worker, e as edições aparecendo em/workspace/<project>em vez de/opt/repos/<project>.
Por que isso importa
A Phase 60 entregou o Standard Cloud como "máquina Linux sempre ligada com
Claude Code". A Phase 69 conectou seus repos à org do bot. Mas até a
Phase 70, o fluxo de chat ainda fazia spawn de claude -p no VPS master —
seu container Cloud ficava ocioso e seu trabalho de chat vivia no master.
A Phase 70 roteia o chat para dentro do container para que a proposta de valor realmente entregue:
- Feche o laptop, continue pelo Telegram. Mesma sessão, mesmos arquivos, mesmo estado do repo. Sem aquele "tive que esperar voltar para o laptop".
- Edições caem dentro do container. Seus repos da org do bot em
/workspace/recebem as mudanças; o auto-commit em idle + push (Phase 69.3 + #349) os envia para o GitHub;arc pullno seu laptop sincroniza tudo de volta. - Sem round-trip pelo Cloudflare. A saída do worker faz streaming container ↔ master ↔ chat — sem salto pela rede pública no inner loop.
Como o roteamento funciona
Toda mensagem de chat passa por esta árvore de decisão:
chat msg arrives → child-bot on master
│
▼
getWorkerTarget(ownerChatId)
│
├─ user.plan === 'cloud' AND container.status ∈ (ready, paused) ?
│ │ YES
│ ▼
│ ensureAwake(target) ← Phase 70.4: wake if paused (~1s)
│ │
│ ▼
│ spawnWorker → docker exec ← Phase 70.2: --workdir /workspace/<slug>
│ (Phase 70.3 maps project_name → slug)
│ │
│ ▼
│ bash -lc 'exec "$@"' bash claude -p "..."
│ ↑ login shell sources ~/.profile so
│ ANTHROPIC_API_KEY env reaches claude
│
└─ otherwise → Bun.spawn(["claude", ...]) on master (today's default)
Essa decisão acontece uma vez por mensagem. O log do worker emite uma única linha indicando o alvo escolhido:
claude spawn: container/ready
claude spawn: container/ready (woke from paused)
claude spawn: local
Se você quiser saber onde sua última mensagem rodou, é essa a linha que você procura com grep.
Como verificar que está funcionando
1. Pill no header
Após provisionar seu Cloud Workspace, Configurações → recarregue o CRM e olhe para o canto superior direito do header. Você deve ver um destes:
| Pill | Significado |
|---|---|
Cloud (ponto verde) |
Container ativo, chat será roteado para ele |
Cloud · asleep (ponto cinza) |
Container pausado; a próxima mensagem de chat o desperta |
| (nada) | Nenhum container provisionado ainda |
O pill consulta o status do container a cada 30 segundos, então ele atrasa um evento de wake/pause em até meio minuto.
2. Log do worker
Se você tem acesso ao terminal do master (tmux attach -t citadel-child
para o bot dev do Arc OS), acompanhe a saída do worker e observe a linha de
spawn em cada mensagem.
3. Arquivos realmente dentro do container
Abra o terminal Cloud em /cloud, então:
cd /workspace/<your-project>
git log -3
ls -lh
Se edições disparadas por chat aparecem aqui (e arc cloud sync do seu
laptop sinaliza behind com a contagem certa de commits), o roteamento
está conectado corretamente.
Ciclo de vida: pause, wake, push, fetch
O ciclo de vida da Phase 69 continua valendo:
- Ocioso por 30 min → o cron pausa o container.
- Antes do pause →
snapshotAndPushfaz auto-commit das árvores sujas em todos os repos sob/workspace/e dá push para a org do bot. - Próxima mensagem de chat →
ensureAwakedespausa o container (~1 s), depois disparafetchAllem background para que o ponteiroorigin/mainde cada repo esteja atualizado. arc pull <project>no seu laptop lê esses auto-commits.
O custo de pause/wake está embutido na latência do próximo chat — você não precisa pensar nisso.
Continuidade de sessão
O Claude Code armazena o histórico da conversa em
~/.claude/projects/<cwd-hash>/<session-id>.jsonl. Dentro do container,
esse diretório fica num volume montado, então sobrevive a docker pause /
docker unpause. Enquanto seu chat continuar apontando para o mesmo
projeto, o --resume <session-id> do claude encontra o mesmo JSONL após o
wake e continua o thread.
Uma ressalva: o hash do cwd muda entre local ↔ cloud
O hash é derivado do caminho do diretório de trabalho. No master, o claude
roda em /opt/repos/<slug>; no container, ele roda em /workspace/<slug>.
Dois caminhos diferentes → dois hashes diferentes → dois históricos de
conversa diferentes.
O que isso significa na prática:
- Primeiro upgrade de Free/Starter para Cloud: seu histórico de chat anterior está no master e fica por lá. A primeira mensagem cloud inicia um novo thread de conversa.
- Usuário Cloud em regime permanente: toda sessão continua sem solavancos através de pause/wake. Nenhuma ação necessária.
- Se você voltar para o local: o histórico do container fica no container; novas mensagens são roteadas para o master começando do zero.
Consideramos criar um symlink /opt/repos → /workspace no master para
manter os hashes alinhados, mas isso misturava as duas árvores de arquivos
e quebrava os bots master que rodam em planos Free. Dois históricos é o
menor dos males.
Troubleshooting
"claude spawn: local" mas estou no plano Cloud
Verifique, nesta ordem:
subscription.plan === 'cloud'no pill de billing do user dropdown?/cloud/statusretornastatus: 'ready'ou'paused'?- Você criou o projeto DEPOIS de provisionar? Containers só clonam repos de projetos que existiam no momento do provisionamento (Phase 69.3). Reprovisione ou re-dispare o bootstrap para incluir projetos recém-criados.
Chat trava por ~5 segundos, depois responde
É o primeiro wake depois de uma ociosidade longa. O wake leva ~1 s; o restante é a inicialização do próprio claude + a primeira inferência. As mensagens seguintes enquanto o container fica quente têm latência normal.
"Not logged in" do claude
A Phase 70.5 corrigiu um bug em que ~/.bashrc fazia early-return para
shells não-interativos, então ANTHROPIC_API_KEY nunca chegava ao claude.
Correção: a chave agora vive em ~/.profile (carregado pelo wrapper de
login shell). Se você fez upgrade no meio da Phase 70 e nunca re-salvou
seu token, vá em /cloud Step 1, cole seu sk-ant-api03-… novamente.
Tokens recém-salvos vão para o arquivo certo.
Container diz pausado por horas depois de uma mensagem de chat
O polling de 30 segundos no pill do header é uma das causas. A outra: se
wakeContainer() falhou silenciosamente, a linha do DB lê ready mas o
Docker diz paused. Rode arc cloud sync <project> — ele vai expor a
divergência na matriz.
Veja também
- standard-cloud.md — provisionamento do container + terminal web
- cloud-repos.md — os 3 tipos de repo e como
/workspace/está organizado - arc-cli-reference.md —
arc push/arc pull/arc cloud sync docs/architecture/PHASE_69_CLOUD_REPO_MODEL.md— a especificação arquitetural do lado dos repos