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


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:


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:

  1. Idle 30 min → cron pausa el contenedor.
  2. Antes de pausarsnapshotAndPush hace auto-commit de los árboles sucios en cada repo bajo /workspace/ y los empuja a la bot org.
  3. Próximo mensaje de chatensureAwake despausa el contenedor (~1 s), luego dispara fetchAll en segundo plano para que el puntero origin/main de cada repo esté fresco.
  4. 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:

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:

  1. ¿subscription.plan === 'cloud' en el pill de billing del dropdown de usuario?
  2. ¿/cloud/status devuelve status: 'ready' o 'paused'?
  3. ¿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