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

Changelog

Заметные изменения ядра, свежее сверху. Формат: дата — список; ломающие изменения конфига/API помечаются (ломающее).

2026-08-18

  • Вытеснение копий перечитанного внутри тура больше не гоняется каждой итерацией с третьей — оно едет проходом поджатия тура (turn_squeeze_percent), под тем же весовым порогом и удвоением. Замена по капле рвала префикс-кэш провайдера на каждой итерации живого тура (по spend: hit-rate соседних запросов одного чата упал с ~97% до ~47% за волны 10–14.08), и промах всего промпта стоил дороже сэкономленных огрызков. Освобождённое супersede’ом теперь входит в freed_chars события turn_squeezed. turn_squeeze_percent: 0 выключает и вытеснение копий.
  • Плагин eva-books (plugins/books, юнит services.eva-kernel.plugins.books): книжная полка инструментами books_* — список библиотеки, оглавление, постраничное чтение (не больше пяти страниц за вызов, книга целиком в контекст не попадает) и поиск по ключевым словам. Форматы: pdf (через pdftotext/poppler), epub, fb2(+.zip, включая cp1251), txt/md. Сторону модель называет явно (source: server|client): server — полка books.root либо, Господину, диск сервера; client — машина клиента, ведущего тур (client_ops; клиент спрашивает Господина).
  • Двери ядра для модулей: GET /v1/config — снимок секций конфига с затёртыми при старте секретами (именные поля вроде key/token/env гаснут целиком, остальные строки проходят паттерновый редактор); POST /v1/chats/{id}/ops/read_bytes — кусок файла с машины клиента живого тура той же клиентской операцией read_bytes, что у байтовых инструментов; POST /v1/ops/read_bytes — серверная пара, файл читает само ядро. Обе двери только на чтение. В контексте вызова модульного тула (_meta["dev.eva/ctx"]) появился флаг client_ops.
  • eva-sdk: Kernel::ops(Side::Server | Side::Client { chat }) с read_bytes/read_all (куски по 4 МиБ, потолок размера — ошибкой, не усечением), Kernel::config(), поле ToolCtx::client_ops.
  • Крейт eva-sdk переехал из crates/eva-sdk в sdk/, свои плагины ядра живут в plugins/ тем же воркспейсом.

2026-08-14

  • Структурированный ответ MCP-сервера едет компактным JSON, а не лесенкой: отступы стоили четверть байт на ровном месте, а читается он так же.
  • (ломающее дефолт) Бюджеты файлов проекта опущены вдвое: context.project.max_bytes 32 → 16 KiB, memory_max_bytes 16 → 8 KiB. Вместе они занимали до 12k токенов стабильной части промпта. Усечение по-прежнему подписывается — какой файл пострадал и какой не показан вовсе; кому нужен прежний объём, поднимает поле.
  • Семейство agent_* считается по живым агентам, а не по роли: новый гейт needs_agents держит agent_send, agent_wait, agent_inbox, agent_list и agent_stop закрытыми, пока команды нет. Follow-up к работе, которой никто не поручал, — около четырёх килобайт схем в каждом мастерском туре, включая разговор про кофе. Флаг считается одним запросом на входе в тур и поднимается прямо посреди него: agent_spawn открывает семейство с ближайшего шага, как tool_load открывает доложенное. В каталоге GET /v1/tools гейт виден полем needs_agents.
  • Каталожные блоки промпта (навыки, наборы, реестр скрытого) делят ОДИН бюджет. Модуль обещал «один распределитель на все три списка», а на деле каждый брал свою долю окна: 2% превращались в 6%, а потолок в 16000 символов — в 48000. Теперь модули отдают каталог (шапка, строки, хвост), а укладывает их разом catalog_budget::fit_many — короткий список отдаёт свою долю соседям, и ярус «не влезают даже имена» режет каждый блок по кругу, а не выбрасывает последний целиком: «скрытых инструментов нет» модель прочла бы как правду.
  • У выборки памяти появилась нижняя граница: memory.recall_floor (0.15) — доля от лучшего счёта в этой же выборке. top_k без порога добирал список до счёта, и запись, разделившая с запросом одно частое слово, ехала в промпт наравне с попаданием. Порог относительный: BM25 между запросами не нормирован, абсолютный отсекал бы наугад. core идёт мимо скоринга и порогом не задет; 0 возвращает прежнее поведение.
  • Тул-трафик закрытых ходов подчиняется одному правилу вместо двух частных случаев: context.past_tool_traffic (keep / stub / drop, дефолт stub) и context.telegram.past_tool_traffic со своим дефолтом drop. Раньше выброс трафика прошлых ходов был жёстко привязан к телеграмному окну истории (telegram.history_limit > 0) — два несвязанных знания в одном условии. Ключ теперь поверхность тура, а не размер окна: чат с history_limit: 0 тоже чистится.
  • Длинный тур поджимает собственный хвост. Элизия гонялась один раз на входе в тур, и сорокаитерационный ход нёс весь свой выхлоп до самого конца — единственной реакцией было переполнение окна провайдера, свёртка задним числом и падение тура с «retry the turn». Теперь трафик тура, переросший context.turn_squeeze_percent (50) профильного порога компакции, схлопывается прямо на ходу: свежими остаются последние четыре сообщения, записи чтений внутри тура не отменяют, повторно — только на удвоенном весе. Проход, которому нечего схлопывать, молчит; сработавший говорит модели строкой в [state] («re-read anything you still need»), клиенту — новым SSE-событием turn_squeezed, трассе — turn_squeeze.
  • У сценария бенчмарка появился свой кусок конфига (поле config): пороги вроде поджатия тура иначе не спровоцировать. На нём стоит новый селфтест 06-selftest-turn-squeeze — длинный тур доигрывает план после того, как ядро схлопнуло его старый выхлоп.

2026-08-13

  • Живое окно свежего тул-трафика меряется теперь и в символах — context.tool_result_keep_chars (24000). Счёт сообщений сам по себе врал в разы: выхлоп ОДНОЙ итерации ложится одним сообщением, поэтому шесть сообщений — это то шесть последовательных вызовов (~12k символов), то восемь параллельных на каждой из шести итераций (~96k). Строже та мера, которая раньше кончилась; последнее сообщение свежо всегда — то, ради чего модель только что ходила, схлопывать незачем. 0 возвращает прежний счёт сообщений. Границу окна считает одна функция — она же ставит якорь кэша промпта, разойтись им нельзя.
  • У оконных читателей появился свой потолок и свой рез: новый метод трейта Tool::windowed_result и context.reader_result_max_chars (8000). read_file, find_pattern, read_hex и history_search нарезают выхлоп сами и говорят в шапке, как читать дальше, — общий рез серединой выбрасывал у них ровно то, за чем звали, а шапка продолжала обещать весь диапазон. Теперь режется хвост, с пометкой и приглашением сузить окно. read_file и find_pattern при этом перестали быть uncapped_result: без потолка они клали в окно по 16k символов на вызов.
  • Гигиена контекста взялась за вход вызова, а не только за его ответ. За живым окном длинные строки tool_use.input заменяются пометкой длины ([elided: N chars]): путь, флаги и смещения целы — по ним видно, ЧТО было сделано, — а тело записанного файла, патч и скрипт уходят. Асимметрия была вопиющей: ответ записи стоил 160 символов огрызка, а сам файл лежал в промпте дословно до самой компакции. Граница та же, что у элизии результатов, — новых разрывов префикс-кэша нет.
  • Перечитка файла ДРУГИМ окном больше не копится: чтение вытесняется, если более свежее накрывает его диапазон целиком (пересекающиеся, но не вложенные окна остаются оба — правило вложенных запросов памяти). Раньше в контексте оседали три-четыре неполных копии одного файла.
  • Запись по пути обесценивает чтения этого файла выше: они помечаются [superseded: the file was written after this read; read it again]. Это уже не про деньги — модель видела содержимое файла, который сама же переписала. Пути берутся из входа (write_file, write_diff, write_hex, включая батч-формы) и из текста патча (apply_patch); shell в инвалидаторы не пишем — путь из произвольной команды честно не разобрать. Внутри живого тура правки чтений не отменяют: модель правила сама и помнит что именно, а огрызок стоил бы ей перечитки после каждого write_diff.
  • Модуль объявляет правила своей поверхности сам: Module::surface_rules в eva-sdk, _meta["dev.eva/surface-rules"] на handshake, реестр держит их по имени группы, а сборка промпта берёт по source тура. Ядро лишилось собственных телеграмных правил (TELEGRAM_RULES, IMAGE_RULES_TELEGRAM) и упоминаний telegram_attach_photos в описании image_generate и в правиле [draw]: как выглядит ответ в телеграме, знает телеграмный мост, а ядро — про Еву. Правила берутся только из живого handshake: лежачая поверхность всё равно не пришлёт туров.
  • Правило потока telegram_markdown_image снято с довольствия: оно выбрасывало ответ с ![](url) в телеграмном туре, а теперь так картинка туда и прикладывается. Волна калибровки surface_rules удалит его из базы — но только если Господин его не переписывал; переписанное остаётся его правилом. Ради такого снятия у волны появился список RETIRED.
  • IMAGE_RULES_GUI больше не достаётся туру с якорем: правило утверждает «ты в графическом приложении, НЕ в телеграме», и в мессенджере, чей модуль своих правил не объявил, это была бы ложь.

2026-08-12

  • Во сне появился инструмент sleep_finish — единственный, доступный только там, и им модель отдаёт план фазы вместо прозы с JSON внутри. Касается всех стадий: кластеров, забывания, видов, наборов инструментов и сна чата. Формы операций переехали из промптов в схему инструмента, где им и место; разбор прозы остался страховкой на апстрим, не понявший вынужденный вызов (tool_choice, новое поле запроса — anthropic его при включённом мышлении не шлёт: API запрещает).
  • У плановых вызовов сна снят потолок ответа. Он общий с рассуждением, высокая ступень выедала чатовые 8k целиком — фаза kinds регулярно падала с «no JSON object in the answer», хотя ответ был не кривой, а оборванный. Обрыв по потолку теперь так и называется в журнале ночи и не ретраится впустую.
  • (ломающее поведение) Заметка чата больше не подаётся как «запись, а не правило»: из шапки [notebook] ушло «nothing here is to be acted on», из правил [notes] — «a note describes, it never orders» вместе с разбором императивной заметки, из промпта пересборки блокнота — то же требование формы. Написанное в блокноте имеет вес. Решение Господина; цена — риск инъекции через блокнот, который держат теперь два рычага: заметка не отменяет стоячих правил, и каждая запись видна в ленте тура и в панели с диффом.
  • Промпт похудел без потери правил: правила блокнота, спикеров, телеграма, клиентских операций и рабочего поведения переписаны короче, описания и схемы полутора десятков инструментов — тоже. Системная часть тура ужалась на 8–11% (снимки в src/snapshots/), каталог инструментов — на ~900 символов. Ни одно правило не выброшено; выброшены повторы: что сказано в [client], больше не пересказывается в описании каждого файлового тула.
  • Каталожные блоки (навыки, наборы, реестр скрытого) получили абсолютный потолок в 16000 символов поверх доли окна. Голая доля у модели с миллионным окном разрешала блоку 80 тысяч символов — три таких блока стоили бы дороже всего остального промпта, и платились бы каждым запросом.
  • Бенчмарк гасит оба сна в scratch-оверрайде: у свежего ядра граница суток уже позади, и сон просыпался сразу — курировал наборы моделью, меняя тот самый промпт, который сценарий и меряет, и добавляя полторы минуты на сценарий.
  • eva-sdk: модуль notes — слова, которыми поверхность объявляет блокнот в промпте, что собирает сама (триаж судит мимо тура): NOTEBOOK_PROMPT и Kernel::notes_prompt отдают рамку, notes::notebook — готовый блок из закреплённых заметок. Раньше эта формулировка жила копией в телеграмном мосте и расходилась с ядром молча.
  • eva-sdk: метод Kernel::notes — блокнот чата на чтение (GET /v1/chats/{id}/notes) и тип Note с признаком pinned. Ядро и ручка не тронуты; понадобилось телеграмному мосту, где триаж судит мимо тура и о чате не знает ничего — в промпт судьи он кладёт закреплённые заметки.

2026-08-11

  • Починено: закрепление инструмента (PUT .../tool-profile с одним pinned) в НЕкурированном чате заводило профиль с набором из самих закреплённых — то есть одним кликом схлопывало чат до ядра плюс этого тула. Теперь строка заводится с пустым набором, curated остаётся false, а булавка ждёт первого сна и связывает ему руки. Пустой набор в записи читается как «курирования ещё не было», а не как «видно ничего»; нечитаемый — так же, и булавки при этом не теряются.

  • GET /v1/chats/{id}/tool-profile отдаёт in_context и reachable — разрешённые имена, раскрытые до самих тулов по гейтам и доступу этого чата. Длина visible/effective для показа не годится: их записи — имена тулов И семейств, и клиенты, считая их, врали бы на порядок, каждый по-своему. Оба поля null, пока чат ни разу не ходил.

  • Тул chat_sleep_start (M, семейство chat): курирование набора этого чата прямо сейчас, не дожидаясь его ночной границы. Крутится внутри тура и репортит фазы живым прогрессом (tool_progress) — как свёртка истории, — а отчёт возвращает модели текстом. Пороги по данным и ранний выход по бюджету при этом остаются: тул честно скажет, почему пропустил, не потратив ни одного вызова модели. Срок двигает так же, как ночной запуск; второй запуск поверх идущего не проходит.

  • Сон чата (chat_sleep): у каждого чата свой сон, ленивый и фоновый — время проходит молча, запуск идёт нуджем на первом сообщении после срока и туров не держит. Этап tools решает, что этот чат видит в контексте по умолчанию, а что уезжает за tool_find/tool_load. Курирование — слой внимания, а не прав: скрытое им остаётся в реестре скрытого и возвращается одним tool_load, whitelist чата и гейты личности им не двигаются (Reach.curated поверх проверки права). Этап включается только от тесноты: досягаемый набор влезает в tools.budget_percent окна — модель не зовётся вовсе, и ни один инструмент не прячется «за молчание». Знаменатель метрик — туры, а не дни; loads (сколько раз тул пришлось вернуть) обязывает ядро оставить его в наборе. Ручки: GET/PUT/DELETE /v1/chats/{id}/tool-profile, POST /v1/chats/{id}/sleep, GET /v1/chats/{id}/tool-stats, GET /v1/chats/{id}/sleep/runs, GET /v1/sleep/chat-runs; /v1/chats/{id} отдаёт next_sleep_at. Секция конфига необязательна: без неё сон чата включён и применяет решения сразу (dry_run: false).

  • Статистика инструментов по чатам: дневные бакеты chat_tool_stat и chat_day_stat (вызовы, покрытие турами, loads, доля обращений прямо к Еве), снимок окружения тура chat_turn_env. Жнёт её гигиена глобального сна по chat_sleep.keep_days; глобальный сон теперь ждёт дренажа снов чата наравне с живыми генерациями.

  • POST /v1/stats/triage — поверхность, судящая сама, чему отвечать в бойкой группе, сообщает итог пачки: engaged (вовлечено судьёй) и responded (реально отвечено). Ядро складывает их в triage_stat, и /v1/stats наконец показывает не нули: разрыв между цифрами — это судья, будящий Еву впустую. В eva-sdk — метод Kernel::triage_stat.

2026-08-09

  • Стадии middleware в eva-sdk типизированы: маркер стадии несёт тип нагрузки (stage::ToolCallpayload::ToolCall), Decision::Patch принимает её же, а поля, которых версия SDK не знает, переживают round-trip через rest. Обработчик получает StageCtx — контекст тура плюс клиент API ядра: ядро теперь называет модулю свой адрес в handshake (_meta["dev.eva/api"] запроса initialize). (ломающее) для SDK: Module::stage берёт маркер вместо строки, констант stage::TOOL_CALL и трейта StageHandler больше нет.
  • Тулы на один тур: extra_tools в send_message объявляет модели тулы поверх реестра, исполняет их сам объявивший клиент. Вызов уезжает в стрим событием extra_tool_call (ядро ждёт), ответ — в ops/result под id вызова, лучше блоками (blocks: [{title, content}] — ядро рендерит == title-секциями). ends_turn в объявлении делает тул вердиктом. Имя, тенящее тул реестра, — 400. В eva-sdk — типизированный контракт: Turn::extra_tool::<T>() (схема из JsonSchema типа), событие ExtraToolCall {call, response} с одноразовым респондером и call.parse::<T>(); (ломающее) для SDK — TurnEvent больше не Clone/Deserialize, событие собирает стрим Turn::send.
  • Общий блочный формат ответов инструментов (tools::blocks): == title секции, как у батчевого read_file. shell и local_shell принимают массив команд — каждая отвечает своим блоком, пачка встаёт на первом провале с честным отчётом; имитация секций через echo ==x== больше не нужна и описания тулов прямо это запрещают.
  • Телеграмное правило про telegram_skip ужесточено: причина — внутрь поля reason вызова, не текстом в чат (текст снаружи — сообщение, убивающее сам skip).

2026-08-08

  • Словарь мета-решений decisions (discuss, answer_in_text) теперь вклеен и в исторический элемент tool_call вызова ask_questions в GET /v1/chats/{id}/items — раньше его несло только живое событие, и карточка оборванного вопроса после перезагрузки клиента рисовала голые варианты без «Дообсудить». Ввод модели в истории и промпте по-прежнему нетронут.

  • Книга и OpenAPI проговаривают две ловушки GET /v1/chats/{id}/items и кривых тел: limit считает сообщения-источники, а не элементы (конец истории — пустая страница, не «элементов меньше limit»), а несходящееся со схемой тело axum отвергает статусом 422, не 400.

  • Рабочий каталог чата теперь ставит и сам клиент, не только модель тулом workdir: поля workdir/workdir_on (client|server, дефолт client) в POST /v1/chats и ручка PUT /v1/chats/{id}/workdir (null снимает). Раньше выставить каталог умел только инструмент, и первый тур свежей кодовой сессии уходил без блока [project] — клиент тратил целую итерацию на «выставь workdir». Валидация общая с тулом (кривая машина — 400, пустой путь — снять); действует со следующего тура.

  • Инженерная трасса тура (по мотивам rollout-trace из Codex): переменная EVA_TRACE_DIR включает запись сырых фактов на диск — на корневой тур каталог с manifest.json, только-дописываемой лентой trace.jsonl (монотонный seq) и payloads/ с полными телами запросов к провайдеру (плюс человекочитаемый снимок промпта *-prompt.txt), ответов, аргументов и выхлопов инструментов. Подагенты пишут в каталог тимлида под id своих туров. Выключено (дефолт) — накладных расходов ноль; никуда не выгружается. Разбор — новый бинарь eva-trace <каталог>: таймлайн похода по провайдерам и инструментам, привязка вызова к породившей его итерации, итоги по времени и деньгам. Книга: новая страница «Инженерная трасса».

  • Тесты, стерегущие инварианты: мок-провайдер сверх сопряжённости пар проверяет паникой непустую системную часть, последний ход за пользователем и отсутствие thinking-блоков в пользовательских сообщениях; снимковые тесты собранного промпта (insta, src/snapshots/) на четырёх типовых конфигурациях тура — мастерский чат, гостевой телеграм, кодовая сессия с руками, подагент; тестовый мок с воротами на каждом куске стрима (GatedMock) — детерминированные тесты отмены между дельтами и посреди пачки инструментов, реплея /live с переподключением посреди ответа и замолчавшего навсегда провайдера.

  • Каталожные блоки промпта — списки навыков, наборов и реестр скрытых инструментов — уложены в бюджет: 2% окна модели на блок (по мотивам распределителя скиллов Codex). Влезает — едет целиком, байт в байт как раньше; тесно — описания ужимаются по кругу, по символу с каждого, имена держатся до последнего; не влезли даже имена — хвост выброшен и посчитан. О деградации говорит warning в логе — списки не худеют молча. Пометки режима наборов переживают любое усечение.

  • MCP-серверы на старте ядра разрешаются параллельно: старт платит за самый медленный сервер, а не за сумму всех; порядок регистрации (и инструментов в промпте) остаётся детерминированным.

  • У MCP-сервера появился lazy: true — на старте ядра к нему не ходить вовсе: тулы поднимаются из кэша прошлого запуска, сервер стартует первым живым вызовом. Для тяжёлых серверов; первый запуск (кэша ещё нет) подключается как обычно. HTTP-транспорт при этом хендшейкается лениво — раньше поднятый из кэша HTTP-сервер падал бы на первом вызове.

  • Инструмент MCP-сервера, объявивший _meta.ui.visibility без "model" (UI-виджеты из MCP apps), в реестр не попадает: видимость модели — по явному согласию сервера, старые серверы без поля видны как раньше.

  • Контракт исполнителя хуков добит и зафиксирован в docs/src/hooks.md (сторона ядра по-прежнему не построена): стадия tool.result блокирует результат, а не исполнение; fail-open по умолчанию с fail-closed по объявлению гварда; последовательная цепочка вместо кодексового параллелизма; один запуск на вызов при матчере с алиасами; доверие по хэшу нормализованной личности с состоянием Modified до повторного одобрения; слив болтливого выхлопа в файл; зажатый таймаут хуков выхода.

  • Мостов не задевает: всё внутри сборки промпта и старта ядра. eva-sdk не задет — протокол модулей не менялся; lazy для модулей смысла не имеет (они локальные и дешёвые), поле есть только у mcp.servers.

  • У сна появились именованные этапы и новый этап tools — курирование наборов инструментов. Раз в ночь, после фаз памяти, модель этапа (sleep.tools.model → smartest → general) видит все наборы, весь реестр (имена, назначения, семейства — без схем) и статистику ношения и отвечает планом: дельта-правки состава (add/remove, не замена списка), новые наборы под устойчивый род работы, переписанные описания. Применяет ядро со своими правилами: наборы не удаляются, не переименовываются и не пустеют, каждый добавляемый тул существует в реестре, битые списки не трогаются, режимные флаги — только руками Господина; потолок — sleep.tools.max_ops (10). План — в том же журнале /v1/sleep/runs, общий dry_run действует, о применённых правках уходит уведомление.

  • (ломающее) Секция sleep перекроена под этапы: пер-этапное уехало в подсекции sleep.memory (бывшие плоские model, min_memories, max_ops, max_forget, keep_recent, archive_ttl, cluster_* + свой enabled) и sleep.tools (enabled, model, max_ops). Старые плоские ключи молча игнорируются — прод-конфиг переписать тем же деплоем. Мелкий корпус (min_memories) теперь пропускает только этап памяти, не ночь целиком; сон не стартует, только когда обоим этапам нечего делать.

  • Мостов не задевает: этап работает внутри ядра, поверхности видят только журнал сна и уведомления, как раньше. eva-sdk не задет — API не менялся.

  • Ветвление чата (по мотивам thread/fork из Codex): POST /v1/chats/{id}/fork {before_message, title?} — новый чат с копией истории строго до сообщения, неразрушающая альтернатива DELETE .../messages/{id} (тот остаётся — иногда надо именно стереть, но клиентам правку прошлого стоит делать веткой по умолчанию). Мать не меняется ни на строку; ветка — копия, а не ссылка, и переживает удаление матери. Наследуются полка, проект, модель, уровень рассуждения, набор инструментов, профиль компакции, рабочий каталог, блокнот (с булавками) и план работы; резюме компакции с дословным хвостом едет с перепривязкой якоря на id копии (срез раньше якоря — резюме не едет), якорь честного счёта токенов — по тому же правилу, одноразовые пинки окна вместе с ним. Срез посреди тура закрывается той же синтетикой [not executed: turn cancelled], что у живой отмены (общая функция fragments::cancelled_closures), — ветка едет с первого запроса. Связь в полях чата forked_from/forked_at_message; первым сообщением ветки — служебный маркер [forked] … (новый вид фрагмента fork_marker, в ленте элементов — machine_note, в промпт модели не едет). Форк — жест Господина, автоматически ветки не плодятся; форк от форка разрешён.

2026-08-07

  • Живой вывод команд и exec-сессии (по мотивам unified_exec из Codex). shell показывает вывод по ходу: снимки хвоста уезжают событием tool_progress (в ленте элементов — item_delta элемента вызова), с троттлингом, потолком числа событий, резкой по границе UTF-8 и затиранием секретов у истока снимка; в историю живой хвост не пишется. yield_ms у shell превращает не уложившуюся команду в сессию вместо убийства по таймауту: ответ — {session_id, output, chunk_id, wall_time_seconds}, дальше новый инструмент shell_write (M, семья shell) дописывает ввод и забирает свежий вывод (у shell_write объявлена output_schema). Потолки: 16 сессий, смерть по бездействию (5 мин) вместе с группой процессов, отмена тура прибивает сессии этого тура; удержание вывода — 1 МиБ головой и хвостом со счётом выброшенного, дренаж пайпов после смерти процесса ограничен 2 с (внуки, держащие stdout, не вешают вызов). Таймаут разового shell теперь отдаёт частичный вывод в ошибке, а процессы убиваются группой, не одним прямым потомком. Клиентский брат: POST /v1/chats/{id}/ops/progress — клиент стримит снимки хвоста долгого local_shell, ядро затирает секреты и превращает их в тот же tool_progress (клиенты пока не постят — контракт готов).

  • Единая лента элементов (по мотивам ThreadItem из Codex): один тип шага тура — user_message, agent_message, thinking, tool_call с вложенным результатом, client_op, image, compaction, machine_note, plan — и для живого стрима, и для реплея /live, и для чтения истории. Живой тур зеркалится событиями item_started → типизированные item_deltaitem_completed (started оптимистичен, completed авторитетен) параллельно легаси-набору; ненужный набор клиент глушит полем mute в send_message или query-параметром mute у /live. История отдаётся тем же типом через GET /v1/chats/{id}/items (пагинация как у /messages), элементы собираются из существующих сообщений детерминированно — хранилище не переезжало; связка живого с историческим — call_id/image_id. Сообщения тура теперь несут turn_id (один id у генерации, ленты и записей базы) — по нему клиенты группируют шаги истории; компакция видна и после перезагрузки: её сообщение-маркер отдаётся элементом compaction, не репликой и не заметкой (по маркеру на каждую свёртку; свёртки до появления маркера границы в ленте не оставляют).

  • Видимость инструментов — одной решёткой (заимствовано у Codex): каждому туру инструмент выставлен ровно одним состоянием — Visible (спека у модели), Reserved (реестр скрытого, ждёт tool_load), Hidden (диспетчеризуется, но не рекламируется) или Forbidden с причиной словами. Спеки шага, блок Hidden tools и отказ исполнения строятся из неё и разойтись не могут; GET /v1/tools показывает полный набор гейтов-полей (privileged_only, agent_only, lead_only, anchored_only, indexed_notes_only, planning_only, unlisted — в дополнение к прежним).

  • Гейт unlisted (_meta["dev.eva/unlisted"] у MCP/модулей): миграционный псевдоним — вызов по имени работает, рекламы нигде нет. Переименование инструмента больше не ломает реплей старых чатов.

  • Коллизия имён инструментов — ошибка конфигурации, а не тихая тень: повтор имени пропускается (первый остаётся), кричит в лог на старте и виден полем collisions в GET /v1/tools. Раньше новичок молча подменял прежний тул.

  • Раздутые схемы MCP-инструментов ужимаются в бюджет 5000 байт четырьмя всё более потерянными проходами (описания до первой фразы → $defs за борт → глубже третьего уровня в {} → ветвления в {}); имена аргументов верхнего уровня выживают всегда. Один сервер больше не съедает бюджет описаний целого тура.

  • Слишком длинное имя MCP-инструмента получает стабильный отпечаток вместо немой обрезки: два длинных имени с общим началом не схлопываются, имя не плывёт между перезапусками (стабильность имени — это префикс промпт-кэша).

  • tool_find ищет и по схеме: документ инструмента несёт имена и описания его параметров, ранжирование — BM25 той же формулой, что у памяти. «Тул с параметром X» теперь находится с первой, бесплатной ступени каскада.

  • Компакция как передача смены (заимствовано у Codex). Промпт суммаризации переписан из «сожми это» в сдачу дежурства: прогресс и решения с причинами, ограничения и предпочтения Господина, что осталось сделать, данные без которых не продолжить; резюме в [state] получило принимающую рамку — «работа прошлой смены уже сделана: продолжай, не переспрашивай». Самые свежие реплики Господина переживают свёртку ДОСЛОВНО, рядом с резюме (chats.summary_verbatim): формулировка задания и есть задание, пересказ её теряет. Бюджет — context.compact_verbatim_percent (15% от профильного порога), отбор от свежих к старым, первая не влезшая режется серединой, окно без его реплик (тул-марафон) сохранённое не стирает.

  • Свёртка оставляет след в истории: служебное сообщение-маркер (байт-стабильный текст [compacted] …) — клиент рисует границу компакции и после перезагрузки, а не только по живому событию compacting. В промпт модели маркер не едет и Господину не приписывается (to_chat_messages его отфильтровывает).

  • Компакция при смене модели (downshift): если у модели тура объявлен context_window, а накопленный контекст в него не влезает, история сворачивается ДО первого запроса — а не об ошибку переполнения с ретраями. Работает для смены модели чата, override клиента и цепочки fallback_models: запасная, чьё окно меньше собранного запроса, пропускается в пользу первой подходящей; не влезает ни в одну — свёртка сейчас и честная просьба повторить тур.

  • Типизированный отчёт подагента: agent_spawn принимает report_schema (JSON Schema) — доклад agent_done тогда обязан быть одним объектом этой формы (result принимает и объект), тимлид разбирает поля, а не прозу. Схема хранится на подагенте (agents.report_schema), едет секцией [report format] его первого сообщения; тур, законченный прозой без agent_done, проходит запасной разбор — из последнего текста выкапывается JSON-объект, не нашлось — уезжает проза как раньше.

  • Инструмент может объявить схему результата: Tool::output_schema() (JSON Schema) дописывается хвостом описания — wire-поля для этого нет ни у Chat Completions, ни у Messages API, — а выхлоп становится сериализованным объектом. Первый носитель — agent_list: тимлид ветвится по status и складывает cost_usd, а не выискивает их в строке; пустая команда — честный {"agents": []} по той же схеме.

  • Ошибка инструмента — двух классов, явно (заимствовано у Codex: FunctionCallError = RespondToModel | Fatal). Обычная, как и раньше, уезжает модели текстом в tool_result с is_error, и тур продолжается. Новый класс ToolError::Fatal — для состояния, в котором продолжать нельзя (ядро гасится или неконсистентно): тур рвётся, но только после закрытия всех пар tool_use/tool_result — история чата остаётся валидной, неисполненные соседние вызовы получают синтетический результат, клиент видит ошибку тура. Первый носитель — команда подагентов: «kernel is not up yet» больше не возвращается модели как починимая ошибка. Советчика фатальность не касается: он совещательный и рабочий тур не рвёт.

  • Структурированный ответ вместо разбора прозы (заимствовано у Codex): запрос провайдера несёт опциональную JSON-схему финального ответа. openai шлёт response_format c json_schema — только при объявленной поддержке апстрима (новое поле llm.structured_output, дефолт «выключено»: часть шлюзов ломается на незнакомом поле, как с reasoning); anthropic эмулирует схему вынужденным вызовом синтетического инструмента (с включённым мышлением — предлагает, не вынуждает: форс несовместим с thinking), и его аргументы возвращаются вызывающему текстом; mock отвечает валидным по схеме образцом — фазы, просящие строгий JSON, тестируются без ключей. Первый потребитель — план фазы сна: схема уезжает вместе с прежней текстовой просьбой, ретрай с текстом ошибки остаётся (схема уменьшает частоту промахов, а не отменяет валидацию — vet-правила сна нетронуты). /v1/complete принимает опциональные schema и strict: модули получают структурированный ответ, не изобретая парсер; поле совещательное — апстрим без поддержки его молча игнорирует, разбирать ответ стоит по-прежнему настороженно.

  • Служебные вставки — протокол, а не текст (по мотивам Codex context-fragments): реестр src/fragments.rs перечисляет всё, что ядро дописывает в историю от чужого имени ([state], [wrapup], [kernel], огрызки элизии, маркеры картинок), и один предикат is_machine_text отвечает всем местам разом. Окно живой истории меряется только репликами людей — синтетика роли user (предупреждение о лимите итераций, теперь со своим маркером [wrapup] вместо [state]) ход не открывает и границей компакции не становится; компакция не тащит в резюме «модель залипала» — заметки [kernel], огрызки повторов и метки «пусто» вычищаются перед резюмированием; history_search по умолчанию ищет только речь, флаг include_service возвращает всё. Классификация матчит только явные маркеры реестра — чужой текст с [state] в середине остаётся речью.

  • Клиентский context из send_message получил явный потолок (context.client_context_chars, дефолт 32000 символов; 0 — снять): перерост усекается серединой с честной пометкой — чужие байты не забьют окно, дисциплина клиента больше не механизм.

  • Инструменты-вердикты: _meta["dev.eva/ends_turn"] (в eva-sdk — Module::ends_turn) объявляет тул, чей успешный вызов закрывает тур — управление модели не возвращается, ядро сразу уходит в завершение (стоимость, компакции, план). Первым помечен telegram_skip: после сказанного «молчу» модели нечего добавить, а возврат управления уводил её в циклы повторных skip. Ошибка вызова тур не закрывает.

  • Честный счёт токенов вместо символов (заимствовано у Codex). Контекст меряется якорем провайдера плюс оценкой хвоста: prompt_tokens + completion_tokens последнего ответа уже честно взвесили персону, схемы тулов, резюме и историю (chats.ctx_tokens/ctx_upto), оценивается только добавленное после — байты JSON через самокалибрующийся коэффициент «байты на токен» (скользящее среднее на модель, дефолт 4); картинка идёт фиксированной ценой, а не длиной base64 — скриншот больше не выглядит как двести тысяч токенов (src/tokens.rs). Пороги компакции переведены в токены: context.compact_after_tokens/compact_keep_tokens (и так же в telegram/cron); старые *_chars-ключи читаются как символы ÷ 4 с warning на старте. Ошибка провайдера «окно переполнено» — больше не транзиент: без ретраев в ту же стену счёт пинится в потолок, история сворачивается сразу.

  • Два потолка окна: порог компакции получил область (context.compact_scope, дефолт body_after_prefix) — меряется разговор, а не кэшируемый префикс (персона, правила, схемы тулов), и богатый промпт не наказывается частой компакцией; total возвращает старое поведение. Жёсткий потолок модели — новое необязательное поле context_window записи llm.models: остаток считается минимумом по обоим, клиентам он уезжает в context_windows (GET /v1/models), а оценка стабильного префикса — в context.prefix_tokens (GET /v1/chats/{id}/spend), чтобы проценты окна начинались со 100 на пустом чате.

  • «Сажай самолёт»: давление контекста в [state] выросло до трёх ступеней — тихо → «осталось N» → «сворачивайся». Третья ступень приходит ДО компакции (она необратима), за context.pressure.wrap_up_reserve_tokens (8000) токенов до ближайшей стены, один раз на окно (chats.wrap_nudged_upto) и только когда компакция не сработает сама прямо сейчас. Тексты всех трёх ступеней настраиваются (context.pressure.notice/wrap_up/just_folded, плейсхолдеры {used}/{limit}/{left}).

  • Лимиты провайдера — показание, а не ошибка: заголовки rate-limit (Anthropic- и OpenAI-имена) снимаются с каждого ответа, едут в Usage.rate_limits и последним снимком отдаются в /v1/stats полем rate_limits — «Ева тупит» теперь отличимо от «выбран лимит».

  • Запрос ядра к клиенту стал первоклассной сущностью (по мотивам Codex serverRequest/resolved). Кнопочные вопросы ask_questions и клиентские операции client_op ждут ответа в одной таблице (src/pending.rs вместо questions.rs и pending-мапы client_ops), и любой исход — ответ, истечение срока, конец тура, отмена — уезжает в живой стрим событием request_resolved {id, kind, reason, question?}: клиент закрывает диалог и вычищает очередь одобрений по факту, а не по таймеру, угаданному «чуть короче ядрового». Снятие шлётся всегда, включая ответ самого клиента, и только ПОСЛЕ полного завершения тура — висящий вопрос обрывается, а не схлопывается в «отказ» от имени Господина.

  • Реплей живого стрима помечен: события из накопленного лога несут replayed: true, и контракт (docs/src/api.md, OpenAPI) требует их рисовать, но не исполнять. client_op теперь едет в реплей помеченным, а не выпадает молча: переподключившийся клиент видит и операцию, и её снятие — это чинит повторное исполнение операций при переподключении, описанное в планах mtl (dock-tool-dedupe). Клиенты, исполняющие client_op из реплея, обязаны научиться признаку до деплоя ядра.

  • Набор кнопок одобрения задаёт ядро: client_op несёт decisions (allow, allow_session, allow_always, deny), ask_questions — словарь мета-решений в input tool_call-события; клиент рисует известные, неизвестные пропускает — новое решение вводится правкой ядра без выпуска клиентов. Выбор возвращается полем decision в ops/result; deny без error становится каноническим отказом для модели. Зарезервирован kind: "permission" — запрос профиля разрешений с ответом-подмножеством.

  • Инварианты тура: запрос к провайдеру всегда валиден (заимствовано у Codex, каждое — своим механизмом). Снимок шага (StepTools): список инструментов и его досягаемость замораживаются на один поход к провайдеру, и вызов исполняется по снимку, который его объявил, — смена набора, tool_load или отцепка модуля посреди шага больше не оборачиваются отказом на то, что ядро само предложило модели; мутации видны со следующей итерации, а физически исчезнувший инструмент (отвалившийся MCP) отвечает честной ошибкой с причиной, а не «нет такого». Починка пар tool_use/tool_result при сборке промпта, не в базе: висячему вызову (процесс умер между вызовом и результатом — SIGKILL, OOM, перезапуск на деплое) дописывается байт-стабильная синтетика [kernel] tool call interrupted; no result was produced, результат-сирота выбрасывается — один обрыв больше не валит 400-й каждый следующий тур чата до /clear, база остаётся честной, кэш промпта цел. Отмена даёт доиграть: cancel_all/cancel_chat коротко (до 200 мс) ждут штатного сворачивания тура, чтобы следующий ход не читал историю посреди записи; инструменты с внешним исполнителем (shell, MCP-вызовы, клиентские записи и local_shell — новый предикат Tool::waits_on_cancel) на отмене ждут до их собственного таймаута, а не дропаются — брошенных процессов и «не исполнено» об исполненном больше нет, успевший прийти результат побеждает гонку с отменой; отмена с висящим ask_questions оставляет в истории обрыв, а не ответ за Господина (закреплено тестом). Мок-провайдер теперь проверяет сопряжённость пар на каждом запросе и падает паникой — весь тестовый набор стережёт инвариант попутно.

  • Секреты в выхлопе инструментов затираются у истока: cat .env, env или вывод MCP-тула больше не увозит токен провайдеру, в базу (messages — не архив секретов) и клиенту в стрим; читающие инструменты советчика — под тем же щитом. Правила — данные (secrets.rules, дефолтный набор в коде): Bearer, sk-, AKIA, AGE-SECRET-KEY, glpat-, gh*_, PEM-блок приватного ключа целиком и присвоения token=…/GITHUB_TOKEN=…; порядок несущий (Bearer первым — Bearer sk-… гаснет одной заменой), плейсхолдер называет вид ([redacted: bearer token]), ложные срабатывания прибиты тестами («Bearer of good news» и юникодный кельвин живы). Приём — из codex-rs secrets/sanitizer; HMAC-плейсхолдеры oh-my-pi отложены до сценария, где два затёртых токена нужно различать. Затирание — сеть безопасности, не разрешение читать .env.

  • SSRF-щит web_fetch дожат: редирект на приватный IP-литерал ходил мимо DNS-резолвера (а только он и проверял адреса) — теперь его режет политика редиректов; скачивание source для image_generate ходило голым клиентом вовсе — встало под тот же щит. Исключения — явным списком web.allow_private_hosts (пуст по умолчанию): пустить Еву во внутреннюю графану — решение Господина, а не модели. Адреса MCP-серверов приходят только из конфига, им щит не нужен.

  • Файлы проекта читает ядро, а не каждый клиент по-своему: блок [project] в стабильной части промпта собирает ВСЕ AGENTS.md от корня проекта (маркеры context.project.root_markers, дефолт .git/.jj/flake.nix; пустой список выключает подъём) вниз до рабочего каталога чата, ближний — последним, плюс память проекта .eva/MEMORY.md со своим бюджетом. Бюджет на AGENTS.md ОБЩИЙ (context.project.max_bytes, 32 КБ; 0 выключает сбор): не влезшее усекается с подписью, какой именно файл пострадал, пустые файлы бюджета не тратят; AGENTS.override.md заменяет соседний AGENTS.md; каждый кусок несёт путь-происхождение — «в web/AGENTS.md написано иначе» теперь можно сказать. Серверный рабочий каталог ядро сканирует само (один заход шелла на весь подъём — и на ssh-удалёнке), клиентский достаёт клиент служебным client_op list_up + read_bytes до сборки промпта, без участия модели; поддержку клиент объявляет новым флагом project_docs в send_message (без него незнакомая операция к старым клиентам не прилетает) и перестаёт клеить AGENTS.md в context сам. Подагенты наследуют флаг от тимлида — их сбор идёт через его клиента. Читается один раз на входе в тур: блок байт-стабилен между итерациями, и кэш промпта живёт — в отличие от перечитывания на каждой отправке, как делал tui. eva-sdk не задет: модули поверхностей клиентских операций не ведут.

  • Пачка правок одним вызовом: инструмент apply_patch рядом с write_diff (тот остаётся — для одиночной замены он проще). Формат взят у Codex почти дословно (*** Begin Patch / Update|Add|Delete File / Move to): ни одного номера строки, место правки находится поиском процитированных строк с четырьмя уровнями строгости — вплоть до нормализации типографики, так что ASCII-патч ложится на файл с «ёлочками» (сами «ёлочки» добавлены к списку Codex: русские тексты и переводы в mtl). Применение двухфазное: сперва весь патч читается и проверяется против диска, и только потом пишется — неверный ханк не оставляет три файла наполовину переписанными; частичное применение возможно только при отказе самой записи, и тогда ответ честно перечисляет, что легло. Ошибки цитируют вход — модель чинится со второй попытки, тур не рвётся. Клиенты не тронуты: на проводе те же read/write, поэтому Delete File/Move to исполняются только на рабочей машине (на машине Господина — local_shell rm/mv). Наборы тоже: группа client в кодовом наборе подхватывает инструмент сама. Мостам фича не нужна: в телеграме файлы и так живут серверной рукой, и инструмент там доступен как есть. eva-sdk не задет: ни API, ни протокол модулей не менялись. Бенчмарк: сценарий apply-patch-batch — две правки в двух файлах одним вызовом.

  • Описание image_generate перестало пересказывать собственную схему: панель из четырёх семейств моделей (96 вызовов probe-харнеса) стабильно восстанавливала перечисление source и «опиши в prompt» из одной схемы, а blame показал фичевой коммит, не шрам. Уникальные детали (текст в картинке, чья машина у файлового пути) переехали в описания полей схемы — информации не убыло, повторов не осталось. Остальные спорные строки прогона — шрам пагинации read_file и маршрутизация «не через shell» — оставлены по дисциплине шрамов.

  • Советчик: вторая, дешёвая модель смотрит на рабочий тур со стороны. Раз в advisor.every_iterations итераций она получает дельту транскрипта, ходит только читающими инструментами (read_file, find_pattern, history_search — белый список в коде, не просьба в промпте) и вкладывает в тур одну заметку <advisory severity="…" guidance="weigh, don't blindly obey"> тем же каналом, что и правила: спереди к результату вызова и только в промпт-копии. Ева про советчика в промпте не знает — тег со своим guidance её единственная подсказка. Включает его роль модели llm.model.advisor (не задана — советчика нет вовсе) и только рабочий тур Господина: клиентские операции, рабочий набор, подагент; в болтовне вторая модель стала бы комментатором бытовых реплик за живые деньги. Шум держит щит в коде, а не промпт советчика: нормализация NFKC, чёрный список пустых фраз (молчание — правильная форма «всё хорошо»), дедуп кольцом, одна заметка за цикл, кулдаун после вклеенной, сброс всей памяти о чате при перезаписи истории — и подавление самому советчику невидимо, иначе он перефразирует ту же мысль в обход дедупа. Прерывания идущего тура нет: severity (nit/concern/blocker) доезжает до Евы, но все три ступени обрабатываются одинаково мягко — жёсткая половина сядет вместе с жёсткой половиной правил. Заметки, включая подавленные, пишутся в advisor_notes и видны в GET /v1/stats полем advisor.

  • Правила поверх генерации, жёсткая половина: текстовое правило (scope: text, mode: interrupt) судит готовый ответ перед фиксацией. Промах — ответ выбрасывается целиком (в базу не попадает, клиенту не уходит, объявленные в нём вызовы не исполняются), а ход переписывается с <system-interrupt reason="rule_violation" rule="..."> в промпт-копии. Потолок — один ретрай на ход: второй промах ответ уже не выбрасывает и едет мягким напоминанием на ближайшем результате инструмента. Пока текстовое правило в туре заряжено, речь и объявленные вызовы придерживаются от клиента до вердикта (ожидания не добавляет — уходит одним куском, когда ответ дописан); без заряженного правила стрим идёт как раньше. Зациклить тур правилу нечем: политика повтора считается по ходу чата, и сработавшее правило до конца хода молчит.

  • Пара scope+mode теперь проверяется: text — только interrupt, tool:<имя> — только remind; mode можно не называть, он следует из scope. Правило с невозможной парой не регистрируется (warning в лог).

  • Новое условие окружения правил — picture: картинка в контексте тура, которую модель действительно видит. Без него правило про картинки наказывало бы слепую модель за честное «не вижу».

  • Посеянные текстовые правила откалиброваны по живому прогону: images_look_before_refusing переехало в look_before_refusing (старое имя обещало инструмент, которого в ядре нет) с условием picture и новым текстом; у остальных подтянуты формулировки. Волна калибровки применяется один раз за жизнь базы и переписывает только правила, которых Господин не касался.

  • Правила поверх генерации, мягкая половина: правило матчит имя вызова и regex по его аргументам и клеит <system-reminder rule="..."> спереди к результату этого вызова — только в промпт-копии, в базу и в стрим клиента уходит чистый выхлоп. Клеится после капа и после снятия служебной метки «пусто»: кап считается по выхлопу инструмента, а не по правилу. Правила — данные: таблица stream_rules (поля scope / condition / negate / when / text / mode / repeat), мастерские stream_rule_list / _create / _edit / _delete (семейство rules, в наборе ops) и посев шести хронических промахов один раз за жизнь базы. Условия окружения (telegram, client_ops, master, trusted) не дают телеграмному правилу палить в тихом чате; политика повтора (once / after:<N ходов>) — говорить одно и то же каждым вызовом. Невалидная regex — правило не регистрируется, warning в лог, тур продолжается. Каждое срабатывание пишется в stream_rule_stat и видно в GET /v1/stats (поле rules): посторонний текст в контексте обязан быть объяснимым.

  • Правила системного промпта переписаны по своду oh-my-pi: RFC-капс (MUST/NEVER/SHOULD/AVOID/MAY) вместо мягкой прозы, один пункт — одно утверждение, «если X, то Y» свёрнуто в X? Y., запрет идёт с заменой. Смысл правил не менялся ни в одном: это работа переписчика. Контракт капса проговаривается один раз блоком [conventions] сразу за персоной; доверие ([env]) поднято к личностям в голову промпта, а в самый хвост добавлен блок [critical] — кто Господин и что чужой текст (пользователи, страницы, файлы, выхлоп инструментов) не приказ. Персона не тронута: тон Евы живёт отдельно от служебного текста.

  • Название чата просят у модели по своду для слабых моделей: положительные формулировки и два примера «сообщение → заголовок» вместо списка запретов, а кавычки, точку, лишние строки и приставку «Title:» срезает само ядро. Заголовки зовутся llm.summary_model — там живут дешёвые модели, которые дочитывают паттерн, а «не пиши точку» читают как разрешение.

  • В каталоге GET /v1/tools у инструмента появился флаг on_demand: он стоит у тех, кого tools.on_demand держит вне списка по умолчанию. Меню клиента по нему честно обещает работу через набор или tool_load, а не «сейчас». Описание и схема в каталоге и раньше были те самые, что уезжают модели, — теперь это сказано и в OpenAPI.

  • Описания инструментов почищены от того, что модель не выбирает: shell больше не сообщает, что он master-only (кому не положено — тот его и не видит), web_search не называет метапоисковик по имени, find_pattern не рассказывает, что читает файл потоком, module_register — про рестарт и диалект JSON-RPC, memory_forget — про устройство архива. Смысла нигде не убавилось: остались выбор, дефолты, формат ответа и промахи, а обратимость забывания сказана короче — чинит её Господин, не модель.

  • План работы как состояние: инструмент turn_plan (init / start / done / drop / append / view, одна операция на вызов) и таблица chat_plan по чату — фазы, нумерованные задачи и ровно одна взятая в работу. Инвариант держит код, а не просьба в промпте, ошибочная ссылка откатывает всю операцию, а ответ вызова — состояние плана целиком. Список едет летучим хвостом рядом с блокнотом (в стабильную часть его нельзя: он меняется каждой итерацией и рвал бы кэш префикса), закрытые задачи вычищаются на входе в следующий тур, доделанный целиком план стирается по концу тура — живёт только незавершённое. Инструмент виден не всегда: рабочему туру (клиентские операции, набор с narrate, подагент) и тому, где план уже открыт; в болтовне пустой план — мёртвый вес в каждом запросе. Клиентам: событие стрима plan с состоянием целиком и GET/DELETE /v1/chats/{id}/plan. Смысл — длинный тур теряет вторую половину задачи молча, когда окно свернулось, а выхлоп начала тура схлопнулся.

  • Уровень рассуждения стал ручкой тура, а не флагом на провайдера: llm.reasoning теперь ступень off|low|mid|high, и старое булево читается как раньше (truemid, falseoff), так что конфиги на машинах править не надо. Ступень выбирается по убыванию силы: поле effort в send_message → слово ultrathink целым словом и вне кода в реплике Господина (llm.magic_keywords, по умолчанию включено; из текста слово не вырезается, инструкция едет отдельной строкой состояния тура) → настройка чата (PUT /v1/chats/{id}/effort) → поле effort записи модели → глобальная. Каталог GET /v1/models отдаёт efforts, effort_levels и effort_default. Служебные вызовы (компакция, триаж, названия чатов) как думали, так и не думают — сменился только тип. У билдера тура в eva-sdk появились .effort(Effort) и .surface_effort(…) — перечислением, а не строкой: ступеней ровно четыре, и опечатке взяться неоткуда.

  • Поверхность может назвать свою ступень рассуждения по умолчанию: поле surface_effort в send_message, симметрично surface_model. Встаёт слабее настройки чата и слова Господина, сильнее записи модели и конфига — мессенджер удешевляет свои туры, но не перебивает то, что Господин выбрал конкретному чату, и не глушит ultrathink.

  • Мастерский think_harder: поднять уровень на оставшиеся итерации хода, когда задача оказалась глубже, чем выглядела. Понизить до конца хода нельзя, подъём пишется в журнал — это деньги. Сидит в стартовых наборах coding, ops и research (посев одноразовый: в живой базе набор правится тулами набора).

  • Anthropic-провайдер научился расширенному мышлению: думающая ступень просит адаптивный режим с показом мысли, off говорится вслух — иначе думающая по умолчанию модель думает всегда. Мысль капает в стрим и возвращается провайдеру вместе с подписью в пределах тура: без подписи API отвергает вторую итерацию хода с инструментами. Прошлые ходы по-прежнему уезжают без мыслей, а неподписанная мысль (из истории или от другого провайдера) на провод не идёт.

  • Правило рисования в промпте идёт за досягаемостью image_generate, а не за одной настройкой llm.image_model. Прежде мастерскому туру обещали оффлоад всегда, когда рисующая модель настроена, — и в чате, чей белый список инструмент не пускает (телеграмная группа со своим набором тумблеров), Ева читала в промпте про руку, которой у неё там нет: в списке тула не видно, tool_load его из-под недоверенного потолка не достанет, tool_find честно отвечает «ничего не скрыто» — и она шла докладывать Господину о поломке ядра вместо ответа. Заодно исчез абзац «рисовать нечем»: нет инструмента — нет и правила, сказать «не могу» модель умеет без подсказки в каждом запросе.

2026-08-06

  • Личная память (private) требует теперь доверенного окружения, а не только мастерского тура (ломающее): допуск считается по аудитории, а не по говорящему. Прежде хватало того, что говорит Господин, — и его личное уезжало в промпт и в выдачу memory_search в открытой группе телеграма, где ответ читают все, кто там сидит. memory_list в недоверенном месте больше не показывается вовсе (дамп идёт вместе с личным), соседи в ответе memory_save и текст записи при слиянии фильтруются так же. Тур Господина из недоверенного места получает полную выборку без личного и сноску о том, что личное придержано. Задевает и крон: его туры недоверенные, и личного они теперь не видят.

  • У крон-джобы появился compaction_profilelow, med или high в cron_create и cron_update (в правке ещё auto/null — обратно на автоматику). Это тот же профиль, что ставит чату chat_compact_profile, только прибивается он чату джобы: раньше сменить джобе аппетит к истории можно было, лишь дойдя до её чата отдельным инструментом, и модель об этой возможности не догадывалась. Прибитый профиль виден в cron_list и в поле compaction_profile у GET /v1/cron.

  • Гостевых чатов у крона больше нет (ломающее): чат заводится джобе при создании — свой, на полке cron, — и уходит вместе с ней. POST /v1/cron больше не принимает chat_id (поле игнорируется — сериализация тела нестрогая), и заодно перестал плодить чаты на полке «Входящие»: этим путём таймер селился в чужую переписку, шёл по общей истории с людьми и говорил вслух в их группу. Инструмент cron_create свой чат заводил и раньше, у него меняется только описание.

2026-08-04

  • Whitelist крон-джобы больше не принимается на веру: cron_create / cron_updatePOST /v1/cron) сверяют каждое имя с реестром — тулом или группой регистрации. Незнакомое имя — ошибка с ближайшими похожими и списком групп, чтобы модель поправила список сама. Прежде опечатка («telegram_send» вместо группы telegram) молча рождала немую джобу: она стреляла по расписанию, но отправить результат ей было нечем. Сверка — ToolRegistry::validate_access, общая для любого сохраняемого списка доступа. Регрессия — benchmark/scenarios/02-selftest-cron-whitelist.yaml; в чеках бенча у cron_exists появилось поле tools (точный whitelist) и обратный чек cron_not_exists.

  • Новый cron_firePOST /v1/cron/{id}/fire): выстрелить таймером сейчас, не дожидаясь расписания. Задачу подбирает ближайший тик планировщика, следующий регулярный запуск — через интервал от этого (семантика та же, что у обычного срабатывания). Тул в группе cron — наборы, ссылающиеся на группу, получают его сами.

  • Экономика кэша промпта видна целиком: cached_tokens теперь и в разрезах by_model/by_source статистики, рядом новый cache_write_tokens — сколько записано в кэш (Anthropic отдаёт cache_creation_input_tokens, OpenAI-шлюзы поля не имеют — там 0). Записи без чтений — маркер инвалидатора или кэша, включённого впустую. Новая колонка в spend, миграция автоматическая. Оконный блок статистики заодно отдаёт prompt_tokens — знаменатель доли кэша, который клиенты прежде добывали вычитанием completion из тотала.

  • Mock-провайдер выучил сценарный режим: блок mock-plan: в реплике играется как точная последовательность вызовов, по одному на итерацию, с финальным say. На нём — регрессия спасения застрявшего тура: benchmark/scenarios/01-selftest-stuck-rescue.yaml воспроизводит ловушку «думает cron_create — произносится cron_list» по нотам и проверяет след (джоба существует). До фикса сценарий падает, теперь обязан оставаться зелёным; модель и ключи не нужны.

  • Тур, трижды подряд повторивший один вызов байт в байт, считается застрявшим: обычно это попытка дотянуться до скрытого соседа — провайдер со строгой грамматикой не даёт модели даже произнести имя вне списка, и «хочу cron_update» раз за разом выходит вызовом видимого cron_list, сколько ни объясняй текстом. Ядро дописывает к выхлопу такого вызова записку и сразу докладывает в список скрытых соседей инструмента по семьям регистрации (гейты и потолок — те же, что у tool_load), так что нужное имя становится произносимым со следующего шага. Стрик рвётся любым другим вызовом, поэтому честный поллинг и перечитки между правками записку не ловят.

  • Провайдер venice (Venice.ai): только картинки — image_generate с нуля через их /image/generate и правка source-картинки через /image/edit, открытые модели без вшитой цензуры (safe_mode выключен, водяной знак снят). Правка уходит парной *-edit модели по имени (qwen-image-2qwen-image-2-edit); имя, уже несущее «edit», едет как есть. Текстовые туры не обслуживает — ему место именованным апстримом (llm.providers) при рисующей записи llm.models, не основным провайдером. Расход Venice в usage не отдаёт, так что в статистике трат его картинки не видны. Мостов не касается: картинка приходит тем же событием и блоб-стором.

  • Провайдер выбирается per-model: секция llm.providers держит именованные апстримы (те же поля, что соединительные в llm: provider, api_key, base_url, proxy, reasoning, temperature, prompt_cache), а запись llm.models полем provider: <имя> уводит модель на свой — остальные ходят через основной. Роли и model.fallback свободно смешивают модели разных апстримов; ссылка на необъявленное имя роняет ядро на старте, как и прочие опечатки конфига моделей. Мостов не касается: клиенты видят тот же список моделей, кто их обслуживает — забота ядра.

2026-08-03

  • Библиотека YouTube переехала в ядро: /v1/youtube/subscriptions (список по названиям, upsert по ucid, идемпотентное удаление) и /v1/youtube/history (PUT по video_id, удаление одной записи и всей истории). Подписки и просмотренное лежали у каждого клиента свои, и телефон с браузером расходились в том, что Господин уже смотрел; теперь полка одна. Место остановки ядро хранит только между 2% и 97% длины ролика — ниже это случайный клик, выше досмотрено, при неизвестной длине мерить не от чего; запись при этом из истории не пропадает, а ответ отдаёт строку такой, какой она легла. История помнит 200 последних видео. Страницы режутся курсором before по video_id, а не по времени: импорт истории с телефона приезжает одной секундой и на сравнении по времени терял бы строки на границе страницы. Правила живут в ядре, чтобы клиенты не переписывали их каждый у себя.

  • Сон по будильнику (ломающее): sleep.interval упразднён, вместо него sleep.at — время суток в зоне хоста (дефолт "00:00"), раз в сутки. Пропущенная граница (ядро лежало, шла генерация) не сгорает — досыпается первым подходящим тиком, дважды за сутки сон не приходит. idle_for теперь opt-in: по умолчанию расписание не ждёт тишины разговора (только дренажа генераций); задан — прежнее поведение. next_at в GET /v1/sleep всегда заполнен: непроспанная прошедшая граница или следующая.

  • Ответы на кнопочные вопросы больше не съезжают после первого: поле question в POST /v1/chats/{id}/questions/answer — стабильная позиция вопроса в массиве questions вызова, как и документировано, а не индекс в живом pending-множестве чата. Ответ на первый вопрос из батча больше не превращает второй в «404 pending question» и не отправляет нажатие в соседний вопрос. Новое опциональное поле call_id (id из tool_call-события) развязывает одновременные ask_questions в одном чате; без него ответ идёт в самый ранний живой вызов — старые клиенты работают как прежде.

  • Сид набора social знает про рисование: группа images (image_generate) и слово о ней в описании — просьба «нарисуй» в социальном режиме больше не упирается в спрятанный инструмент. Сиды сеются один раз, работающим базам набор правится на месте. В AGENTS.md — обязанность актуализировать наборы и их описания при изменении инструментов.

  • Проект кодовой сессии — поле, а не полка (ломающее): полки code:<проект> упразднены, все кодовые чаты живут на одной обычной тихой полке code, а привязка к проекту — новое поле project (POST /v1/chats, колонка chats.project). GET /v1/chats принимает project=<имя> — точное имя, сочетается с folder/surface; явный project — явная просьба, тихие полки его не режут. Миграция на старте: folder='code:<X>'project='<X>', folder='code'; она идемпотентна и конвертирует и заведённую руками code:<X> при следующем запуске. Дефолт chats.quiet_folders["youtube", "cron", "code"]. eva-sdk::create_chat теперь берёт структуру CreateChat { title, folder, surface, project } с Default — будущие поля не будут ломать сигнатуру; в eva_sdk::Chat добавлено project.

  • Фильтр folder в GET /v1/chats понимает хвостовую * как префикс: folder=code:* отдаёт чаты всех полок code:<проект> одним списком (грамматика — как у chats.quiet_folders, без * — точное имя). Явный folder — явная просьба: тихость полок и скрытие agents к нему не применяются.

  • Тихие полки: полки автоматики больше не топят общий список — их чаты выпадают из GET /v1/chats, когда не задан ни folder, ни surface; folder=<имя> отдаёт полку целиком, surface=<имя|any> — явные срезы «покажи всё», там не тихо. Список — chats.quiet_folders, по умолчанию ["youtube", "cron", "code:*"]: хвостовая * — префикс, без неё — точное имя. GET /v1/chats/folders тихие полки отдаёт как обычно — клиенты строят по ним переключатель.

2026-08-02

  • Великое объединение папок (ломающее): полка чата больше не означает «какой клиент его завёл» — общие клиенты (tui, web, app, android) смотрят в один и тот же список. У чата появилось поле surface: специализированная поверхность (telegram, mtl) помечает им свои чаты при создании (POST /v1/chats, новый параметр eva-sdk::create_chat), и в общий список они не попадают. GET /v1/chats и GET /v1/chats/folders принимают surface=<имя|any>; без него — только обычные чаты, поэтому клиент, ходивший за своей папкой (folder=tui и т.п.), увидит общую ленту. Папка стала человеческой полкой «о чём чат» и меняется из любого клиента: PUT /v1/chats/{id}/folder (null/пусто — «Входящие» default). Профиль компакции context.telegram.* выбирается по surface чата, а не по строке папки и не по якорю тура. Миграция на старте: folder='telegram'surface='telegram', mtl:<игра>surface='mtl' (полка игры остаётся), происхожденческие папки tui/android/web/telegram складываются в default. Папки с поведением (agents, problems, youtube, code:<проект>, mtl:<игра>) живут как жили.

2026-08-01

  • image_generate.source понимает больше источников: last теперь выбирает свежайшую картинку чата — нарисованную ИЛИ присланную фотографией (раньше — только нарисованную), а путь к файлу читается файловыми руками ядра: машина Господина, когда клиент на связи, иначе хост ядра. «Отредактируй ~/фото.jpg» работает прямо из TUI-сессии. Формат источника проверяется магик-байтами, расширению веры нет.
  • Генерация картинок — Ева умеет отдавать картинку, а не только смотреть. Два пути: модель тура с флагом image_outputllm.models) рисует сама — ядро просит modalities: ["image","text"] и ловит картинку в стриме; прочим даётся master-only инструмент image_generate — оффлоад на llm.image_model (дефолт — первая рисующая из списка), генерация по prompt и правка существующей (source: id / last / URL). Умеющей рисовать модели инструмент не показывается — второй платный вызов на ровном месте не нужен.
  • Блоб-стор картинок: файлы в images.dir (дефолт — рядом с базой), таблица images, ретенция images.keep (30 дней, чистка на старте и раз в сутки). Наружу — публичная ручка GET /images/{id} вне /v1 и токена (ключ доступа — неугадываемый uuid, ответ immutable) и ссылка от images.base_url. В историю чата ложится маркер [image <id>] <url>, а не base64 — контекст не толстеет на мегабайт с каждой картинки.
  • Клиентам: SSE-событие image {id, media_type, url} в стриме тура, массив image в GET /v1/models; eva-sdk — TurnEvent::Image и Kernel::image(id) для скачивания байтов по id. Расход генерации пишется в spend на чат (source = image_generate).
  • POST /v1/complete умеет кэш промпта: поле cache (по умолчанию выкл., оно же Complete::cache в eva-sdk) помечает system-шапку провайдерским маркером. Модулю, который шлёт сотни вызовов с одной и той же многокилобайтной шапкой (переводческая мастерская), она перестаёт стоить каждый раз; TTL берётся общий, llm.prompt_cache. Без поля вызов по-прежнему глушит маркеры: одиночному перечитывать кэш некому, а запись дороже обычного входа.
  • POST /v1/complete перестал выдавать икоту провайдера за ошибку вызывающего: 429, перегрузка и оборванная сеть отвечают 503, отказ по существу запроса (нет такой модели, не тот аргумент) — прежним 400. Тур такую икоту ретраит сам, а одноразовый вызов не ретраит никто, и клиент решает по статусу: 503 — повтори, 400 — чини запрос. Текст ошибки в теле не изменился.

2026-07-31

  • Медленноволновой сон — консолидация памяти (секция sleep, включена по умолчанию; min_memories бережёт мелкий корпус). Раз в сутки, дождавшись тишины, ядро кластеризует корпус по лексике и семантике и разбирает его самой сильной моделью — новая роль llm.model.smartest, оверрайд sleep.model: слить дубли, развести противоречия с датировкой, расщепить склейки, вывести из череды однотипных событий факт, подтвердить забывание арифметических кандидатов (модель может список только сократить), поднять и опустить core, перекалибровать сползшую важность. Инструментов во сне нет: модель отвечает строгим JSON-планом с обязательным why на каждой операции, применяет ядро — с потолками max_ops/max_forget на ночь; core не стирается никогда (только понижение вида), записи моложе keep_recent не забываются и не переписываются, private не снимается и заражает слияние. Устоявшиеся кластеры (consolidated_at) модели не уезжают — вторая ночь на неизменном корпусе не стоит ни одного вызова. Спящая Ева не отвечает: тур ждёт пробуждения в очереди, его SSE-поток открывается событием sleeping. Ручки GET|POST /v1/sleep, POST /v1/sleep/wake (недоделанная фаза не применяется, применённые остаются), GET /v1/sleep/runs; тул memory_consolidate — уснуть, не дожидаясь ночи. О заметной ночи Господину уходит уведомление; dry_run пишет план в журнал, не применяя, — режим первых недель. Расход виден отдельными источниками sleep:cluster/sleep:forget/sleep:kinds.
  • Забывание стало обратимым: memory_forget, DELETE /v1/memories/{id} и сон кладут запись в архив (GET /v1/memories/archive, POST /v1/memories/archive/{id}/restore), окончательно выносит её оттуда sleep.archive_ttl. Слияние записей сохраняет id подкреплённейшей и суммирует историю обращений — spaced repetition не обнуляется.
  • Отскоренная память снова доезжает до промпта. Бюджет секции был общим на core и выборку, и разросшийся core съедал его целиком: на живом корпусе (102 записи, 24 из них core) в промпт попадали девять самых свежих core — и ни одного fact, ни одного event, сколько бы они ни набрали. Считался бюджет в байтах, так что на кириллице реального места было вдвое меньше обещанного, а первая не влезшая строка обрывала секцию вместе со всем хвостом. Теперь потолок — memory.prompt_budget (8000 символов, именно символов, а не байт), core занимает не больше 60% его, остаток принадлежит выборке; не влезшая строка пропускается, а не рубит хвост, и секция вслух говорит, сколько записей осталось за бортом и что с этим делать. Молча урезанная память читается как «это всё, что я помню», и выпавшее не всплывает уже никогда.
  • Личное не уезжает в чужой тур. У воспоминания появилась пометка private (memory_save, memory_update, POST /v1/memories): такая запись не попадает ни в промпт не-мастерского тура, ни в выдачу memory_search — раньше core целиком ехал в промпт публичного телеграм-чата, и здоровье, вес и адрес Господина защищала одна строчка правил. Существующие записи считаются общими: разметить их — решение Господина.
  • Векторы больше не протухают молча. embedding_model писалась в базу, но с активной моделью не сверялась: смена модели эмбеддингов оставляла корпус с векторами чужого пространства навсегда, а косинус между пространствами шёл в скоринг с весом 0.5. Теперь старт пересчитывает и такие записи (батчами, best-effort), а косинус считается только с вектором активной модели.
  • Сохранение показывает, с чем новое соседствует. memory_save возвращает до трёх ближайших записей: момент записи — единственный, когда видно, что чему противоречит. Дедуп при слиянии берёт более подробную формулировку (факт повторяют ради уточнения), memory_update научился менять вид — эпизод, записанный в core по ошибке, опускается в fact, а не живёт в промпте вечно. Строка воспоминания несёт возраст и важность (id | kind | age | importance | content): без возраста вчерашняя правда читается наравне с годовалой.
  • Семантическая половина скоринга перестала быть константой. Порог косинуса стоял на 0.3, а у эмбеддингов базовая близость любых двух текстов высока: на живом корпусе (bge-m3, 5151 пара) медиана заведомо несвязанных пар — 0.39, и порог пропускал 90.7% пар, отдавая половину веса скоринга за то, что оба текста написаны по-русски. Теперь близость считается как (cos − 0.4) / (1 − 0.4): фон весит ноль, родство (от ≈ 0.7) весит по-настоящему.
  • GET /v1/memories/prompt?q=… показывает секцию # Memory ровно в том виде, в каком её несёт тур (&public=true — как чужой), и ничего не подкрепляет. Что именно доезжало до модели, снаружи было не видно — а обрезалось молча.
  • POST /v1/memories действительно дедуплицирует, как и обещает описание ручки: похожая запись подкрепляется, а не дублируется.
  • Запрос к памяти собирается из текущей реплики целиком и подрезанных предыдущих: полными они размывали запрос, и выборка отвечала на позавчерашнюю тему.
  • Рассуждения видны и по ходу тура, и по команде. SSE-событие usage несёт reasoning_tokens — счётчик клиента показывает, на что уходит ожидание, не дожидаясь конца ответа. GET /v1/agents и agent_list отдают ту же ось на каждого подагента и итогом по команде: кто из них думает дороже всех, было не видно.
  • Расход на рассуждения — отдельной осью статистики. /v1/stats (итог, по моделям, по источникам), /v1/chats/{id}/spend и usage_stats показывают reasoning_tokens — долю выходных токенов, ушедшую в мысли, — и reasoning_cost, если у модели задана новая опция llm.models[].price_out (цена выходного токена, USD за миллион). Без цены поле остаётся null: провайдер отдаёт стоимость запроса одним числом, и разложить её на вход и выход нечем — при кэшированном контексте вход перевешивает выход, так что пропорция по токенам врёт кратно.
  • Заметку разрешённого размера можно закрепить. Бюджет булавок был втрое меньше потолка тела (800 против 2000), и заметка на 1530 символов сохранялась, но не закреплялась — ни одного размера, при котором булавка не упирается раньше записи, не оставалось; лимит в три штуки при этом был недостижим. Теперь pinned_chars — 2500, не ниже max_note_chars, и дословного состояния при переходе в режим index не становится меньше, чем было в inline. Отказ теперь говорит, что чинить: не влезающая сама заметка — резать её, перебор суммой — называет, кто бюджет занял и сколько.
  • Закреплённая заметка сверх бюджета названа, а не пропущена молча. В блокнот булавка приезжает и мимо проверки — возвратом после пересборки, из архива, снижением потолка в конфиге; блок цитировал закреплённые до первой не влезшей и терял вместе с ней весь хвост, включая те, что влезали. Теперь берётся всё, что помещается, а остальные названы по имени с пометкой сходить за телом — модель видит, что состояние задачи не перед глазами.
  • Тильда в пути файловых инструментов ведёт в домашний каталог. На рабочей машине путь уезжает в кавычках, и ~/.ssh/config доезжал именем файла с тильдой — отказ «нет такого файла» на самом обычном пути. Теперь голова пути разворачивается подстановкой домашнего каталога той машины, где вызов исполняется; на машине Господина это давно умеет сам клиент.
  • Рассуждение возвращается модели вместе с вызовом, который из него выросло. История собирается заново на каждой итерации тура, и мысль из неё выбрасывалась при сериализации: reasoning-модель получала голые tool_calls и переоткрывала план с нуля каждый шаг — теряла намерение между вызовами и повторяла сделанное. Теперь Thinking-блоки едут полем reasoning того же assistant-сообщения (поле шлюзов, проксирующих рассуждения; кто его не знает — игнорирует). Дальше тура мысли по-прежнему не живут. Заодно ловится reasoning_content — формат нативного Moonshot и vLLM.
  • Кривые аргументы вызова возвращаются ошибкой, которая говорит, что чинить. Нераспарсенный JSON доезжал до инструмента строкой, тот не находил поля и отвечал «path is required» — модель считала себя правой и слала тот же вызов снова. Теперь реестр проверяет аргументы до инструмента: объект в лишних кавычках чинится молча, битый JSON называется битым, а пропущенное поле приходит вместе со списком присланных и подсказкой по ближайшему имени («paht → path»).
  • Правка файла возвращает вид на своё место. write_diff отвечал числом замен, и проверить результат можно было только перечитав файл — итерация на каждую правку. Теперь в ответе три строки вокруг замены с номерами.
  • Пропавший файл на рабочей машине называет соседей по каталогу. Голое No such file or directory промах по имени не чинит, и следующая попытка была такой же догадкой.
  • Чтение отдаёт за вызов вчетверо больше текста (16k символов): страница в ~100 строк превращала файл на 800 строк в восемь походов к провайдеру.
  • code_task больше не выглядит дорогой к работе с репозиторием. У делегата нет ни инструментов, ни файлов, ни истории — он видит только текст task; описание теперь говорит это прямо и отправляет за работой внутри репозитория к agent_spawn с кодовым набором. Расход делегата попал в общий счёт — раньше его не было видно вовсе.
  • Служебные пометки в контексте — по-английски ([re-read further down], [same call as above …], [middle cut: N chars of M], [empty], [image — this model cannot see it …]). Машинный слой англоязычный, и русская пометка среди него читается моделью как чья-то реплика.
  • Удаление чата снимает его живых подагентов. DELETE /v1/chats/{id} мягкое: строка остаётся, каскаду срабатывать не от чего — и команда продолжала работать, докладывая в чат, которого нет в списках. Никто этих писем не читал, а туры стоили денег. Теперь живым ставится stopped и отменяются идущие туры; письмо о снятии уходит наверх, так что восстановленный чат объясняет молчание команды. restore возвращает переписку, но не воскрешает команду.
  • Жнец уносит команду вместе с тимлидом. Временный чат, домолчавший до chats.ephemeral_ttl, удалялся один: строка подагента висит на parent_chat_id, а внешнего ключа там нет, и чат подагента persistent — жнец его не берёт. Оставался призрак: агент в GET /v1/agents, его почта, целый чат с историей и тратами, а читать доклады некому. Теперь жатва — одна транзакция: сперва чаты подагентов (каскадом уносит и строки, и переписку), следом чаты тимлидов; живые туры унесённых отменяются.
  • Снятый подагент больше не воскресает письмом. Тимлид умеет позвать agent_spawn и agent_send в одном ходу; такое письмо приходит, когда подагент уже на последней итерации тура, доставить внутрь его некуда — и оно ждёт нового тура. Тур заводился и после снятия: stopped из ручки API держался пару секунд, а потом подагент снова оказывался idle, потратив ещё один ход. Теперь перед подхватом письма спрашивается статус: снятому тур не заводится вовсе, письмо остаётся непрочитанным. done и failed ставит себе сам подагент, и письмо тимлида по-прежнему вправе его продолжить.
  • Правила блокнота говорят, КОГДА писать, а не только что такое заметка. Писать в тот же тур, когда узнала: следующий помнит хуже, и не спросит никто. Сказано и про потерю внутри длинного тура — выхлоп инструментов капается и схлопывается по ходу работы, так что прочитанное десять вызовов назад из промпта уже ушло; это самый честный довод за запись, и раньше его в правилах не было вовсе. Поводы переписаны под кодовую сессию (где что лежит, чем собирается, какой инвариант легко нарушить, куда ходила и не нашла), добавлено «состояние длинной задачи — закрепи». Пустой блокнот больше не выдаёт себе индульгенцию «ничего не стоило сохранить»: он просто ещё не начат. Рамка «заметка описывает, а не велит» осталась дословно — она про безопасность.
  • Давление истории видно рядом с блокнотом. Модель не знала ни что окно вот-вот свернут, ни что его уже свернули: сутки работы и девятнадцать компакций дали ноль заметок. Теперь в [state] едет строка [window] — сколько живой истории осталось до свёртки этого чата (с 60% порога, ниже это шум), а сразу после свёртки вместо цифр одноразовый пинок: подробности ещё в свежем резюме, но через тур забудутся. Пустой блокнот под давлением тоже говорит — молчать в том состоянии, из которого надо выбираться, хуже всего. Новых полей конфига нет, пороги — константы; схема доросла колонкой chats.notes_nudged_upto (какая свёртка уже отработала пинком).
  • Инструменты блокнота заперты от подагента (lead_only). Блокнот ему не показывают сознательно — а тур подагента мастерский по флагам, и одного master_only не хватало: поднятый с набором coding подагент видел запись в блокнот, которого нет у него перед глазами и о котором ему не сказано ни слова. Дока про «свой блокнот у подагента» была неправдой и исправлена.
  • Описание agent_spawn говорит и о том, когда подагента звать НЕ надо. Оно учило только, как звать, а накладные у подагента настоящие: тысячи входных токенов на итерацию и холодный кэш на первом ходу. Теперь там прямо сказано, чем он окупается — изоляцией контекста, веером и чужой моделью — и что «прочитай файл и скажи коротко» дешевле сделать read_file в собственном туре.
  • /v1/statsusage_stats) знает про команду: блок team — сколько подагентов завели за окно, кто чем кончил и какая доля их туров прошла мимо agent_done. Последнее и есть метрика здоровья агентики: доклад, оставшийся текстом в чате подагента, тимлид получает уже аварийным путём. Что фича молчала две недели, до этого выяснялось запросом к SQLite.
  • Снятый подагент больше не выглядит упавшим: у него свой статус stopped, а письмо о снятии уходит тимлиду обоими путями — и из agent_stop, и из POST /v1/agents/{id}/stop. Раньше письмо слал только API и мимо доставки, поэтому тимлид, ждущий в agent_wait, досиживал до таймаута вместо мгновенного пробуждения. Таблица подагентов в базах прежних версий пересобирается на старте под новый статус; переписка при этом цела.
  • Подагент знает, что он подагент. Про роль ему говорила одна строка первого сообщения — после первой итерации она уезжала вверх истории, и дальше побеждала персона: живые прогоны показали модель, уверенную, что она отвечает Господину в чат, и молчащую наверх. Теперь роль держат два места вне летучего хвоста: блок [team] в конце стабильной части системного промпта и строка в якоре тура, переезжающая на свежий tool_result каждой итерации. Заодно из агентского тура убрано неприменимое — правила блокнота, чатов проблем, картинок и подсказка «каталог не задан»: около 2 тысяч символов в каждом запросе, за которые подагенту нечем платить.
  • agent_send доходит до работающего подагента. Письмо ложилось в базу, и читателя у него не было во всём ядре: спящего оно будило текстом промпта, а работающему не доставлялось никогда — тимлид переспрашивал в пустоту, хотя инструмент обещал «reaches it on its next step». Теперь письмо приезжает подагенту отдельным сообщением в конце ближайшей итерации, за выхлопом инструментов, и остаётся в его истории (в [state] оно не пережило бы тур). Посланное на последнем шаге тура подхватывается новым туром сразу; законченному подагенту письмо не пишется вовсе — отказ с предложением завести нового.
  • Тимлидские инструменты не показываются подагенту: гейт lead_only — зеркало agent_only. Подагенту доставался agent_inbox, читающий почту от его собственных подагентов (которых не бывает), а с названным набором ещё и agent_spawn/agent_wait/agent_send/agent_stop/agent_list: пять инструментов, отвечающих пустотой или отказом, и заведомо тупиковый выбор в каждом запросе. Глубина дерева теперь держится тем же гейтом, а не проверкой внутри agent_spawn.
  • agent_spawn проверяет, что называет, и рожает подагента в рабочем каталоге тимлида. Несуществующий набор инструментов был не ошибкой, а выдачей всего реестра: опечатка в имени открывала подагенту шелл, память и крон вместо узкого набора «читать и докладывать». Теперь неизвестный набор и неизвестная модель — отказ с перечислением известных. Каталог чата тимлида (вместе с машиной) наследуется чату подагента, поэтому относительные пути в задаче ведут туда же, куда у тимлида. Новое необязательное поле context: то, что подагенту не выяснить самому, едет отдельной секцией и не размывает задачу.
  • Последнее слово подагента доезжает до тимлида. Доклад существовал, только если модель звала agent_done; задачу вида «просто ответь» она заканчивает обычным текстом, и тот оставался в чате подагента — тимлид видел «отработал (idle)», а почта была пуста, и agent_wait досиживал таймаут впустую. Теперь тур, закончившийся без agent_done, сам кладёт свой ответ письмом наверх и будит ждущего; молчаливый тур говорит, что промолчал, упавший — докладывает ошибкой.

2026-07-30

  • Ядро просит вести долговременную память, а не только читать её: правило [memory] в кэшируемой части промпта туров Господина. До него ни одна строка промпта не звала сохранять — секция # Memory подаёт уже найденное, блокнот лишь перенаправляет («факты о Господине идут в memory_save»), а персона у оператора своя и про память может молчать вовсе. Правило общее: обеднеет ли следующая Ева, в другом чате, не зная этого, — и три отсечки, включая границу с блокнотом. Виды и важность в нём не пересказываются: это работа схемы инструмента.
  • Оборвавшееся посреди стрима соединение с провайдером считается транзиентной икотой и ретраится, как соседний 502: раньше «error decoding response body» не совпадал ни с одной фразой отбора и не содержал кода статуса, поэтому тур падал с первой попытки — обиднее всего в турах по расписанию, где переспросить некому. Заодно причина обрыва разворачивается до самой нижней и попадает в журнал: обёртка потока событий своих источников не отдаёт, и настоящая причина (соединение закрылось на полуслове, тело недочитано) в логах не появлялась вовсе. Ретрай, как и прежде, живёт только до первого содержательного события — после утёкшего клиенту текста повтор задвоил бы вывод.
  • Документация назвала клиентов поимённо и со ссылками: раздел «Чем подключаться» в быстром старте (веб, десктоп, андроид, терминал, мастерская переводов — чем какой хорош), список в README и обновлённая таблица в ECOSYSTEM.md, где веб больше не «планируется». Заодно закрыты две дыры: lift_turn_limit (снятие потолка итераций изнутри тура) и tools.on_demand (что держать вне списка по умолчанию до tool_load) описаны в главах об инструментах, наборах и конфигурации.
  • Блокнот чата (chat_note_save / chat_note_forget / chat_note_read / chat_note_compact, секция конфига notes, глава «Блокнот»): пер-чатная выжимка собственной работы Евы — решение и почему, инвариант проекта, путь/команда/id, добытые раскопками, тупик, состояние длинной задачи. Переживает и окно живой истории, и компакцию, и chat_clear. Выдача не ранжированием, а по размеру: помещается — тела едут в промпт дословно и инструмента чтения не существует; разросся — в промпте только индекс имя — описание, тела по chat_note_read; перерос и это — после тура пересобирается моделью самого чата, с проверками (пропавшую закреплённую заметку возвращаем дословно, не уменьшившийся результат отвергаем) и архивом на шаг назад. Пересборка видит и резюме компакции чата — по нему решается, какая заметка уже отработала. Порог показа с гистерезисом: без него список инструментов мигал бы каждый ход и рвал кэш промпта. Писать может только Господин; заметка описывает, а не велит, и правил Евы не отменяет. Клиенты правят блокнот через /v1/chats/{id}/notes*.
  • Флаг quiet у инструмента и в каталоге GET /v1/tools: «не событие в ленте чата». Ленты-мессенджеры такие шаги не рисуют, клиенты с отдельной колонкой шагов (TUI, web, Android) флаг игнорируют. MCP-тулы объявляют его сами — _meta["dev.eva/quiet"]. Первым помечено семейство chat_note_*.
  • Выхлоп, который зовут ради ПОЛНОГО текста (skill, agent_wait, agent_inbox, memory_list, chat_summary_get, read_file, find_pattern), больше не получает дыру в середине на следующем ходу: гигиена контекста узнаёт помеченный инструмент по имени вызова и внутри живого окна его не режет. За окном схлопывается всё по-прежнему — возраст сильнее флага. Заодно потолки: тело навыка — до 20000 символов, проверка на записи (плейбук с дырой хуже отсутствующего, поэтому длинный делится на навыки поменьше); почта подагентов уезжает наверх порциями до 8000 символов целыми письмами, остаток ждёт следующего забора и назван вслух.
  • find_pattern — поиск по текстовому файлу регулярным выражением, на обеих машинах: строки с их номерами, флаги буквами (i, x, U), продолжение с from_line, потолок находок. Поиск построчный, как у grep, и потоковый: файл не ложится в память, а набрав своё, чтение гаснет на месте — попадание в начале лога на сотни мегабайт стоит миллисекунды, а не полного прочтения. Начало пропускает сама рабочая машина; клиенту файл возится окнами, и просмотренное там ограничено 32 МБ — до потолка дошли, так и сказано в ответе. Длинная строка показывается окном вокруг вхождения, двоичный файл отправляется в find_hex. Новых клиентских операций не появилось: инструмент собран из тех же байтовых чтений.
  • Длинный ответ read_file и find_pattern обрывается по границе строки (находки) и называет точку продолжения: [lines 1-84 of 500 — read on with offset 85]. Раньше такой ответ доставался общей гигиене контекста, а она режет середину: пропадало ровно прочитанное, шапка продолжала обещать весь диапазон, и дочитать с нужного места было нельзя — только перечитать сначала. Объём тот же, порядок целый.
  • (ломающее) tools в GET /v1/toolsets и GET /v1/cron — список, а не внутренняя развилка ядра: раньше поле приезжало объектом {"Ok": …}, теперь это массив имён (у крон-джобы null — ограничений нет). Набор или джоба, чей список не разобрать, отдаёт пустой массив и строку tools_error рядом — состояние базы видно явным полем, а не формой значения.
  • Список событий стрима в документации и в OpenAPI-схеме полон: usage и compacting в нём не хватало, хотя клиент обязан их обрабатывать — по первому двигается счётчик расхода, второй объясняет паузу перед done. write_diff в docs/ назван тем, что он есть: инструментом, который ядро собирает из чтения и записи; клиентской операции с таким именем на проводе нет.
  • (ломающее) Встроенного телеграм-моста в ядре больше нет: поверхность целиком ведёт модуль eva/telegram-bridge со своим токеном и своей базой, а ядру он приходит обычным модулем с инструментами telegram_*. Что это меняет снаружи:
    • секции telegram в конфиге ядра нет — её поля (токен, допуски, триаж, дебаунсы, master_tools, био, прокси) стали конфигом моста. Потолок жизни кнопочных вопросов переехал в tools.ask_ttl;
    • ручки /v1/telegram/* (дебаунс, потолок ответа, краткость по чатам и пользователям) отвечают 404: эти настройки живут в базе моста, и правка через ядро молча не доезжала до поверхности. Клиентам ходить в инструменты telegram_*, не в ядро;
    • зрения нет: llm.vision_model и images_look удалены. Единственным входом зрения был телеграмный тул, и регистрировался он только с токеном моста. Картинки видят мультимодальные модели (флаг в llm.models) — прямо в сообщении;
    • notify.telegram заменён на notify.tool + notify.tool_args: ядро зовёт названный оператором инструмент реестра и кладёт текст полем text. Личка Господина настраивается как tool: "telegram_send_to_other_chat" с её chat_id в аргументах — телеграмной семантики у ядра при этом не остаётся. Имя пути в notify_waystool вместо telegram;
    • telegram_*-таблицы в базе ядра остались лежать: данные переживают код, но их больше никто не читает.
  • Мелкое из того же разбора: isError и гейт anchored от MCP-сервера, приехавшие не булевым значением, теперь читаются в строгую сторону (ошибка и гейт включён), а не как «успех» и «гейта нет»; отмена туров больше не отменяет молча ноль из-за замка, отравленного паникой чужого тура, — карта читается и отмена доходит.
  • Доступ тура к реестру проверяется на исполнении, а не только при выдаче спек. Whitelist чата, крон-джобы и набора отбирал список, уезжающий провайдеру, но вызов исполнялся по любому имени, какое модель назовёт, — а приехать оно могло из истории, памяти или чужого сообщения в чате. Теперь имя вне доступа тура отклоняется реестром с подсказкой, чем его открыть (toolset, tool_load); доложенное на тур зовётся как прежде. Заодно anchored_only проверяется при вызове, как остальные гейты: он единственный держался на одной видимости.
  • Уведомление из тура, заведённого уведомлением, больше не заводит следующий тур. Тур-путь notify мастерский, а chat_attention и request_approval в нём доступны — круг замыкался, и каждый его виток стоил сообщения в личку, стука в push и полного тура по токенам. Теперь тур знает свой источник (ToolCtx::source), и тур-путь из такого тура пропускается с логом; прочие пути идут как обычно, повод не теряется.
  • Непрочитанный whitelist инструментов больше не открывает весь реестр. Отказ базы был неотличим от «ограничений не настроено», а «не настроено» законно означает «всё»: под затыком sqlite тур в группе получал ровно те инструменты, которые Господин там выключил, и меню настроек рисовало их включёнными. Теперь три места различают «нет ограничений» и «не знаю»: настройки чата сужают тур до пустого набора (toolset из него по-прежнему выводит), меню вместо тумблеров наугад честно говорит, что не прочитало настройки, крон-джоба с нечитаемой колонкой не стреляет и не правится (правка соседнего поля перезаписала бы whitelist), а набор инструментов с неразобранным списком не открывает ничего. «Весь реестр» остаётся только честным NULL.
  • Отказ триаж-модели больше не вовлекает всю пачку. В часы, когда апстрим отдаёт 503, Ева влезала в каждое сообщение группы, включая пустые: сломанный триаж считался поводом не «съедать» сообщения. Теперь отказ основной модели уводит пачку на запасную (telegram.triage_fallback_model), а отказ обеих — молча в историю: вламываться во всю болтовню чата хуже, чем пропустить вопрос, а история туру всё равно видна. Вердикт основной модели заодно получил потолок в 256 токенов — он глушит рассуждения, которые выжигали выхлопа больше, чем весь промпт; запасная едет без потолка, потому что часть моделей не даёт выключать рассуждения.

2026-07-29

  • Разбор ревью. Починено: поиск текста в UTF-16 искал байты UTF-8 и молча ничего не находил (encoding_rs кодирует по правилам веба и подменяет UTF-16 на UTF-8, не считая это потерей) — теперь UTF-16 кодируется сама, а любая другая подмена стала ошибкой; отмотка чата могла снять сообщение, на которое смотрит саммари компакции, и тогда из промпта исчезала вся живая история — саммари снимается, а пропавший якорь отдаёт полную историю вместо пустоты; рабочий каталог стал живым (смена посреди тура действует со следующего вызова, а не со следующего тура), и надетый набор снимает потолок итераций сразу; файловые инструменты получили выбор машины on во всех шести схемах и называют машину в ответе; запись за концом файла отвергается вместо тихой дыры из нулей; правило про отсутствующий клиент перестало врать, что файлы недоступны.
  • Файловых инструментов нет в публичном чате: ответ там читают все, кто в нём сидит, и содержимое файла становится сообщением в группе. Гейт на личность от этого не спасал — Господин спрашивает из группы сам. Крон и подагенты недоверенные, но не публичные: у них файлы остаются.
  • Чат удалённой крон-джобы удаляется вместе с ней, если заводился под неё.
  • Крон-джоба живёт в собственном чате, а не в том, откуда её завели. cron_create брал текущий чат, поэтому таймер, заведённый посреди разговора в группе, навсегда селился в ней: его туры шли по общей с людьми истории и говорили вслух — блоговая джоба так восемь раз подряд ответила на чужое упоминание и обошлась вдесятеро дороже обычного, потому что тащила историю группы в контекст. Чат джобе по-прежнему нужен как её память и след, но это её собственная память; новые чаты ложатся в папку cron.
  • Отмена тура пишется в журнал: жалоба «кнопка не работает» иначе не отличается от «клиент не отправил запрос».
  • eva-sdk отдаёт код ответа ядра: ошибки HTTP-клиента несут ApiError (код + тело), а api_status достаёт код из отчёта даже сквозь wrap_err. Модулю это нужно, чтобы отличать «делать было нечего» (404 на отмене) от настоящего сбоя, не разбирая текст ошибки.
  • Набор инструментов стал ещё и режимом работы: рабочий каталог (workdir), снятый потолок итераций (uncapped), озвучивание шагов (narrate) и отключённая краткость (no_brevity). Кодовый режим включал клиент при старте, поэтому из телеграма и из крона его не было вовсе — хотя из шести его частей клиента требуют только две. Теперь Ева надевает режим по ходу разговора и снимает так же. Части применяются только к турам Господина; наборы coding и ops посеяны с рабочим режимом.
  • workdir — тул смены рабочего каталога чата, и поле cwd в событии client_op: относительные пути считаются от него, команды идут в нём, и cd в каждой команде больше не нужен.
  • ask_questions регистрируется всегда, а не только с телеграм-токеном ядра. Кнопки рисует любой клиент из tool_call и отвечает ручкой questions/answer — но там, где телеграм ведёт модуль, встроенный тул не появлялся, и спросить кнопками было нечем ни в tui, ни в android.
  • Правило «не переписывай дважды» стало общим: рассуждения — чтобы решить, а не чтобы набело написать код, дифф или список ссылок, которые тут же уедут в ответ. Модель писала их в мыслях, потом ещё раз в тул — Господин платил за один и тот же текст двумя счетами.
  • Файловые инструменты работают двумя руками: параметр on выбирает машину — «client» (ноутбук Господина, через подключённый клиент) или «server» (та же, где бежит shell, локальная или ssh-удалёнка). Правка кода на удалённой машине сводилась к cat <<EOF через шелл: ни точечной замены, ни номеров строк, а из телеграма и крона файлов не было вовсе. Гейт выровнен по shellmaster_only; путь к машине Господина дополнительно требует доверенного тура и клиента с поддержкой операций. Поиск образца на сервере идёт потоком с перекрытием окна и обрывается, набрав нужное число находок: игровые архивы бывают на гигабайты, а grep построчный — в двоичном файле «строка» бывает во весь файл.
  • Двоичные файлы: read_hex, write_hex, find_hex — дамп куска файла, точечный патч по смещению и поиск образца. read_file от бинаря отказывался и раньше, но теперь говорит, куда идти. Реверс формата делался через xxd в шелле, которого на машине может и не быть, а сборка патча руками портит файл молча. Колонку расшифровки можно попросить декодировать cp932, utf-16le или любой кодировкой — без этого строки японских игр в дампе выглядят частоколом точек. Клиент возит сырые байты в base64 новыми операциями read_bytes/write_bytes/find_bytes, формат дампа и разбор хекс-строки живут в ядре.
  • Модель, упомянутая в конфиге, обязана быть в llm.models, иначе ядро не стартует и называет поле (ломающее). Проверяются model.general, model.fastest, model.fallback, code_model, summary_model, vision_model и telegram.triage_model. Не перечисленная модель тихо работала хуже: без своего стира, без отметки о мультимодальности, невидимая и в меню, и в блоке ## Models, — а опечатка вскрывалась отказом провайдера посреди тура.
  • DELETE /v1/chats/{id}/messages/{message_id} — отмотать чат к состоянию до этого сообщения: уходит оно само и всё сказанное после. На этом стоят «удалить» и «переписать» в клиентах: правка — это отмотка плюс обычная отправка нового текста, и ход повторяется с него. Хвост из вызова инструмента, чьи результаты уехали под нож, снимается следом (без пары провайдеры отвергают всю историю), а саммари компакции, накрытое обрезкой, снимается — история, которую оно описывало, уже не та.
  • Крон считает деньги: cron_log показывает цену каждого срабатывания, cron_list — итог джобы за всю жизнь и число запусков. Расход тура привязывается к запуску новым spend.run_id, поэтому цена точная — окно по времени в общем чате поймало бы и параллельный живой тур. Итог копится на самой джобе (cron_jobs.spent_total/runs_total): журнал подрезается пятьюстами записями, а cron_update правит поля UPDATE-ом по id и счётчик не сбрасывает. Считает с момента обновления — задним числом расход по джобам не восстановить, в старых строках spend связи с запуском нет.
  • Размышления Евы сохраняются блоком thinking и приезжают клиентам вместе с историей: раньше они жили только в живом стриме и после перезагрузки ленты исчезали. Обратно в контекст модели не идут — выбрасываются на входе в историю, так что вход провайдеру не вырос.
  • Вики-инструменты отдают картинки сами: параметр images: N возвращает до десяти иллюстраций статьи (логотипы, иконки и шаблоны отсеиваются), а описание наконец говорит про главную картинку, которую тул отдавал и раньше. Молчание описания стоило похода в MediaWiki API через web_fetch всякий раз, когда нужна была галерея.
  • Подагент работает с файлами и шеллом Господина через тимлида: флаг client_ops наследуется от породившего тура, а client_op уезжает в живой стрим чата тимлида с именем просящего (новое поле agent в событии) — Господин видит и разрешает работу команды в одном месте. Ответ находит операцию по op_id, поэтому маршрут ответа не менялся. Стрим есть, только пока идёт ход тимлида: подагент, трогающий файлы, ходит в паре с agent_wait, а без открытого хода операция сразу отказывает с объяснением вместо ожидания до таймаута.
  • Каталог GET /v1/tools отдаёт гейты достижимости trusted_only и client_only (только когда взведены), а ToolInfo в eva-sdk читает их с дефолтом false. Меню поверхности теперь может не показывать тулы, которые из её чата всё равно не исполнятся, — как это делает menu_available встроенного телеграм-моста.
  • Агентские команды: Ева поручает часть работы подагентам на выбранных моделях и переписывается с ними в обе стороны. Подагент — это чат ядра (id агента = id чата), поэтому своя модель, история, компакция, набор инструментов и учёт расхода достаются ему даром. Вниз — agent_spawn и agent_send, наверх — agent_say и agent_done (только внутри тура подагента, гейт agent_only); плюс agent_wait, agent_inbox, agent_list, agent_stop. Доклад забирается синхронно в agent_wait или приезжает блоком [subagents] в [state] следующего тура. Предохранители: подагент не спавнит подагентов, chats.max_agents (по умолчанию 4) ограничивает ширину, упавший агент не пропадает молча. Новое: таблицы agents и agent_messages, папка чатов agents (скрыта из общих списков), GET /v1/agents с расходом на каждого и итогом, POST /v1/agents/{id}/stop. Блок ## Models в промпте собран из description моделей — по нему выбирается исполнитель.
  • read_file и local_shell прямо разводят обязанности: файлы читаются read_file, а не сбросом через шелл. Шелл отдаёт сырьё, которое режется по символам вслепую, тогда как read_file нумерует строки и говорит, сколько их в файле, — то есть даёт чем листать дальше и по чему потом править write_diff. Фильтрация, конвейеры и слежение за потоком остаются работой шелла — запрета на команды нет.
  • eva-sdk забрал обвязку модуля: Kernel::up(&cfg.kernel) подключается и дожидается ядра одним вызовом, module.component() отдаёт готовый компонент супервизора (сокет, корректная остановка и одноразовость — внутри), а секции KernelConfig и ToolsConfig теперь общие. Модуль стартует вместе с ядром, а на передеплое и раньше — цикл ожидания писал каждый заново, как и serve_uds с select! и обёртку Mutex<Option<…>> против перезапуска компонента.
  • Ядро на остановке дожидается идущих туров (server.drain, по умолчанию 120 с) и только потом сворачивает их штатной отменой, дописывающей частичный ответ. Передеплой посреди тура рвал его на полуслове: SSE-ответ живёт, пока идёт тур, и процесс добивал systemd своим таймаутом. TimeoutStopSec = 180 в NixOS-модуле даёт запас над окном слива.
  • eva-sdk: число, присланное моделью строкой, приводится к типу по схеме. Большие id модели сплошь и рядом заворачивают в кавычки, а строгий разбор отвечал «invalid type: string, expected i64» — модель читала это как «число слишком длинное» и начинала чинить не то. Строка, которая числом не является, остаётся честной ошибкой.
  • Подмена модели тянет за собой её стир. Стир вклеивался в системный промпт один раз в начале тура, поэтому запасная модель ехала с промптом, скроенным под отказавшую, — и упиралась в цензуру, от которой её собственный стир и спасал.
  • Цепочка запасных моделей (llm.model.fallback): отказ, привязанный к конкретной модели (упавший ключ на одном апстриме, снятая с раздачи модель, кончившаяся квота), передаёт тур следующей вместо того, чтобы ронять его вместе с уже сделанной работой. Транзиентную икоту по-прежнему лечит ретрай той же моделью — менять её там незачем.
  • Схемы MCP-инструментов чистятся на приёме: $schema, title, additionalProperties, examples и прочая мета уезжали провайдеру в каждом запросе. Замер на живом реестре: 155 схем, 23 686 → 19 181 токенов, минус 19%. default, format, enum, pattern и границы оставлены — это смысл, по которому модель решает, что подставить.
  • Блок правил спикеров едет только туда, где спикеры бывают (тур с якорем или с названным отправителем). В кодовой сессии и кроне маркерных строк не появится ни одной, а объяснение их формата весило в каждом запросе.
  • read_file без окна тоже отдаёт шапку «[lines A-B of N]». Оффсеты построчные, а ответ режется по символам — не зная числа строк, модель не могла посчитать, откуда читать дальше.
  • Перечитанный файл вытесняет прежнюю копию из контекста. read_file по тому же пути делает старый результат не просто балластом, а вредным — модель видела бы вчерашний текст. Чистится только за пределами живого окна: там байты и так переписываются элизией, и префикс-кэш не теряет сверх уже потерянного.
  • /v1/stats разрезает расход по источнику тура (by_source): без этой оси расход подагентов растворялся в общем итоге и бюджетировать команду было нечем.
  • Неинтерактивное окружение у shell: PAGER=cat, TERM=dumb, GIT_TERMINAL_PROMPT=0, NO_COLOR. Программа, решившая, что у неё есть терминал, вешала вызов до самого таймаута — git log уходил в пейджер и ждал нажатия, — а раскраска засоряла выхлоп escape-кодами.
  • Тур предупреждает о близком потолке итераций за два хода до него: упереться в лимит молча значило потерять всю работу тура (Err и ничего больше). Теперь Ева успевает свернуться и отдать частичный отчёт.
  • Инструмент может пометить свой ответ пустым (tools::uneventful): «0 совпадений», пустой инбокс, ожидание по таймауту. Такое выбрасывается из контекста первым — эвристике по возрасту и размеру короткий «no matches» не отличить от находки, а инструмент знает наверняка. Метка нулевой ширины, модели не показывается.
  • Выхлоп инструмента обрезается серединой, а не хвостом. Сборщики, тесты и линтеры кладут вердикт в конец — отрезав его, модель получала простыню предупреждений без единого слова о том, что сломалось, и гоняла ту же команду заново с | tail: лишний поход к провайдеру и полное время пересборки на ровном месте.
  • Гейт якоря: Tool::anchored_only() и поле anchored в Flags. Инструмент, которому нужно исходное сообщение поверхности, не едет в тур без якоря — в кодовой сессии и кроне его вызов кончился бы только ошибкой. Модуль объявляет это сам через _meta["dev.eva/anchored"] (в eva-sdk — Module::anchored), так что ядро не узнаёт, какая у него поверхность. Помечены telegram_skip и telegram_attach_photos.
  • PUT /v1/chats/{id}/toolset — закрепить набор за чатом; поверхностям нечем было его переключать. В eva-sdk: toolsets(), set_toolset(), chat(), а у Chat появились model и toolset.
  • Два новых события тура. usage — расход очередного похода к провайдеру записан: клиент двигает счётчик по ходу тура, а не одним прыжком в конце. compacting — ядро сворачивает историю: это идёт после ответа, перед Done, и на длинной истории пауза выглядела зависанием.
  • POST /v1/chats/{id}/clear — стереть переписку чата начисто (сообщения и саммари компакции), чтобы следующий тур начался с пустого контекста. Тулом chat_clear это умела только сама Ева; теперь умеют и клиенты.
  • NixOS-модуль: у services.eva-kernel.mcp.<имя> появился description — без него новое поле конфига было недоступно из модуля.
  • eva-sdk: у TurnEvent появился Unknown под #[serde(other)]. Ядро развивается быстрее модулей, и без этой ветки первое же новое событие ломало бы разбор и рвало стрим тура целиком — модулю его достаточно пропустить.
  • Иконки лент: toolset / tool_load / tool_find — 🧰, семейство agent_* — 🤖.
  • tool_find — поиск скрытого инструмента по тому, что он должен делать, когда имени не знаешь. Каскад: сперва лексика по основам (та же токенизация, что у памяти — ноль токенов и доли миллисекунды), и только если она промахнулась — быстрая модель с реестром скрытого в промпте. Вторая ступень нужна для «запрос по-русски, описания по-английски»; ответ модели сверяется с реестром, выдуманное имя наружу не выходит. Найденное сразу догружается на текущий тур.
  • Наборы инструментов: именованный срез реестра под род задачи. Список наборов висит в системном промпте, Ева переключает его тулом toolset (scopechat по умолчанию или turn, имя null снимает). Всё вне набора уезжает ей блоком «Hidden tools» — имя и назначение, без схем; крупные семейства свёрнуты в строку. Понадобилось скрытое — tool_load возвращает тул или семейство на текущий тур. toolset, tool_load и skill видны при любом наборе: из узкого набора обязан быть выход. Заводит и правит наборы Господин (toolset_create / toolset_edit / toolset_delete). Гейты личности — поверх всегда. В доверенном туре набор заменяет турный whitelist, в недоверенном может только сузить: белый список публичного чата остаётся потолком. Новое: таблица toolsets, колонка chats.toolset, GET /v1/toolsets, поле toolset в POST /v1/chats/{id}/messages (набор на один тур), выбор набора в /settings рядом с моделью. На первом запуске засеваются пять стартовых наборов (coding, ops, social, research, money); посев одноразовый — защёлка в новой служебной таблице kernel_meta, удалённый набор не воскресает.
  • У модели в llm.models появилось description — для чего она хороша. Едет клиентам в GET /v1/models полем descriptions (карта имя → строка) и ляжет в основу выбора исполнителя, когда появятся агентские команды.
  • MCP-сервер и модуль представляются ядру сами: _meta["dev.eva/about"] в ответе initialize (на eva-sdk — Module::about(...)). Этой фразой семейство показывается модели в реестре скрытого. Новое поле description у mcp.servers.* и modules.servers.* — ручной override поверх сказанного сервером и единственный способ описать тех, кто _meta выставить не может (FastMCP не может).
  • (ломающее) eva-sdk: вход инструмента — тип под #[data], а не сырой Value с рукописной схемой. Module::tool берёт три аргумента (имя, описание, обработчик), схема генерируется из типа: доки полей становятся описаниями, Option — необязательным полем. Обещанное модели и прочитанное инструментом больше не могут разъехаться; вход не по схеме — ошибка инструмента, а не молчаливый null в середине обработчика. Модули на Rust переписывают объявления входов; NoInput — инструмент без аргументов.

2026-07-28

  • Инструменты модуля зовутся <модуль>_<инструмент> и живут в группе с именем модуля: модуль объявляет короткое react, в реестре стоит telegram_react. Префикс mcp_ и общая группа mcp остались чужим MCP-серверам — гостям, чьи имена не должны сталкиваться с нашими.

  • Книга разводит понятия: модуль — часть Евы (свои имена инструментов, контекст тура, middleware), MCP-сервер — сторонний гость с префиксом mcp_. Раньше глава мешала их в одно и вводила в заблуждение.

  • Глава «Хуки: middleware и события» — механика перехвата с правом вето и подписок на события, с примерами на обоих SDK. Введение книги и ECOSYSTEM переписаны вокруг модульности: каналы связи ядру не принадлежат.

  • Крейт eva-sdk (crates/eva-sdk) — им пишутся модули: клиент API ядра (чаты, туры со стримом, complete, личности, реестр), MCP-сервер модуля и стадии middleware (middleware/handle, объявляются в handshake через _meta["dev.eva/middleware"]). Ядро от него не зависит, версионируется вместе. Python-версия — репозиторий eva/sdk-py. Книга — глава «Модули и SDK».

  • POST /v1/complete — одноразовый вызов модели без тура, истории и инструментов: модуль сам триажит и сочиняет служебные строчки.

  • tools/call к модулю несёт контекст тура в _meta["dev.eva/ctx"] (чат, говорящий, доверие, якорь сообщения) — без него инструмент поверхности не знает, чей ответ гасит.

  • POST /v1/chats/{id}/messages принимает поля поверхности: anchor, chat_title, reply_to_ext_id/reply_to_text, guest, max_tokens, brief, surface_model, tools. Раньше так умел только встроенный телеграм-мост, мимо API.

  • (ломающее) Телеграмные ручки свелись к двум: telegram_setting (scope, id, key, value — дебаунс, потолок ответа, краткость, мультимодалка, «смелый» typing, спонтанность) вместо десяти отдельных и telegram_access (action, kind, id) вместо пяти allow/deny/list. Имена в telegram.master_tools и в чатовых whitelist’ах нужно обновить; минус ~1400 токенов из каждого мастерского тура.

  • Строку «↩ #12, #15» из конца ответа читает и ядро: закрытые id ложатся в базу и уезжают в служебную шапку прошлой реплики — «[#43 ↩#41, #38]». Модель видит всё, что уже закрыто, а не только цель реплая.

  • Пачку телеграм-сообщений закрывает ОДИН тур (по последнему вовлечённому): раньше на каждое упоминание уходил свой тур с полным контекстом. Ответ всегда кончается строкой «↩ #12, #15» — какие сообщения он закрыл.

  • Ревизия промптов: сжаты правила спикеров, окружения, клиентских операций, спойлеров, ресёрча и проблем; снято противоречие «не комментируй» против «комментируй ход работы»; правило картинок узнало про telegram_attach_photos. Сниппет цитаты не дублируется, когда родитель лежит в том же окне истории.

  • uncapped_iterations в POST сообщения: доверенный клиент (кодовая сессия) снимает потолок итераций тура, не спрашивая через lift_turn_limit — длинная задача не обрывается на середине.

  • Крон-джоба объявляет свой whitelist тулов (tools в cron_create/cron_update, имена и группы): всё вне списка ей недоступно, спеки лишних тулов не уезжают провайдеру на каждое срабатывание. Доступ тура к реестру описывает явный ToolAccess (All / Only) вместо двусмысленного Option<Vec<String>>.

  • Одноразовые вызовы (триаж, титульник, компакция, squeeze, vision, code_task, оффлоад навыка, декоративные реплики) больше не пишут промпт в кэш: запись стоит дороже входа, а перечитывать её некому.

  • GET /v1/chats/{id}/spend — во что обошёлся чат (деньги, токены, запросы) и сколько весит его контекст сейчас: клиенты рисуют попап расхода сессии.

  • Ева знает свою сборку: версия и ревизия гита попадают в бинарь (EVA_COMMIT из флейка или build.rs), едут строкой в системный промпт и отдаются в /health.

  • Внимание Господина: у чата есть needs_attention (клиенты показывают такие первыми) и мягкое удаление — DELETE /v1/chats/{id} прячет чат, ?deleted=true показывает корзину, POST /v1/chats/{id}/restore возвращает. Ева заводит чат на проблему в папке chats.problem_folder, чинит сама и зовёт, только когда не вышло (тулы chat_attention, chat_delete).

  • Очередь одобрений: approval_request/approval_status для Евы, GET /v1/approvals + POST /v1/approvals/{id}/decide для клиентов — разрешение спрашивается асинхронно, тур не блокируется.

  • Секция notify: телеграм-личка, чат ядра, пуш на телефон и тур по инструкции — включать можно сколько угодно путей сразу; тул notify_master шлёт вручную, любой из путей можно сузить на вызов.

  • Поле context в POST сообщения: клиент присылает системный довесок тура (фрейминг кодовой сессии, AGENTS.md проекта) — уходит в system-промпт блоком [client context]; чтится только у доверенных клиентов.

  • Процессные модули: MCP по UDS/TCP (line-JSON-RPC), регистрация на лету тулами module_list/register/approve/unregister — тулы появляются и исчезают без рестарта ядра (реестр стал живым), решения в базе, socket_dir — витрина кандидатов, вотчер замечает уход/возврат сокета. Протухшее соединение переподключается с одним автоповтором — рестарт модуля невидим для модели.

  • NixOS-модуль: секция services.eva-kernel.mcp прячет stdio-бинари за стабильные симлинки /run/eva-mcp/* — бамп пакета сервера не рестартует ядро, activation подменяет процесс.

  • MCP-серверы — горячезаменяемые модули: tools/list кэшируется в базе, лежащий на старте сервер поднимается с тулами из кэша и оживает первым вызовом (переподключение stdio уже было ленивым).

  • Документация переехала в mdbook (docs/, пакет .#docs, живёт на https://eva.desu.church); README ужат до визитки.

  • API-справочник — OpenAPI из utoipa-аннотаций (англ.): /v1/openapi.json

    • Swagger UI на /apidocs; рукописная таблица из книги удалена.
  • Кнопка «⏳ В фон» на стрим-сообщении: тур доезжает в фоне, воркер чата сразу берёт следующие сообщения; «✋ Отмена» — с иконкой.

  • Меню настроек пользователя (пресеты: дебаунс, потолок, краткость, мультимодалка) и тул telegram_settings_menu — Ева открывает меню сама.

  • Прямая мультимодалка настраивается по цепочке user > chat > global (telegram_multimodal / telegram_user_multimodal); резолв модели фото учитывает telegram.model.

  • Отчёт о chat_rebuild шлётся только при успешном tool_result и несёт фактический итог — упавшая пересборка «Готово!» больше не рапортует.

  • telegram.menu_quips — вкл/выкл LLM-реплики меню; на реплики — дебаунс 15s на чат. Отчёт о chat_rebuild пишет модель, «печатает…» на время пересборки гаснет, финал — реплаем на сообщение тура.

  • Ответ, закрывший несколько сообщений, помечается строкой «↩ #12, #15»; промпт телеграма ужат, реакция-вместо-текста — первым классом.

  • model в cron_create/cron_update; выхлоп chat_summary_get не режется гигиеной; параллельные тул-колы разрешены провайдеру.

  • Иконки и brief-шаблоны тулов: MCP-серверы несут _meta["dev.eva/icon"] (эмодзи) и _meta["dev.eva/brief"] («{from} → {to}» по входу вызова); каталог /v1/tools отдаёт icon/brief/group, у встроенных тулов — своя карта иконок, телеграм-лента рисует brief вместо выжимки JSON.

  • (ломающее) llm.model — секция {general, fastest} вместо строки; fastest — мгновенная модель декоративных реплик (меню, inline).

  • llm.proxy — прокси до LLM-эндпоинта: WAF провайдера блочил прямой IP хоста ядра.

  • /settings в телеграме: не-LLM меню на callback-кнопках — тумблеры тулов чата (whitelist в базе) и выбор модели чата. telegram.master_tools (точные имена и группы тулов) переключает только Господин; в групповом меню такие тулы скрыты целиком, Господину консоль чата уезжает в личку. Меню двухуровневое: обзор групп → страница группы (сотня тулов одной простынёй не влезала в лимит reply markup). Показываются только достижимые из чата тулы (client_only и trusted_only в группах — нет); консоль с master-тулами уезжает в личку кнопкой, а не автоматически.

  • Явный reasoning: {enabled: false} для служебных вызовов (когда глобальный reasoning включён): думающие по умолчанию модели сжигали бюджет ответа рассуждением — био выходило пустым.

  • Явные группы тулов при регистрации (register_in): memory, person, skill, cron, chat, web, client, telegram, mcp, mcp_<сервер>.

  • Самооформление бота: setMyCommands и описание на старте; живое био — telegram.bio {interval, model}.

  • Упавший телеграм-тур больше не шлёт «turn failed» в чат: ошибка в логе, жалоба с греп-id — в коротком описании бота; здоровый тур её снимает.

  • Inline-режим (@бот вопрос) — ответ статьёй, только разрешённым.

  • Комментарий к посту канала несёт полный текст поста в контексте.

  • telegram.model — дефолт модели телеграм-чатов (слабее per-чат выбора).

2026-07-24

  • Сжатие формата ввода: короткие person id, сигилы, дата в маркерах — по смене дня.
  • /v1/stats: календарные день/неделя/месяц с ?tz=.
  • jiff с забандленным tzdb — бинарь не зависит от системной базы зон.

2026-07-23

  • Клиентские операции: read_file/write_file/write_diff/local_shell исполняются клиентом (событие client_op + /ops/result), гейт client_only; правила промпта — комментировать ход работы с кодом.
  • Поле jailbreak у модели в llm.models: per-model стир против встроенной цензуры, частью кэшируемого промпта.
  • own-note щит: срез стопки маркеров, независимость от разделителя; срез служебной шапки [msg N · reply_to msg M] из вывода модели.

2026-07-22

  • Мультимодальность: картинки прямо в сообщение мультимодальных моделей (флаг в llm.models), телеграм-мультимодалка — привилегированным, вики-картинки; окружение тура в промпте.
  • Компакция: агрессивный профиль по папкам (aggressive_folders), закрепление первого сообщения (pin_first_message_folders), отдельный cron-профиль — экономия на фоновых джобах.
  • lift_turn_limit (master-only): снятие лимита итераций хода.
  • shell.output_cap: вывод shell по умолчанию не режется.
  • eva bump: env приоритетнее файлов, вложенность через __.
  • Доки: ECOSYSTEM.md, пример конфига по образцу infra, правило «не спойлерить» в промпте.