Отложенные задачи (Cron)
Планируйте задачи для автоматического запуска — на естественном языке или через cron-выражения. Hermes открывает управление cron через единый инструмент cronjob_manage с операциями в стиле действий, вместо отдельных инструментов schedule/list/remove.
Что умеет cron сейчас
Cron-задачи умеют:
- планировать разовые или повторяющиеся запуски
- ставить на паузу, возобновлять, редактировать, запускать вручную и удалять задачи
- подключать ноль, один или несколько навыков к задаче
- доставлять результат обратно в исходный чат, в локальные файлы или на настроенные целевые платформы
- запускаться в новых сессиях агента с обычным статическим списком инструментов
- работать в режиме без агента — скрипт по расписанию, чей stdout доставляется дословно, без участия LLM (см. раздел режим без агента ниже)
- срабатывать по внешним событиям — маршрут вебхука с заданным
cron_jobзапускает задачу в тот момент, когда что-то случилось (на PR пришёл отзыв, сервис прислал алерт), не дожидаясь следующего такта расписания. См. Cron-задачи по событиям.
Всё это доступно самому Hermes через инструмент cronjob_manage, так что вы можете создавать, ставить на паузу, редактировать и удалять задачи, просто попросив об этом обычным языком — без CLI.
На какой модели работает cron-задача? Порядок разрешения в момент запуска: закрепление на уровне задачи → cron.model в config.yaml → основная модель агента из hermes model.
- Закрепление на уровне задачи — задача несёт собственную модель. Конкретную модель можно задать через дашборд,
hermes cron create/edit --model … --provider …или прямым редактированием~/.hermes/cron/jobs.json; либо зафиксировать текущую основную модель черезhermes cron create/edit --pin(инструмент агентаcronjob_manageтоже умеет это сpinned=true, но только по вашей просьбе).--unpin(pinned=false) снимает фиксацию. Направить задачу на другую модель агент не может — закрепления инференса принадлежат пользователю. cron.model/cron.model_provider— значение по умолчанию для всего парка cron-задач: каждая незакреплённая задача работает на этой модели независимо от вашей модели в чате. Задайте один раз (hermes config set cron.model <name>), и переключение модели чата черезhermes modelили/modelбольше не затрагивает cron-парк.- Основная модель агента — если не задано ни то, ни другое, задача работает на той модели, что выставлена в
hermes model//modelв момент запуска. Смените основную модель — и каждая незакреплённая задача перейдёт на неё со следующего запуска.
Какого бы провайдера ни разрешила задача, его специфичные настройки запроса (например, request_overrides вроде extra_body/extra_headers для пользовательских провайдеров) переносятся в запланированный запуск точно так же, как в интерактивной сессии.
hermes setup --portal — самый безболезненный вариант для автономных запусков, поскольку обновление OAuth происходит автоматически. См. Nous Portal.
Усилие рассуждений для отдельной задачи. Задача может закрепить за собой свой уровень размышлений, независимо от закрепления модели: одно из значений none, minimal, low, medium, high, xhigh, max, ultra. Заданное, оно перекрывает для запусков этой задачи и глобальное agent.reasoning_effort, и agent.reasoning_overrides для конкретной модели (none отключает размышления). Задаётся через hermes cron create/edit --reasoning-effort high; чтобы снять закрепление и снова следовать конфигу, передайте при редактировании пустую строку. (В инструменте агента cronjob_manage этого параметра намеренно нет — настройка модели остаётся решением пользователя.) Уровни, которые модель не поддерживает, провайдер урезает или отбрасывает в момент запроса: закреплённый xhigh на модели с потолком high отработает на high. На задачи с no_agent закрепление не влияет — там нет вызова LLM, который можно было бы настроить. Пригодится, чтобы тяжёлую аналитику по расписанию гонять на high, а дешёвые регулярные задачи — на minimal, не трогая глобальное значение по умолчанию.
Сессии, запущенные из cron, не могут рекурсивно создавать новые cron-задачи. Hermes отключает инструменты управления cron внутри cron-запусков, чтобы предотвратить неконтролируемое лавинообразное планирование.
Создание отложенных задач
В чате через /cron
/cron add "in 30m" "Remind me to check the build"
/cron add "every 2h" "Check server status"
/cron add "every 1h" "Summarize new feed items" --skill blogwatcher
/cron add "every 1h" "Use both skills and combine the result" --skill blogwatcher --skill maps
Через отдельный CLI
hermes cron create "every 2h" "Check server status"
hermes cron create "every 1h" "Summarize new feed items" --skill blogwatcher
hermes cron create "every 1h" "Use both skills and combine the result" \
--skill blogwatcher \
--skill maps \
--name "Skill combo"
Через обычный диалог
Просто попросите Hermes на естественном языке:
Every morning at 9am, check Hacker News for AI news and send me a summary on Telegram.
Hermes сам воспользуется единым инструментом cronjob_manage.
Предзапусковая проверка конфигурации
Прежде чем собирать какую-либо агентскую машинерию для запланированного запуска, планировщик проверяет, что конфигурация задачи действительно может дать успешный результат:
- API-ключ провайдера разрешается (для незакреплённой задачи проверка пропускается, если настроена цепочка
fallback_providers, поскольку резервный путь может выручить при отсутствующем основном ключе; закреплённая задача этой цепочкой не пользуется, поэтому для неё проверка выполняется всегда); - подключённые навыки готовы к работе (нет недостающих обязательных переменных окружения, команд или файлов учётных данных);
- целевые платформы доставки известны, и для них настроены учётные данные шлюза (
local/originцели никогда не проверяются); - каждый MCP-сервер, который задача перечисляет в собственном
enabled_toolsets, дал для этого профиля хотя бы один инструмент. Сервер, который в этом шлюзе уже подключался и лишь переподключается после сетевого сбоя (перезагрузка роутера, отказ DNS), запуск не блокирует: задача выполняется с теми инструментами, что удалось получить, а в журнале шлюза отмечается, какие серверы были пропущены (один раз за сбой). Сервер, который для этого профиля не подключился ни разу (неверный URL или учётные данные, либо сервер принадлежит другому профилю под мультиплексором), или сервер, застрявший на постоянной ошибке вроде отозванных учётных данных, запуск блокирует.
Если проверка не проходит, last_status задачи становится blocked_config, отправляется ОДНО предупреждение (оно не повторяется на каждом такте), и вызов LLM не выполняется — неправильно настроенная задача никогда не тратит токены. Следующий успешный запуск снимает заблокированное состояние, так что новый сбой конфигурации в будущем снова вызовет предупреждение.
Вердикт об отсутствующих учётных данных называет профиль и HERMES_HOME, которые прочитал
планировщик, например provider credential missing: No Codex credentials stored … [profile 'default', HERMES_HOME /opt/data]. Если интерактивная сессия с «теми же» учётными данными
работает, сравните этот путь с HERMES_HOME в оболочке: шлюз, запущенный без окружения
оболочки (Docker HOME против HERMES_HOME, юнит сервиса), или спутниковый профиль под
мультиплексором читают не те auth.json и .env, что оболочка.
Чтобы отключить проверку и вернуть старое поведение (запуск продолжается и падает уже во время выполнения):
cron:
preflight: false
Либо: hermes config set cron.preflight false
Перевод незакреплённых задач на новое значение по умолчанию
Незакреплённая задача следует за основной моделью агента, так что hermes model переводит на новую модель и парк cron.
Когда нужно, чтобы задача осталась на своей модели:
hermes cron edit <job_id> --pin # зафиксировать текущую основную модель за одной задачей
hermes cron edit <job_id> --provider <provider> --model <model> # закрепить конкретную модель
hermes cron edit <job_id> --unpin # снова следовать за основной моделью
hermes config set cron.model <model> # все незакреплённые задачи, не трогая чат
hermes cron list и инструмент cronjob_manage показывают pinned для каждой задачи.
Cron-задачи с навыками
Cron-задача может подгрузить один или несколько навыков перед выполнением промпта. Каждый навык загружается ровно так же, как по /skill-name в чат-сессии, включая блок [Skill config ...] с разрешёнными значениями metadata.hermes.config из config.yaml.
Один навык
cronjob(
action="create",
skill="blogwatcher",
prompt="Check the configured feeds and summarize anything new.",
schedule="0 9 * * *",
name="Morning feeds",
)
Несколько навыков
Навыки загружаются по порядку. Промпт становится инструкцией задачи, накладываемой поверх этих навыков.
cronjob(
action="create",
skills=["blogwatcher", "maps"],
prompt="Look for new local events and interesting nearby places, then combine them into one short brief.",
schedule="every 6h",
name="Local brief",
)
Это удобно, когда вы хотите, чтобы запланированный агент унаследовал многократно используемые процессы, не запихивая полный текст навыка в сам cron-промпт.
Запуск задачи внутри директории проекта
По умолчанию cron-задачи запускаются в отрыве от любого репозитория — файлы AGENTS.md, CLAUDE.md и .cursorrules не загружаются, а инструменты терминала / файлов / выполнения кода работают из той рабочей директории, в которой был запущен шлюз. Передайте --workdir (CLI) или workdir= (вызов инструмента), чтобы это изменить:
# Отдельный CLI (расписание и промпт — позиционные аргументы)
hermes cron create "every 1d at 09:00" \
"Audit open PRs, summarize CI health, and post to #eng" \
--workdir /home/me/projects/acme
# Из чата, через инструмент cronjob
cronjob(
action="create",
schedule="every 1d at 09:00",
workdir="/home/me/projects/acme",
prompt="Audit open PRs, summarize CI health, and post to #eng",
)
Когда workdir задан:
AGENTS.md,CLAUDE.mdи.cursorrulesиз этой директории подмешиваются в системный промпт (тот же порядок обнаружения, что и в интерактивном CLI)terminal,read_file,write_file,patch,search_filesиexecute_codeиспользуют эту директорию как рабочую- Путь должен быть существующей абсолютной директорией — относительные пути и несуществующие директории отклоняются при создании / обновлении
- Передайте
--workdir ""(илиworkdir=""через инструмент) при редактировании, чтобы сбросить значение и вернуть прежнее поведение
Каждый запуск агента привязывает свой workdir к уникальному идентификатору задачи этого запуска. Поэтому задачи с workdir используют обычный параллельный пул, не затрагивая процесс-глобальное состояние терминала и не смешивая пути между параллельными запусками. Если нужно ограничить общую параллельность cron, задайте cron.max_parallel_jobs.
Редактирование задач
Не нужно удалять и заново создавать задачу только ради того, чтобы её изменить.
Плейсхолдер <job_id> ниже (а также в разделе Действия жизненного цикла) принимает и имя задачи (без учёта регистра) — удобно, когда вы помните morning-digest, но не шестнадцатеричный ID. Точное совпадение по ID имеет приоритет над совпадением по имени; если переданное значение не является ID и имени соответствует сразу несколько задач, команда откажется выполняться и выведет список ID-кандидатов, чтобы вы могли уточнить выбор.
Чат
/cron edit <job_id> --schedule "every 4h"
/cron edit <job_id> --prompt "Use the revised task"
/cron edit <job_id> --skill blogwatcher --skill maps
/cron edit <job_id> --remove-skill blogwatcher
/cron edit <job_id> --clear-skills
Отдельный CLI
hermes cron edit <job_id> --schedule "every 4h"
hermes cron edit <job_id> --prompt "Use the revised task"
hermes cron edit <job_id> --skill blogwatcher --skill maps
hermes cron edit <job_id> --add-skill maps
hermes cron edit <job_id> --remove-skill blogwatcher
hermes cron edit <job_id> --clear-skills
Примечания:
- повторный
--skillзаменяет список подключённых навыков задачи целиком --add-skillдобавляет к уже существующему списку, не заменяя его--remove-skillубирает конкретные подключённые навыки--clear-skillsубирает все подключённые навыки
Действия жизненного цикла
Теперь у cron-задач более полный жизненный цикл, чем просто create/remove.
Чат
/cron list
/cron pause <job_id>
/cron resume <job_id>
/cron run <job_id>
/cron remove <job_id>
Отдельный CLI
hermes cron list
hermes cron pause <job_id_or_name>
hermes cron resume <job_id_or_name>
hermes cron run <job_id_or_name>
hermes cron remove <job_id_or_name>
hermes cron edit <job_id_or_name> [...flags]
hermes cron status
hermes cron tick
Что они делают:
pause— оставить задачу, но перестать её планироватьresume— снова включить задачу. Если у повторяющейся задачи слот подошёл, пока она стояла на паузе, этот слот так и остаётся к исполнению: следующий такт выполнит один наверстывающий запуск (или запишет пропуск в лог приcron.catch_up_missed: false), а не перескочит молча на следующее вхождение; в остальных случаях вычисляется следующий будущий запускrun— запустить задачу на ближайшем такте планировщикаremove— удалить её полностьюedit— изменить расписание, промпт, доставку и т. д.
Поиск по имени. Все четыре изменяющих глагола (pause, resume, run, remove, edit), а также инструмент агента cronjob_manage, теперь принимают имя задачи (без учёта регистра) вместо шестнадцатеричного ID. И агент, и CLI отдают предпочтение точному совпадению по ID, если оно есть; при неоднозначном совпадении по имени (несколько задач с одинаковым именем) операция отклоняется с полным списком ID-кандидатов, чтобы вы могли выбрать нужную явно. Имена не уникальны, поэтому эта защита важна для безопасности — она не даёт незаметно изменить не ту задачу, если две из них делят одно имя.
Пауза для всего сразу: hermes pause
hermes pause [--reason ...] — глобальный аварийный стоп (снимается командой hermes resume). Пока он включён, ни одно запланированное срабатывание cron не стартует, через какую бы дверь оно ни пришло: встроенный тикер пропускает диспетчеризацию, вебхук срабатывания управляемого cron (хостингового планировщика) отвечает 503 с Retry-After: 60, чтобы планировщик повторил доставку после возобновления, а проход наверстывания пропущенных срабатываний простаивает вместо того, чтобы принудительно запустить всё накопившееся. Уже идущие запуски никогда не прерываются, и ничего не теряется: подошедшая по времени работа навёрстывается на первом же такте или проходе после hermes resume. Явные ручные запуски (hermes cron run, кнопка Trigger на дашборде) — это переопределение со стороны оператора, и они выполняются даже во время паузы.
Создание задачи на паузе (безопасный «канарейка»)
Создать канарейку без гонки «создали — потом поставили на паузу»:
hermes cron create "every 1h" "Post the digest" --paused --paused-reason "Awaiting review"
hermes cron resume <job_id>
Флаг --paused в первой же записи под блокировкой сохраняет enabled: false, state: paused, next_run_at: null, метку времени паузы и внятную причину для аудита — и триггер не регистрирует. Без указания причины сохранится «Created paused; awaiting operator approval». Не передавайте --paused, чтобы задача создавалась включённой, как раньше. Флагу --paused-reason нужен --paused; неверные значения отклоняются до сохранения.
Тот же логический paused и необязательная строка paused_reason принимаются в cron.jobs.create_job, в действии create инструмента управления cron, в POST /api/jobs на шлюзе и в POST /api/cron/jobs дашборда. Возобновление планирует следующий будущий запуск. Пауза мешает автоматическим срабатываниям, но не операторским: прежнее поведение явного Run now и принудительного запуска остаётся доступным и способно возобновить и выполнить задачу. Границей безопасности от оператора, который может запускать задачи, это не является.
Управление расписанием со стороны агента (cron-задачи, которые управляют cron-задачами)
По умолчанию агенты, запущенные планировщиком, не могут пользоваться инструментом cronjob_manage — запланированная задача не может создавать, редактировать или удалять другие задачи. Включите это через config.yaml:
cron:
allow_agent_scheduling: true # по умолчанию: false
Когда это включено, запланированный агент может управлять таблицей cron так же, как любая чат-сессия: планировать разовые продолжения прямо из запланированной работы, подстраивать собственную периодичность или запускать задачу-«библиотекаря cron», которая приводит в порядок всю таблицу (сначала список, затем обновление/удаление/создание по необходимости). Два свойства не дают этому выйти из-под контроля:
- Одна плоская таблица, принадлежащая пользователю. Задачи, созданные из cron-запуска, попадают в тот же
jobs.json, что и любые другие, без особого владения — вы можете просматривать, редактировать или удалять их точно так же, как если бы создали их сами. - Никакой доставки в никуда. Cron-запуск эфемерен, поэтому
deliver: originизнутри него разрешается в момент создания в конкретную цель самой создающей задачи (platform:chat_id[:thread_id], либоlocal, если создающая задача никуда не доставляет). Задача, созданная запланированным агентом, никогда не сможет направить вывод в сессию, которой больше не существует. Явные цели (local,all,telegram:<chat_id>) соблюдаются буквально. - Задача может удалить сама себя и всё равно отчитаться. Паттерн «следи за X, скажи один раз и остановись» — повторяющаяся задача, чей запуск вызывает
cronjob(action="remove", job_id=<её собственный id>)и затем отвечает — доставляет этот финальный ответ и записывает запуск какcompleted; запись задачи и её каталогcron/output/<job_id>/после этого исчезают, и финальный запуск туда не пишется. Удаление записи извне запуска (другим процессом или задачей-заменой, переиспользующей тот же id) по-прежнему отбрасывает вывод устаревшего запуска, как и раньше.
Предпочитайте промпты, которые обновляют существующие задачи (сначала список, затем обновление по ID), промптам, которые создают новые задачи при каждом запуске.
Как это работает
Выполнение cron обрабатывается демоном шлюза. Шлюз опрашивает планировщик каждые 60 секунд, запуская все подошедшие по времени задачи в изолированных сессиях агента.
hermes gateway install # Установить как пользовательскую службу
sudo hermes gateway install --system # Linux: системная служба, запускаемая при загрузке, для серверов
hermes gateway # Или запустить в текущем терминале
hermes cron list
hermes cron status
hermes cron status сообщает, жив ли планировщик (процесс шлюза, heartbeat тикера, последний успешный такт), и ближайший запланированный запуск среди активных задач — упорядоченный по реальному моменту, даже если задачи хранят разные смещения UTC. next_run_at, ушедший в прошлое больше чем на 15 минут, никогда не показывается как предстоящий «Next run»: cron status печатает ⚠ Next run <time> is OVERDUE — passed 7h ago but the job has not fired, cron list и /cron list в чате помечают строку как Overdue:, дашборд и панель cron в десктопе (включая карточку Routines у бота) показывают Overdue since, а когда планировщик перестал тикать, status (при лежащем шлюзе) и страница Cron в дашборде ещё и сообщают, когда был последний такт. Это верный признак планировщика, который перестал тикать: перезапустите шлюз (hermes gateway restart), чтобы следующий такт подхватил просроченную задачу, или запустите её сразу через hermes cron run <id>.
Для именованного профиля, который обслуживает мультиплексор профиля по умолчанию, hermes cron status называет этот хост планировщика и показывает здоровье heartbeat самого именованного профиля. При отсутствующем или устаревшем heartbeat команда советует hermes --profile default gateway restart. cron list и cron create тоже предупреждают, если heartbeat отсутствует или устарел; cron status вдобавок проверяет последний успешный такт и сообщает об ошибках тактов.
Хранилище cron недоступно для записи (диск заполнен, монтирование только для чтения, права)
Если хранилище cron (~/.hermes/cron/ или собственный каталог cron/ профиля) не удаётся записать, например потому что диск заполнен (ENOSPC), раздел смонтирован только для чтения (EROFS) или неверны права (EACCES), планировщик не запускает задачу, запуск которой не может зафиксировать. Так после восстановления не случается двойных срабатываний. Тикер при этом продолжает работать: он перестаёт пытаться делать заведомо обречённые записи и примерно раз в минуту перепроверяет хранилище. Сбой заметен в четырёх местах.
hermes cron statusсам проверяет хранилище и начинает вывод со строки⚠ Cron store is NOT writable — scheduled jobs are being skipped. Ниже идут путь к хранилищу, ошибка ОС, последняя успешная запись и число назревших запусков, которые не сработали.hermes cron listпечатает однострочный баннер.hermes doctorпредупреждает, если хранилище недоступно для записи или на его файловой системе меньше 100 МБ свободного места.- Шлюз отправляет в домашние каналы профиля одно уведомление, когда хранилище стало недоступным, и ещё одно, когда оно час оставалось доступным (если хранилище снова отказывает в течение этого часа, новых уведомлений не будет, так что последнее уведомление всегда соответствует состоянию). Оба уведомления учитывают
display.suppress_warning_notificationsи используютdisplay.languageпрофиля. - Мониторинг экспортирует
hermes.cron.store.writable(0, пока хранилище любого профиля, который обслуживает этот шлюз, недоступно для записи, иначе 1) иhermes.cron.store.skipped_runs(сумма по этим хранилищам).
Чтобы это исправить, освободите место на файловой системе с хранилищем, смонтируйте её заново для чтения и записи или поправьте владельца и права каталога хранилища, чтобы пользователь шлюза мог в него писать. Ничего перезапускать не нужно. На ближайшем такте, который сможет записать, каждая задача, остававшаяся назревшей, сработает один раз по обычным правилам наверстывания пропущенных срабатываний, а не по разу за каждый пропущенный такт. Разовая задача, срок которой наступил во время сбоя, сработает один раз, а не истечёт (если только шлюз не перезапустится, пока хранилище недоступно).
Поведение планировщика шлюза
На каждом такте Hermes:
- загружает задачи из
~/.hermes/cron/jobs.json - сверяет
next_run_atс текущим временем - запускает новую сессию
AIAgentдля каждой подошедшей задачи - при необходимости подмешивает в эту новую сессию один или несколько подключённых навыков
- выполняет промпт до завершения
- доставляет итоговый ответ
- обновляет метаданные запуска и время следующего запуска
Файловая блокировка ~/.hermes/cron/.tick.lock предотвращает наложение тактов планировщика друг на друга и повторный запуск одной и той же партии задач.
Воркеры, переживающие перезапуск, под systemd
Когда шлюз работает как служба systemd, каждая подошедшая задача передаётся внешнему процессу-воркеру, который запускается во временном пользовательском scope (systemd-run --user --scope), — поэтому перезапуск шлюза посреди задачи её не убивает. Для создания такого scope нужна пользовательская сессия systemd; хосты без неё (контейнеры, минимальные LXC, служебный пользователь без linger) её обеспечить не могут.
По умолчанию cron в этом случае деградирует: задача всё равно выполняется отдельным внешним процессом с той же передачей исполнения, но без изоляции cgroup, так что перезапуск шлюза во время задачи её убивает (журнал выполнения это фиксирует). Предупреждение пишется один раз на процесс шлюза. Чтобы вместо этого закрываться с ошибкой — пропускать задачу и записывать ошибку в её строку, — задайте:
cron:
require_restart_safe_scope: true
Настоящее решение — пользовательская сессия для пользователя шлюза: sudo loginctl enable-linger <gateway-user> (плюс XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS в юните для установок системного уровня), затем перезапуск шлюза. Kanban-воркеры под управляемым шлюзом требуют scope всегда, независимо от этого ключа: запуск, для которого хост не может выделить scope, записывается на карточку как инфраструктурный сбой и позже повторяется, но карточке не засчитывается (см. документацию Kanban).
Воркер — это собственный интерпретатор шлюза, выполняющий python -m cron.scheduler, причём checkout шлюза жёстко прописан в его PYTHONPATH (вместе с записями, с которыми был запущен сам шлюз), так что он импортирует то же дерево Hermes, на котором работает шлюз, — независимо от того, куда указывает editable-установка в venv, каков WorkingDirectory юнита и включён ли на хосте PYTHONSAFEPATH. Воркер, погибший до подтверждения передачи, записывает хвост собственного stderr в последнюю ошибку задачи и в журнал выполнения, поэтому неудавшийся импорт (или что бы его ни убило) называется по имени, а не остаётся голым кодом выхода.
Журнал выполнения
Hermes записывает каждую подхваченную попытку cron-запуска в локальную для профиля базу ~/.hermes/cron/executions.db ещё до диспетчеризации исполнителю или провайдеру. Попытка проходит через состояния claimed, running и одно из неизменяемых терминальных состояний: completed, failed или unknown. После перезапуска — а также перед каждым ручным hermes cron run / /cron run, чтобы и разовый вызов без работающего планировщика приводил журнал в порядок, — Hermes помечает брошенную попытку как unknown только тогда, когда исходный PID и отпечаток момента старта процесса доказывают, что её владелец действительно исчез, либо когда живой владелец молчит дольше производной границы устаревания (max(3 × HERMES_CRON_TIMEOUT, script timeout, 2 h)). Монитор запуска проставляет попытке progress_at, пока агент вызывает инструменты или стримит ответ, поэтому здоровую многочасовую задачу не отберут посреди выполнения, а воркер, взаимно заблокированный на локе, перестаёт ставить метку и освобождается, когда граница пройдена. Записи unknown — это аудиторские записи, они никогда не перезапускаются автоматически.
Просмотреть недавние попытки можно командой hermes cron runs [job-id] --limit 20 (псевдоним: history). Терминальная история ограничена по объёму; активные попытки никогда не вычищаются. Журнал включается в быстрые резервные копии.
Запланированные попытки вдобавок записывают точный момент, на который были назначены, — отдельно от времени, когда их подхватили. Если старый снимок jobs.json снова взводит срабатывание, которое в сохранённом журнале уже помечено завершённым, Hermes этот повтор пропускает и заново привязывает повторяющиеся задачи. Работает это и тогда, когда снимок старше отметки диспетчеризации, и когда исходный запуск начался с опозданием. Явные ручные запуски идентичность запланированного срабатывания не расходуют.
Гарантией «ровно один побочный эффект» это не является: старые строки без идентичности, вычищенная история, недоступные журналы и прерванные попытки завершение доказать не могут. Откат самого журнала на более старую копию эти свидетельства тоже уносит. Внешние колбэки срабатывания опознают принятую в данный момент заявку в хранилище, а не восходящий запланированный слот, которого в колбэке нет.
Напоминание о проверке при повторных сбоях
Каждая задача отслеживает failure_streak — число подряд идущих неудачных запусков (сбои доставки не учитываются). Запуск, упавший ещё до того, как дело дошло до агента — сломанный импорт после наполовину применённого обновления, клиент провайдера, который не удалось создать, — считается и вызывает предупреждение наравне с тем, где ошибся сам агент. Когда серия сбоев повторяющейся задачи достигает порога, сообщение об ошибке, доставляемое в чат, дополняется напоминанием: оно сообщает, что задача провалилась N раз подряд, и предлагает исправить её, поставить на паузу (hermes cron pause <job>) или удалить. Любой успешный запуск сбрасывает серию, а hermes cron list показывает её длину рядом с последним запуском проблемной задачи. Разовые задачи никогда не получают такое напоминание.
cron:
failure_nudge_threshold: 3 # значение по умолчанию; 0 отключает напоминание
Автоматические повторы, когда модель была недоступна
Повторяющаяся задача, чей запуск упал из-за временной ошибки сети или DNS ещё до первого вызова модели — классический случай: срабатывание сразу после пробуждения компьютера, пока VPN или Wi-Fi ещё переподключаются, — не пропускает целый период. Планировщик перезапускает её автоматически через 5, 15 и 30 минут (идея подсмотрена у повторов запланированных задач в Claude Cowork), а затем возвращается к обычному расписанию. Поскольку ни одного API-вызова сделано не было, повтор ничего не стоит и не может продублировать какой-либо побочный эффект. Повторы не засчитываются и в лимит repeat задачи: вхождение, которое они повторяют, уже было засчитано один раз.
Пока повтор ожидается, промежуточное уведомление о сбое подавляется — вы получите настоящий результат, когда повтор удастся, либо обычное оповещение о сбое, когда лестница повторов исчерпана. Любой запуск, добравшийся до модели (успешный или нет), сбрасывает лестницу. Разовые задачи исключены: их учёт диспетчеризации работает по принципу «не больше N раз», и израсходованная диспетчеризация никогда не воскрешается. Повторы никогда не срабатывают позже следующего собственного вхождения расписания, если оно наступает раньше.
cron:
retry_unreachable: false # по умолчанию true; отключает автоматические повторы
Удержание задачи, пока закрыто окно использования провайдера
Зеркальный случай: провайдер прямо говорит, сколько он ещё будет закрыт. Когда планировщик
разрешает провайдера по подписке (сейчас это проба использования OpenAI Codex), провайдер
сообщает, что лимит использования исчерпан, с подсказкой retry after <N>s (часто это много
часов), а вся цепочка резервных провайдеров недоступна, повторный запуск задачи с периодом
меньше часа в это окно гарантированно упадёт одинаково на каждом такте — и каждый раз пришлёт
оповещение. 429, который API модели возвращает посреди запуска, так не удерживается: его
повторяют в обычном ритме.
Вместо этого планировщик паркует задачу: единственное оповещение о сбое сообщает, что окно
закрыто и задача удерживается. Если провайдер открывается заметно раньше следующего естественного
вхождения редкой cron-задачи (хотя бы на половину периода расписания раньше), заблокированное
запланированное вхождение один раз повторяется на этой границе восстановления; второй сбой по
квоте ждёт уже естественного расписания. Частые расписания, ручные запуски и интервальные задачи
сохраняют свой естественный следующий запуск. В остальных случаях пропущенные вхождения
схлопываются, и next_run_at переезжает на первое запланированное вхождение после окна. Момент
парковки хранится как quota_hold_until; до него ничего не запускается и не оповещает. Любой
запуск, добравшийся до модели, снимает удержание. Разовые задачи не удерживаются.
Инциденты сбоев: одно оповещение, напоминание по кулдауну, подтверждение
Повторяющаяся задача, которая раз за разом падает с одной и той же ошибкой, оповещает вас
один раз, а не на каждом запуске. Каждый сбой записывается как долговременный инцидент —
с ключом из задачи и нормализованной сигнатуры текста ошибки (регистр, пробелы и измеренные длительности вроде idle for 603s игнорируются) — в ту же локальную для профиля
базу-журнал, что и история выполнения. Первый сбой с данной сигнатурой доставляется всегда,
а повторы придерживаются, пока инцидент в состоянии alerted (сам запуск всё равно
записывается — его видят hermes cron runs и серия неудач, придерживается только пинг).
cron:
failure_repeat_alert_hours: 6 # всё ещё сломано спустя столько часов → один пинг-напоминание,
# потом снова тишина; 0 = оповещать на каждом неудачном запуске
Всё, что меняет картину, оповещает сразу: другая ошибка заводит собственный инцидент и пингует немедленно, а успешный запуск заново взводит сигнатуру, так что та же ошибка после зелёного запуска снова вызовет оповещение. Если журнал инцидентов прочитать не удалось, пинг доставляется, а не проглатывается.
hermes cron incidents # список инцидентов (сначала новая активность)
hermes cron incidents --state alerted # фильтр: detected | alerted | resolved | closed
hermes cron incidents ack <id> # подтвердить — заглушить эту сигнатуру насовсем
Подтверждение инцидента заглушает пинг о сбое только для этой конкретной сигнатуры — вместе с напоминаниями. Всё остальное не меняется: история запусков по-прежнему фиксирует каждый сбой, серия неудач продолжает считаться, а в момент, когда задача начинает падать с другой ошибкой, заводится новый инцидент, и оповещения снова срабатывают.
Успешный запуск помечает все открытые инциденты этой задачи как resolved, так что список отражает текущее здоровье, а не каждый сбой за всю историю задачи. Если позже задача упадёт с той же ошибкой, решённый инцидент снова открывается как detected, и вы получаете оповещение заново. Исключение — подтверждённые (closed) инциденты: успех их не трогает, а повтор остаётся тихим.
Жизненный цикл инцидента: detected (сбой зафиксирован) → alerted (хотя бы один пинг о сбое дошёл до доставки; alerted_at — время последнего, от него отсчитывается кулдаун напоминания) → resolved (после этого задача отработала успешно; открывается заново при повторе) или closed (подтверждён; терминальное состояние для этой сигнатуры). Сохранённый текст ошибки предварительно очищается от секретов и обрезается перед записью.
Проверка здоровья парка: hermes cron doctor
hermes cron doctor — это доступная только для чтения проверка здоровья всех активных задач. Она печатает проблемы, сгруппированные по задачам, и завершается с кодом 1, пока остаётся хоть одна находка, включая исторические опоздания и наверстывающие запуски (0, когда находок не осталось).
Успешное наверстывание предупреждение об опоздании не снимает — его снимает следующий запуск вовремя. Поэтому сторож вроде hermes cron doctor || alert может продолжать оповещать целый интервал расписания после пробуждения хоста, даже если наверстывание прошло успешно.
hermes cron doctor
Что проверяется для каждой активной задачи:
- последний запуск завершился сбоем (
last_statusне ok, с записанной ошибкой), - последняя доставка не удалась (результат был получен, но так и не дошёл до вас),
- последний запуск опоздал или наверстал пропущенное расписание (
last_dispatch); предупреждение снимается при следующем срабатывании вовремя, - запланированное срабатывание не дошло до исполнителя (
last_fire_error) — с записанной меткой времени и сокращённой причиной; предупреждение снимается после успешного запуска, next_run_atотсутствует или застрял в прошлом за пределами 15-минутного окна допуска тикера — сигнал «задача молча не срабатывает» (планировщик мёртв, шлюз не работает, или заклинило захват срабатывания),- скрипт отсутствует, не является файлом или разрешается за пределами
HERMES_HOME/scripts, - задача с
no_agentбез указанного скрипта, - заданный
workdir, которого больше не существует.
Doctor никогда не меняет задачи или состояние — он только отчитывается. При разборе проблемной задачи сочетайте его с hermes cron incidents (долговременные записи о сбоях) и hermes cron runs (журнал попыток).
Варианты доставки
При планировании задачи вы указываете, куда пойдёт результат:
| Опция | Описание | Пример |
|---|---|---|
"origin" | Обратно туда, где была создана задача | По умолчанию для мессенджеров |
"local" | Сохранить только в локальные файлы (~/.hermes/cron/output/) | По умолчанию для CLI |
"telegram" | Домашний канал Telegram | Использует TELEGRAM_HOME_CHANNEL |
"telegram:123456" | Конкретный чат Telegram по ID | Прямая доставка |
"telegram:-100123:17585" | Конкретная тема Telegram | Формат chat_id:thread_id |
"discord" | Домашний канал Discord | Использует DISCORD_HOME_CHANNEL |
"discord:#engineering" | Конкретный канал Discord | По имени канала |
"slack" | Домашний канал Slack | |
"whatsapp" | Домашний WhatsApp | |
"signal" | Signal | |
"matrix" | Домашняя комната Matrix | |
"mattermost" | Домашний канал Mattermost | |
"email" | ||
"sms" | SMS через Twilio | |
"homeassistant" | Home Assistant (плагин) | Использует HASS_HOME_CHANNEL; нужен плагин homeassistant |
"dingtalk" | DingTalk | |
"feishu" | Feishu/Lark | |
"wecom" | WeCom | |
"weixin" | Weixin (WeChat) | |
"bluebubbles" | BlueBubbles (iMessage) | |
"qqbot" | QQ Bot (Tencent QQ) | |
"bot-chat" | Канонический Bot Chat этого профиля — бот читает результат и отвечает | Только в пределах машины |
"bot-chat:research" | Bot Chat другого локального профиля | Проверяется при создании задачи |
"all" | Разослать во все подключённые домашние каналы | Определяется в момент запуска |
"telegram,discord" | Разослать по конкретному набору каналов | Список через запятую |
"origin,all" | Доставить в исходный чат плюс во все остальные подключённые каналы | Можно комбинировать любые токены |
Итоговый ответ агента доставляется автоматически на настроенную цель deliver: — агент не отправляет сообщения самостоятельно, так что вызывать в cron-промпте нечего.
На выходе доставляемый вывод очищается от секретов на каждой дорожке: сообщение на платформу,
зеркало сессии (сам текст и вклеенное вокруг него имя задачи) и ход bot-chat. Похожие на
учётные данные строки (API-ключи с префиксом вендора, токены, присваивания KEY=value)
маскируются даже при security.redact_secrets: false — эта настройка управляет вашими
собственными логами, а не тем, что покидает машину, — а если сломался сам вычищатель, вывод
заменяется, а не отправляется непроверенным. Параметры запроса в URL с «учётными» именами не
вырезаются (magic-ссылки и заранее подписанные URL — законный вывод cron), а выбранные
пользователем секреты без узнаваемой формы не распознаются. Документ запуска в
cron/output/<job_id>/ хранит ответ агента в исходном виде.
Сбой доставки — это отдельный статус
Выполнение и доставка учитываются раздельно. Когда прогон агента прошёл успешно,
а вывод до цели не добрался (5xx у платформы, лимит частоты, протухшая сессия,
адаптер не дал положительных свидетельств отправки), задача записывает
last_status: delivery_failed — но никогда не простое ok, — а причину кладёт
в last_delivery_error. Команда hermes cron list показывает это жёлтым как
delivery_failed: <причина>, hermes cron doctor сообщает о проблеме доставки,
а ручной cronjob run возвращает success: false с ошибкой доставки. В
failure_streak задачи сбой доставки не засчитывается — агент своё отработал;
первый же полностью успешный прогон вернёт статус в ok.
Доставка в Bot Chat (bot-chat)
bot-chat кладёт результат в канонический чат профиля («Bot Chat») настоящим сообщением. У всех остальных целей получатель — человек, читающий канал; здесь получатель — сам бот: он принимает результат как входящее сообщение, отрабатывает то, что требует действий, и отвечает у себя в чате. Берите этот вариант, когда вывод по расписанию нужно обработать, а не просто куда-то опубликовать.
bot-chatбез уточнений адресует собственный профиль задачи.bot-chat:<profile>адресует другой профиль на той же машине. Имена сверяются сhermes profile listпри создании задачи; профили на других шлюзах и машинах целью стать не могут, поэтому одинаковые имена профилей на разных машинах не создают путаницы.- Каждая доставка стоит боту-получателю одного полного хода агента — не частите с расписанием.
- Комбинируется с другими целями (
bot-chat,telegram), но вallне входит никогда. - Если канонический чат открыт в бэкенде десктопа или TUI с поддержкой почтового ящика, доставка сразу становится устойчиво поставленной в очередь — простаивает бот или занят. Входящий ход выполняет только этот живой владелец, конкурирующего писателя из CLI cron не поднимает. Если чат держит владелец только из CLI или старый, не поддерживающий ящик, cron сохраняет так и не начатый вывод в
cron/bot_chat_pending/<receipt-id>.jsonу отправляющего профиля. Последующие такты планировщика доставят его после того, как этот владелец отпустит чат, — в порядке приёма. Отложенная работа сохраняет принятый домашний каталог назначения и ID квитанции, даже если корень запуска планировщика изменился; отсутствующее или переименованное назначение не создаётся заново и не переразрешается в другой профиль. Отложенная запись со статусомtransferredуказывает на квитанцию живого владельца, а не на провалившийся ход. Записи с повреждённым JSON сохраняются и логируются, не блокируя остальные выводы в очереди. Без владельца остаётся прежняя дорожкаhermes chat -c "Bot Chat" --create-if-missing(обычные проверки владения сессией при этом действуют). Такой дочерний процесс использует ровно тот домашний каталог назначения, который cron уже проверил, включая пользовательские корни; унаследованныйHOMEили сменившийся активный профиль перенаправить его не могут. Всё его окружение — это окружение профиля назначения, каким его собрал бы отдельныйhermes -p <profile>: настройки.envотправляющего шлюза, переданная политикаTERMINAL_*, проверки авторизации платформ и учётные данные отбрасываются, а поверх накладываются собственные секреты назначения. Отсутствующий каталог назначения отклоняется до запуска, а не создаётся заново. Отложенный запрос забирается (claimed) до запуска этой дорожки; прерывание или неопределённый результат подпроцесса никогда не приводят к автоматической повторной отправке. - У не начатых выводов нет TTL: если неподдерживающий владелец так и не отпустит чат, они остаются в очереди, а не отбрасываются молча. Квитанции хранят свои полезные данные бессрочно. Неожиданное исключение при доставке логируется и сохраняется как
ambiguous, не останавливая соседние доставки в том же проходе; попытки со статусом claimed/ambiguous автоматически никогда не проигрываются повторно. - Queued — это не выполнено. Cron записывает ID квитанций и статусы
queued/claimedвlast_delivery_queued, а исходом доставки ставитqueued— ни доставлено, ни провалено. Успешная задача показываетdelivery_queued; настоящие ошибки на других целях по-прежнему перевешивают и дают сбой доставки. Бот может закончить позже. Авторитетна устойчивая квитанция вruntime/bot_live_delivery/<receipt-id>.jsonцелевого профиля; исторический статус cron сам собой не обновляется. - Повторная проверка того же запуска смотрит в его существующую квитанцию, даже если владелец исчез. После приёма откат на другого писателя не случается никогда. Квитанции
failed,cancelledиambiguousавтоматически не проигрываются: прежде чем осознанно начинать новую работу, посмотрите чат и квитанцию. У каждого нового запуска cron свой отдельный ID доставки.
Маршрутизирующее намерение (all)
all позволяет отправить одну cron-задачу во все настроенные вами каналы мессенджеров, не перечисляя их по имени. Значение разрешается в момент запуска, так что задача, созданная до того, как вы подключили Telegram, подхватит Telegram на следующем такте после того, как вы зададите TELEGRAM_HOME_CHANNEL.
Семантика: all разворачивается в каждую платформу с настроенным домашним каналом. Ноль каналов — это нормально; задача просто не производит целей доставки и фиксируется выше по цепочке как сбой доставки.
all комбинируется с явными целями. origin,all доставляет в исходный чат плюс во все остальные подключённые домашние каналы, устраняя дубликаты по (platform, chat_id, thread_id).
Тема Telegram для cron (TELEGRAM_CRON_THREAD_ID)
Когда включён режим тем Telegram, корневой личный чат зарезервирован как системное лобби — ответы, отправленные туда, отклоняются напоминанием о лобби, а reply_to_message_id сбрасывается, так что ответить на cron-сообщение, попавшее в основной чат, нельзя.
Вместо этого направьте cron в отдельную тему форума:
- В Telegram откройте личный чат с ботом и создайте тему, например
Cron. Долгое нажатие на заголовок темы → Copy link; конечное число в ссылке — этоmessage_thread_idтемы. - Задайте
TELEGRAM_CRON_THREAD_ID=<этот id>в вашем.env.
Это применяется только к доставке cron. TELEGRAM_HOME_CHANNEL_THREAD_ID (используется в других местах, например для уведомлений о перезапуске) не меняется. Явные цели deliver="telegram:chat_id:thread_id" по-прежнему имеют приоритет над переменной окружения. Ответы на cron-сообщения теперь попадают в уже существующую сессию темы, так что вы можете реагировать на них напрямую.
Обёртка ответа
По умолчанию доставленный cron-вывод оборачивается заголовком и подвалом, чтобы получатель понимал, что это результат отложенной задачи:
Cronjob Response: Morning feeds
-------------
<agent output here>
Note: The agent cannot see this message, and therefore cannot respond to it.
Чтобы доставлять необработанный вывод агента без обёртки, установите cron.wrap_response в false:
# ~/.hermes/config.yaml
cron:
wrap_response: false
Push-уведомления (cron.delivery.notify)
Вывод cron — это финальная доставка, а не сообщение о ходе работы, поэтому по
умолчанию он уходит с поднятым флагом уведомления. В Telegram это значит, что
сводка вызывает push даже при режиме уведомлений important у адаптера (иначе
она ушла бы с disable_notification=true, и пользователи описывают такую тихую
сводку словами «вообще не пришла»). Вернуть тихие доставки:
# ~/.hermes/config.yaml
cron:
delivery:
notify: false # по умолчанию: true
Флаг едет и с текстовой отправкой, и с вложениями, так что прогон не может звякнуть на одном и промолчать на другом.
Подтверждение доставки и состояние UNVERIFIED
Доставка через живой адаптер записывается как состоявшаяся только при
положительных свидетельствах от адаптера: явный success, который не является
отфильтрованным отбросом (delivered: false), плюс message_id или
raw_response. Результат с success, но без обоих свидетельств — так отвечают
адаптеры Slack, Matrix и Mattermost — всё равно принимается (доказательством
сбоя он не является), но прогон записывается в задачу как
last_delivery_unverified и всплывает в hermes cron list:
⚠ Delivery UNVERIFIED: adapter acked slack:C0123456 without message_id/raw_response
а в hermes cron doctor — как last delivery unverified (...). Метку снимает
следующий прогон, доставивший вывод со свидетельствами. Пустую полезную нагрузку
(ни текста, ни медиа) адаптеру не отдают вовсе: такая доставка закрывается
отказом и попадает в last_delivery_error, а не в журнал доставленных.
Продолжаемые задачи (ответ на доставку cron)
По умолчанию доставка cron — это разовое сообщение без обратной связи: сообщение отправляется, но не сохраняется в истории беседы чата, поэтому если вы ответите на него, у агента не будет записи о том, что он сказал. Сделайте задачу продолжаемой — и доставленная сводка станет беседой, в которую можно ответить: агент увидит сводку в контексте, вместо того чтобы спрашивать «что за Задача №2?».
Опция отключена по умолчанию, включается по желанию. Включите глобально в конфиге или для отдельной задачи через параметр attach_to_session инструмента cronjob (он переопределяет глобальную настройку для этой конкретной задачи):
# ~/.hermes/config.yaml
cron:
mirror_delivery: false # установите true, чтобы сделать доставки cron продолжаемыми
Поведение предпочитает треды (потоки) и ограничено собственной беседой задачи:
- Платформы с поддержкой тредов (темы Telegram, треды Discord/Slack/Matrix): каждая доставка открывает собственный выделенный тред, и сводка загружается в сессию этого треда, так что ответ внутри треда продолжает беседу с полным контекстом. Повторяющаяся задача (например, ежедневная сводка) открывает новый тред при каждом запуске, изолируя обсуждение каждой доставки.
- Платформы только с личными сообщениями (WhatsApp, Signal, SMS): тредов не существует, поэтому сводка зеркалируется в сессию целевого личного чата — исходного либо домашнего, если задача идёт по резерву или адресована просто платформе, — и сам личный чат становится поверхностью для продолжения.
Затрагивается только собственная беседа задачи:
- исходный чат, в котором задача была создана;
- резервный домашний канал — когда
deliver: originне поймал происхождение (задачи, созданные скриптами или из сессии на платформе запрос/ответapi_server, которая не может принять доставку) — тогда роль исходного чата берёт на себя основная беседа пользователя; - единственная явная цель
platform:chatзадачи, но только если сама задача включаетattach_to_session: true— тем самым автор задачи объявляет эту цель беседой. Один лишь глобальный флагmirror_deliveryникогда не делает явно адресованный чат продолжаемым.
Широковещательные раскрытия (all) продолжаемыми не становятся никогда. А вот написанное вами просто имя платформы (deliver: slack) адресует её домашний канал осознанно и подчиняется тем же правилам, что и резерв по домашнему каналу выше. После обновления существующие задачи с deliver: <платформа> и cron.mirror_delivery: true могут открывать новый тред на каждый запуск там, где платформа треды поддерживает. Чтобы отказаться от такого поведения, поставьте задаче attach_to_session: false.
Зеркальная копия записывается как помеченная реплика пользователя ([Cron delivery: <task name>]), что сохраняет чередование в истории беседы безопасным для всех провайдеров моделей.
Плоское продолжение в канале (Slack)
Описанное выше поведение с предпочтением тредов открывает выделенный тред при каждой доставке. Если вы хотите, чтобы продолжаемая задача выводилась плоско, в общей ленте канала — без треда, установите для Slack поверхность продолжения в in_channel:
# ~/.hermes/config.yaml
slack:
cron_continuable_surface: in_channel # по умолчанию: thread
reply_in_thread: false # обязательная пара (см. ниже)
require_mention: false # чтобы обычный ответ продолжал задачу
В режиме in_channel сводка доставляется как обычное сообщение верхнего уровня в канале (тред не открывается), а ваш ответ продолжает задачу через общую сессию канала. Вместе работают три настройки:
cron_continuable_surface: in_channel— пропускает создание треда при доставке.reply_in_thread: false(обязательно) — заставляет бота отвечать на ваш ответ плоско, в канале, и привязывать его к той же общей сессии канала, в которую была загружена сводка. Без этого продолжение всё равно работает, но приходит в треде (безопасный откат к продолжению в стиле треда, а не потерянный ответ — шлюз выводит предупреждение при запуске, чтобы вы заметили несоответствие).require_mention: false(или добавление канала вfree_response_channels) — чтобы можно было ответить обычным сообщением; иначе бот реагирует только когда вы упоминаете его через@в каждом ответе.
Поскольку продолжение происходит в сессии всего канала, она общая: остальная переписка в канале — и вторая продолжаемая in-channel-задача — присоединяются к той же непрерывной беседе. Это неотъемлемое свойство «плоского режима в канале» и тот же компромисс, на который уже соглашаются пользователи reply_in_thread: false; используйте поверхность thread по умолчанию, если хотите изолировать обсуждение каждой доставки.
Сегодня эта возможность доступна только в Slack. Другие платформы принимают этот ключ, но откатываются к поверхности thread (их примитивы продолжения отличаются); выбор делается для каждой платформы отдельно, в её собственной секции конфига. Это флаг конфигурации на стороне шлюза — /restart подхватывает его; переустанавливать приложение Slack не нужно.
cron_continuable_surface — это настройка канала: в личном чате 1:1 нет разделения «тред против ленты», между которым нужно выбирать (личный чат уже плоский), поэтому там этот ключ не действует. Продолжаемость cron-доставки в личном чате определяется отдельной, уже существующей настройкой slack.dm_top_level_threads_as_sessions:
false— все личные сообщения верхнего уровня делят одну общую сессию личного чата, так что продолжаемая cron-сводка и ваш ответ попадают в одну и ту же сессию, и задача продолжается в контексте. Именно это нужно для продолжаемого cron в личном чате.true(по умолчанию) — каждое сообщение личного чата верхнего уровня — это отдельная сессия, так что ответ на доставленную сводку начинает новую сессию, у которой нет записи о сводке. В этом режиме продолжение не работает (ни для cron, ни для любой другой плоской доставки).
Так что для продолжаемой cron-задачи, доставляемой в личный чат 1:1, установите slack.dm_top_level_threads_as_sessions: false. cron_continuable_surface для личных чатов не требуется (и игнорируется).
Тихое подавление
Если итоговый ответ агента содержит [SILENT], доставка полностью подавляется. Результат всё равно сохраняется локально для аудита (в ~/.hermes/cron/output/), но сообщение на цель доставки не отправляется.
Это полезно для мониторинговых задач, которые должны сообщать только тогда, когда что-то не так:
Check if nginx is running. If everything is healthy, respond with only [SILENT].
Otherwise, report the issue.
Неудачные запуски всегда доставляются вне зависимости от маркера [SILENT] — заглушить можно только успешные запуски. Для тихих мониторинговых задач попросите агента отвечать только [SILENT], когда сообщать не о чем.
Объявить запуск неудачным
Неудачным запуск помечают только сбои времени выполнения (исключения, тайм-ауты, недоступная модель). Когда агент
сам завершил ход, но работа не сделана — например, упал делегированный субагент или запущенный им скрипт, —
он может объявить запуск неудачным, поставив [CRON_FAILURE] отдельно на первой строке ответа, а дальше
дать объяснение:
[CRON_FAILURE]
The nightly export subagent exited with "disk full"; no report was produced.
Тогда запуск записывается как неудачный (last_status, серия сбоев, hermes cron runs и hermes cron incidents
отражают это), а уведомление о сбое доставляется, как и при любом другом провале. Полный ответ по-прежнему сохраняется
в ~/.hermes/cron/output/ для разбора. Маркер строгий: если [CRON_FAILURE] упомянут или процитирован где-то
ещё в отчёте, запуск остаётся успешным. Скриптовые задачи (no_agent) его игнорируют — скрипт сигнализирует о сбое
ненулевым кодом выхода.
Тайм-аут скрипта
Скрипты, выполняемые перед запуском (подключаются через параметр script), имеют тайм-аут по умолчанию 3600 секунд (1 час). Это ограничение действует только на скрипт: задачи на основе навыков / LLM работают по отдельному бюджету неактивности и этим значением не ограничены. Если вашим скриптам нужен другой лимит, его можно изменить:
# ~/.hermes/config.yaml
cron:
script_timeout_seconds: 1800 # 30 минут
Либо задайте переменную окружения HERMES_CRON_SCRIPT_TIMEOUT. Порядок разрешения: переменная окружения → config.yaml → значение по умолчанию 3600 с.
Cron также ограничивает по времени очистку сессии и агентских ресурсов после запуска. Это происходит уже после возврата хода LLM, поэтому не связано с тайм-аутом неактивности. По умолчанию — 10 секунд на операцию очистки. Если финализатор хранилища или клиента перестаёт отвечать, планировщик записывает ошибку в лог, снимает блокировку «задача выполняется» и разрешает диспетчеризацию последующих запусков вместо того, чтобы пропускать эту задачу навсегда.
# ~/.hermes/config.yaml
cron:
cleanup_timeout_seconds: 10
Установите cleanup_timeout_seconds: 0, только если хотите вернуть старое неограниченное по времени поведение очистки.
Тайм-аут отправки медиа
Когда доставка cron включает медиавложения (сгенерированный PDF, TTS-аудио, экспортированный отчёт) через живой адаптер шлюза, каждая загрузка вложения ограничена по времени — по умолчанию 300 секунд. Крупным файлам на медленных каналах может потребоваться больше:
# ~/.hermes/config.yaml
cron:
media_send_timeout_seconds: 600 # 10 минут на вложение
Либо задайте переменную окружения HERMES_CRON_MEDIA_SEND_TIMEOUT. Порядок разрешения: переменная окружения → config.yaml → значение по умолчанию 300 с. Вложение, не уложившееся в тайм-аут, фиксируется в статусе запуска задачи как частичный сбой доставки (текст всё равно доставляется).
Тайм-аут доставки в Bot Chat
Доставка через bot-chat запускает полный ход агента в чате бота-получателя, поэтому её предел измеряется минутами, а не секундами — по умолчанию 600 с:
# ~/.hermes/config.yaml
cron:
bot_chat_delivery_timeout_seconds: 900
Доставка, не уложившаяся в тайм-аут, попадает в last_delivery_error; при этом ход бота вполне может завершиться сам по себе.
Лимит ограничивает только ход бота. Если в этом ходе бот пишет коллеге (message_agent), процесс доставки после этого остаётся жив — в пределах terminal.oneshot_completion_wait_seconds, — чтобы ответ коллеги успел попасть в Bot Chat; это ожидание не входит в доставку, в лимит не засчитывается и им не обрывается.
Тайм-аут автономной отправки
Когда живой адаптер шлюза не может доставить сообщение (или шлюз вообще не запущен), цель получает его через автономный отправитель платформы. Такая отправка ограничена тайм-аутом по реальному времени — по умолчанию 60 секунд, — чтобы транспорт посреди переподключения не мог бесконечно держать запуск задачи (а за ним и ожидающий слив перед перезапуском):
# ~/.hermes/config.yaml
cron:
standalone_send_timeout_seconds: 120
Отправка, не уложившаяся в тайм-аут, записывается в last_delivery_error как standalone send to <target> timed out after Ns; сообщение всё же может дойти, если адаптер уже успел его принять.
Режим без агента (задачи только со скриптом)
Для повторяющихся задач, не требующих рассуждений LLM — классические сторожевые скрипты (watchdog), оповещения о диске/памяти, heartbeat-проверки, пинги CI, — передайте no_agent=True при создании. Планировщик запускает ваш скрипт по расписанию и доставляет его stdout напрямую, полностью минуя агента:
hermes cron create "every 5m" \
--no-agent \
--script memory-watchdog.sh \
--deliver telegram \
--name "memory-watchdog"
Семантика:
- stdout скрипта (обрезанный) → доставляется дословно как сообщение.
- Пустой stdout → тихий такт, доставки нет. Это и есть паттерн watchdog: «сообщать только когда что-то не так».
- Ненулевой код выхода или тайм-аут → доставляется оповещение об ошибке, так что сломанный watchdog не может отказать молча.
{"wakeAgent": false}в последней строке → тихий такт (тот же гейт, что используют LLM-задачи).- Ни токенов, ни модели, ни резервного провайдера — задача вообще не обращается к слою инференса.
Файлы .sh / .bash выполняются через bash из PATH, если он доступен, иначе через /bin/bash (это важно в Windows Git Bash). Всё остальное запускается через текущий интерпретатор Python (sys.executable). Скрипты должны разрешаться внутри $HERMES_HOME/scripts/ — относительные имена, абсолютные пути и пути с ~ принимаются, если итоговый путь остаётся внутри этой директории; пути, выходящие за её пределы, отклоняются. Python-скрипту (script или monitor_script) можно также закрепить venv, которым управляете вы сами (для пакетов, которых нет в среде Hermes): передайте --interpreter ~/venvs/.../bin/python при создании или редактировании задачи, см. Использование собственного окружения Python. Venv, которым управляет Hermes, остаётся под его управлением: ничего не устанавливается и не восстанавливается автоматически. Окружение подпроцесса очищается, поэтому учётные данные API провайдеров и другие секреты, которыми управляет Hermes, cron-скриптам не передаются.
Как передать скрипту учётные данные
Скрипт, которому нужно авторизоваться во внешнем сервисе (API-токен, ключ сервисного аккаунта), получает их так же, как дочерние процессы терминала и execute_code: объявите имя переменной в config.yaml профиля-владельца, а значение задайте в .env этого профиля (или во внешнем источнике секретов):
terminal:
env_passthrough:
- MY_SERVICE_TOKEN
Переменная передаётся в окружение скрипта со значением профиля-владельца: для задачи профиля, который обслуживает многопрофильный шлюз или бэкенд десктопа/дашборда, значение разрешается через область секретов этого профиля, а не из окружения процесса профиля, с которого всё запущено, и учётные данные из .env одного профиля никогда не попадают в скрипты другого. Учётные данные провайдеров, которыми управляет Hermes (OPENAI_API_KEY, токены шлюза, …), объявить нельзя — очиститель их отвергает. На однопрофильной установке скрипт, как и раньше, наследует то, что .env шлюза положил в окружение процесса. Логируйте только наличие (set/MISSING), но не само значение: вывод скрипта доставляется дословно.
Агент настраивает это за вас
Схема инструмента cronjob_manage открывает no_agent напрямую для Hermes, так что вы можете описать watchdog в чате и позволить агенту всё настроить самому:
Ping me on Telegram if RAM is over 85%, every 5 minutes.
Hermes запишет скрипт проверки в ~/.hermes/scripts/ через write_file, а затем вызовет:
cronjob(action="create", schedule="every 5m",
script="memory-watchdog.sh", no_agent=True,
deliver="telegram", name="memory-watchdog")
Он автоматически выбирает no_agent=True, когда содержимое сообщения полностью определяется скриптом (watchdog-проверки, пороговые оповещения, heartbeat). Тот же инструмент также позволяет агенту ставить на паузу, возобновлять, редактировать и удалять задачи — так что весь жизненный цикл управляется через чат, без обращения к CLI.
Рабочие примеры смотрите в руководстве по cron-задачам только со скриптом.
Связывание задач через context_from
Cron-задачи выполняются в изолированных сессиях без памяти о предыдущих запусках. Но иногда результат одной задачи — это ровно то, что нужно следующей. Параметр context_from автоматически устанавливает эту связь: в промпт Задачи B во время выполнения подставляется самый свежий результат Задачи A.
# Задача 1: Сбор сырых данных
cronjob(
action="create",
prompt="Fetch the top 10 AI/ML stories from Hacker News. Save them to ~/.hermes/data/briefs/raw.md in markdown format with title, URL, and score.",
schedule="0 7 * * *",
name="AI News Collector",
)
# Задача 2: Отбор — получает результат Задачи 1 как контекст
# Получите ID Задачи 1 из: cronjob(action="list")
cronjob(
action="create",
prompt="Read ~/.hermes/data/briefs/raw.md. Score each story 1–10 for engagement potential and novelty. Output the top 5 to ~/.hermes/data/briefs/ranked.md.",
schedule="30 7 * * *",
context_from="<job1_id>",
name="AI News Triage",
)
# Задача 3: Публикация — получает результат Задачи 2 как контекст
cronjob(
action="create",
prompt="Read ~/.hermes/data/briefs/ranked.md. Write 3 tweet drafts (hook + body + hashtags). Deliver to telegram:7976161601.",
schedule="0 8 * * *",
context_from="<job2_id>",
name="AI News Brief",
)
Как это работает:
- Когда срабатывает Задача 2, Hermes читает самый свежий результат Задачи 1 из
~/.hermes/cron/output/{job1_id}/*.md - Этот результат автоматически подставляется перед промптом Задачи 2
- Задаче 2 не нужно жёстко прописывать «прочитай этот файл» — она получает содержимое как контекст
- Цепочка может быть любой длины: Задача 1 → Задача 2 → Задача 3 → …
Что принимает context_from:
| Формат | Пример |
|---|---|
| Один ID задачи (строка) | context_from="a1b2c3d4" |
| Несколько ID задач (список) | context_from=["job_a", "job_b"] |
Результаты объединяются в перечисленном порядке.
Непрерывность: передавать вывод предыдущего запуска
Установите continuity=true — и задача будет подмешивать свой собственный самый свежий вывод в каждый запуск. Повторяющиеся задачи обычно начинают каждый запуск с чистого листа: сборщик новостей заново рассказывает про те же истории, монитор заново оповещает об одном и том же условии. С включённой непрерывностью задача «просыпается», уже видя, что она сообщила в прошлый раз, и может дедуплицировать данные и продолжить с того места, на котором остановилась:
cronjob(
action="create",
prompt="Scan HN and arXiv for new agent-tooling papers. Report only items NOT already covered in your previous run's output.",
schedule="every 6h",
continuity=True,
name="Agent Tooling Scout",
)
У первого запуска нет предыдущего вывода, так что промпт выполняется как есть. При выборе контекста пропускаются тихие тики мониторинга (no_change), пустой вывод и аудиторские записи с wakeAgent=false, поэтому спокойный период сохраняет последний содержательный вывод. Сами файлы аудита с диска не убираются. Документы с ошибками из выборки не исключаются — они дают следующему запуску контекст для восстановления; фильтром «только успешные» это не является. В последующих запусках предыдущий вывод подставляется перед промптом с обрамлением о непрерывности («не повторяй то, что уже было сообщено»). Это свободно сочетается с вышестоящими задачами (context_from=["<other_job_id>"] плюс continuity=true), а continuity=false при обновлении отключает эту функцию, сохраняя остальные записи context_from. Внутри флаг хранится как зарезервированная запись self в context_from.
Из CLI: hermes cron create "every 6h" "Scan for news" --continuity, а hermes cron edit <job_id> --continuity / --no-continuity переключает её для уже существующей задачи. Тот же переключатель есть в редакторе cron на дашборде и в диалоге режима Bot Mode десктопного приложения.
Когда это использовать:
- Многоэтапные конвейеры (сбор → фильтрация → форматирование → доставка)
- Зависимые задачи, где работа шага N зависит от результата шага N−1
- Паттерны разветвления и слияния (fan-out/fan-in), где одна задача агрегирует результаты нескольких других
- Повторяющиеся сборщики/мониторы, которым нужно дедуплицировать данные относительно своего же предыдущего отчёта (
continuity=true)
Восстановление провайдера
Если основной API-ключ упирается в ограничение по частоте запросов или провайдер возвращает ошибку, cron-агент может:
- Перейти на следующие учётные данные в вашем пуле учётных данных для того же провайдера. Это работает для любой задачи, закреплённой или нет.
- Переключиться на альтернативного провайдера из
fallback_providers(или устаревшегоfallback_model) вconfig.yaml— только для незакреплённых задач. Это касается и сбоя при разрешении учётных данных до старта запуска, и ошибки провайдера посреди запуска.
Задача с собственными provider, model или base_url (заданными через --provider / --model, --pin, дашборд или jobs.json) никогда не уходит на глобальную цепочку. Закрепление говорит, по какому маршруту работает задача, а резервная запись — это другой провайдер и обычно другая модель, поэтому при сбое закреплённого маршрута запуск падает, и оповещение о сбое об этом сообщает. То же правило делегирование субагентам применяет к закреплённому потомку. Чтобы у задачи оставался резерв, не закрепляйте её: она пойдёт за cron.model / cron.model_provider (или основной моделью) и пройдёт по цепочке, как любая незакреплённая задача.
До этого правила закреплённая задача, чей провайдер упал, могла отработать на первой рабочей записи fallback_providers — с однострочной пометкой в выводе. Если вы на это полагались, снимите закрепление (hermes cron edit <job_id> --unpin) и задайте модель через cron.model.
Поэтому один упёршийся в лимит ключ не валит запуск, если для того же провайдера есть другие учётные данные, а незакреплённые задачи при настроенной цепочке переживают и простой провайдера.
Сбои запуска (last_error)
Сорвавшийся запуск агента записывает краткий last_error — он виден в списках задач и в
/cron list, причём шаблоны учётных данных и логины-пароли в URL вычищаются (в том числе в
ранее сохранённых ошибках). Это отдельное поле от last_fire_error (передача от
планировщика) и last_delivery_error (доставка). Когда сорвался сам агент, те поля
справедливо остаются пустыми.
При сбое соединения смотрите документ запуска в cron/output/<job_id>/ внутри активного
домашнего каталога Hermes. В его разделе ## Error лежит сцепленный traceback — с той же
вычисткой учётных данных и логинов в URL. Файл сохраняет прежние приватные права на файлы
вывода; локальные переменные из traceback не снимаются. В уведомлениях о доставке и в
last_error остаётся краткая ошибка, а не полный traceback. Прежде чем чем-то делиться,
просмотрите диагностику: вычистка не гарантирует, что произвольные данные приложения
не окажутся чувствительными.
Пропущенные срабатывания по расписанию (last_fire_error)
В хостинговых развёртываниях (управляемый cron) запланированное срабатывание проходит путь от планировщика платформы через дашборд до внутреннего API-сервера шлюза. Если эта последняя передача не удаётся — процесс шлюза не работает, или его слушатель API-сервера так и не запустился, — запуск вообще не начинается, поэтому не остаётся ни записи о выполнении, ни last_status, который можно было бы проверить. Характерный признак: задача срабатывает каждый раз, когда вы запускаете её вручную, но никогда не срабатывает автоматически.
Такие пропуски фиксируются в записи задачи как last_fire_error (метка времени + причина) и показываются:
- инструментом
cronjob_manage→action: "list"— полеlast_fire_error - командой
hermes cron list— красным предупреждением о пропущенном срабатывании под задачей - командой
hermes cron doctor— находкой о пропущенном срабатывании для задачи, из-за которой команда завершается с кодом1 - в представлении задачи на дашборде
Отметка всегда отражает текущее состояние автозапуска: она перезаписывается более новыми пропусками и автоматически сбрасывается следующим успешным запуском. Если вы её видите, с самой задачей и её расписанием всё в порядке — внимания требует сторона шлюза в цепочке срабатывания (чаще всего достаточно перезапустить шлюз через его супервизор, чтобы он загрузил полное окружение профиля: hermes gateway restart).
Локальная политика пропущенных запусков
Если шлюз был выключен (или перезапускался), когда подошло время повторяющейся задачи, после возвращения планировщика задача навёрстывает один раз: слот, пропущенный в промежутке перезапуска, срабатывает ровно один раз, слот, который уже отработал до перезапуска, повторно не запускается никогда, а долгий простой схлопывается в один запуск вместо запуска на каждый пропущенный слот. Задачи на паузе не навёрстывают никогда. Каждое такое наверстывание видно в hermes cron list как ⚠ late / ⚠ catch-up after missed fire.
Чтобы не получать эту наверстывающую нагрузку после плановой остановки шлюза, задайте:
cron:
catch_up_missed: false # по умолчанию: true
Или выполните hermes config set cron.catch_up_missed false. С этим отказом повторяющаяся задача, опоздавшая сильнее своего существующего окна ожидания (половина периода, ограниченная диапазоном от 120 секунд до 2 часов), переякоривается на следующее будущее вхождение без запуска прямо сейчас. Пропуск логируется. Задачи в пределах окна ожидания и явные ручные запуски работают как обычно; если следующее вхождение вычислить нельзя, сохраняется прежний откат «запустить один раз». Это не меняет истечение разовых задач, поведение при возобновлении и описанный ниже проход для хостингового провайдера. Переопределения на уровне отдельной задачи нет.
Наверстывание пропущенных срабатываний
Когда активен внешний провайдер планировщика (управляемый cron в хостинговых развёртываниях), шлюз также выполняет наверстывающий проход: задача, чьё запланированное время прошло без доставленного срабатывания — и чьё окно ожидания истекло, — подхватывается и запускается локально, так что сбой в передаче срабатывания обходится в минуты, а не в целый день. Этот проход дедуплицируется относительно запоздалых повторов планировщика тем же механизмом захвата в хранилище, что используется для обычных срабатываний.
cron:
misfire_grace_minutes: 10 # столько ждать собственных повторов планировщика
# перед локальным наверстыванием; 0 отключает наверстывание
Локальным развёртываниям (со встроенным тикером) это не нужно — тикер и так подхватывает просроченные задачи на следующем такте.
Форматы расписания
Итоговый ответ агента автоматически доставляется на цель deliver: задачи — агент больше сам не отправляет сообщения, так что пользовательское содержимое просто попадает в итоговый ответ. Чтобы доставлять на дополнительные или другие цели, перечислите несколько целей deliver: в cron-задаче (через запятую, например deliver: "telegram,discord") — вместо того чтобы поручать отправку агенту.
Относительные задержки (разовый запуск)
in 30m → Запуск один раз через 30 минут
in 2h → Запуск один раз через 2 часа
in 1d → Запуск один раз через 1 день
Интервалы (повторяющиеся)
30m → Каждые 30 минут (голая длительность — это тоже повтор)
every 30m → Каждые 30 минут
every 2h → Каждые 2 часа
every 1d → Каждый день
every hour → Каждый час (единица без числа = 1)
Расписания на естественном языке по дням/времени (повторяющиеся)
every monday 9am → Еженедельно, по понедельникам в 9:00
every day at 9am → Ежедневно в 9:00
weekdays at 9am → По будням в 9:00
weekends at 10am → По субботам и воскресеньям в 10:00
daily at 7am → Ежедневно в 7:00
monday, wednesday at 9am → По понедельникам и средам в 9:00
Время принимается в форматах 9am, 9:30pm, 14:00, голые 24-часовые часы (at 7), а также noon и midnight. Внутри эти формы компилируются в cron-выражения (для этого нужен пакет croniter, он устанавливается по умолчанию).
Cron-выражения
0 9 * * * → Ежедневно в 9:00
0 9 * * 1-5 → По будням в 9:00
0 9 * * MON-FRI → По будням в 9:00 (принимаются и названия дней недели/месяцев)
0 */6 * * * → Каждые 6 часов
30 8 1 * * → Первого числа каждого месяца в 8:30
0 0 * * 0 → Каждое воскресенье в полночь
Метки времени ISO
2026-03-15T09:00:00 → Разовый запуск 15 марта 2026, 9:00
Поведение повторов
| Тип расписания | Повтор по умолчанию | Поведение |
|---|---|---|
Разовый (in 30m, метка времени) | 1 | Выполняется один раз |
Интервал (every 2h) | forever | Выполняется до удаления |
| Cron-выражение | forever | Выполняется до удаления |
Это можно переопределить:
cronjob(
action="create",
prompt="...",
schedule="every 2h",
repeat=5,
)
Управление задачами программно
API для агента — это один инструмент:
cronjob(action="create", ...)
cronjob(action="list")
cronjob(action="update", job_id="...")
cronjob(action="pause", job_id="...")
cronjob(action="resume", job_id="...")
cronjob(action="run", job_id="...")
cronjob(action="remove", job_id="...")
Для update передайте skills=[], чтобы убрать все подключённые навыки.
Ручные запуски асинхронны
cronjob(action="run") запускает задачу немедленно в фоне (как delegate_task): вызов инструмента возвращается сразу же, с хэндлом, а результат задачи — успех/неудача, цель доставки, следующий запланированный запуск и фрагмент вывода — возвращается в беседу новым сообщением, когда запуск завершится. Агент (и вы) тем временем можете продолжать работу, а задача, которая уже выполняется, отклоняет повторный запуск с сообщением «already running» вместо того, чтобы запуститься дважды.
Вы также можете передать prompt вместе с action="run", чтобы добавить временный контекст только для этого запуска:
cronjob(action="run", job_id="...", prompt="CONTEXT: focus on the EU region today")
Этот контекст добавляется к сохранённому промпту задачи под заголовком ## Run Context только для этого единичного срабатывания — он никогда не сохраняется в определении задачи и проходит ту же проверку на prompt-injection, что и сохранённые промпты.
Окружения выполнения, которые не могут получать отсоединённые результаты (разовый hermes -z, hermes cron run из CLI, дочерние cron-сессии, воркеры Kanban), автоматически откатываются на синхронное выполнение.
Наборы инструментов, доступные cron-задачам
Cron запускает каждую задачу в новой сессии агента без подключённой платформы чата. По умолчанию cron-агент получает набор инструментов, настроенный вами для платформы cron в hermes tools — не значение по умолчанию для CLI, не всё подряд.
hermes tools
# → выберите платформу "cron" в curses-интерфейсе
# → включайте/выключайте наборы инструментов так же, как для Telegram/Discord и т. д.
Более точечный контроль на уровне отдельной задачи доступен через поле enabled_toolsets в cronjob.create (или в уже существующей задаче через cronjob.update):
cronjob(action="create", name="weekly-news-summary",
schedule="every sunday 9am",
enabled_toolsets=["web", "file"], # только web + file, без terminal/browser/и т. д.
prompt="Summarize this week's AI news: ...")
Если enabled_toolsets задан на уровне задачи, он побеждает; иначе побеждает конфигурация cron-платформы из hermes tools; иначе Hermes откатывается к встроенным значениям по умолчанию. Если конфигурацию наборов инструментов cron-платформы вообще не удаётся прочитать (например, из-за повреждённого блока platform_toolsets в config.yaml), запуск завершается с записанной ошибкой, а не тихо выполняется со всеми инструментами — смотрите hermes cron list / hermes cron doctor. Это важно для контроля затрат: если тащить browser, delegation в каждую мелкую задачу «получить новости», это раздувает промпт со схемой инструментов при каждом вызове LLM.
Если задача работает с сайтом, где вы залогинены, вход должен быть выполнен до запуска — на такте расписания некому ответить на запрос. Как это настроить, описано в разделе Запланированные и автономные запуски.
Полностью пропустить агента: wakeAgent
Если ваша cron-задача подключает предпроверочный скрипт (через script=), этот скрипт может во время выполнения решить, нужно ли Hermes вообще вызывать агента. Выведите в конце stdout строку вида:
{"wakeAgent": false}
…и cron полностью пропустит запуск агента на этом такте. Это полезно для частых опросов (каждые 1–5 минут), которым нужно будить LLM только когда состояние действительно изменилось — иначе вы платите за пустые ходы агента раз за разом.
# предпроверочный скрипт
import json, sys
latest = fetch_latest_issue_count()
prev = read_state("issue_count")
if latest == prev:
print(json.dumps({"wakeAgent": False})) # пропустить этот такт
sys.exit(0)
write_state("issue_count", latest)
print(json.dumps({"wakeAgent": True, "context": {"new_issues": latest - prev}}))
Если wakeAgent не указан, по умолчанию используется true (будить агента как обычно).
Рецепты: дешёвые предпроверочные гейты
Гейт wakeAgent даёт бесплатный способ решить, стоит ли запланированной задаче тратить хоть какие-то токены LLM. Три паттерна покрывают большинство случаев использования.
Гейт по изменению файла — запускать только тогда, когда в отслеживаемом файле появилось новое содержимое с момента последнего успешного такта. Планировщик фиксирует last_run_at каждой задачи; сравните его со временем модификации файла.
#!/usr/bin/env bash
# ~/.hermes/scripts/feed-changed.sh
FEED="$HOME/data/feed.json"
STATE="$HOME/.hermes/scripts/.feed-changed.last"
test -f "$FEED" || { echo '{"wakeAgent": false}'; exit 0; }
mtime=$(stat -c %Y "$FEED")
last=$(cat "$STATE" 2>/dev/null || echo 0)
if [ "$mtime" -le "$last" ]; then
echo '{"wakeAgent": false}'
else
echo "$mtime" > "$STATE"
echo '{"wakeAgent": true}'
fi
cronjob(action="create", name="process-feed",
schedule="every 30m",
script="feed-changed.sh",
prompt="A new ~/data/feed.json has landed. Summarize what changed.")
Гейт по внешнему флагу — запускать только когда какой-то другой процесс сигнализировал о готовности (например, деплой-хук создаёт файл, CI-задача устанавливает значение в вашем хранилище состояния).
#!/usr/bin/env bash
# ~/.hermes/scripts/flag-ready.sh
if test -f ~/.hermes/cache/scratch/new-data-ready; then
rm -f ~/.hermes/cache/scratch/new-data-ready
echo '{"wakeAgent": true}'
else
echo '{"wakeAgent": false}'
fi
cronjob(action="create", name="nightly-analysis",
schedule="0 9 * * *",
script="flag-ready.sh",
prompt="Run the nightly analysis over today's batch.")
Гейт по количеству строк в SQL — запускать только тогда, когда в вашей собственной базе данных появились новые строки для обработки. Скрипт может также передать это число агенту через context, чтобы агент знал объём данных, не делая повторный запрос.
#!/usr/bin/env python
# ~/.hermes/scripts/new-rows.py
import json, sqlite3
conn = sqlite3.connect("/home/me/data/app.db")
n = conn.execute(
"SELECT COUNT(*) FROM messages WHERE ts > strftime('%s','now','-2 hours')"
).fetchone()[0]
if n < 1:
print(json.dumps({"wakeAgent": False}))
else:
print(json.dumps({"wakeAgent": True, "context": {"new_rows": n}}))
cronjob(action="create", name="summarize-new-msgs",
schedule="every 2h",
script="new-rows.py",
prompt="Summarize the new messages from the last 2 hours.")
Тот же паттерн работает для любого источника данных, который можно опросить из скрипта — Postgres, HTTP API, собственное хранилище состояния, — без встраивания SQL-интерпретатора в подсистему cron.
Собственная база ~/.hermes/state.db у Hermes — это внутренняя схема, которая меняется от релиза к релизу. Не запрашивайте её из предпроверочного гейта — используйте вместо неё собственную базу данных или ленту.
Благодарность: этот набор рецептов появился благодаря исследованию @iankar8 в #2654, где предлагалось добавить sql/file/command-триггеры как параллельный механизм. Гейт script + wakeAgent уже покрывает все три случая бесплатно, так что вместо кода родилась документация.
Связывание задач: context_from
Cron-задача может использовать самый свежий успешный результат одной или нескольких других задач, перечислив их имена (или ID) в context_from:
cronjob(action="create", name="daily-digest",
schedule="every day 7am",
context_from=["ai-news-fetch", "github-prs-fetch"],
prompt="Write the daily digest using the outputs above.")
Самые свежие завершённые результаты указанных задач подставляются перед промптом для этого запуска. Каждая ссылка на вышестоящую задачу должна быть действительным ID или именем задачи (см. cronjob action="list"). Обратите внимание: связывание читает самый свежий завершённый результат — оно не ждёт вышестоящие задачи, которые выполняются в том же такте.
Хранение задач
Задачи хранятся в ~/.hermes/cron/jobs.json. Результат выполнения задач сохраняется в ~/.hermes/cron/output/{job_id}/{timestamp}.md.
Определения задач — обычный JSON на диске: они переживают hermes update, перезапуски шлюза и перезагрузки машины. Задача, которая находилась в процессе выполнения на момент перезапуска, помечается в журнале выполнения как unknown — её не перезапускают автоматически, но следующий запланированный такт задачи срабатывает как обычно. Подробности — в разделе Журнал выполнения.
Просите агента управлять задачами через инструмент cronjob_manage, hermes cron edit или /cron — а не правкой jobs.json напрямую. Прямые правки могут молча не сработать, если безопасность записи файлов блокирует путь (например, когда задана HERMES_WRITE_SAFE_ROOT), а подвал верификатора изменения файлов — авторитетный сигнал о том, что ничего не было сохранено.
Если ручная правка оставила jobs.json повреждённым, планировщик при следующей загрузке чинит файл, а не останавливается: элементы списка jobs, не являющиеся JSON-объектами, отбрасываются, а repeat.completed, не равный неотрицательному целому, сбрасывается до допустимого значения (0, если прочитать его не удалось). Каждое исправление пишется в лог как предупреждение (только типы значений, без содержимого).
Задачи могут хранить model и provider как null. Когда эти поля опущены, Hermes определяет их во время выполнения из глобальной конфигурации. Они появляются в записи задачи только тогда, когда задано переопределение на уровне задачи.
Переопределение base_url на уровне задачи требует явного provider. Для провайдера с сохранённым ключом (именованного пользовательского или встроенного) у переопределения должен быть тот же origin, что и у настроенной конечной точки этого провайдера: та же схема, хост и порт. Другая схема, порт или поддомен отклоняются, так что сохранённый ключ уходит только туда, куда вы его настроили. Голый provider: custom принимает любой base_url, которому не соответствует ни один сохранённый ключ. Если же сохранённый ключ подходит к имени хоста из URL (например, DEEPSEEK_API_KEY для api.deepseek.com), действует то же правило: у base_url должен быть origin настроенной вами конечной точки или встроенного провайдера.
Хранилище использует атомарную запись файлов, так что прерванные записи не оставляют после себя частично записанный файл задачи.
Самодостаточные промпты по-прежнему важны
Cron-задачи выполняются в полностью новой сессии агента. Промпт должен содержать всё, что нужно агенту и что ещё не предоставлено подключёнными навыками.
ПЛОХО: "Check on that server issue"
ХОРОШО: "SSH into server 192.168.1.100 as user 'deploy', check if nginx is running with 'systemctl status nginx', and verify https://example.com returns HTTP 200."
Безопасность
Промпты отложенных задач сканируются на паттерны prompt-injection и утечки учётных данных при создании и обновлении. Промпты, содержащие трюки с невидимыми Unicode-символами, попытки SSH-бэкдора или очевидные полезные нагрузки для эксфильтрации секретов, блокируются.