В первой части я собрал ассистента, который слушает созвон, различает голоса, пишет стенограмму и складывает встречу в граф знаний в Obsidian. Всё на своём Маке, ничего не уходит в облако. С тех пор прошло полтора месяца ежедневной работы, и оказалось, что записать встречу — это меньшая половина дела. Большая — сделать так, чтобы через месяц из этих записей можно было достать правду, а не то, что модель придумала по мотивам.

Эта часть про то, как граф живёт неделями: что в нём хранится, как он не врёт, что с ним делает ночь, как из него отвечает поиск и как встречу можно забыть целиком. Кода будет мало, а про то, как оно устроено, — много.

Три слоя и никакой базы

Схему я описывал в первой части — напомню коротко. Граф — это папка с markdown-файлами, которую открывает Obsidian:

Встречи/2026-07-17_1400_Платёжный провайдер.md   ← эпизоды, сырьё
Люди/  Системы/  Команды/                       ← сущности, обратные ссылки
Ядра/                                           ← сквозные темы: статус + хроника
Досье/                                          ← готовые сводки по темам
Встречи-архив/2026-07-17 14-00 — …/             ← то же для Finder

Это та же трёхслойная схема «эпизоды → сущности → сообщества», что и у графовых систем памяти вроде Graphiti или Zep, только без графовой базы. Файлы. Работает grep, git, любой редактор, и главное — файлами владеет человек, а не код. Если завтра я перестану развивать проект, папка останется читаемой.

Пример из демо-графа: встреча про выбор платёжного провайдера порождает узел Ядра/Выбор платёжного провайдера.md, в нём строка статуса «Решено 17.07 — ЮPay.» и ниже история со ссылками на встречи, где к этому шли. Люди (Мария Соколова, Игорь Ветров) получают по узлу с обратными ссылками на встречи. Всё остальное в этой статье — про то, как эти файлы не превращаются в помойку.

Правило номер один: факт живёт с датами «с» и «по»

Первое, что я понял за месяц: на рабочих встречах ничего не бывает окончательным. Решение принимают в среду, отменяют в пятницу, а через две недели возвращаются к первому варианту. Если граф хранит только последнее состояние, он врёт про историю. Если хранит всё подряд, он врёт про настоящее.

Поэтому у ядра два раздела. «Статус» перезаписывается каждой встречей, а вытесненный статус уезжает в «Хронику» с пометкой, с какого дня он держался. У факта появляются «с» и «по». Прочерк в статусе не считается статусом; повторный разбор той же встречи дописывает уточнение к её строке, а не плодит вторую.

Это звучит очевидно, но модели этого не хочется. Ей хочется написать красивое резюме. Пришлось делать промпт скучным и жёстким.

Правило номер два: каждая строка хроники ведёт к реплике

Самое неприятное открытие я сделал как-то вечером: заметил, что граф уверенно цитирует людей, и цитаты выглядят правдоподобно, а в стенограмме их нет.

Механика была такая. Живые минутки, которые модель пишет каждые две с половиной минуты по ходу встречи, дописывались в тот же файл, с которым потом сверяются цитаты. Модель второго прохода находила «подтверждение» в тексте первого. Выдумка подтверждалась выдумкой. Три внешних ревьюера (об этом ниже) нашли четыре дыры одного происхождения: поле «кто сказал» бралось из ответа модели, а не из стенограммы; парсер не знал формат конвейера; заголовок реплики не считался границей; заметки, которые модель пишет сама себе по ходу встречи (раздел «Ко-мышление»), подписывались как реплики человека.

Отдельный круг добавил пятую: разборщик принимал за имя говорящего любое короткое слово перед двоеточием, и в «спикеры» попадали строки «Итог:», «Решения:», «Присутствовали:». Всего по этой правке было шесть кругов ревью.

Сейчас источник факта — только стенограмма: «кто» и «когда» читаются из строки реплики, цитата сверяется с текстом, и если реплики нет — факт идёт без подписи, а не с придуманной. Модель может интерпретировать, но не может назначить автора.

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

Досье: этаж между поиском и графом

Вопрос «что у нас в итоге по теме X» — самый частый. Поиск по графу отдаёт десяток кусков из разных встреч, и модель каждый раз собирает ответ заново — и всякий раз чуть иначе.

Досье — это готовая сводка по теме: состояние, хроника, что решено, что открыто, кто в теме, со ссылками на узлы. Кластер темы — ядро плюс его соседи по ссылкам: встречи, документы, соседние ядра. Границы тем задаёт сам человек, расставляя ссылки. Никаких графовых алгоритмов кластеризации не понадобилось — иерархия уже есть.

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

Поиск смотрит в индекс досье первым и лезет в граф только за деталями. Индекс лексический, по основам слов с префиксным совпадением, без эмбеддингов: работает, даже если Ollama не запущена.

Что я взял из чужих работ: идею сводок по сообществам из GraphRAG, инкрементальное обновление и двухуровневый поиск из LightRAG, инвалидацию по событию, а не по расписанию, из Graphiti. Рекурсивное дерево абстракций из RAPTOR не пригодилось: дерево уже нарисовано ссылками.

Ночь

В 04:15 запускается ночной прогон: утренний бриф, ревизия ядер (поиск дублей и противоречий между темами), досье, необязательные облачные ревизии, удаление дублей файлов, замер памяти. Днём всё это мешало бы встречам, потому что модель одна и машина одна.

Несколько правил ночи, которые появились после инцидентов, а не из головы:

  • Живая встреча важнее ночи. Если в семь утра начался созвон, ночной шаг отдаёт модель встрече и ждёт. Раньше ревизия ядер продолжала делить модель с живыми подсказками на встрече, и запись рвалась.

  • Ночь кончается ночью. У прогона есть потолок по времени, и его видят все шаги, включая хвостовые. Раньше дедуп и замер шли мимо потолка и поднимали модель уже утром.

  • «Ok» только когда работа сделана. Ночь без единого графа не зеленеет — как и ревизия, прошедшая вхолостую. Пример буквально сегодняшний: ночью обновился Claude CLI, бинарник сменился под ногами, семь тем облачной ревизии легли с ошибкой exec, а ночь отчиталась об успехе, потому что каждая тема ловила исключение и писала «сбой» в служебный файл. Теперь перед первой темой стоит зонд claude --version, у сломанного CLI свой код возврата, и статус ночи честно красный.

  • Ноутбук с закрытой крышкой спит, и это не поломка: монотонные часы системы стоят, а настенные идут. Разница попадает в статус как «проспано», и ночь, в которой отвалились только поздние шаги, получает состояние «slept», а не «failed».

Облако правит копию, а не граф

Локальная модель пересказывает исправно, но связей не видит: одно решение отменяет другое, срок протух, два узла говорят разное. Это видит большая облачная модель. По умолчанию облако выключено. Включаешь тумблер — и ночью Opus проходит по досье и ядрам ещё раз.

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

Два писателя в одну папку без замка — это всегда потеря данных, вопрос только когда.

Поиск: блоки вместо файлов и три сюрприза

Поиск по графу склеивает два сигнала: лексику (стемминг, IDF, свежесть) и семантику (bge-m3 через Ollama). По отдельности не хватает ни того, ни другого: лексика ловит внутренние идентификаторы, которые вектор не различит, семантика закрывает разрыв, когда вопрос задан не теми словами.

По дороге всплыли три сюрприза.

Первое: один вектор на файл по первым 12 000 знаков для узла работает, а для стенограммы переворачивает результат — решения принимают в конце встречи. На рабочем графе 325 файлов оказались длиннее границы, и 63% содержимого в индекс не попадало вовсе. Теперь файл режется по заголовкам на блоки, каждый блок несёт в эмбеддер свой путь «Файл → H1 → H2», потому что блок «ну да, давайте так и сделаем» сам по себе не значит ничего.

Второе: Ollama молча режет вход bge-m3 примерно на 12 300 знаках при заявленных 8192 токенах. Проверено бинарным поиском: уникальный маркер, приписанный к концу, не меняет вектор.

Третье: iCloud метит элементы своего контейнера флагом «скрытый», а стандартный обходчик с опцией «пропускать скрытые» молча их пропускает. На рабочем графе так спрятались «Люди», «Системы», «Встречи» и почти вся документация: индекс видел 546 файлов из 1172. Поиск больше не смотрит на флаг вовсе: служебные папки отсекаются по имени.

Качество меряю сквозным контуром: поиск → синтез на закрытом наборе вопросов с ожидаемыми фактами, температура генерации — ноль. Блочный индекс и вес по роли документа (сырьё приглушается, выжимка поднимается) подняли долю фактов, попавших в ответ, с 11 из 14 до 14 из 14 на полном индексе. Отрицательный результат тоже был: требование «переноси аббревиатуры и коды дословно» выглядело улучшением на одном прогоне и оказалось регрессом, как только температуру зафиксировали. Инструкция толкала модель цитировать вместо честного «неизвестно». Отменил.

Гигиена памяти

Граф как память портится тихо. Аудит в конце августа нашёл 626 битых ссылок и семнадцать узлов-людей, которые на самом деле были метками диаризации: «Собеседник 3», «Speaker 2». Разные люди с разных встреч склеивались в один файл с сотней входящих ссылок.

Правила после аудита: метка диаризации — не человек, и узла она не получает; имена узлов сравниваются по ключу без пунктуации, скобок и дефисов, так что «Иван (Иванов)» и «Иван Иванов» — один узел; перенос строки внутри ссылки из ответа модели склеивается при записи, иначе для Obsidian ссылка мертва. Ночью скрипт-доктор без модели проверяет здоровье каждого графа: битые ссылки, метки среди людей, сироты, дубли. Утренний бриф показывает сводку.

Две встречи в одну минуту

Это отдельная глава страданий. Имя стенограммы и имя записи берутся из одной отметки времени. Пока отметка была минутной, две встречи в одну минуту (падение и перезапуск демона, две записи с телефона подряд, повторная обработка) молча писали поверх друг друга или склеивались в одну.

Теперь отметка с секундами, файлы записи открываются в режиме «только создать», и коллизия — видимая ошибка, а не тихая обрезка. Рядом с каждой встречей лежит маленький служебный файл, который помнит точную отметку времени и хеши стенограммы и минуток. Когда встречу переименовывают по теме, файл переезжает вместе с ней. Это спасло от сценария, который три внешних ревьюера нашли независимо: если стенограмму переименовывали в имя, за которым уже числился служебный файл-сирота соседней встречи, чужая запись молча становилась нашей.

Если это звучит как много возни ради редкого случая, то да. Но каждая такая редкость на моей машине случалась.

Забыть встречу

Обещание приватности мало чего стоит, если встречу нельзя убрать. Аудио удаляется само через два дня, а всё остальное живёт вечно и в шести местах: стенограмма и её производные, папка встречи в архиве, узел встречи, копия стенограммы в документации, строки хроники в ядрах, ссылки в досье и узлах людей. Человек, которому нужно убрать один чужой разговор, не должен помнить все шесть.

Скрипт «забыть» собирает план: что удалить, что переписать. Строка хроники, которая ссылается на забытую встречу, уходит целиком вместе с фактом, который из неё пришёл. Ссылка в связном тексте заменяется пометкой, а не ведёт в пустоту. По умолчанию скрипт только показывает план, удаление требует явного согласия, правки выживших узлов уходят в резервную копию. И честно называет, до чего не дотягивается: до копий, уже уехавших в iCloud или Time Machine, и до файлов у других участников.

Сегодняшний аудит добавил седьмое место: копия аудио в папке импорта. Теперь и она уходит вместе со встречей.

Записи не только с Мака

За последнюю неделю добавились две вещи. Вкладка «Внешняя запись»: диктофон телефона, чужая запись звонка, экспорт Zoom — перетащил файл, и он идёт тем же конвейером, что и живая встреча. Копия живёт два дня и удаляется вместе с аудиоисходником в архиве; текстовые исходники остаются. Сбойный файл не удаляется и не перегоняется через распознавание каждые две минуты, а ждёт кнопки «Повторить».

И компаньон на iPhone, который слушает сразу: открыл приложение, запись уже идёт. Если открыл во время звонка, микрофон занят звонком, и приложение честно ждёт: как только звонок кончится, оно само начнёт запись, если всё ещё открыто. Свёрнутое приложение само не проснётся, но если вернуться в течение получаса, запись начнётся при возврате. Записать сам звонок на iPhone не может никто: это свойство платформы, а не слабость приложения. Звонки пишет Мак.

Вторые головы

Про процесс, без которого ничего из этого не держалось бы. Каждое изменение кода перед мержем читают внешние модели: GLM и DeepSeek через API, иногда Codex. Не как «второе мнение для галочки», а как команда приёмки с правилом: пока круг приносит Critical, гоним следующий круг; первый круг без Critical закрывает тему.

Это долго (круг идёт 10–15 минут) и постоянно неприятно. Сегодня две головы независимо нашли одну и ту же гонку в моей же правке: я вынес копирование файла в фон, чтобы не вешать интерфейс, а сканер начал забирать недописанные файлы. Лечилось не заплаткой, а тем самым шаблоном, который уже был в проекте: копия под скрытым именем и одно атомарное переименование. Без второй головы это ушло бы в релиз.

Что дальше

Слепок голоса владельца, чтобы «где я» определялось по эталону, а не по каналу. Уборка копий импорта из демона, а не только из запущенного приложения. Компаньон в App Store. И честный замер новых локальных моделей распознавания: в моём прогоне MOSS на MLX работал в 10–15 раз быстрее реального времени, но на двадцатиминутной встрече расползся до девятнадцати меток спикеров, так что GigaAM пока остаётся.

Код: github.com/charoiteai/Charoite_audio, Apache 2.0. Первая часть: habr.com/ru/articles/1061864/.