Неделю назад я выложил skillmem — локальную память процедур для кодовых агентов. За прошедшие сутки вышло одиннадцать релизов: с 0.9.3 по 0.10.5. Не потому, что чесались руки, а потому что я сел проверять собственный продукт и нашёл в нём подряд одиннадцать вещей, которые надо было починить. Это пост про то, что именно, — почти каждый пункт переносится на любой инструмент, который живёт внутри цикла агента.

Начало: 4083 сессии за сутки

Началось с безобидного вопроса: а всё ли у нас работает. Полез в лог хуков и увидел, что Stop-хук за день сработал 4083 раза, притом что днём раньше — шесть.

Механизм: хук на Stop просит модель сделать выжимку сессии через claude -p. Дочерний процесс — это полноценная сессия Claude Code, и на своём Stop он дёргает тот же хук. Дна нет. За сутки: 3901 фантомная сессия, гигабайт транскриптов на диске, ~286 млн кэш-токенов подписки, а мусорные выжимки вида «сессия пуста, пришлите текст» лезли мне же в контекст при следующем старте.

Самое обидное: этот баг у нас уже был — 09.09, и был закрыт тремя предохранителями. При переименовании проекта модуль хуков перенесли, и потерялись все три: env-флаг в потомке, слот-семафор и --strict-mcp-config. Я вернул сначала один, потом, разобравшись, все.

Урок, который стоит повесить над столом: любой хук, запускающий агента, обязан глушить себя в потомке. И если вы переписываете модуль, где были предохранители, — переносите все, а не те, что помните.

Дальше — по списку

0.9.3 — анти-рекурсия (env с флагом отказа в дочерний процесс) плюс тест на то, что флаг реально доезжает. Старый тест проверял только «если флаг выставлен снаружи — молчим», поэтому регрессия и прошла зелёные тесты.

0.9.4 — частотный лимит. Выяснилось, что Stop в Claude Code срабатывает после каждого ответа, а не при закрытии сессии: в обычный рабочий день это вызов модели на каждый ход и куча почти одинаковых заметок. Плюс: метка ставится на попытку, а не на успех (иначе падающая модель покупает новый вызов на каждом ходу), проверяется код возврата (иначе текст ошибки «usage limit reached» длиной больше 100 символов записывался как память и затирал хорошую выжимку), и вернулся слот-семафор.

Там же — маленькая, но приятная находка: дочерний claude -p теперь запускается с --no-session-persistence, и фантомные транскрипты не появляются вообще. Гигабайт мусора больше не воспроизводится.

0.9.5 — четыре тихих дефекта, которые нашло внешнее ревью:

  • recall для NotebookEdit был мёртв: хук читал file_path, а Claude Code для ноутбуков присылает notebook_path;

  • 7793 заметки из 7793 лежали без привязки к сессии — писатель кладёт в frontmatter source_session, а импортёр знал только originSessionId;

  • страж MCP врал цифрой: считал все серверы, а не пересечение с эталоном, и мог сказать «12 из 10 подключено»;

  • леджер показанных записей жил в общем /tmp, где сосед по машине может создать файл заранее и заглушить чужой recall.

0.9.6 — гонки за финальную выжимку. Медленный Stop и SessionEnd за ним перекрывались, и старый результат затирал финальный. Заметка теперь хранит, по какому объёму транскрипта она сделана, и тот, кто прочитал меньше, не пишет поверх. Плюс честный бюджет: Claude Code поднимает бюджет SessionEnd до таймаута хука, но не выше 60 секунд — значит финальный вызов должен просить у модели меньше.

0.9.7 — граница доверия (о ней ниже), плюс skillmem recap и skillmem hooks-status. Второе появилось из простого наблюдения: хуки по дизайну глушат свои ошибки, чтобы не ломать сессию, — а значит хук, который тихо умер, выглядит ровно как хук, которому нечего делать. Теперь есть команда, которая показывает запуски, пропуски, падения и последнюю строку каждого.

0.9.8 — пропущенный ход больше не читает транскрипт. Проверка лимита стояла после подсчёта строк файла, то есть на каждом пропущенном ходу мы прочитывали весь транскрипт впустую. На моей машине это файлы до 59 МБ и 840 МБ суммарно.

0.9.9 — публикация выжимки стала compare-and-swap: проверка свежести и замена файла были двумя шагами, и Stop, прошедший проверку до того, как SessionEnd записал финал, затирал его после. Базис перечитывается внутри короткого лока — миллисекунды, не вызов модели.

0.10.0 — граница доверия, самый большой кусок. Об этом отдельный пост, коротко: у каждой записи есть происхождение (owner/agent/imported/derived), доверие даёт только владелец командой skillmem trust, правка текста одобренной записи снимает одобрение, а всё неодобренное подмешивается агенту внутри помеченной рамки «данные, не инструкции». Суммаризатор транскриптов запускается вообще без инструментов (--tools "" + --strict-mcp-config), а если CLI этих флагов не знает — выжимка не делается совсем. Закрывает путь «чужой README → память агента → правило пользователя».

0.10.1 — оказалось, что три манифеста (включая server.json для официального MCP Registry) всё ещё рекламировали версию 0.9.2 — то есть кто поставил бы нас через реестр, получил бы версию с той самой рекурсией.

0.10.2 и 0.10.3 — два конца одного дефекта, и его нашёл не я, а CI. Тест падал на всех ОС, и тест был прав: FTS-запрос резался по пробелам, поэтому /work/analysis.ipynb превращался в один фразовый токен и не находил ничего. А хук tool-recall передаёт в запрос именно путь файла. На обычной установке pip install skillmem (без семантики) recall для Edit/Write/NotebookEdit был мёртв целиком — локально это прятал эмбеддер. Копнув глубже: индекс сам выбрасывал токены короче трёх символов, так что db, py, js, ci отсутствовали во всех записях.

Второй урок на стену: держите в CI прогон без опциональных зависимостей. У нас он был, и только поэтому дефект всплыл.

Там же поймал свою ошибку в миграции: перестроение индекса на живой базе (8917 записей) занимает 66 секунд, а init_schema вызывается каждым хуком с таймаутом 10 секунд. Миграция теперь только ставит флаг, тяжёлую работу делает ночное задание или skillmem reindex-lexical.

0.10.4 — нашёл при проверке настоящей установки на Ubuntu: чистая база сообщала, что ей нужна переиндексация. На свежем install doctor пугал бы человека на ровном месте.

0.10.5 — Dockerfile в репозитории и честный serverInfo. Каталоги MCP собирают контейнер, чтобы оценить сервер, и если их AI-сборка не проходит, сервер не показывается в поиске каталога. Своя сборка надёжнее угадайки: образ 297 МБ, initialize отвечает реальной версией, tools/list отдаёт все 9 инструментов по stdio. А до этого сервер представлялся версией SDK 1.30.0 — версией, которой у пакета никогда не было.

Кто всё это нашёл

Счёт честный, и он не в мою пользу:

Кто

Что нашёл

Я сам

рекурсию, потерянные предохранители, recall_skills без поля доверия, 66-секундную миграцию в хуке

Внешнее ревью (Codex/GPT-6, читал код целиком)

дыру в ТЗ до кода, три P1 в реализации, два остаточных блокера

CI

мёртвый lexical-recall по пути файла — на всех ОС сразу

Проверка на Ubuntu

ложный флаг переиндексации на чистой базе

Петля, которая себя оправдала: ТЗ → ревью другой моделью → реализация → ревью → деплой. Причём каждое утверждение ревьюера проверялось командой на живой системе, а не принималось на слово: из шести проверенных одно оказалось неверным (баг уже был починен по ходу).

Что в итоге

214 тестов, и каждая защита проверена откатом правки — ломаю фикс, тест краснеет, иначе тест не считается. Кроссплатформенность проверена не декларацией, а установкой: macOS и Ubuntu из PyPI, Windows в CI.

pip install -U skillmem
skillmem init            # Claude Code: хуки на пяти событиях + 9 MCP-инструментов
skillmem init --codex    # та же память для Codex CLI
skillmem hooks-status    # что хуки реально делали: запуски, пропуски, падения

Если вы на 0.9.0–0.9.2 — обновитесь. Там рекурсия, и она стоит вам квоты.

И вопрос к залу, на который у меня нет хорошего ответа: как строго проверить, что читатель недоверенного текста действительно лишён прав? У меня есть канарейка (файл с уникальной строкой, которую суммаризатор не должен суметь прочитать), но у неё нет положительного контроля — ошибка авторизации даст такой же зелёный результат. Если у вас есть идея лучше, напишите в issues.