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_bytes32 → 16 KiB,memory_max_bytes16 → 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снято с довольствия: оно выбрасывало ответ св телеграмном туре, а теперь так картинка туда и прикладывается. Волна калибровки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::ToolCall→payload::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_delta→item_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_formatc 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-rssecrets/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_oplist_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_shellrm/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, и старое булево читается как раньше (true—mid,false—off), так что конфиги на машинах править не надо. Ступень выбирается по убыванию силы: поле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_profile—low,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_update(иPOST /v1/cron) сверяют каждое имя с реестром — тулом или группой регистрации. Незнакомое имя — ошибка с ближайшими похожими и списком групп, чтобы модель поправила список сама. Прежде опечатка («telegram_send» вместо группыtelegram) молча рождала немую джобу: она стреляла по расписанию, но отправить результат ей было нечем. Сверка —ToolRegistry::validate_access, общая для любого сохраняемого списка доступа. Регрессия —benchmark/scenarios/02-selftest-cron-whitelist.yaml; в чеках бенча уcron_existsпоявилось полеtools(точный whitelist) и обратный чекcron_not_exists. -
Новый
cron_fire(иPOST /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-2→qwen-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_output(вllm.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/stats(иusage_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_ways—toolвместо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через шелл: ни точечной замены, ни номеров строк, а из телеграма и крона файлов не было вовсе. Гейт выровнен поshell—master_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(scope—chatпо умолчанию или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; рукописная таблица из книги удалена.
- Swagger UI на
-
Кнопка «⏳ В фон» на стрим-сообщении: тур доезжает в фоне, воркер чата сразу берёт следующие сообщения; «✋ Отмена» — с иконкой.
-
Меню настроек пользователя (пресеты: дебаунс, потолок, краткость, мультимодалка) и тул
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, правило «не спойлерить» в промпте.