Напоминания живут в data/, диспетчер — минутный Schedule
Владелец поставил две планки: «надёжно как кусок железа» и «использовать то, что eve уже умеет». Разбор 12.09 показал, что штатный путь напоминаний мёртв сразу по нескольким причинам.
Модель не видит инструкцию. Живой системный промпт — не agent/instructions.md из репозитория,
а личный оверрайд владельца data/custom/agent/instructions.md: блок про напоминания велит слать
мини-скрипт, слов iva remind в нём нет. Репозиторная инструкция (agent/instructions.md:82-97)
на этой установке мертва; пока оверрайд жив, туда не доедет и правка про напоминания.
Напоминание — абзац прозы, а не инструмент. У модели нет тула со словом remind; единственный
долговременный примитив — bash, и обходами он уже воспользовался дважды: 24.08 — свой скрипт с
инлайн-токеном и curl на Bot API, 31.08 — отправка через userbot-прокси tg.
Носитель не durable и требует ручной арифметики. systemd-run --on-calendar пишет транзиентный
юнит в /run (tmpfs): ожидающее напоминание исчезает при ребуте, и Persistent=true не помогает —
исчезает сам файл юнита. Явную зону systemd принимает (deploy/iva-brain.timer:5 —
OnCalendar=*-*-* 05:00:00 __TIMEZONE__), но инструкция велела конвертировать время руками; хост и
systemd --user живут в UTC, сервис — в TZ=Asia/Tashkent (scripts/cli/systemd.ts:161), отсюда
инцидент 24.08: напоминание на 13:00 Ташкента сработало в 08:00 UTC. Адресации нет ни у
systemd-run, ни у iva remind: ни топика форума, ни userbot-адресата, хотя оба самописных
сценария хотели именно их.
Отказ невидим. За 01.08–12.09 в журнале нет ни одного вызова iva remind; транзиентные юниты
run-r<hex> не попадают в фильтр /^iva-/ на экране /menu (scripts/lib/menu/crons.ts:106).
Что решено
Таблица напоминаний в data/. Разовые и повторяющиеся строки лежат в data/ — единственной
части установки, которая переживает iva update и откат (scripts/lib/version-store.ts:37:
STATE_DIRS = ["data", "vault", ".eve/.workflow-data"]). Стор строится на существующем
agent/lib/json-store.ts (атомарная запись saveJsonAtomic:61, строгое чтение
loadJsonStrict:36) по образцу agent/tools/tasks.ts:6-9. У файла есть schemaVersion; файл
новее кода — явная ошибка, а не «прочитаем как-нибудь».
Минутный Schedule — диспетчер. Один authored-файл agent/schedules/reminders.ts с
defineSchedule({ cron: "* * * * *" }) на каждом тике забирает созревшие строки и запускает
каждой своего ребёнка scripts/reminders/fire.ts. Взятие строки — один атомарный переход
pending → fired с firedAt; повторное срабатывание невозможно по построению, поэтому у
строки нет ни аренды, ни счётчиков попыток, ни окна повторов. Повторяющаяся строка тем же
переходом получает от croner следующий срок и остаётся pending; факт срабатывания
(firedAt, delivered, error) при ней. Сработавшие разовые строки живут сутки — столько
владелец видит факт в remind_list, — и убираются тиком одной строкой. Пульс тика — mtime
файла data/reminders.tick, обновляется каждый проход: по нему iva doctor и remind_list
говорят, жив ли диспетчер. Пропущенное время догоняется бесплатно: всё, у чего срок в
прошлом, берётся на следующем тике.
Две независимые ветки в ребёнке. В срок scripts/reminders/fire.ts делает два дела
сразу, через Promise.allSettled, и ни одно не ждёт другого: (а) код отправляет текст
владельцу как есть через sendTelegramHtml (scripts/lib/telegram-send.ts:193) — тот же
Outbox-шов с гейтом, которым уходят ночные отчёты, — и сразу пишет в строку delivered и
причину; (б) агент просыпается отдельной сессией (scripts/lib/reminder-turn.ts) с номером
напоминания, читает факт через remind_list и, если текст не дошёл, говорит владельцу сам.
Ветка (а) работает без модели и её токенов, ветка (б) не отменяет отправку и не зависит от
её провала. Если ребёнок не стартовал или умер, не сказав факта, тик пишет delivered: false
с причиной по коду выхода. Ни повторов, ни alertOnce, ни окон: сбой виден строкой и ходом,
а не лестницей попыток.
Инструмент вместо прозы. remind_add, remind_list, remind_remove. Срок — разовый
или повторяющийся (расписание с явной зоной владельца); выбора режима нет: в срок происходят
обе ветки, описанные выше. Время задаётся словами или ISO в зоне владельца. Абсолютный момент
считает код и возвращает модели готовый next_run_at; ответ говорит, жив ли диспетчер.
remind_list показывает и недавно сработавшие строки с фактом delivered/error.
Гвард удалён 19.10.2026. Решение владельца: без запретов в bash; риск — в скилле.
Расписание — инструмент remind, отправка — ответ хода, во внешний чат — iva post.
Перезапуск через iva из хода сохраняет заявку в data/; мост запускает её после конца хода
отдельным CLI-процессом через service-manager. Заявка и факт исполнения переживают рестарт моста.
Пока команда ожидает или исполняется, другая команда получает явный отказ; одинаковая команда подтверждает уже принятую заявку.
Что отвергнуто
- Политики повторов и две оси режимов. Первая редакция этой схемы несла аренду строки,
троттлинг повторов, окно на 6 часов для разовых и 30 минут для повторяющихся,
alertOnceи отдельный режим на строку (verbatim/agent). Владелец отверг это 12.09 как переусложнение: надёжность даёт один атомарный переходpending → firedи две независимые ветки в срок, а не лестница повторов и не выбор режима. - Вариант A — усилить
systemd-run. Транзиентный юнит живёт в tmpfs, ребут теряет ожидающие напоминания; durability требует своих постоянных юнитов, а это противоречит правилу «scripts/cli/systemd.ts— единственный источник правды для юнитов». Остаются зависимость от linger, ручная конверсия зоны и отсутствие адресации. - Вариант C — durable workflow-тул («Remind me later» из документации eve). Его состояние
лежит в
.eve/.workflow-data, аiva updateэтот каталог карантинит (scripts/lib/wf-store.ts:69-82): каждый апдейт уничтожал бы спящие напоминания. Вдобавок при пинеai7.0.39 не линкуется@ai-sdk/code-mode, и любой workflow-тул роняет старт сервера (docs/tech-debt.md:296). Вариант отложен до апгрейдаai— пометка вdocs/tech-debt.md. - Отправка напоминания в живую сессию. Отменяет ход пользователя или ждёт его; для повторяющихся напоминаний под нагрузкой это худший из вариантов (см. «Что решено»).
- Свой планировщик-процесс. Лишняя трущаяся деталь: минутный Schedule уже есть, а состояние
обязано жить в
data/независимо от носителя тика.
Последствия. В data/ появляется новая поверхность данных с версией схемы и правилом «файл новее
кода — ошибка»; она обязана переживать откат версии. Наблюдаемость переносится в /menu и
iva doctor (ближайшие напоминания в списке). Рецепт systemd-run из agent/instructions.md:82-97
удаляется целиком; но пока живой промпт — оверрайд владельца, это не действует, и сведение
оверрайда с authored-файлом идёт первым шагом.
Обновление (2026-09-12)
Три тула из «Инструмент вместо прозы» свёрнуты в один — remind с полем action
(add | list | remove) и плоской схемой. Поведение и ответы прежние, причина одна:
три схемы тулов ехали в контекст на каждом ходу, а действий у напоминания три и сами они
не пересекаются. Имена remind_add / remind_list / remind_remove выше — запись
решения на момент его принятия; живой тул один, и это сторожит
scripts/remind-tool.test.ts.
Обновление (2026-09-17)
Двух веток в срок больше нет: текст напоминания — это промпт, и в срок агент выполняет его
одним ходом. scripts/reminders/fire.ts запускает ход свежей сессией
(scripts/lib/reminder-turn.ts, один промпт на срабатывание и на iva remind), а код
отправляет финальный текст хода в чат и тему строки. Отправки «текста как есть» остались
ровно одной — страховкой на случай, когда хода не было.
Почему. Напоминание просят действием: «через 3 минуты найди новости и пришли» (@andrey_mic, 17.09). Ветка (а) слала обратно формулировку просьбы («Поиск новостей про Австралию и отправка в чат»), а ветка (б) просыпалась только проверить доставку и молчать — то есть агент в срок не делал ничего. Так же это решено у Hermes Agent и OpenClaw: текст напоминания идёт промптом в свежую сессию с инструментами, ответ уходит туда, откуда просили.
Что изменилось. Ветка (б) и её промпт-проверка (firePrompt) удалены вместе с фенсами
«ждать ветку (а)», «row moved to firedAt» и функцией стора recordWakeError: судить о чужой
отправке больше некому. Ход один, отправка одна, recordDelivery пишет delivered по итогу
send. status === "waiting" — нормальный конец хода (T40), провал — только failed.
Промпт запрещает агенту слать что-либо самому и ставить новые напоминания этим ходом
(list и remove разрешены). Описание тула и agent/instructions.md говорят модели то же
самое: text — инструкция самой себе, писать самодостаточно.
Что осталось. Одна попытка: ни повторов, ни лестницы, ни окон. Факт живёт в строке
(firedAt, delivered, error), схема стора и минутный тик не тронуты. Страховка без
модели: ход упал, вернул failed или промолчал — код шлёт row.text как есть, delivered
по итогу отправки, error — причина провала хода.
Обновление (2026-10-19)
schedule добавляет перенос с сохранением id и текста, запуск в ближайший тик и
команды без модели. remind сохраняет прежний контракт add/list/remove текстовых
напоминаний. Оба инструмента используют одно хранилище и минутный диспетчер.
Команды лежат в data/schedule-commands.json в существующем формате строки:
text содержит команду, id начинается с c-. Отдельный файл нужен для отката.
Строгий читатель 0.5.5 отправляет строки с неизвестными ключами в карантин,
а следующее сохранение убирает их из активной таблицы. Файл команд старый выпуск
вообще не открывает; напоминания сохраняют прежний путь и схему версии 2.
После возврата новой версии просроченные команды забирает ближайший тик.
Один минутный Schedule запускает диспетчер обеих таблиц и шаг helper/goal независимо. Диспетчер возвращается после атомарного взятия строк и запуска детей; результаты детей наблюдаются в фоне. Долгий ход напоминания оставляет следующий минутный тик доступным помощникам. Порядок внутри helper/goal сохранён.
Команда выполняется в bash из корня установки с окружением сервиса и сроком
2 минуты по умолчанию; timeout_minutes у add/move задаёт целое 1–60.
Срок текущего запуска хранится отдельно в running_timeout_minutes: перенос меняет срок
следующего запуска, сохраняя текущий. Оба поля живут только в файле команд. Таймаут убивает группу процессов. Последний факт остаётся в строке,
журнал в data/ ограничен 32000 символами; командные строки хранятся до снятия.
Успех тихий, сбой идёт через существующие Alert и Outbox с недельным дросселем.
Атомарное взятие допускает потерю запуска при остановке между взятием и спавном,
как у прежних напоминаний; автоматических повторов после этой границы нет.
Код держит состояние, дедупликацию, точное время, срок процесса, предел журнала и
доставку Alert. Выбор текста или команды, самодостаточное задание и порядок
переноса systemd-таймера описывает скилл scheduling.
Ремонт после проверки 2026-10-19
Шаг helper/goal наблюдается отдельно от waitUntil общего тика; одновременно идёт
один такой шаг. Его сбой попадает в журнал, напоминания и команды продолжают свой
минутный ход.
Команда наследует IVA_AGENT_TURN=1, как bash-инструмент. Её 120 секунд считаются
от взятия строки, ребёнок диспетчера ограничен 130 секундами. Таймаут закрывает
захваченные потоки. Диспетчер завершает ожидание в срок и при потомке со своей
POSIX-сессией. Идущая команда определяется по firedAt и отсутствию
факта: до результата или предельного срока следующее взятие ждёт её окончания.
Текстовая строка использует ту же защиту: до факта доставки действует RUN_STALE_MS
от firedAt, а обновление активности её running-сессии продлевает этот срок. run_now и move
сохраняют первый факт и сообщают об отложенном переносе в ответе инструмента.
Формат строки сохраняется. Для команд доступен минутный cron; десятиминутный
предел частоты текстовых напоминаний сохранён.
Факт команды и журнал сохраняются после проверки наличия строки под замком таблицы.
После удаления строки поздний результат оставляет журнал удалённым. Снятие
задачи также удаляет её ключ Alert; доставка, завершившаяся после снятия, очищает
его повторно. Человеческое имя команды и причина проходят notice.redactNotice
перед транспортом Outbox.