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
- Utilisateurs du plan Cloud : chaque message de chat traité par ton worker spawn
claude -pà l'intérieur de/workspace/<project>dans ton conteneur. - Utilisateurs Free / Starter : inchangé — le chat tourne toujours sur le serveur master.
- Conteneur en pause ? Le premier message le réveille (~1 seconde supplémentaire) et reprend la conversation là où tu l'avais laissée.
- Trois nouvelles surfaces te disent que ça marche : une pill
Clouddans le header, une ligneclaude spawn: container/readydans le log du worker, et les modifications qui apparaissent dans/workspace/<project>plutôt que dans/opt/repos/<project>.
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 :
- Ferme le laptop, continue depuis Telegram. Même session, mêmes fichiers, même état du repo. Plus de « il a fallu attendre d'être revenu sur mon laptop ».
- Les modifications atterrissent dans le conteneur. Tes repos de la bot org sous
/workspace/reçoivent les changements ; auto-commit à l'idle + push (Phase 69.3 + #349) les envoient vers GitHub ;arc pullsur ton laptop les synchronise en local. - Pas d'aller-retour Cloudflare. Le output du worker stream entre conteneur ↔ master ↔ chat — pas de saut réseau public dans la boucle interne.
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 :
- Idle 30 min → le cron met le conteneur en pause.
- Avant la pause →
snapshotAndPushauto-commit les arbres dirty dans chaque repo sous/workspace/et les push vers la bot org. - Prochain message de chat →
ensureAwakeréveille le conteneur (~1 s), puis déclenchefetchAllen arrière-plan pour que le pointeurorigin/mainde chaque repo soit à jour. 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 :
- Premier upgrade de Free/Starter vers Cloud : ton historique de chat précédent est sur le master et y reste. Le premier message cloud démarre un nouveau thread de conversation.
- Utilisateur Cloud en régime de croisière : chaque session continue de façon transparente à travers pause/wake. Aucune action nécessaire.
- Si tu redescends vers local : l'historique dans le conteneur reste dans le conteneur ; les nouveaux messages routent vers le master en repartant de zéro.
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 :
subscription.plan === 'cloud'dans la pill billing du user dropdown ?/cloud/statusretournestatus: 'ready'ou'paused'?- 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
- standard-cloud.md — provisioning du conteneur + terminal web
- cloud-repos.md — les 3 types de repos et comment
/workspace/est organisé - arc-cli-reference.md —
arc push/arc pull/arc cloud sync docs/architecture/PHASE_69_CLOUD_REPO_MODEL.md— la spec architecturale côté repo