RUGitHubInstall

Напоминания живут в 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. Заявка и факт исполнения переживают рестарт моста. Пока команда ожидает или исполняется, другая команда получает явный отказ; одинаковая команда подтверждает уже принятую заявку.

Что отвергнуто

Последствия. В 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.

View original Markdown ↗