Началось с того, что я полвечера дебажил утечку коннектов в postgres, которую — я это смутно помнил — уже дебажил. Не похожую, а ту же самую: тот же сервис, тот же pgxpool, те же симптомы. Фикс в итоге нашёлся около полуночи, и не в git-истории и не в заметках (заметок, разумеется, не было). Он лежал в транскрипте Claude Code от апреля — в JSONL-файле под ~/.claude/projects/, где пролежал всё это время.
Тут я задумался, сколько такого добра у меня вообще накопилось. Оказалось — 3,3 ГБ на одном только ноутбуке, порядка пятидесяти тысяч сообщений: Claude Code, Codex, opencode. Каждый разобранный баг вместе с фиксом, каждая команда, которая сработала, каждый тупик, в который я уже заходил. Лучший инженерный дневник из всех, что я не вёл. Проблема одна: его никто не может прочитать.
Попытка номер один: grep
grep -r "connection pool" ~/.claude/projects/ | head
Формально работает. Практически — нет. Транскрипты Claude Code — это JSONL, где текст лежит в .message.content, причём иногда это строка, а иногда массив типизированных блоков. Основной объём файла — вывод tool-вызовов, поэтому большинство совпадений находятся внутри сорокакилобайтного результата cat, а не в том, что говорил я или модель. Можно собрать jq-пайплайн — на один формат уходит минут десять. Но у меня агентов не один.
Я сел и составил каталог, где вообще лежит история у современных агентов:
-
Claude Code —
~/.claude/projects/<закодированный-путь>/*.jsonl. Путь проекта закодирован заменой/на-, то есть-Users-me-my-app— этоmy/appили папка с именемmy-app? По строке не определить в принципе, надо ходить по файловой системе и проверять, что из этого существует; -
Codex CLI —
~/.codex/sessions/ГГГГ/ММ/ДД/rollout-*.jsonl, плюс отдельныйhistory.jsonl, который частично дублирует те же сообщения; -
opencode — SQLite;
-
Cursor — SQLite
state.vscdbдля чатов IDE и отдельные транскрипты у CLI-версии, два разных мира внутри одного продукта; -
Gemini CLI — одна и та же сессия пишется параллельно в .json (чекпоинт) и .jsonl (лог событий), причём в jsonl бывают события
$rewindTo— файл нельзя «прочитать», его надо реплеить; -
aider — markdown
.aider.chat.history.mdв каждом репозитории, где его запускали; -
Antigravity —
transcript.jsonlглубоко в~/.gemini/antigravity/brain/; -
Grok Build —
~/.grok/sessions/<url-encoded-cwd>/<id>/updates.jsonl, ACP-поток.
Восемь форматов. Документации — ноль, всё восстановлено по живым файлам.
Что получилось
deja-vu — один бинарь на Go без зависимостей, который парсит все восемь форматов в общий локальный индекс:
deja install --autodeja "connection pool exhausted"
Результаты приходят сразу из всех агентов: апрельский фикс найдётся независимо от того, с кем именно я тогда разговаривал. И поскольку индексируется то, что уже лежит на диске, нет проблемы холодного старта — не надо ставить прокси и месяц копить историю. Она уже накоплена.
Почему не эмбеддинги
Каждый второй проект про «память для агентов» начинается с векторной базы. Я от неё отказался по скучной причине: когда я ищу в этих логах, я ищу строку ошибки, имя функции или флаг. SSL_ERROR_SYSCALL, resolveEncodedPath, --force-with-lease. Точные токены. Семантическая близость здесь не помогает, а мешает: мне не нужны сессии похожие на мою ошибку, мне нужна сессия, где встретилась буквально эта строка, — потому что фикс именно там.
Поэтому индекс предельно классический: файл записей, varint-постинги токенов, gob-манифест. Холодная пересборка 3,3 ГБ — около четырнадцати секунд, тёплый запрос — единицы миллисекунд. Демона нет: CLI на каждом запуске сверяет размеры и mtime файлов и доливает изменившееся.
Грабли, на которые я наступил
Самое интересное в этой задаче — не поиск, а инкрементальная индексация файлов, в которые прямо сейчас кто-то пишет.
Рваные хвосты. Агент может писать в файл в момент индексации, и файл заканчивается половиной JSON-строки. Пропустить битую строку легко. Баг вылезает на следующем проходе: если продолжить с нового конца файла, то сообщение, которое в прошлый раз было написано наполовину, теперь дописано целиком — и пропущено навсегда. Лечится тем, что в манифест сохраняется смещение последнего полного \n на момент индексации, и следующий проход стартует с него, а не с прежнего конца файла.
История, которая расхлопывается. Я наивно считал транскрипты append-only. Gemini и Grok это ломают: rewind усекает файл и наращивает его заново с другим содержимым. Детектор изменений по размеру видит, что файл вырос и радостно доливает хвост — всё, что было до точки усечения, задваивается. Теперь такие файлы при любом изменении перечитываются целиком, а записи сессии заменяются, не расширяются.
filepath.Dir на корне. Цикл подъёма по директориям с условием dir != "/" на Windows не завершается никогда: filepath.Dir("C:\\") возвращает "C:\\". Нашлось это через повисший Windows-джоб в CI — десять минут тишины и дамп горутин, указывающий в filepath.Clean. Теперь цикл останавливается на Dir(dir) == dir, как и следовало писать сразу.
Дедупликация между форматами. Gemini пишет сессию в двух форматах одновременно, Codex дублирует сообщения в history.jsonl. Сообщения схлопываются по ключу «роль + время + FNV-хеш текста» — без хеша ключ коллизился на коротких служебных сообщениях с одинаковым таймстампом.
Синк между машинами
У меня агенты живут на ноутбуке и на mac mini, и хотелось, чтобы память была общая. deja sync ssh <host> экспортирует новые записи JSONL-батчами по обычному SSH и забирает встречные. Дедупликация контентная, реимпорт идемпотентный, импортированное не эхается обратно при следующем экспорте. Аккаунтов и облака нет. На фоне свежей истории, когда Grok Build уличили в отправке репозиториев в облако xAI, deja читает локальные файлы, пишет локальный индекс, MCP-сервер — stdio, и единственная сетевая операция во всём инструменте — тот самый sync, который вы запускаете руками.
Отдельная тема — секреты: в транскриптах лежат когда-то вставленные ключи и токены. Перед индексацией всё проходит через паттерны редактирования (AWS-ключи, JWT, PEM-блоки, api_key=...), а deja share <id> печатает вычищенный дайджест сессии для коллеги. Паттерны — не понимание, перед отправкой дайджест стоит прочитать глазами.
Чего инструмент не умеет
Найти «ту штуку с авторизацией, которую мы обсуждали» — не умеет, поиск токенный, не семантический. Чаты Cursor IDE требуют sqlite3 в PATH — брать C-зависимость ради одного харнесса я не стал. И главная ложка дёгтя: восемь парсеров под восемь недокументированных форматов — это подписка на вечную гонку с апстримом. Частично спасают фикстуры всех форматов в тестах и кросс-харнесс сьют в CI, остальное закрывают баг-репорты: поддержку Grok Build, например, принёс внешний контрибьютор через сутки после открытия исходников — от issue до смерженного PR.
Репозиторий: github.com/vshulcz/deja-vu, MIT. Если ваш агент не поддержан или формат уехал — issue с обезличенным куском файла ускоряет дело сильнее всего.
ссылка на оригинал статьи https://habr.com/ru/articles/1060020/