В первой части я собрал ассистента, который слушает созвон, различает голоса, пишет стенограмму и складывает встречу в граф знаний в 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 свой код возврата, и статус ночи честно красный.Ноутбук с закрытой крышкой спит, и это не поломка: monotonic-часы стоят, а настенные идут. Разница уходит в статус как «проспано», и ночь, в которой отвалились только поздние шаги, получает состояние «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, который слушает сразу: открыл приложение, запись уже идёт. Если открыл во время звонка, микрофон занят звонком, и приложение честно ждёт: как только звонок кончится, оно само начнёт запись, если всё ещё открыто. Свёрнутое приложение iOS не разбудит, но если вернуться в течение получаса, запись начнётся при возврате. Записать сам звонок на 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/.