Cloud Chat Routing — Гайд

Phase 70. Когда ты общаешься с воркером из CRM или Telegram, этот разговор теперь выполняется внутри твоего Hetzner Cloud-контейнера, а не на мастер-сервере. Закрой ноутбук, переключись в Telegram, продолжай работу — сессия, файлы и открытые репозитории живут в твоём Cloud-воркспейсе.


TL;DR


Зачем это нужно

Phase 60 запустил Standard Cloud как «всегда включённую Linux-машину с Claude Code». Phase 69 подключил твои репозитории к bot org. Но до Phase 70 рабочий процесс чата всё ещё спавнил claude -p на мастер-VPS — твой Cloud-контейнер простаивал, а твоя работа в чате жила на мастере.

Phase 70 направляет чат внутрь контейнера, чтобы value prop реально работало:


Как работает роутинг

Каждое сообщение в чате проходит через это дерево решений:

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 всё ещё применим:

  1. Idle 30 мин → cron ставит контейнер на паузу.
  2. Перед паузойsnapshotAndPush авто-коммитит грязные деревья в каждом репозитории под /workspace/ и пушит их в bot org.
  3. Следующее сообщение в чатеensureAwake снимает контейнер с паузы (~1 с), затем запускает fetchAll в фоне, чтобы указатель origin/main каждого репозитория был свежим.
  4. 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>. Два разных пути → два разных хеша → две разные истории разговоров.

Что это значит на практике:

Мы рассматривали симлинк /opt/repos/workspace на мастере, чтобы хеши оставались согласованными, но это смешивало два файловых дерева и ломало master-ботов, работающих на Free-плане. Две истории — меньшее зло.


Траблшутинг

«claude spawn: local», но я на Cloud-плане

Проверь по порядку:

  1. subscription.plan === 'cloud' в billing-пилюле user dropdown?
  2. /cloud/status возвращает status: 'ready' или 'paused'?
  3. Создал ли ты проект ПОСЛЕ провизионинга? Контейнеры клонируют только проектные репозитории, существовавшие на момент провизионинга (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> — он покажет расхождение в матрице.


Смотри также