Cloud Chat Routing — Guía
Phase 70. Cuando chateas con un worker desde el CRM o Telegram, esa conversación ahora corre dentro de tu contenedor Hetzner Cloud, no en el master server. Cierra tu laptop, cambia a Telegram, sigue trabajando — la sesión, los archivos y los repos abiertos viven todos en tu workspace Cloud.
TL;DR
- Usuarios del plan Cloud: cada mensaje de chat que tu worker maneja lanza
claude -pdentro de/workspace/<project>en tu contenedor. - Usuarios Free / Starter: sin cambios — el chat sigue corriendo en el master server.
- ¿Contenedor en pausa? El primer mensaje lo despierta (~1 segundo extra) y reanuda la conversación desde donde la dejaste.
- Tres nuevas superficies te indican que está funcionando: un pill
Clouden el header, una líneaclaude spawn: container/readyen el log del worker, y ediciones apareciendo en/workspace/<project>en lugar de/opt/repos/<project>.
Por qué esto importa
Phase 60 lanzó Standard Cloud como "caja Linux siempre activa con Claude Code".
Phase 69 conectó tus repos a la bot org. Pero hasta Phase 70, el flujo de chat
seguía lanzando claude -p en el master VPS — tu contenedor Cloud estaba
inactivo y tu trabajo de chat vivía en el master.
Phase 70 enruta el chat al contenedor para que la propuesta de valor realmente se cumpla:
- Cierra la laptop, continúa desde Telegram. Misma sesión, mismos archivos, mismo estado del repo. Sin "tuve que esperar hasta volver a mi laptop".
- Las ediciones aterrizan en el contenedor. Tus repos de la bot org bajo
/workspace/reciben los cambios; auto-commit en idle + push (Phase 69.3 + #349) los envía a GitHub;arc pullen tu laptop los sincroniza de vuelta. - Sin viaje de ida y vuelta por Cloudflare. El output del worker fluye contenedor ↔ master ↔ chat — sin saltos por la red pública en el loop interno.
Cómo funciona el enrutamiento
Cada mensaje de chat se enruta a través de este árbol de decisión:
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)
Esa decisión ocurre una vez por mensaje. El log del worker emite una sola línea diciéndote el target elegido:
claude spawn: container/ready
claude spawn: container/ready (woke from paused)
claude spawn: local
Si alguna vez te preguntas dónde corrió tu último mensaje, esa es la línea que tienes que buscar con grep.
Cómo verificar que está funcionando
1. Pill del header
Después de aprovisionar tu Cloud Workspace, Ajustes → recarga el CRM y mira al header arriba a la derecha. Deberías ver uno de:
| Pill | Significado |
|---|---|
Cloud (punto verde) |
Contenedor vivo, el chat se enrutará dentro de él |
Cloud · asleep (punto gris) |
Contenedor en pausa; el próximo mensaje de chat lo despierta |
| (nada) | Ningún contenedor aprovisionado aún |
El pill consulta el estado del contenedor cada 30 segundos, así que tarda hasta medio minuto en reflejar un evento de wake/pause.
2. Log del worker
Si tienes acceso a la terminal del master (tmux attach -t citadel-child
para el bot de dev de Arc OS), haz tail al output del worker y busca la línea de spawn
en cada mensaje.
3. Archivos realmente dentro del contenedor
Abre la terminal Cloud desde /cloud, luego:
cd /workspace/<your-project>
git log -3
ls -lh
Si las ediciones por chat aparecen aquí (y arc cloud sync desde tu laptop
marca behind con el conteo de commits correcto), el enrutamiento está bien cableado.
Ciclo de vida: pausa, wake, push, fetch
El ciclo de vida de Phase 69 sigue aplicando:
- Idle 30 min → cron pausa el contenedor.
- Antes de pausar →
snapshotAndPushhace auto-commit de los árboles sucios en cada repo bajo/workspace/y los empuja a la bot org. - Próximo mensaje de chat →
ensureAwakedespausa el contenedor (~1 s), luego disparafetchAllen segundo plano para que el punteroorigin/mainde cada repo esté fresco. arc pull <project>en tu laptop lee esos auto-commits.
El costo de pausa/wake se mete dentro de la latencia del próximo chat — no tienes que pensar en ello.
Continuidad de sesión
Claude Code guarda el historial de conversación en ~/.claude/projects/<cwd-hash>/<session-id>.jsonl.
Dentro del contenedor, ese directorio está en un volume mount, así que sobrevive a
docker pause / docker unpause. Mientras tu chat siga apuntando al
mismo proyecto, el --resume <session-id> de claude encuentra el mismo JSONL después del
wake y continúa el hilo.
Una advertencia: el hash del cwd cambia entre local ↔ cloud
El hash se deriva de la ruta del working directory. En el master, claude
corre en /opt/repos/<slug>; en el contenedor, corre en /workspace/<slug>.
Dos rutas distintas → dos hashes distintos → dos historiales de conversación distintos.
Lo que esto significa en la práctica:
- Primer upgrade de Free/Starter a Cloud: tu historial de chat previo está en el master y se queda ahí. El primer mensaje en cloud arranca un nuevo hilo de conversación.
- Usuario Cloud en estado estable: cada sesión continúa sin interrupciones a través de pausa/wake. Sin acción necesaria.
- Si bajas de vuelta a local: el historial dentro del contenedor se queda en el contenedor; los nuevos mensajes se enrutan al master arrancando desde cero.
Consideramos hacer symlink de /opt/repos → /workspace en el master para mantener
los hashes alineados, pero eso mezclaba los dos árboles de archivos y rompía los master
bots que corren en planes Free. Dos historiales es el mal menor.
Troubleshooting
"claude spawn: local" pero estoy en el plan Cloud
Verifica, en orden:
- ¿
subscription.plan === 'cloud'en el pill de billing del dropdown de usuario? - ¿
/cloud/statusdevuelvestatus: 'ready'o'paused'? - ¿Creaste el proyecto DESPUÉS de aprovisionar? Los contenedores solo clonan repos de proyectos que existían al momento de aprovisionar (Phase 69.3). Re-aprovisiona o re-dispara el bootstrap para incluir los proyectos recién creados.
El chat se cuelga por ~5 segundos, luego responde
Eso es el primer wake después de un idle largo. El wake es ~1 s; el resto es el propio startup de claude + la primera inferencia. Los mensajes siguientes mientras el contenedor sigue caliente tienen latencia normal.
"Not logged in" de claude
Phase 70.5 arregló un bug donde ~/.bashrc retornaba temprano para shells
no interactivas, así que ANTHROPIC_API_KEY nunca llegaba a claude. Fix: la key ahora vive
en ~/.profile (cargada por el wrapper del login shell). Si actualizaste a media
Phase 70 y nunca volviste a guardar tu token, ve a /cloud Paso 1, pega tu
sk-ant-api03-… de nuevo. Los tokens recién guardados aterrizan en el archivo correcto.
El contenedor dice paused por horas después de un mensaje de chat
El polling de 30 segundos del pill del header es una causa. La otra: si
wakeContainer() falló silenciosamente, la fila de la DB lee ready pero Docker dice
paused. Corre arc cloud sync <project> — surgirá la divergencia
en la matriz.
Ver también
- standard-cloud.md — aprovisionamiento del contenedor + terminal web
- cloud-repos.md — los 3 tipos de repo y cómo se organiza
/workspace/ - arc-cli-reference.md —
arc push/arc pull/arc cloud sync docs/architecture/PHASE_69_CLOUD_REPO_MODEL.md— la especificación arquitectónica del lado de los repos