Skip to main content
Назад к блогу
Claude CodeAIAutomationMonitoringlaunchdResilience

system-droid: сторож, который сам проверяет и сам чинит мои системы

Мои локальные сервисы ломались тихо: бриф не приходил, токены протухали, бэкап замирал — а я узнавал последним. Поэтому я собрал дроида на launchd, который каждые два часа всё проверяет, чинит через агента Claude то, что безопасно, и пишет мне лишь тогда, когда действительно нужен человек.

Опубликовано 17 августа 2026 г.10 мин чтения

TL;DR

system-droid — небольшая программа на Go, которую launchd запускает каждые два часа и после каждого пробуждения Mac. Она проходит по списку проверок всех моих локальных сервисов, молчит, пока всё работает, и пишет в Telegram только тогда, когда что-то действительно сломалось. Раз в день присылает короткую сводку «всё работает», чтобы тишина никогда не означала «неизвестно». А если что-то всё же падает — сначала пытается починить сама: отдельный агент Claude (Opus) находит причину и устраняет её, и лишь если не справился — зовёт меня.

◆ Усі системи в нормі · 10/10 · 2026-08-17 09:00

— OpenClaw / Resonance
◆ styletts2-ua (tts) — локальний український TTS :8123
◆ openclaw gateway (:18789) — ядро агента Resonance (launchd + порт)
◆ openclaw hooks — hooks-ендпоінт увімкнено

— Інфраструктура
◆ cli-proxy Claude auth — робочий Claude-токен (валідність, авто-оновлення)
◆ system-droid — сам вартовий (надсилає ці звіти)
◆ otel-block — корпоративна телеметрія заблокована у firewall
◆ vault git backup — git-копія Obsidian-сховища на GitHub

Зачем всё это

За последние месяцы у меня незаметно скопился целый парк локальной автоматизации: агент OpenClaw («Резонанс»), украиноязычный голосовой сервер StyleTTS2, LLM-прокси перед Claude, ежедневные генераторы заметок для Obsidian, git-копия хранилища, блокировка корпоративной телеметрии в файрволе. Каждая из этих вещей полезна ровно до тех пор, пока работает. А ломались они всегда одинаково — тихо.

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

Как это устроено

Вся система — один бинарник плюс config.json со списком проверок двух типов: HTTP (сервис отвечает на /health) и командных (скрипт завершился с кодом 0). launchd запускает дроида по расписанию; тот проходит проверки параллельно и решает, стоит ли меня беспокоить вообще. Поставить новую систему под наблюдение — одна строка в конфигурации, без перекомпиляции.

Главный принцип — экономия внимания. Никаких дашбордов, которые нужно открывать, никакого потока «всё ок» каждые два часа. Сообщений ровно три типа: ежедневная сводка, что всё зелёное; «сломалось — починил сам»; и «сломалось — нужен ты». Всё остальное — тишина.

Самолечение

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

🔧 system droid — полагоджено автоматично, 1 спроба (2026-08-02 07:41)

Було зламано:
• news digest freshness — застарілий дайджест: 2026-07-31 (2 дні тому)

✅ Зараз усі перевірки зелені.

↩️ Відкат змін агента (git reset --hard на знімок):
• ~/Developer/system-droid  @ f037cfb
• ~/Developer/obsidian-cron @ e98746c

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

Что он уже поймал

За семь недель дроид превратил несколько тихих поломок — каждая могла жить днями — в сообщения, которые я увидел сразу:

  • Убитый генератор брифов. Ядро macOS пристрелило процесс сразу после пересборки бинарника — утренний бриф просто не вышел бы, и никто бы этого не заметил.
  • Протухший refresh-токен. Прокси перестал обновлять токен доступа, и все LLM-вызовы возвращались с 401. Дроид написал прямо: это лечится только повторным входом — одна команда, две минуты.
  • Заглушка вместо аналитики. Генерация завершалась «успешно», но вместо AI-анализа в заметке стоял текст-заглушка. Проверка содержимого заметки, а не кода выхода, поймала это в первый же день.
  • Зависший бэкап хранилища. Git-плагин продолжал коммитить локально, а push на GitHub молча умер — резервная копия застыла бы, и выяснилось бы это в худший из возможных моментов.

Чего он сознательно не лечит

Самое важное решение в этой системе — не что автоматизировать, а что нет. Часть проверок помечена «только сообщить»: всё, что касается авторизации, чужих систем и сессий моего собственного агента. Протухший OAuth не «чинится» скриптом — его должен перевыпустить человек в браузере. Агенту прямо запрещено трогать токены, перезапускать мои рабочие сервисы и «подгонять» проверки, чтобы они позеленели.

Есть и физический предел: лекарь — это тот же Claude CLI, и когда падает сеть или авторизация, он падает вместе со всем остальным. То, что ломает систему, ломает и руки, которыми её чинят. Поэтому честное поведение в таких случаях — не героические попытки, а чёткое «нужен ты» с объяснением, что именно сделать.

Мелочи, на которых всё держалось

Больше всего времени заняли не проверки, а надёжность самого сторожа. Первое: пересборка Go-бинарника на месте меняет его подпись, а launchd кеширует требования к подписи — и следующий плановый запуск система просто убивает. Теперь сборка всегда заканчивается перезагрузкой задачи:

deploy.sh
# Перезбирання бінарника на місці змінює його cdhash, а launchd кешує
# вимоги до підпису (LWCR) на момент bootstrap. Після збирання кеш
# застаріває — і наступний плановий запуск ядро вбиває (exit 9) або
# launchd відмовляється його запускати (exit 78 / EX_CONFIG).
# Тому збирання ЗАВЖДИ закінчується перезавантаженням задачі:
go build -o system-droid .
codesign --verify --strict system-droid
launchctl bootout  "gui/$(id -u)/com.oleksii.system-droid" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.oleksii.system-droid.plist
./check-self.sh   # свіжий запуск має бути зеленим

Второе: для сервисов с KeepAlive код выхода последнего запуска — обманчивая метрика. После каждого перезапуска там SIGTERM, и вполне живой сервис выглядит сломанным. Правильный вопрос — не «как он завершился», а «работает ли он сейчас»:

check-launchd-running.sh
# У KeepAlive-демона LastExitStatus після КОЖНОГО перезапуску
# ненульовий (15/SIGTERM) — тож перевірка "останній запуск успішний"
# хибно валить цілком живий сервіс. Правильне питання інше:
# чи працює він ПРЯМО ЗАРАЗ?
info=$(launchctl list "$1" 2>/dev/null) || { echo "not loaded"; exit 1; }
pid=$(printf '%s\n' "$info" | awk -F'= ' '/"PID"/{gsub(/[ ;]/,"",$2);print $2}')
[ -n "$pid" ] && [ "$pid" != "0" ] && { echo "running (pid $pid)"; exit 0; }
echo "loaded but not running"; exit 1

Третье: проверять нужно именно тот объект, от которого зависит система. Моя первая проверка токена считала «дата выдачи плюс год» — и сияла зелёным, пока настоящий токен уже сутки возвращал 401:

# Календарна перевірка сяяла зеленим — річному токену ще жити й жити:
claude oauth token — OK: valid, 365d left

# А справжній робочий токен тим часом помер, і логи проксі це знали:
token refresh failed: {"error":"invalid_grant","error_description":"Refresh token expired"}
POST /v1/chat/completions → 401 "OAuth access token has expired. Re-authenticate."

И четвёртое: ночной обрыв сети на один цикл — не повод будить человека. Тревога «нужно вмешательство» теперь отправляется, только если проблема пережила два запуска подряд. Разовые сбои проходят сами; настоящие всё равно доходят — просто на два часа позже:

main.go
// Тривога "потрібне втручання" — лише якщо перевірка провалилася
// два запуски поспіль. Нічний обрив мережі, демон посеред перезапуску,
// пробудження зі сну — все це минає само до наступного циклу,
// і будити людину через таке не можна. Успішне лікування,
// навпаки, повідомляється одразу — це добра новина, а не шум.
counts := loadHealCounts()          // ~/.system-droid-heal-state.json
for _, r := range stillFailing {
    counts[r.Name]++
}
var persistent []Result
for _, r := range stillFailing {
    if counts[r.Name] >= 2 {        // поріг ескалації
        persistent = append(persistent, r)
    }
}
// сповіщення — лише якщо len(persistent) > 0

Глаза для другого агента

Помимо Telegram, дроид работает и как MCP-сервер. Это значит, что мой локальный агент может сам спросить его о состоянии систем: вместо чтения отчётов я говорю «проверь, всё ли работает» — агент вызывает get_health, прогоняет все проверки вживую и отвечает. Мониторинг перестал быть страницей, на которую смотрят, и стал сервисом, к которому обращаются.

# Дроїд працює і як MCP-сервер — локальний агент питає його сам:
Resonance › виклич get_health у system-droid і скажи, чи все зелене

get_health →
overall: 🟢 OK — 10/10 green (2026-08-17 09:00)
🟢 openclaw gateway (:18789) — ядро агента Resonance (launchd + порт)
🟢 cli-proxy Claude auth — робочий Claude-токен (валідність, авто-оновлення)
...

Итог

Технически здесь нет ничего сложного: Go, launchd, несколько shell-скриптов и бот в Telegram. Но именно эта простая связка изменила ощущение от всего парка автоматизации: я перестал держать в голове список вещей, которые «надо не забыть проверить». Теперь это работа дроида.

Формула проста: проверяй реальное поведение, а не косвенные признаки; чини автоматически то, что безопасно; честно зови человека там, где без него никак; и молчи всё остальное время.

Семь недель он в основном молчит. Каждое его сообщение за это время было либо хорошей новостью об уже починенном, либо точным указанием, где нужны мои две минуты. Это и есть определение удобства: система, которая охраняет другие системы — и уважает твою тишину.

Заметили ошибку?

Неверный факт, кривой перевод, что-то звучит неправдой в этой статье? Напишите мне — на своём языке.