Cloud Chat Routing — Гайд
Phase 70. Когда ты общаешься с воркером из CRM или Telegram, этот разговор теперь выполняется внутри твоего Hetzner Cloud-контейнера, а не на мастер-сервере. Закрой ноутбук, переключись в Telegram, продолжай работу — сессия, файлы и открытые репозитории живут в твоём Cloud-воркспейсе.
TL;DR
- Пользователи на Cloud-плане: каждое сообщение в чате, которое обрабатывает воркер, спавнит
claude -pвнутри/workspace/<project>в твоём контейнере. - Пользователи на Free / Starter: без изменений — чат по-прежнему работает на мастер-сервере.
- Контейнер на паузе? Первое сообщение разбудит его (~1 секунда сверху) и возобновит разговор с того места, где ты остановился.
- Три новых индикатора показывают, что всё работает: пилюля
Cloudв шапке, строкаclaude spawn: container/readyв логе воркера и правки, появляющиеся в/workspace/<project>, а не в/opt/repos/<project>.
Зачем это нужно
Phase 60 запустил Standard Cloud как «всегда включённую Linux-машину с Claude Code».
Phase 69 подключил твои репозитории к bot org. Но до Phase 70 рабочий процесс
чата всё ещё спавнил claude -p на мастер-VPS — твой Cloud-контейнер
простаивал, а твоя работа в чате жила на мастере.
Phase 70 направляет чат внутрь контейнера, чтобы value prop реально работало:
- Закрыл ноутбук — продолжил с Telegram. Та же сессия, те же файлы, то же состояние репозитория. Никаких «придётся подождать, пока вернусь к ноутбуку».
- Правки попадают в контейнер. Твои репозитории bot org под
/workspace/получают изменения; auto-commit на idle + push (Phase 69.3 + #349) отправляют их на GitHub;arc pullна твоём ноутбуке синхронизирует их вниз. - Никакого round-trip через Cloudflare. Вывод воркера стримится контейнер ↔ мастер ↔ чат — никаких прыжков через публичную сеть во внутреннем цикле.
Как работает роутинг
Каждое сообщение в чате проходит через это дерево решений:
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)
Это единственное решение принимается один раз на сообщение. Лог воркера выдаёт одну строку с указанием выбранного таргета:
claude spawn: container/ready
claude spawn: container/ready (woke from paused)
claude spawn: local
Если когда-нибудь захочешь узнать, где запустилось твоё последнее сообщение — грепай именно эту строку.
Как проверить, что всё работает
1. Пилюля в шапке
После провизионинга Cloud Workspace зайди в настройки → перезагрузи CRM и посмотри на верхний правый угол шапки. Ты должен увидеть одно из:
| Пилюля | Что означает |
|---|---|
Cloud (зелёная точка) |
Контейнер живой, чат будет уходить в него |
Cloud · asleep (серая точка) |
Контейнер на паузе; следующее сообщение в чате разбудит его |
| (ничего) | Контейнер ещё не провижионен |
Пилюля пуллит статус контейнера каждые 30 секунд, поэтому отстаёт от события wake/pause максимум на полминуты.
2. Лог воркера
Если у тебя есть терминальный доступ к мастеру (tmux attach -t citadel-child
для dev-бота Arc OS), стримь вывод воркера и смотри на строку спавна
при каждом сообщении.
3. Файлы реально внутри контейнера
Открой Cloud-терминал из /cloud, потом:
cd /workspace/<your-project>
git log -3
ls -lh
Если правки из чата появляются здесь (а arc cloud sync с твоего ноутбука
помечает behind с правильным количеством коммитов) — роутинг подключён корректно.
Lifecycle: pause, wake, push, fetch
Lifecycle из Phase 69 всё ещё применим:
- Idle 30 мин → cron ставит контейнер на паузу.
- Перед паузой →
snapshotAndPushавто-коммитит грязные деревья в каждом репозитории под/workspace/и пушит их в bot org. - Следующее сообщение в чате →
ensureAwakeснимает контейнер с паузы (~1 с), затем запускаетfetchAllв фоне, чтобы указательorigin/mainкаждого репозитория был свежим. arc pull <project>на твоём ноутбуке вычитывает эти авто-коммиты.
Стоимость pause/wake встроена в латенси следующего сообщения — об этом думать не нужно.
Непрерывность сессии
Claude Code хранит историю разговора в ~/.claude/projects/<cwd-hash>/<session-id>.jsonl.
Внутри контейнера этот каталог лежит на volume mount, так что он переживает
docker pause / docker unpause. Пока твой чат продолжает таргетить
тот же проект, --resume <session-id> от claude находит тот же JSONL после
wake и продолжает тред.
Один нюанс: cwd hash меняется на стыке локали ↔ облако
Хеш выводится из пути рабочего каталога. На мастере claude
запускается в /opt/repos/<slug>; в контейнере — в /workspace/<slug>.
Два разных пути → два разных хеша → две разные истории
разговоров.
Что это значит на практике:
- Первый апгрейд с Free/Starter на Cloud: твоя предыдущая история чата на мастере и там и останется. Первое cloud-сообщение начнёт новый разговорный тред.
- Установившийся Cloud-пользователь: каждая сессия бесшовно продолжается через pause/wake. Действий не требуется.
- Если ты даунгрейднешься обратно на локаль: in-container история остаётся в контейнере; новые сообщения уходят на мастер с нуля.
Мы рассматривали симлинк /opt/repos → /workspace на мастере, чтобы хеши
оставались согласованными, но это смешивало два файловых дерева и ломало master-ботов,
работающих на Free-плане. Две истории — меньшее зло.
Траблшутинг
«claude spawn: local», но я на Cloud-плане
Проверь по порядку:
subscription.plan === 'cloud'в billing-пилюле user dropdown?/cloud/statusвозвращаетstatus: 'ready'или'paused'?- Создал ли ты проект ПОСЛЕ провизионинга? Контейнеры клонируют только проектные репозитории, существовавшие на момент провизионинга (Phase 69.3). Перепровижени контейнер или перезапусти bootstrap, чтобы подхватить новосозданные проекты.
Чат висит ~5 секунд, потом отвечает
Это первое пробуждение после долгого простоя. Wake — ~1 с; остальное — собственный startup claude + первый инференс. Последующие сообщения, пока контейнер тёплый — нормальное латенси.
«Not logged in» от claude
Phase 70.5 пофиксил баг, где ~/.bashrc early-return'ил для неинтерактивных
шеллов, поэтому ANTHROPIC_API_KEY никогда не доходил до claude. Фикс: ключ теперь живёт
в ~/.profile (источится login-shell wrapper'ом). Если ты апгрейдился посреди
Phase 70 и так и не пересохранил токен, иди в /cloud Step 1, вставь свой
sk-ant-api03-… заново. Свежесохранённые токены приземляются в правильный файл.
Контейнер показывает paused часами после сообщения в чате
Одна из причин — 30-секундный поллинг пилюли в шапке. Другая: если
wakeContainer() молча упал, строка БД читается как ready, но Docker говорит
paused. Запусти arc cloud sync <project> — он покажет расхождение
в матрице.
Смотри также
- standard-cloud.md — провизионинг контейнера + web-терминал
- cloud-repos.md — 3 типа репозиториев и как устроен
/workspace/ - arc-cli-reference.md —
arc push/arc pull/arc cloud sync docs/architecture/PHASE_69_CLOUD_REPO_MODEL.md— архитектурный спек со стороны репозиториев