Сон чата
Кроме глобального сна у каждого чата есть свой, независимый. Он ленивый: время сна проходит молча, а сам сон запускается фоном на первом сообщении после срока — тур при этом не ждёт ничего, нудж уходит в bounded-канал и на горячем пути не стоит ни одного запроса к базе. Курирование, посчитанное этой ночью, применяется со следующего тура: спеки текущего уже собраны.
Первый (и пока единственный) этап — tools: модель, установленная на этот
чат, смотрит на статистику использования инструментов этим чатом и
решает, что он видит в контексте по умолчанию, а что уезжает за
tool_find/tool_load. Решение приезжает вызовом sleep_finish — того же
инструмента, которым отдаёт план глобальный сон,
и с той же поправкой: во сне действовать нечем, вызов и есть ответ.
Телеграм-чат про аниме перестаёт таскать
mcp_forgejo, кодовая сессия — maps_search, а долгий чат становится тем
умнее, чем дольше живёт.
Секция конфига — chat_sleep (см. Конфигурация).
Слой внимания, а не прав
Курирование ничего не запрещает. Оно ложится поверх проверки права
(решётка видимости): скрытое им остаётся в реестре скрытого
именем и назначением и возвращается одним tool_load. Белый список чата,
whitelist крон-джобы и гейты личности им не двигаются вовсе, а в
недоверенном туре потолок доложенного по-прежнему держит whitelist
Господина.
Отдельным слоем это сделано не из эстетики. Телеграм-мост при первом же
переключении тумблера материализует весь реестр в свою таблицу, и любой
чат, где Господин хоть раз трогал настройки, приезжает в ядро с
ToolAccess::Only([почти весь реестр]). Выражай ядро курирование сужением
доступа или через on_demand — в таких чатах оно бы либо не работало
вовсе, либо превратилось в отзыв прав.
Курирование не применяется, когда у тура есть активный
набор инструментов (набор — это уже осознанно выбранный узкий
вид, второй фильтр поверх него только запутает) и в турах подагента (их
состав задаёт тимлид). Тулы, объявленные клиентом на тур (extra_tools),
не курируются: их нет в реестре.
Курирование включается от тесноты, а не от молчания
Главное решение всей фичи. Ядро считает цену схем всех инструментов,
досягаемых этому чату. Влезает в бюджет (tools.budget_percent — доля окна
модели чата) — этап не запускается вовсе: ни вызова модели, ни расхода,
ни курирования. Прятать нечего, места хватает.
Инструмент никогда не прячется «потому что им не пользовались» — только «потому что набор не влез, и он проиграл ранжирование». Это одним махом убивает весь класс ошибок «в чат месяц не писали → у него что-то отобрали» и заодно делает фичу бесплатной для маленьких инсталляций.
Что видит модель
Знаменатель — туры, а не дни. Главная метрика инструмента не «сколько
раз звали за месяц», а покрытие: в скольких турах чата он звался хоть раз,
из скольких туров всего. «Пишу раз в месяц и всегда maps_search» в этой
метрике выглядит как 1/1, а не как «1 вызов за 30 дней».
Окна отчёта усечены по фактическому покрытию статистики: есть данные за 20
дней — окна 1 и 20, а не «за год: 0» там, где года нет. В каждом окне
названы не только вызовы, но и туры, сообщения и активные дни — по ним
видно, что выборка тощая. Окно с числом туров ниже thin_turns ядро
помечает thin, и в промпте прямым текстом: на тощей выборке набор можно
только расширять. Незнакомый чат остаётся при дефолте.
Обратная связь: loads
Ядро считает, сколько раз в этом чате инструмент пришлось достать обратно
через tool_load/tool_find. Это прямое измерение того, что прошлое
курирование ошиблось, и оно не зависит от календаря: копится только тогда,
когда чатом реально пользуются.
Правило стоит в ядре, а не на усмотрение модели: инструмент с loads >= 2
с прошлого курирования обязан остаться в наборе. Петля
самокорректируется — молчащий чат просто не курируется дальше, а зря
скрытый тул возвращается после двух промахов. Рост loads в чате и есть
метрика качества курирования: стабильно высокий значит, что модель прячет
не то, и лечится budget_percent либо chat_sleep.tools.model.
Предохранители
- Порог входа по данным, а не по возрасту чата:
min_turnsтуров в статистике. Возраст чата и возраст его статистики расходятся на форке, миграции и заброшенном чате. - Обязательное ядро (
tools.core, плюс закреплённое Господином): курирование его не прячет. Имена сверяются с реестром на старте — опечатка кричит в лог, а не тихо не работает. - Потолок за ночь (
max_hide_per_night): даже согласившись, ядро не даст одной ночи выпилить полнабора; обратно возвращаются те, у кого выше покрытие. Плохая ночь стоит восьми промахов, а не тридцати. - Сон пустого набора не пишет: план, оставляющий чат ни с чем, отбрасывается целиком. Пустой набор в записи означает не «чат видит ничего», а «курирования ещё не было»: такую строку заводит одно лишь закрепление (см. ниже), и чат при ней видит весь досягаемый реестр.
- Неизвестное имя роняет всю операцию: полуприменённый набор выглядит решением модели, а на деле это её решение минус выпавшее имя.
- Пустой план — валидный и частый ответ: набор уже хорош.
dry_runпишет план в журнал, не трогая базу.
Тулсет в наборе (set:<имя>) раскрывается в момент чтения — правки его
состава доезжают до курированных чатов сами. Тулсет с пустым или
нечитаемым списком раскрывается в ничто, а не во «всё».
Расписание и захват
Срок живёт в колонке chats.next_sleep_at. NULL — сон не планировался
(чат жил до апгрейда или только что заведён): первый нудж по такому чату не
спит, а только ставит границу. Захват — один UPDATE, двигающий срок на
retry_after; им закрываются сразу три случая: дубли нуджей, повторный
нудж во время сна и падение посреди ночи (ночь не потеряна, повторится
через час).
min_gap (дефолт 20 часов) — не интервал, а защита от двойного сна:
сообщение в 23:59 иначе запускало бы сон, тот заводил бы срок на завтрашние
00:00 — на минуту вперёд, — и сообщение в 00:05 будило бы его снова.
Не курируются: эфемерные чаты, удалённые, и полки из skip_folders
(по умолчанию подагенты и крон — у них свой узкий whitelist и однообразные
туры).
Одновременно спит не больше max_parallel чатов. Глобальный сон ждёт
дренажа снов чата наравне с живыми генерациями: иначе оба гоняли бы модель
одним провайдером. Замка над турами сон чата не поднимает — в этом вся
идея.
Снимок окружения
Сон идёт вне тура, а базовый доступ чата, гейты и модель существуют только
внутри тура: доступ приезжает полем tools от вызывающего, гейты
собираются на входе, модель резолвится цепочкой «чат → поверхность →
конфиг». Реконструировать их эвристиками — гадание, поэтому ядро
запоминает фактическое в конце каждого тура (chat_turn_env), и только
если что-то изменилось: обычно это ноль записей.
Гейты копятся по ИЛИ за цикл курирования — один чат ходит и из TUI (с
client_ops), и с телефона, и набор обязан годиться обоим; после удавшегося
курирования копилка начинается заново. Доступ и модель, наоборот, берутся
от последнего тура: это текущая воля Господина, а не история. Строки нет
(чат ни разу не ходил после апгрейда) — этап пропускается.
Запустить руками
chat_sleep_start (M, семейство chat) — курирование ЭТОГО чата прямо
сейчас, не дожидаясь границы суток. В отличие от ночного пути он крутится
прямо в туре: фазы капают клиенту живым прогрессом (tool_progress),
как у свёртки истории, а отчёт возвращается модели текстом — ей же и решать,
что с новым набором делать дальше.
Пороги при этом остаются: слишком мало данных или досягаемый набор и так влезает в бюджет — тул честно скажет, почему пропустил, и не потратит ни одного вызова модели. Срок он двигает так же, как ночной запуск, иначе форс и расписание передрались бы. Второй запуск поверх идущего не пройдёт — та же защёлка, что у ночного пути.
Снаружи то же самое делает POST /v1/chats/{id}/sleep, только фоном и без
прогресса.
Смотреть и вмешиваться
GET /v1/chats/{id}/tool-profile— что чат видит по умолчанию;visible— хранимое решение,effective— оно же раскрытое (тулсеты развёрнуты, ядро и закреплённое добавлены), то самое, что применяет тур. Длину этих списков показывать нельзя: их записи — имена тулов И семейств, и за одним именем стоит хоть сорок инструментов. Для показа естьin_contextиreachable— разрешённые имена, раскрытые до самих тулов по гейтам и доступу ЭТОГО чата: внимание и право двумя честными числами. Пока чат ни разу не ходил, обаnull— гадать ядро не станет.PUT /v1/chats/{id}/tool-profile— поставить набор руками или закрепить инструмент (pinned): закреплённое ни одна ночь не спрячет. Одно закрепление курировать чат НЕ начинает: набор остаётся пустым,curatedостаётсяfalse, чат по-прежнему видит всё досягаемое — булавка просто ждёт первого сна и связывает ему руки. Так тул защищают заранее, ещё до того, как чат вообще курировался.DELETE /v1/chats/{id}/tool-profile— снять курирование.POST /v1/chats/{id}/sleep?dry_run=true— усыпить чат сейчас, не дожидаясь срока. Пороги по данным и ранний выход по бюджету при этом остаются: они не про расписание, а про смысл.GET /v1/chats/{id}/tool-stats?window=30— та самая таблица, что видит модель.GET /v1/chats/{id}/sleep/runsиGET /v1/sleep/chat-runs— журнал.
Уведомлений по каждому чату нет — их были бы десятки за ночь. Вместо них строка в отчёте глобального сна: «chat curation: 7 chats, +12/−31 tools».
Цена
Смена набора двигает префикс спек и рвёт кэш промпта
этого чата один раз. Раз в сутки на чат приемлемо — но именно поэтому
курирование обязано быть редким и стабильным, а не «каждую ночь
по-новому»; отсюда max_hide_per_night и правило «пустой план — валидный
ответ».
Вес не исчезает, а переезжает: из пространства спек (полные схемы, дорого)
в реестр скрытого, который делит с навыками и наборами общий потолок —
2% окна, но не больше 16000 символов на все каталоги. Реестр скрытого при
этом заметно разрастается, и его жалоба на сильное усечение описаний значит
«скрытого стало столько, что имена перестают влезать». Лечится это не
курированием, а tools.on_demand и составом реестра.