В первой части ассистент записывал созвон и складывал его в граф, во второй граф учился не врать неделями, в третьей я трижды ошибся с вопросом, кто это говорит. Эта часть про двадцать пять минут после кнопки Стоп: на экране ничего не происходит, на диске происходит всё. Там на этой неделе нашлась дыра, которая жила с июля. Облачная ревизия находила ошибки локальной модели, аккуратно их описывала, и никто её не слушал.

Одна встреча, четыре файла, двадцать пять минут

Пятница, рабочий созвон на два часа. Запись остановлена в 17:33. По логам:

17:33  стоп; живой черновик стенограммы закрыт
17:36  финальная стенограмма: диаризация по каналам, распознавание заново, имена
17:38  заметка встречи в графе, узлы людей, минутки, разбор локальной модели,
       семь фактов ушли в память
17:58  облачная ревизия готова: 20 минут, 57 правок графа перенесены,
       в минутки дописано 7 поручений

Локальная модель записала в минутки три решения и список поручений. Ревизия, прочитав стенограмму целиком, два решения из трёх отвергла: одно было шуткой про бюджет (один токен на всех, следующий запрос в следующем году), второе — задачей соседней команды. У третьего верное решение и не тот исполнитель. Три поручения сняла: человеку, которого на встрече не было, со сроком до 25-го, который никто не называл; без срока и исполнителю, который со следующей недели в отпуске; и одно с числом, прочитанным наоборот — план принят за разницу. Плюс имя: дорожку ведущего распознавание назвало чужим именем по обращению, ревизия по тем же обращениям нашла настоящее.

Всё это лежало в файле ревизии с цитатами и таймкодами. Дальше:

  • в минутки ревизия умела только дописывать: семь восстановленных поручений добавились, три ложных остались и уехали во вкладку Задачи;

  • в заметке встречи шутка осталась с пометкой 📌 решение, опровержение лежало ниже, в разделе для человека;

  • семь фактов ушли в память в 17:38, за двадцать минут до того, как ревизия признала два из них несуществующими. Поиск по памяти потом спокойно отдавал единый токен для всех команд как принятое решение.

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

Что происходит после стопа

Конвейер в пяти шагах, про первые два подробно было в прошлых частях.

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

Граф. Локальная модель (35B, MoE, через Ollama) разбирает стенограмму в JSON: люди, системы, решения, поручения. Из этого — заметка встречи, узлы людей и систем, строка в оглавление, факты в память. Тут же пишется разбор: что это было и что делать дальше.

Ревизия. Если включён облачный слой, отдельный процесс отдаёт стенограмму, минутки, разбор и контекст графа большой модели через CLI по подписке. Модель пишет ревизию: где локальная ошиблась, что пропустила. В режиме правки она правит клон графа, и правки переносятся под замком с проверкой каждого файла, это было во второй части.

Доставка. Ревизия кладётся рядом со стенограммой, в архив и в документацию, минутки перечитываются во вкладку Задачи.

Ночь. Досье, ревизия ядер, утренний бриф.

Три модели: распознавание, локальная, облачная. И вопрос, который я полтора месяца не задавал: кто отвечает, если шаг три не согласен с шагом два?

Зачем второй проход

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

Большая модель читает стенограмму один раз и целиком. Она видит, что на вопрос к Игорю отвечает дорожка с другим именем. Видит, что рекомендации в разборе пришли с другой встречи через контекст графа. Один проход большой модели после встречи дешевле, чем заставлять маленькую думать как большую на ходу.

Но тогда второй проход должен уметь снимать, а не только дописывать. То есть менять файлы, которые уже прочитали вкладка Задачи, граф и память. Этим я и занимался всю неделю.

Снимать, а не дописывать

Ревизия говорит с кодом не прозой, а строгими разделами в конце файла. Проза остаётся человеку.

## Восстановленные поручения
- [ ] **Имя** — что сделать — срок, если назван

## Снятые поручения
- **Имя** — что сделать — причина: …

## Исправления имён
- **Метка** → **Имя** — основание: …

Восстановленные были и раньше. Снятые и Исправления имён — новые. Модель просят писать имя и дело так же, как в минутках, чтобы код нашёл строку.

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

  • один пункт ревизии снимает ровно одну строку;

  • если подходят несколько — точное совпадение перевешивает, а без точного не снимается ничего, в лог уходит строка: подходит к двум пунктам, не гадаем;

  • вложение засчитывается от двух общих значимых слов: одно слово позвонить не снимает позвонить в банк;

  • пункт, который человек отметил сделанным, не снимается никогда;

  • снятая строка не удаляется, а переезжает в раздел Снято ревизией зачёркнутой, с причиной. Из Задач она исчезает, из минуток нет.

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

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

С решениями проще. Облако помечает ошибочное решение в заметке на месте: 📌 становится ⛔, в конце строки причина. Всё, что читает заметку — досье, поиск, память, — знает, что ⛔ не решение.

Память: догнать ревизию

У памяти ассистента (отдельный сервис с векторной базой и Postgres, общий с моим чат-ботом) нет ключа факта, только ключ встречи. Нельзя забыть одно решение, можно забыть встречу. Поэтому после переноса правок код сравнивает список живых решений в заметке до и после. Изменился — встреча забывается целиком и присылается заново без ⛔. Не изменился — ничего не трогаем.

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

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

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

Вторую память я выключаю. Не сегодня и не удалением: сначала личное, несколько сотен фактов, переезжает в граф обычными markdown-файлами; подсказки получают свой поиск по графу без сервиса; диктовка и дневник пишутся в папку. Потом дамп, остановка служб и через неделю удаление. Старая память работает, пока новая не отвечает на те же вопросы. Две памяти без общего ключа — как два писателя в одну папку без замка из второй части: та же потеря данных, только медленнее.

Имена: чья это дорожка

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

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

  1. Поручения не переименовываются. Игорь — согласовать план могло относиться к настоящему Игорю из речи, не к дорожке с этим именем. Такие поручения ревизия снимает и возвращает сама, теми же разделами.

  2. Обмен меток одним проходом. Последовательная замена А на Б, потом Б на А превращала двух участников в одного. Critical от внешней головы.

  3. Слияние дорожек запрещено. Два голоса под одним именем без отката хуже, чем чужое имя на одной дорожке.

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

И решение, с которым я спорил сам с собой. Перештамповка идёт только когда облаку разрешено править граф и перенос сверен. В режиме только чтения стенограмма не переименовывается, иначе получится расколотая правда: в стенограмме верное имя, в узле человека чужое, и никто их не сведёт. Обе головы назвали это риском, я согласился. Но восстановленное поручение с верным именем в этом режиме получало бы пометку, что исполнитель не участник. Поэтому карта имён из ревизии всё равно строится, файлы не трогает и отдаётся мосту поручений как список участников.

Повтор и честный статус

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

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

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

Аудит по зонам за семьдесят девять центов

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

Пять зон конвейера после встречи: пересборка и имена, граф, облачная ревизия, мост минуток, память и статусы. На зону — чистый клон репозитория, инструменты только на чтение, список файлов в порядке чтения и строка бюджета: двенадцать минут и не больше сорока ходов, дальше вердикт по прочитанному. Без бюджета широкая зона не читается: в прошлый раз одна голова дала три пустые попытки, вторая ответила с третьей. Обе головы получают одни и те же пять зон, десять процессов стартуют вместе.

10 зон (5 DeepSeek 4.1 flash + 5 GLM 5.3 flash), все параллельно
стена           10 мин 13 с (самая долгая зона — 10 мин)
на зону         6–10 мин, 17–39 ходов инструментов
токенов на зону 120–155 K входных, 1–2,8 M кэшированных, 33–61 K выходных
цена            $0,38 + $0,40 = $0,79 за десять вердиктов

DeepSeek — 12 Important и 16 Minor, GLM — 1 Critical, 9 Important и 23 Minor. Critical — про фильтр эха, который режет речь владельца при одновременном разговоре: известный компромисс, померенный в третьей части, отклонил. Из двадцати одного Important по коду подтвердились все, после слияния дублей — семнадцать дефектов. Три вердикта из десяти сошлись на одном и том же дефекте, и это лучший сигнал, который может дать такой аудит.

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

Одна поломка была в самой обвязке — скрипте вне репозитория, который поднимает клоны и собирает вердикты. Его гейт на исчерпанный лимит искал в ответе подстроки вроде 402 и rate_limit_error. Два вердикта из десяти он объявил остановкой по лимиту, потому что аудируемый код содержит те же строки, а вердикт их цитировал. Восстановил из сырого ответа, гейт теперь смотрит только на короткие ответы. Любой фильтр по подстроке однажды встретит текст, который про этот фильтр рассказывает.

Что изменилось за неделю

  • Ревизия умеет снимать: ложные поручения уходят из задач в раздел Снято ревизией, ошибочные решения помечаются на месте, память встречи переотправляется без них.

  • Имена дорожек перештамповываются по строгому разделу, только под сверенной правкой графа и без гаданий.

  • Ревизия повторяется после сбоя запуска и показывает свой этап в статусе.

  • Разбор перестал тащить рекомендации соседних встреч: контекст графа теперь только для имён и терминов.

  • Вторая память признана дублем и идёт на выключение.

  • Аудит по зонам: десять минут, меньше доллара, семнадцать дефектов.

Что дальше

Семнадцать дефектов — в пяти карточках, каждая своим PR со своим кругом голов. Выключение второй памяти по этапам. И слепок голоса владельца, который я обещаю третью часть подряд: единственный способ отвечать на вопрос, где я, не по каналу, а по голосу.

Код и релизы: github.com/charoiteai/Charoite_audio, Apache 2.0. Девлог по-английски, с замерами и отменёнными решениями: charoiteai.github.io/Charoite_audio. Первая часть — про устройство и запись, вторая — про граф встреч как память, третья — про диаризацию и три ложных диагноза.