Chief

Цех на сервере и три подписки в одном пуле

Ты спрашивал голосом про удалённый формат работы с агентами и про то, как я запихнул туда несколько аккаунтов Клода. Отвечаю по порядку, с кодом и граблями.

Если совсем коротко: аккаунт это не токен, а папка.
Переключение между подписками это kill плюс claude --resume под другой папкой.
Диалог при этом не теряется, потому что транскрипты лежат в общей.

Никакого облака. Одна коробка и tmux

Первое разочарование для тех, кто ждёт архитектуру: её почти нет. Это Hetzner, на нём tmux, в каждой панели живёт обычный Claude Code. Тот же бинарник, что у тебя в терминале, с теми же флагами.

Веб-морда это стекло: xterm.js поверх вебсокета плюс поток кадров с tmux capture-pane, чтобы можно было смотреть на десяток сессий сеткой одновременно. Ничего из логики агента в браузере не живёт. Закрыл вкладку, уехал в метро, открыл с телефона: сессия всё это время работала, потому что она никогда и не была во вкладке.

Из этого следует главное свойство, ради которого всё и затевалось. Агент не знает, что он «удалённый». У него настоящий шелл, настоящий git, настоящие права и настоящий деплой. Он не в песочнице с урезанным API, где половина задач упирается в «я не могу выполнить эту команду».

Что превращает «ssh плюс tmux» в цех

Воркtree на сессию. Это единственная причина, по которой можно запускать пять задач в одном репозитории и не собирать потом фарш. Ветку после смерти сессии удаляю только тогда, когда git доказал, что она влита. Обычный git branch -d тут врёт: он считает влитой ветку, которую просто запушили.

Статусы из хуков. Claude Code умеет дёргать скрипт на своих событиях. Хук стучится на localhost, и сервер знает, кто сейчас работает, кто задал вопрос и кто закончил. Плюс эвристика поверх панели, потому что часть состояний хуками не выражается: спиннер, живой вопрос-пикер, баннер про упёршийся лимит.

Пуш в телефон. Правило одно и оно про уважение к себе: пушить только вопросы и финиши. Никаких «я начал», никаких «всё идёт хорошо». Уведомление, на которое не надо реагировать, обесценивает те, на которые надо.

Аккаунт это папка

У Claude Code нет параметра «аккаунт». Он читает конфиг и креды из одной директории. Значит, чтобы получить второй аккаунт, надо дать ему вторую директорию. Всё остальное следствия.

# запуск сессии под конкретной подпиской: это весь секрет
CLAUDE_CONFIG_DIR=/root/.claude-accounts/liveceo IS_SANDBOX=1 \
  claude --dangerously-skip-permissions

Именно так Chief собирает командную строку перед тем, как отправить её в панель tmux. Это путь, а не секрет, поэтому его не страшно оставить в скроллбеке.

/root/.claude-accounts/
├── personal/
│   ├── .credentials.json      свой логин
│   ├── .claude.json           свой конфиг и oauthAccount
│   ├── settings.json    /root/.claude/settings.json
│   ├── hooks/           /root/.claude/hooks/
│   └── projects/        /root/.claude/projects/   ← транскрипты общие
├── liveceo/   то же самое, свой логин
└── client/    то же самое, свой логин

Своего в папке ровно два файла. Всё остальное симлинки в общий /root/.claude. Это выглядит как мелочь по уборке, но на этом держится вся пересадка сессий: раз projects/ общая, то диалог, начатый под одним аккаунтом, целиком виден процессу claude --resume, поднятому под другим. Я не переношу историю, её просто некуда переносить.

Ломает биллинг не файл, а окружение

Это стоило мне отдельного вечера. CLAUDE_CODE_OAUTH_TOKEN и ANTHROPIC_API_KEY в окружении процесса перебивают credentials.json и молча переводят сессию на API-тарификацию. Не ошибка, не предупреждение: просто в баннере вместо Max появляется Claude API, а деньги начинают течь мимо подписки.

# /usr/local/bin/claude, стоит в PATH раньше настоящего бинарника
exec env -u ANTHROPIC_API_KEY -u CLAUDE_CODE_OAUTH_TOKEN \
  /usr/bin/claude "$@"

Шим нужен потому, что на коробке эти переменные легально живут для других инструментов. Он снимает их только у claude. CLAUDE_CONFIG_DIR проходит насквозь.

Логин делается там, где аккаунт будет работать

Скрипт готовит папку заведомо без кред, поэтому claude в ней сразу падает в экран логина. Дальше вручную: /login, выбрать вход по подписке, забрать из панели ссылку на авторизацию, открыть её в браузере, залогиненном нужным аккаунтом, и вставить обратно строку code#state. Полминуты работы, зато у каждой папки собственная линия обновления токена.

Соблазн скопировать креды с ноутбука есть у всех, и он наказуем. Про это в граблях ниже.

Как оно переключается

Тут три отдельных вопроса, и их полезно не смешивать: кто платит за новый запуск, что делать с живой сессией, которая упёрлась в лимит, и что делать, если упёрлись все.

Сначала датчик, потом роутер

Выбирать вслепую нельзя, значит нужен остаток топлива. Публичного API для этого нет, поэтому Chief читает ровно те же цифры, что читаешь ты. Раз в 15 минут он поднимает одноразовую панель tmux, запускает в ней настоящий CLI под нужным CLAUDE_CONFIG_DIR, отправляет /usage, разбирает экран на проценты по окнам и время сброса, и убивает панель.

Метод некрасивый, зато честный: цифра ровно та, которую увидел бы человек. На разборе экрана держатся тесты с настоящими снимками, потому что вёрстка этого экрана однажды поедет.

// то, ради чего всё: единственное место, где решается, чья подписка платит
function candidatesFor(routing) {
  return routing === AUTO
    ? state.accounts.filter((a) => a.poolable)          // только мои
    : state.accounts.filter((a) => a.client === routing); // только его
}

Флаг poolable по умолчанию выключен. Новая подписка не может начать молча оплачивать чужую работу просто потому, что её зарегистрировали.

Кто платит за старт

У проекта есть маршрут: либо конкретный клиент, либо AUTO. Маршрут даёт список кандидатов, из кандидатов выбрасываются остывающие и сломанные, оставшиеся сортируются по худшему из связывающих окон, пятичасового и недельного. Побеждает самый свободный.

Одно исключение: при возобновлении сессия липнет к своему аккаунту, пока он здоров. Пара процентов разницы не стоит того, чтобы дёргать живой контекст. И ещё одно: если я выбрал аккаунт руками, это пин, а не пожелание. Роутер не имеет права заменить его на более здоровый.

Забор между клиентами проходит именно через функцию кандидатов, и это сделано специально. Ротация, восстановление и ручной перенос ходят через неё же, поэтому подписка внешнего клиента не может утечь на мой проект, даже когда у неё самый жирный остаток.

Живая сессия упёрлась в лимит

  1. Панель Каждые 2.5 секунды сервер снимает хвост панели и ищет баннер лимита. Регулярка привязана к нулевой колонке: Клод, который цитирует этот баннер в своём же ответе, отбит отступом и не считается.
  2. Пометка Аккаунт уходит в остывание с временем сброса из баннера, и тут же перепроверяется настоящим /usage. Если баннер прочитан криво, проба снимет пометку через полминуты, а не через пять часов.
  3. Решение Пересадка возможна, если есть за что зацепиться для resume, за баннером живой процесс, в маршруте есть здоровый сосед и последние 10 минут пересадок не было. Последнее это тормоз против петли.
  4. Пересадка Убить панель, поднять claude --resume под другим CLAUDE_CONFIG_DIR. Именно resume, а не новая сессия: транскрипты общие, поэтому новый процесс подхватывает тот же самый диалог со всей историей.
  5. Дожим Если лимит разрезал ход посередине, сервер ждёт прорисовки интерфейса и сам пишет «continue». Если лимит поймали между ходами, не пишет ничего: законченная сессия не должна сама придумывать себе новую работу.

Пуша в телефон на этом пути нет. Сработавший фейловер это не событие для владельца: сессия не останавливалась, она просто сменила плательщика. Рассказывать надо про фейловер, который не сработал.

Если пересаживать некуда

Сессия паркуется: останавливается, показывает отсчёт до сброса и уходит в телефон уведомлением. Каждые 30 секунд сервер проверяет, не появился ли остаток. Тонкость, на которой я один раз обжёгся: живой процесс намертво привязан к своей папке. Разбудить парковку на месте можно, только если восстановился именно его аккаунт. Если свободен другой, это снова kill и resume, а не «снять флажок».

Потрогать
Три аккаунта, одна живая сессия проекта MediaMagicPlatform. Нажимай и смотри, куда её переставит и почему.
personalbohdan.pytaichuk@gmail.com
Max 20x · в пуле 41%
liveceoadmin@liveceo.ai
Max 20x · в пуле 12%
clientподписка внешнего клиента
Team · ⦸ вне пула 63%
парковка
сюда сессия попадает, когда в её маршруте не осталось ни одного живого аккаунта
сессия #a3f1

    Руками и на уровень выше

    На карточке каждой сессии висит чип с текущим аккаунтом, за ним шторка «переехать на». Это тот же примитив, что и автоматическая ротация, и тот же забор кандидатов: руками тоже нельзя пересесть на подписку другого клиента.

    И последнее, что технически тоже переключение, хотя выглядит иначе. Если лучший аккаунт в маршруте уже за 80 процентами, новые задачи стартуют не на Клоде вообще, а на codex или grok, которые сидят на своих подписках. Живую сессию так не переносят: разговор не переезжает между разными CLI, там нет общего транскрипта. Это решение только про то, где начать.

    Грабли, на которые я уже наступил

    Ни одна из них не выглядит проблемой, пока не наступишь. Четыре из пяти стоили мне от вечера до суток.

    Токен в окружении ломает биллинг сразу всем

    Однажды CLAUDE_CODE_OAUTH_TOKEN оказался в общем settings.json. Переменные из настроек применяет сам claude при старте, поэтому шим тут бессилен: они перебивают креды каждой папки, все сессии схлопываются на один токен и уезжают на API. Признак простой и заметный: датчики /usage отваливаются у всех аккаунтов одновременно.

    Не копируй креды между машинами

    У логина одна линия обновления токена. Когда её крутят с двух машин, она рано или поздно рвётся. Единственный аккаунт, который у меня умирал дважды, это ровно тот, чьи креды я скопировал с ноутбука. У остальных был собственный вход на коробке, и они живы.

    Логин истекает по окну, а не по токену

    У свежего входа окно порядка месяца, и оно не едет вперёд бесконечно. CLI заранее пишет в баннер предупреждение про обновление. У меня регулярка «это экран логина» ловила именно это предупреждение и отправляла на скамейку два совершенно живых аккаунта. Проверяется одним запросом профиля по токену.

    Флаг здоровья должен сниматься тем же сигналом, что поставил

    Статус «сломан» у меня был односторонней защёлкой. Креды чинишь, проба проходит, текст ошибки обнуляется, а аккаунт всё равно считается непригодным, потому что снимать флаг было некому. Любой флаг, который ставит автоматика, обязан уметь сняться автоматикой.

    Все дороги ведут в одну и ту же операцию

    Ротация по лимиту, подъём с парковки, ручной перенос и обычное возобновление это буквально одно действие: убить и поднять с resume. Пока их четыре штуки и мьютекса на сессию нет, они однажды встретятся в одной точке, и ты получишь два процесса на один воркtree.

    Если тебе сейчас брать второй аккаунт

    Порядок важен: половина боли ниже возникает только тогда, когда шаги сделаны в другом порядке.

    1. Вторая папка и симлинки на общее. До всякого роутера и до всякой логики. Своё только креды и конфиг, остальное общее, особенно транскрипты.
    2. Логин на той машине, где аккаунт будет работать. Не копией с ноутбука. Это тридцать секунд один раз против отладки мёртвых токенов раз в две недели.
    3. Проверь баннер сразу после входа. Должно быть Max или Team. Если Claude API, ищи токен в окружении, в systemd и в общих настройках.
    4. Сначала датчик, потом переключатель. Пока не видишь остатки по каждому аккаунту, переключать нечего: любой выбор это подбрасывание монеты.
    5. Одна функция выбора кандидатов на весь код. Если мест, где решается «на чьи деньги», больше одного, однажды они разойдутся.
    6. Переключай через resume, а не новой сессией. Иначе каждое переключение это потеря контекста, и лучше уж подождать сброса окна.
    7. Реши заранее, чья подписка платит за какой проект. Это не оптимизация и не фича, это правило. Задним числом его добавить почти невозможно.