Наблюдатель без токенов: модель зовём, только когда есть повод

Фоновая проверка будит модель каждые пять минут ради ответа «всё тихо». Отдайте сравнение скрипту в планировщике OpenClaw, и ходов модели станет столько, сколько было событий: по нашему расчёту в десятки раз меньше.

· 7 мин чтения

Коротко

  • Повесьте на задачу automation условие trigger.script: скрипт сравнивает наблюдение с прошлым и возвращает { fire, message, state }. Ход модели запускается только при fire: true.
  • Если событие приходит строками из лога, держите команду открытой stream-расписанием. Если внешний сервис умеет слать webhook, отсейте шум в transform, маленьком JS-модуле, который возвращает null, когда агента звать не надо.
  • Скрипт работает без присмотра с правами агента: задайте --tools минимумом, а в самом скрипте оставьте только чтение.

Симптом

Агенту поручили «следи за сервисом, скажи, если что-то не так», и он делает это через heartbeat (периодический ход агента) или задачу openclaw automations с --message. Каждый тик — полный ход модели («Heartbeats run full agent turns», пишет документация), и почти всегда он кончается ответом NO_REPLY. Такое решение по силам if в три строки.

Приём: сравнение «было/стало» делает код, модели остаётся решить, что делать с изменением.

Проверено по документации OpenClaw 2026.10.1, октябрь 2026; запуском конфиги не проверялись.

Схема потока

тик расписания (every/cron, не чаще раза в 30 с)
        │
        ▼
trigger.script: exec или MCP, до 5 вызовов, до 30 с   ← модели нет
        │   сравнивает наблюдение с trigger.state
        ├── fire: false → state сохранён, ждём тик (истории запусков нет)
        └── fire: true  → message дописан к --message
                              │
                              ▼
                ход агента (isolated, лёгкая модель)
                              │
             NO_REPLY | отчёт | AUTOMATION_FAILED
                              │
                              ▼
                     --announce → вам в чат

Из чего собрано

Повод Примитив Когда работает модель
Состояние надо опрашивать trigger.script на every/cron только при fire: true
Событие пишет долгоживущая команда --stream-command с --stream-mode match на пачку подходящих строк
Внешний сервис шлёт HTTP hooks.mappings с transform transform вернул действие, а не null
Суждение не нужно command payload (--command) никогда

Command payload запускает процесс на хосте Gateway без модели: stdout уходит в чат, напечатанный NO_REPLY ничего не отправляет.

Как собрать: наблюдатель за health-эндпоинтом

Скрипт-условие. Вызов exec и json(...) повторяет пример из документации, || true нужен, чтобы недоступный сервер дал код 000, а не ошибку команды:

// watch-health.js
const res = await exec({
  command: "curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://status.example.invalid/health || true",
});
const code = String(res?.aggregated ?? "").trim();
if (!/^\d{3}$/.test(code)) throw new Error(`health check failed: ${code.slice(0, 200)}`);
const prev = trigger.state?.code;
json({
  fire: prev !== undefined && code !== prev,
  message: `health: ${prev} -> ${code}`,
  state: { code },
});

Задача:

openclaw automations add \
  --name "Health watcher" \
  --every 1m \
  --trigger-script ./watch-health.js \
  --tools exec \
  --model "<лёгкая модель>" \
  --light-context \
  --session isolated \
  --message "Статус health-эндпоинта изменился. Ответь в три строки: что это значит и что проверить первым." \
  --announce --channel telegram --to "<ваш chat id>"
  • trigger.state — JSON до 16 КБ, который переживает тики. Сигнал уходит только при смене кода, дублей нет.
  • Первый тик молчит (prev === undefined), иначе сразу после создания придёт ложное «изменилось».
  • throw вместо тишины. Наблюдатель, который замолкает при сбое проверки, выглядит здоровым. Исключение становится ошибкой запуска: дальше backoff и оповещение о сбоях, по умолчанию после двух ошибок подряд, если у задачи есть маршрут доставки.
  • message становится всем контекстом события: пишите в нём, что изменилось.
  • --tools действует и на скрипт, и на ход модели. Без него задача хранит *, то есть всю политику агента.
  • --model и --light-context: для проверок статуса документация советует лёгкую модель, а --light-context не подкладывает в ход файлы рабочего пространства.

Если повод — строка в логе, опрос не нужен:

openclaw automations add \
  --name "Errors in app log" \
  --stream-command '["tail","-F","/var/log/app.log"]' \
  --stream-mode match \
  --stream-match '(ERROR|FATAL)' \
  --session isolated \
  --message "Разбери эти ошибки из лога."

Строки, пришедшие за время хода модели или раньше встроенных 30 секунд, склеиваются в одну пачку: на всплеск ошибок придётся один ход.

Если сервис умеет слать webhook, фильтр стоит ещё раньше. Webhooks по умолчанию выключены:

{
  hooks: {
    enabled: true,
    token: "<long-random-hook-token>",
    path: "/hooks",
    allowedAgentIds: ["main"],
    mappings: [
      {
        match: { path: "ci" },
        action: "agent",
        agentId: "main",
        channel: "telegram",
        to: "<ваш chat id>",
        transform: { module: "ci-filter.js" },
      },
    ],
  },
}
// ~/.openclaw/hooks/transforms/ci-filter.js
export default ({ payload }) =>
  payload?.status === "failed"
    ? { kind: "agent", message: `CI упал в репозитории ${String(payload.repo).slice(0, 100)}. Найди причину.` }
    : null;

Без channel и to mapping отправит итог в канал "last", то есть в последний использованный. Применить конфиг: openclaw config validate, затем openclaw gateway restart.

Где человек

Человек получает итог в чат: через --announce у задачи или channel/to у mapping. Ход агента идёт без присмотра, уточнить не у кого, поэтому ответ — готовый отчёт, NO_REPLY, если сообщать нечего, или AUTOMATION_FAILED первой строкой, и тогда запуск записывается как ошибка.

Сколько стоит

Своих замеров нет, считаем по формуле.

  • N — тиков в сутки: раз в 5 минут — 288, раз в минуту — 1440, предел условия (30 с) — 2880.
  • k — сколько раз в сутки есть повод.
  • T — токенов на ход модели. Ориентир из документации есть только для heartbeat: около 2–5 тысяч в изолированной сессии против примерно 100 тысяч с полной историей.

Модель на каждый тик: N × T в сутки. Гейт: k × T (плюс короткий message). Скрипт токенов не тратит, он платит процессором и вызовами exec. Выигрыш — N / k: опрос раз в 5 минут и 4 повода в сутки дают 288 ходов против 4, в 72 раза меньше. Деньги — токены, умноженные на тариф провайдера. Когда повод есть почти на каждом тике (k близко к N), гейт ничего не экономит.

Как проверить

  1. openclaw automations show <job-id>: задача включена, видны расписание, следующий запуск и маршрут доставки.
  2. Пустой openclaw automations runs <job-id> в тихие часы — норма: fire: false истории не создаёт.
  3. Спровоцируйте событие на тестовом эндпоинте и дождитесь записи в openclaw automations runs <job-id> --limit 20 и сообщения в чате.
  4. Сломайте команду (curlx вместо curl): в истории должна появиться ошибка, а не тишина.
  5. Webhook проверьте двумя запросами:
curl -i http://127.0.0.1:18789/hooks/ci \
  -H 'Authorization: Bearer <long-random-hook-token>' \
  -H 'Content-Type: application/json' \
  --data '{"status":"passed","repo":"demo"}'

Ожидается 204: transform вернул null, запуска нет. С "status":"failed" — 200, это значит «запуск принят», а не «модель ответила». 401 — неверный токен, 404 — hooks выключены или путь не совпал с mapping.

Как выключить

  • Одну задачу: openclaw automations disable <job-id> или remove <job-id>.
  • Все условия, script payloads и stream-источники разом: cron.triggers.enabled: false.
  • Webhooks: hooks.enabled: false.

Где ломается

  • Скрипт работает без присмотра. Условия по умолчанию работают с полной политикой инструментов агента, включая exec, stream держит вашу команду запущенной. --tools exec из примера оставляет шелл, поэтому скрипт должен только читать.
  • Одобрения exec не доходят до чата. Если у агента включены одобрения exec, запрос из запуска по расписанию уходит только в Control UI или приложения; когда ни одно не подключено, он отклоняется сразу. Поэтому в примере модель только объясняет событие.
  • После сбоя state не сохраняется. Если упал ход модели, следующая проверка сработает снова: это встроенный повтор, но возможен дубль уведомления.
  • Лимиты условия: не чаще раза в 30 секунд, 30 секунд на проверку, до 5 вызовов, state до 16 КБ. Для реакции за секунды нужен webhook от источника.
  • Stream не повторяет упавшие запуски модели и держит в очереди одну пачку. Если сама stream-команда пять раз подряд завершается быстрее чем за 60 секунд (опечатка, нет файла), задача уходит в ошибку, включать её надо вручную.
  • Webhook не гарантирует обработку. Ошибка после допуска возвращается как данные запуска, повторного HTTP-запроса не будет. Idempotency-Key защищает от двойного запуска, но кэш повторов живёт в памяти и сбрасывается при рестарте.
  • Содержимое webhook — недоверенные данные. Токен даёт право войти, а не подтверждает отправителя. Подставляйте в message только короткие поля и ограничьте инструменты агента.

Когда брать другое

  • Повод есть почти на каждом тике: оставьте обычное расписание.
  • Каждое событие нужно обработать ровно один раз и пережить рестарт: берите очередь или durable-движок вроде Temporal.
  • Чтобы понять, есть ли повод, надо читать текст («это письмо тревожное?»). Скрипт не решит, нужен ход лёгкой модели с NO_REPLY на тишину.