Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Сон чата

Кроме глобального сна у каждого чата есть свой, независимый. Он ленивый: время сна проходит молча, а сам сон запускается фоном на первом сообщении после срока — тур при этом не ждёт ничего, нудж уходит в 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 и составом реестра.