Cloud Chat Routing — Guide

Phase 70. Quand tu discutes avec un worker depuis le CRM ou Telegram, cette conversation tourne désormais à l'intérieur de ton conteneur Hetzner Cloud, et non plus sur le serveur master. Ferme ton laptop, passe sur Telegram, continue à travailler — la session, les fichiers et les repos ouverts vivent tous dans ton workspace Cloud.


TL;DR


Pourquoi c'est important

Phase 60 a livré Standard Cloud sous forme de « box Linux always-on avec Claude Code ». Phase 69 a câblé tes repos dans la bot org. Mais jusqu'à Phase 70, le workflow de chat spawnait toujours claude -p sur le VPS master — ton conteneur Cloud restait inactif et ton travail de chat vivait sur le master.

Phase 70 route le chat dans le conteneur pour que la promesse de valeur soit réellement tenue :


Comment le routing fonctionne

Chaque message de chat passe par cet arbre de décision :

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)

Cette décision unique est prise une fois par message. Le log du worker émet une seule ligne qui t'indique la cible choisie :

claude spawn: container/ready
claude spawn: container/ready (woke from paused)
claude spawn: local

Si tu te demandes un jour où ton dernier message s'est exécuté, c'est la ligne à grep.


Comment vérifier que ça marche

1. La pill dans le header

Après avoir provisionné ton Cloud Workspace, Paramètres → recharge le CRM et regarde le header en haut à droite. Tu devrais voir l'un des éléments suivants :

Pill Signification
Cloud (point vert) Conteneur actif, le chat va y être routé
Cloud · asleep (point gris) Conteneur en pause ; le prochain message de chat le réveille
(rien) Aucun conteneur provisionné pour l'instant

La pill poll le statut du conteneur toutes les 30 secondes, donc elle retarde jusqu'à une demi-minute un événement de wake/pause.

2. Le log du worker

Si tu as un accès terminal au master (tmux attach -t citadel-child pour le bot dev Arc OS), tail le output du worker et regarde la ligne de spawn à chaque message.

3. Les fichiers réellement dans le conteneur

Ouvre le terminal Cloud depuis /cloud, puis :

cd /workspace/<your-project>
git log -3
ls -lh

Si les modifications pilotées par le chat apparaissent ici (et que arc cloud sync depuis ton laptop signale behind avec le bon nombre de commits), le routing est correctement câblé.


Cycle de vie : pause, wake, push, fetch

Le cycle de vie de Phase 69 s'applique toujours :

  1. Idle 30 min → le cron met le conteneur en pause.
  2. Avant la pausesnapshotAndPush auto-commit les arbres dirty dans chaque repo sous /workspace/ et les push vers la bot org.
  3. Prochain message de chatensureAwake réveille le conteneur (~1 s), puis déclenche fetchAll en arrière-plan pour que le pointeur origin/main de chaque repo soit à jour.
  4. arc pull <project> sur ton laptop lit ces auto-commits.

Le coût pause/wake est inclus dans la latence du prochain chat — tu n'as pas à y penser.


Continuité de session

Claude Code stocke l'historique de conversation dans ~/.claude/projects/<cwd-hash>/<session-id>.jsonl. À l'intérieur du conteneur, ce répertoire est sur un volume mount, donc il survit à docker pause / docker unpause. Tant que ton chat continue de cibler le même projet, le --resume <session-id> de claude retrouve le même JSONL après le wake et reprend le thread.

Un piège : le hash du cwd change entre local ↔ cloud

Le hash est dérivé du chemin du working directory. Sur le master, claude tourne dans /opt/repos/<slug> ; dans le conteneur, il tourne dans /workspace/<slug>. Deux chemins différents → deux hashes différents → deux historiques de conversation différents.

Concrètement, ça veut dire :

On a envisagé de symlinker /opt/repos/workspace sur le master pour aligner les hashes, mais ça mélangeait les deux arborescences de fichiers et cassait les bots master qui tournent sur les plans Free. Deux historiques, c'est le moindre mal.


Troubleshooting

« claude spawn: local » alors que je suis sur le plan Cloud

Vérifie, dans l'ordre :

  1. subscription.plan === 'cloud' dans la pill billing du user dropdown ?
  2. /cloud/status retourne status: 'ready' ou 'paused' ?
  3. As-tu créé le projet APRÈS le provisioning ? Les conteneurs ne clonent que les repos de projet qui existaient au moment du provisioning (Phase 69.3). Re-provisionne ou re-déclenche le bootstrap pour récupérer les projets nouvellement créés.

Le chat freeze pendant ~5 secondes, puis répond

C'est le premier wake après une longue période d'inactivité. Le wake fait ~1 s ; le reste, c'est le démarrage de claude lui-même + la première inférence. Les messages suivants tant que le conteneur reste warm sont à une latence normale.

« Not logged in » de la part de claude

Phase 70.5 a corrigé un bug où ~/.bashrc faisait un early-return pour les shells non-interactifs, donc ANTHROPIC_API_KEY n'atteignait jamais claude. Fix : la clé vit désormais dans ~/.profile (sourcé par le wrapper du login shell). Si tu as upgradé en milieu de Phase 70 et que tu n'as jamais re-enregistré ton token, va dans /cloud Step 1, recolle ton sk-ant-api03-…. Les tokens nouvellement enregistrés atterrissent dans le bon fichier.

Le conteneur reste paused pendant des heures après un message de chat

Le polling de 30 secondes de la pill du header est une cause. L'autre : si wakeContainer() a échoué silencieusement, la ligne en DB indique ready mais Docker dit paused. Lance arc cloud sync <project> — il va faire remonter la divergence dans la matrice.


Voir aussi