Dexus: KCP One Ply ONNX-бот — установка и эксплуатация

Summary

Состояние на 14.08.2026: на dexus круглосуточно запущена отдельная копия KCP One Ply под Play-идентичностью dexus/kcp-one-ply-1. Две независимые JVM-реплики получают по одному CPU и 640 MiB, а nginx least_conn распределяет между ними одновременные webhook-запросы. Play разрешает две партии: лестница занимает максимум одну, второй слот зарезервирован человеку. Бот опубликован через существующий Cloudflare Tunnel aurora и находится на рейтинговой лестнице и в human catalog.

Первые 53 webhook-доставки все завершились applied, без отказов и сетевых ошибок. Память контейнера выросла с ~91 MiB в покое до 117–147 MiB под игрой. Одно ядро во время расчёта загружено на 95–98%. Ранняя статистика задержки — p50 2 s, p90 30 s, p99 120 s — показывает, что запас памяти велик, а главный риск остаётся в очень длинном хвосте времени поиска.

Связанные заметки:


1. Зачем понадобилась отдельная копия

На Northflank уже работает northflank/kcp-one-ply-1 с тем же алгоритмом, но только на 0,2 shared vCPU. Он помещается в 512 MiB, однако процессорная доля слишком мала для тяжёлых позиций. Копия на Dexus нужна как отдельный эксперимент:

  1. дать модели целое x86-ядро;
  2. сравнить хвост задержки и качество игры с Northflank;
  3. не смешивать рейтинговую историю двух существенно разных вычислительных сред;
  4. проверить будущий режим «играть только во время ночного окна бэкапов».

Play-идентичность поэтому отдельная: dexus/kcp-one-ply-1. Суффикс -1 фиксирует первую версию алгоритма и модели. Если поменяются веса, признаки или существенная часть поиска, нужна новая идентичность -2, а не продолжение старого рейтинга.

2. Что реально развёрнуто

flowchart LR
    P["Play ladder\nplay.jc.id.lv"] -->|"signed HTTPS POST\n/api/webhook"| C["Cloudflare edge\ndexus-kcp.jc.id.lv"]
    C -->|"Aurora tunnel"| D["Dexus LAN origin\n192.168.10.4:8094"]
    D --> N["nginx least_conn\n64 MiB"]
    N --> A["JVM replica A\n1 CPU / 640 MiB"]
    N --> B["JVM replica B\n1 CPU / 640 MiB"]
    I["Private GAR image\nmodel + 147-row book"] -->|"one-time LAN transfer"| A
    I --> B
ПараметрЗначение
Хостdexus, Ubuntu 24.04, linux/amd64
CPUIntel i5-2500, 4 ядра / 4 потока
RAMоколо 3,3 GiB доступно ОС
Каталог деплоя/home/jegors/dicechess-kcp-one-ply
Compose projectdicechess-dexus-kcp
Compose serviceskcp-one-ply-a, kcp-one-ply-b, webhook-proxy
Container namesdexus-kcp-one-ply-a, dexus-kcp-one-ply-b, dexus-kcp-one-ply-proxy
Локальный образdicechess-bot-onnx-private:edccf9d
Host port8094 → nginx 8080 → JVM replicas 8080
Public URLhttps://dexus-kcp.jc.id.lv/api/webhook
Cloudflare tunnelсуществующий dashboard-managed aurora
Play identitydexus/kcp-one-ply-1
CapacitymaxConcurrentGames=2, ladderAllowance=1
Humans catalogopenToHumans=true; один слот зарезервирован direct game
Restart policyunless-stopped

На Dexus также работают backup-контейнеры bbs и pbs. Старый hunter-all остановлен и ресурсы одновременно с новым ботом не потребляет.

3. Граница публичного кода и приватных артефактов

Код бота остаётся в публичном rabestro/dicechess-bot-gcp-onnx. Там лежат synthetic-модель и пятистрочный пример книги, достаточные для сборки, тестов и понимания формата.

Production-артефакты публично не публикуются:

  • kcp_nodice_serving_20260809.onnx;
  • полная opening_book.tsv на 147 записей.

Они находятся внутри приватного образа в Google Artifact Registry. На Dexus образ получил только локальный тег; ни Google service-account key, ни постоянное право читать GAR на сервер не копировались.

Исходный образ при развёртывании:

europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/dicechess-bot-onnx:edccf9d

Проверенный image ID:

sha256:7458a22d5b18ef993c0a76f52d393798a879a021a9a112295111fd3f7351ebfc

Important

Git SHA в теге недостаточно считать доказательством содержимого. Для точного воспроизведения сверять digest/Image ID и вывод старта с именами модели и книги.

4. Доставка приватного образа без registry-ключа на Dexus

Образ сначала был получен на авторизованной рабочей машине, затем передан по доверенной локальной сети непосредственно в Docker Dexus:

source_image="europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/dicechess-bot-onnx:edccf9d"
 
docker pull --platform linux/amd64 "$source_image"
docker save "$source_image" | ssh dexus docker load
ssh dexus docker tag "$source_image" dicechess-bot-onnx-private:edccf9d

Проверка без раскрытия содержимого модели:

ssh dexus 'docker image inspect dicechess-bot-onnx-private:edccf9d \
  --format "{{.Id}} {{.Architecture}}/{{.Os}}"'

Плюсы такого способа:

  • Dexus не хранит GAR credential;
  • секрет реестра не попадает в compose и shell history сервера;
  • приватные файлы остаются только внутри локального Docker image store;
  • образ можно восстановить с авторизованной машины тем же потоком.

Минус: обновление не происходит через обычный docker compose pull; новый образ нужно передать вручную и явно поменять тег в compose.

5. Docker Compose

Рабочий /home/jegors/dicechess-kcp-one-ply/compose.yaml:

name: dicechess-dexus-kcp
 
x-bot-common: &bot-common
  image: dicechess-bot-onnx-private:edccf9d
  restart: unless-stopped
  init: true
  cpus: "1.0"
  mem_limit: "640m"
  memswap_limit: "640m"
  pids_limit: 256
  environment:
    PORT: "8080"
    MODEL_PATH: "/models/kcp_nodice_serving_20260809.onnx"
    OPENING_BOOK_PATH: "/models/opening_book.tsv"
    ORACLE_FEATURES: "kcp"
    SEARCH_MODE: "one-ply"
    TIME_POLICY: "empirical-v1"
    OVERHEAD_BUFFER_MS: "1000"
    DEFAULT_THINK_MS: "2000"
    JAVA_TOOL_OPTIONS: "-Xmx320m -XX:ActiveProcessorCount=1 -XX:+ExitOnOutOfMemoryError"
    DICECHESS_WEBHOOK_SECRET: "${DICECHESS_WEBHOOK_SECRET:-}"
  expose:
    - "8080"
  healthcheck:
    test: ["CMD-SHELL", "bash -c '</dev/tcp/127.0.0.1/8080'"]
    interval: 10s
    timeout: 5s
    retries: 6
    start_period: 20s
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"
 
services:
  kcp-one-ply-a:
    <<: *bot-common
    container_name: dexus-kcp-one-ply-a
 
  kcp-one-ply-b:
    <<: *bot-common
    container_name: dexus-kcp-one-ply-b
 
  webhook-proxy:
    image: nginx:alpine
    container_name: dexus-kcp-one-ply-proxy
    restart: unless-stopped
    init: true
    cpus: "0.1"
    mem_limit: "64m"
    memswap_limit: "64m"
    pids_limit: 64
    depends_on:
      kcp-one-ply-a:
        condition: service_healthy
      kcp-one-ply-b:
        condition: service_healthy
    volumes:
      - "./nginx.conf:/etc/nginx/conf.d/default.conf:ro"
    ports:
      - "8094:8080"
    healthcheck:
      test: ["CMD-SHELL", "nc -z 127.0.0.1 8080"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 5s
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

nginx.conf использует upstream least_conn, две внутренние цели kcp-one-ply-a:8080 / kcp-one-ply-b:8080, отключённый response buffering и proxy_read_timeout 125s — чуть больше серверного webhook-window Play.

Почему две одноядерные JVM, а не одна двухъядерная

Strategy.chooseMoves в боте объявлен synchronized: одна ONNX-сессия намеренно обслуживает только один поиск за раз. Если дать одному процессу 2 CPU и посадить его в две партии, второй webhook будет ждать monitor lock и тратить часы. Два отдельных процесса имеют две независимые Strategy и ONNX-сессии; nginx least_conn отправляет перекрывающиеся запросы свободной реплике.

Почему именно такие лимиты

  • cpus: "1.0" — жёсткая суммарная квота одного CPU. Это не привязка к конкретному ядру: Linux может переносить процесс, но за период cgroup он не получит больше одного ядра.
  • cpuset сознательно не используется. При жёстком pinning ONNX Runtime пытался выставить affinity собственным worker threads и писал ошибки pthread_setaffinity_np для запрещённых CPU. После удаления cpuset ошибки исчезли, а квота сохранилась.
  • mem_limit: 640m на реплику — общий предел JVM heap + metaspace + direct buffers + native ONNX. Два лимита не являются предварительным резервом; реальное потребление каждой реплики после старта около 75–80 MiB и растёт по мере партий.
  • memswap_limit: 640m — реплика не получает дополнительный swap поверх лимита.
  • -Xmx320m оставляет около половины cgroup-памяти нативной части ONNX Runtime.
  • ActiveProcessorCount=1 не даёт JVM строить внутренние пулы как для четырёх CPU.
  • ExitOnOutOfMemoryError превращает зависший деградировавший процесс в явный restart.
  • Ограничение логов не позволяет контейнеру постепенно заполнить системный диск.

6. Секреты и права доступа

Есть два независимых секрета:

  1. bearer token зарегистрированной Play-идентичности;
  2. HMAC secret webhook, которым Play подписывает доставки.

Они никогда не лежат в compose, README, git или выводе диагностических команд.

Play token

На рабочем Mac token дополнительно сохранён в Keychain:

service: dicechess-play-bot-token
account: dexus/kcp-one-ply-1

На Dexus он лежит отдельно от контейнера:

/home/jegors/dicechess-kcp-one-ply/secrets/play-bot-token

Права:

chmod 700 ~/dicechess-kcp-one-ply/secrets
chmod 600 ~/dicechess-kcp-one-ply/secrets/play-bot-token

Token не передаётся в container environment: он нужен только операторским lifecycle- скриптам для ladder/capacity/status.

Webhook HMAC secret

HMAC secret хранится в:

/home/jegors/dicechess-kcp-one-ply/.env

Файл имеет режим 600 и содержит только:

DICECHESS_WEBHOOK_SECRET=<secret>

Секрет сервер Play показывает один раз при POST /bot/webhook. Регистрация и запись должны происходить в одном защищённом shell-сеансе, без печати JSON-ответа:

ssh dexus 'set -euo pipefail
deploy_dir=$HOME/dicechess-kcp-one-ply
token=$(<"$deploy_dir/secrets/play-bot-token")
response=$(curl --fail --silent --show-error \
  -X POST https://play-api.jc.id.lv/bot/webhook \
  -H "Authorization: Bearer ${token}" \
  -H "Content-Type: application/json" \
  --data "{\"url\":\"https://dexus-kcp.jc.id.lv/api/webhook\"}")
secret=$(printf "%s" "$response" | jq -er .secret)
[[ "$secret" =~ ^[0-9a-fA-F]{64}$ ]]
umask 077
printf "DICECHESS_WEBHOOK_SECRET=%s\n" "$secret" > "$deploy_dir/.env.next"
mv "$deploy_dir/.env.next" "$deploy_dir/.env"
unset response secret token'

Danger

Не запускать эту процедуру с set -x, не добавлять | jq к полному registration- ответу и не вставлять secret в аргументы docker compose. Повторная регистрация заменяет прежний secret, поэтому после неё контейнер нужно сразу пересоздать.

7. Публикация через Cloudflare Tunnel

Отдельный cloudflared на Dexus не нужен. Существующий tunnel aurora уже имеет доступ к LAN и публикует другие локальные сервисы.

В Cloudflare Dashboard добавлен route типа Published application:

ПолеЗначение
Tunnelaurora
Hostnamedexus-kcp.jc.id.lv
Pathпусто, все пути
Service URLhttp://192.168.10.4:8094

Cloudflare автоматически создал DNS-запись. Входящие порты на домашнем роутере не открывались.

Проверки по слоям:

# На Dexus: контейнер слушает локальный порт.
ssh dexus 'curl -fsS -H "content-type: application/json" \
  -d "{\"type\":\"verification\",\"nonce\":\"local-check\"}" \
  http://127.0.0.1:8094/api/webhook'
 
# Из LAN: Aurora действительно достигает Dexus.
ssh aurora 'curl -fsS -H "content-type: application/json" \
  -d "{\"type\":\"verification\",\"nonce\":\"lan-check\"}" \
  http://192.168.10.4:8094/api/webhook'
 
# Снаружи: DNS, TLS, tunnel и origin работают вместе.
dig +short dexus-kcp.jc.id.lv
curl -fsS -H 'content-type: application/json' \
  -d '{"type":"verification","nonce":"public-check"}' \
  https://dexus-kcp.jc.id.lv/api/webhook

Последний ответ должен вернуть тот же nonce. Verification handshake специально работает до установки HMAC secret; игровые доставки без секрета не принимаются.

8. Регистрация в Play

Порядок важен:

  1. контейнер healthy и отвечает локально;
  2. Cloudflare public URL проходит verification;
  3. создать отдельную Play-идентичность;
  4. сохранить token;
  5. установить capacity 2 и openToHumans=true;
  6. зарегистрировать webhook и сразу сохранить одноразовый HMAC secret;
  7. пересоздать контейнер с .env;
  8. проверить, что Play отвечает ladderAllowance=1: лестница не сможет занять человеческий слот;
  9. только после повторной проверки вступить в ladder.

Полезные безопасные проверки:

ssh dexus 'token=$(<"$HOME/dicechess-kcp-one-ply/secrets/play-bot-token")
curl -fsS https://play-api.jc.id.lv/bot/webhook \
  -H "Authorization: Bearer ${token}" | jq "{url, verifiedAt}"
curl -fsS https://play-api.jc.id.lv/bot/capacity \
  -H "Authorization: Bearer ${token}" | jq
unset token'

GET /bot/webhook никогда не возвращает HMAC secret повторно.

9. Обычная эксплуатация

В каталоге деплоя находятся три скрипта, режим 750:

  • start-and-join.sh — запускает compose, ждёт Docker health и вступает в ladder;
  • leave-and-stop.sh — выходит из ladder, ждёт завершения текущей партии и только потом останавливает container;
  • status.sh — показывает безопасные container/capacity/delivery-метрики.

Статус

ssh dexus 'cd ~/dicechess-kcp-one-ply && ./status.sh'

Дополнительная нагрузка:

ssh dexus 'docker stats --no-stream \
  --format "{{.Name}} CPU={{.CPUPerc}} MEM={{.MemUsage}} NET={{.NetIO}}" \
  dexus-kcp-one-ply-a dexus-kcp-one-ply-b dexus-kcp-one-ply-proxy; free -h; uptime'

Запуск и ladder join

ssh dexus 'cd ~/dicechess-kcp-one-ply && ./start-and-join.sh'

Скрипт защищён flock: параллельный start/stop не сможет изменить состояние в середине операции.

Безопасная остановка

ssh dexus 'cd ~/dicechess-kcp-one-ply && ./leave-and-stop.sh'

Порядок принципиален:

  1. POST /bot/ladder/leave запрещает назначать новые игры;
  2. GET /bot/capacity опрашивается до activeGames=0;
  3. только затем выполняется docker compose stop.

По умолчанию drain ждёт 1800 секунд. Если партия не завершилась, скрипт завершается с ошибкой, оставляет контейнер работающим, а бота — снятым с лестницы. Это безопаснее, чем выключить компьютер посреди рейтинговой партии.

Временной лимит можно изменить только для конкретного ручного запуска:

ssh dexus 'cd ~/dicechess-kcp-one-ply && \
  DRAIN_TIMEOUT_SECONDS=3600 ./leave-and-stop.sh'

10. Текущий круглосуточный режим

На RPi4 раньше был cron, который ночью будил Dexus, а затем отправлял его спать. Sleep-задача отключена, потому что новый бот пока должен играть 24×7. Wake остаётся: если Dexus уже включён, это безвредный no-op.

Текущее логическое состояние crontab:

45 23 * * * /home/jegors/dexus-wake-sleep/wake-dexus.sh
# disabled 2026-08-14: dexus kcp-one-ply bot runs 24x7

restart: unless-stopped поднимет обе реплики и proxy после перезагрузки Dexus, но ladder-state живёт в Play отдельно. Если хост долго был недоступен, перед возвратом нужно проверить, что webhook healthy и только затем выполнить start-and-join.sh.

11. Будущий ночной режим

Ночной режим пока не включён. Целевая последовательность должна быть такой:

sequenceDiagram
    participant R as RPi4 scheduler
    participant D as Dexus
    participant P as Play
    R->>D: Wake-on-LAN
    R->>D: start-and-join.sh
    D->>P: join ladder
    Note over D,P: бот играет 2–3 часа
    R->>D: leave-and-stop.sh
    D->>P: leave ladder
    loop пока activeGames > 0
        D->>P: GET /bot/capacity
    end
    D->>D: docker compose stop
    R->>D: shutdown only after successful drain

Критические правила:

  • не назначать жёсткое выключение по времени без drain;
  • если leave-and-stop.sh вернул ненулевой код, не выключать Dexus;
  • сначала накопить длительности реальных партий и выбрать окно с запасом;
  • учитывать, что ladder может назначить игру непосредственно перед leave;
  • backup и бот могут использовать разные ядра, но простой cpus: 1.0 не привязывает их к разным физическим ядрам;
  • backup-контейнеры сейчас не имеют подтверждённой отдельной квоты в один CPU, поэтому обещание «бот 1 CPU + backup 1 CPU и они не мешают» ещё надо проверить нагрузочным ночным замером.

Рекомендуемый первый эксперимент: оставить 30–60 минут запаса после ожидаемого окна игры, записать CPU/load/latency во время Borg/PBS и только после этого автоматизировать shutdown.

12. Первые реальные замеры

Снято 14.08.2026. Сначала измерена одна реплика, затем конфигурация расширена до двух реплик специально для одного ladder-слота и одного human-слота.

Контейнер и хост

СостояниеCPU containerMemory containerHost available RAM
healthy, без хода~0,03%~91 MiB~2,1 GiB
активный ONNX-расчёт95–98%117–147 MiB~2,1 GiB

В тот же момент:

  • bbs показывал около 951 MiB в Docker accounting и около 4% CPU;
  • pbs — около 41 MiB и 0,03% CPU;
  • load average Dexus — примерно 0.71 / 0.84 / 0.62;
  • swap практически не использовался;
  • hunter-all был остановлен.

Это baseline первоначальной одноэкземплярной конфигурации. После запуска двух реплик в покое измерено: A ~79 MiB, B ~76 MiB, nginx ~5,8 MiB, host available ~2,0 GiB.

Вывод по памяти: лимит 640 MiB на реплику имеет большой запас, а -Xmx320m пока не является узким местом. Рост с 91 до ~132 MiB похож на нормальную инициализацию/рабочий набор, а не на доказанную утечку. Для вывода об утечке нужны сутки и сравнение нижних точек после завершения партий.

Webhook и часы

После первых четырёх законченных рейтинговых партий:

{
  "totalDeliveries": 53,
  "outcomes": [{ "outcome": "applied", "count": 53 }],
  "p50Ms": 2000,
  "p90Ms": 30000,
  "p99Ms": 120000,
  "lastFailure": null
}

Это bucket-аппроксимация Play, а не точные процентили, и выборка пока мала. Однако форма уже ясна:

  • типичный ход укладывается примерно в 2 s;
  • отдельные позиции занимают десятки секунд;
  • сетевых, HMAC, HTTP и illegal-move ошибок нет;
  • первая наблюдаемая партия после четырёх доставок ещё имела 250,5 s на часах чёрных, то есть флажок не был близок;
  • первые четыре партии дали две победы и два поражения, но provisional rating после четырёх игр не является оценкой силы модели.

Главный риск — не RAM, а хвост CPU

Полное ядро Dexus заметно лучше 0,2 shared vCPU Northflank, но KCP One Ply всё равно встречает дорогие позиции. p90/p99 нужно наблюдать на сотнях доставок и отдельно во время backup. maxConcurrentGames=2 безопасен только потому, что теперь есть две независимые JVM-реплики. Выше 2 увеличивать нельзя: третьему webhook снова не будет отдельного worker и CPU.

13. Мониторинг в следующие сутки

Минимальный чек-лист:

ssh dexus 'cd ~/dicechess-kcp-one-ply && ./status.sh'
ssh dexus 'docker stats --no-stream \
  dexus-kcp-one-ply-a dexus-kcp-one-ply-b dexus-kcp-one-ply-proxy'
curl -fsS https://play-api.jc.id.lv/bots/dexus/kcp-one-ply-1 | jq

Следить за:

  1. lastFailure должен оставаться null;
  2. outcome applied должен доминировать без timed_out, http_5xx, garbled и refused;
  3. p90/p99 после 100+ доставок;
  4. нижней точкой памяти после партий, а не только пиком во время хода;
  5. OOM/restart count контейнера;
  6. часами в длинных партиях;
  7. поведением во время настоящего backup-окна.

Проверка restart count и OOM:

ssh dexus 'for c in dexus-kcp-one-ply-a dexus-kcp-one-ply-b dexus-kcp-one-ply-proxy; do
  docker inspect "$c" --format \
    "{{.Name}} restarts={{.RestartCount}} oom={{.State.OOMKilled}} status={{.State.Status}} health={{.State.Health.Status}}"
done'

14. Обновление версии

Безопасный порядок:

  1. leave-and-stop.sh и дождаться нулевого activeGames;
  2. передать новый private image с авторизованной машины;
  3. присвоить новый локальный тег, не перетирать работающий;
  4. проверить digest, архитектуру и наличие production assets;
  5. изменить только image: в compose;
  6. docker compose up -d и дождаться healthy;
  7. выполнить публичный verification;
  8. start-and-join.sh;
  9. старый image удалить только после успешной партии на новом.

Если меняется алгоритм или модель, сначала решить, остаётся ли это та же рейтинговая идентичность. Значимое изменение требует нового имени Play-бота.

15. Быстрая диагностика

Container unhealthy

ssh dexus 'cd ~/dicechess-kcp-one-ply && docker compose ps && \
  docker logs --tail 100 dexus-kcp-one-ply-a && \
  docker logs --tail 100 dexus-kcp-one-ply-b && \
  docker logs --tail 100 dexus-kcp-one-ply-proxy'

Нормальный старт содержит:

opening book: /models/opening_book.tsv (147 entries)
ONNX model: /models/kcp_nodice_serving_20260809.onnx (mode=one-ply, features=kcp, timePolicy=empirical-v1, candidateLimit=8)
ONNX custom handler listening on :8080/api/webhook

Local работает, public нет

Проверять по цепочке:

  1. docker compose ps и 127.0.0.1:8094 на Dexus;
  2. 192.168.10.4:8094 с Aurora;
  3. состояние replica tunnel aurora в Cloudflare;
  4. route и DNS для dexus-kcp.jc.id.lv;
  5. публичный verification POST.

Webhook verified, но ходы не приходят

Проверить:

ssh dexus 'cd ~/dicechess-kcp-one-ply && ./status.sh'
  • onLadder — через публичный профиль;
  • ladderAllowance=1;
  • activeGames;
  • totalDeliveries и lastFailure;
  • нет ли уже текущей партии в GET /bot/games.

Ошибки affinity ONNX

Не возвращать cpuset без отдельного теста настройки thread affinity. Рабочая конфигурация использует quota cpus: 1.0 и ActiveProcessorCount=1.

Память растёт

Сравнивать одинаковые фазы:

ssh dexus 'for i in 1 2 3 4 5; do
  date -Is
  docker stats --no-stream --format "{{.Name}} {{.MemUsage}}" \
    dexus-kcp-one-ply-a dexus-kcp-one-ply-b dexus-kcp-one-ply-proxy
  sleep 60
done'

Рост во время загрузки модели и первой партии нормален. Подозрение на leak возникает, если нижняя точка монотонно растёт между завершёнными партиями, а не просто меняется рабочий набор page cache/native allocator.


Итоговое решение на 14.08.2026

Текущую конфигурацию оставляем:

  • Dexus работает 24×7;
  • две KCP One Ply JVM-реплики за nginx least_conn;
  • по 1 CPU и 640 MiB на реплику, maxConcurrentGames=2;
  • openToHumans=true, ladderAllowance=1: один слот лестнице, один человеку;
  • существующий Cloudflare Tunnel на Aurora;
  • production model и book только в private image;
  • безопасный leave → drain → stop уже подготовлен для будущего ночного режима.

Следующее решение принимается после накопления статистики минимум за сутки и отдельного наблюдения во время backup: оставить бота always-on, перевести в ночное окно или вернуть KCP-слот в облако, а Dexus использовать только для экспериментальных сессий.