Компьютер начал тупить в середине дня. VS Code переставал отвечать, новые вкладки редактора не открывались по минуте, а лимит подписки на ИИ-ассистента сгорал к обеду вместо конца недели. Виноватым оказался не ассистент, а плагин, который ведёт для него память между сессиями.
Разбор занял несколько заходов и вскрыл четыре независимые причины. Ни одна из них не падала с ошибкой: всё «работало».
Что вообще происходит
Плагин памяти пишет в SQLite наблюдения о том, что агент делал в сессии, а рядом держит векторную базу для семантического поиска. У нас накопилось 59 030 наблюдений и 400 МБ базы, так что холодный старт — операция не мгновенная.
Векторная база поднимается не как сервис, а как подпроцесс на время сессии. Вот в этом месте всё и началось.
Причина первая: дерево процессов и семантика убийства в Windows
Запуск выглядел так:
bun (воркер) → cmd.exe → uvx → uv → chroma-mcp → python → python
Шесть процессов на одно подключение. Воркер ждёт готовности базы тридцать секунд, база на нашем объёме не успевает прогреться — воркер считает попытку неудачной и убивает подпроцесс.
Убивает он cmd.exe. На Windows это не убивает дерево: cmd.exe умирает, а uvx, uv, chroma-mcp и два python остаются жить сиротами и держат около 400 МБ. Дальше срабатывает backoff, воркер спавнит новое дерево — и через полторы минуты у вас четыре осиротевших дерева. Я наблюдал это вживую: четыре штуки за полторы минуты.
За неделю накопилось 44 процесса chroma-mcp и около сотни python. Один из сирот открутил 131 минуту процессорного времени вхолостую. Загрузка системы стояла в сотне процентов, и «комп тупит» было не жалобой, а диагнозом.
Найти их можно так:
Get-CimInstance Win32_Process -Filter "Name like 'python%' or Name like 'uv%'" | Where-Object { $_.CommandLine -like '*chroma*' } | Select-Object ProcessId, CreationDate
Смотреть надо на дату создания: живое дерево одно и свежее, всё остальное с прошлой недели.
Лечение симптома — прибить лишние. Лечение причины — убрать сам повод спавнить: поднять векторную базу постоянным сервером и переключить плагин в режим клиента.
uvx --from chromadb chroma run --path C:/Users/User/.claude-mem/chroma --port 8000
и в настройках плагина CLAUDE_MEM_CHROMA_MODE=remote, хост 127.0.0.1, порт 8000. Сервер уже тёплый, коннект мгновенный, тридцатисекундный таймаут не срабатывает — деревьям неоткуда взяться. Автозапуск повесили на задачу планировщика при входе плюс ежечасный сторож, который проверяет занятость порта и не поднимает второй экземпляр.
Причина вторая: воркеры размножались параллельными вкладками
Второй источник паразитов — сам воркер памяти. Замок от дублей в плагине рассчитан на нормальную работу, но не на десяток одновременно открытых вкладок редактора, каждая из которых стартует свою сессию. Замок не выдерживал наплыва, и воркеры плодились, конкурируя за одну и ту же базу.
Здесь мы не стали изобретать: обновили плагин с версии 12.1.0 до 13.15.3, где в списке изменений прямо значатся «zombie process detection» и «stale-worker recycle loop». Плюс поставили внешний сторож, который душит расплодившихся.
Причина третья: снежный ком в контексте наблюдателя
Самая дорогая часть истории — не процессы, а токены.
Наблюдения о том, что делал агент, пишет отдельная модель подешевле. Она получает свежее событие и, для связности, хвост предыдущих сообщений. Размер хвоста в настройках стоял двадцать.
Двадцать сообщений агентской сессии — это двадцать полных выводов команд. Один Read большого файла, один grep по репозиторию, один длинный лог сборки. В сумме на один вызов уходило до 243 тысяч токенов входа. И такой вызов делался на каждое действие агента: 2384 вызова за день.
Отдельно выяснилось, что наблюдатель прилежно комментировал работу фоновых кронов: 1576 наблюдений вида «скрипт опубликовал пост» за то же время. Эти наблюдения не нужны никому, но стоят как настоящие.
Что сделали:
Урезали хвост контекста с двадцати сообщений до пяти. Добавили в список пропускаемых инструментов чтение файлов и поиск — Read, Grep, Glob, WebFetch: суть действия видна из самого вызова, содержимое прочитанного файла в память не нужно. И исключили из наблюдения проекты, запускаемые планировщиком.
Заодно нашли причину дневного краш-лупа «Generator restart limit exceeded»: в настройках стоял max_tokens=100000, а у выбранной модели потолок ответа 32 768. Каждый вызов возвращал 400, воркер перезапускался, и так по кругу. Поставили 32000 — луп прекратился.
Причина четвёртая: миграции доезжают наполовину
Через два дня после обновления хук на старте сессии сообщил: память не может сохранять, восемь отказов подряд, FOREIGN KEY constraint failed. К моменту разбора счётчик обнулился сам, а в файле состояния лежала уже другая ошибка:
table session_summaries has no column named discovery_tokens
Миграция ALTER TABLE ... ADD COLUMN discovery_tokens в коде плагина есть. На таблице observations она отработала, на session_summaries — нет. Результат: наблюдения писались нормально, а сводки сессий молча не писались четверо суток.
Это худший вид поломки. Счётчик отказов её не показывает, потому что отказы разные и он обнуляется. Пользователь видит, что «память работает» — она и работает, наполовину.
До этого точно так же не доехала миграция на другой таблице: не хватало трёх колонок в очереди сообщений. Тот же почерк.
Проверка, которая ловит это за секунду, — сравнить свежесть двух таблиц:
SELECT max(created_at) FROM observations; SELECT max(created_at) FROM session_summaries;
Разъехались на сутки — миграция недоехала. Сейчас у нас 12:33 и 12:27 одного дня, то есть в ногу.
Что не сработало
Первым делом я пересадил наблюдателя на другую, бесплатную модель — расход же от модели. Это было неправильно вдвойне: выбор модели тут не мой, а решение владельца, и главное — причина была не в цене за токен, а в количестве токенов. Пересадка спрятала бы симптом на неделю.
Не сработала и ставка на счётчик ошибок плагина. Он показывает «сбоев нет» в ситуации, когда одна из двух таблиц не пишется вовсе.
Не помогло и обновление версии как способ починить схему: свежая версия из npm подтягивается, а недоехавшую колонку не добавляет — потому что в её журнале миграций отметка «выполнено» уже стоит.
Оценка стоимости внутри плагина тоже врала: он показывал 251 доллар за день при реальном биллинге ключа 112 долларов за всё время. Причина прозаическая — устаревшая таблица цен в коде. Считать надо по биллингу провайдера, а не по красивой цифре в интерфейсе.
Цифры до и после
что мерили |
было |
стало |
|---|---|---|
процессов векторной базы |
44 |
1 сервер + 14 штатных |
вход на один вызов наблюдателя |
до 243 000 токенов |
тысячи |
вызовов в день |
2384 |
примерно вдесятеро меньше |
мусорных наблюдений о кронах |
1576 |
0 |
сводки сессий |
не писались 4 суток |
пишутся, 5436 всего |
Лимит подписки перестал выгорать в фоне: пятичасовое окно теперь тратится на работу, а не на пересказ содержимого прочитанных файлов.
Что я забрал из этой истории
Убийство процесса на Windows не убивает дерево. Если ваш код спавнит через оболочку и потом «прибирает за собой» — проверьте, что именно умирает.
Любой хвост контекста, который растёт вместе с сессией, — это счётчик на такси. Двадцать сообщений выглядят безобидно ровно до тех пор, пока в них не попадёт вывод одной большой команды.
И самое неприятное: инструмент, который сообщает о своём здоровье сам, врёт охотнее всего. Счётчик отказов, отметка «миграция выполнена», оценка расходов в интерфейсе — три источника, каждый из которых показывал норму, пока система молча теряла данные и деньги. Проверять надо внешние признаки: реальные процессы в системе, реальный биллинг, реальные даты последних строк в таблицах.