@ -2,12 +2,7 @@
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[+] **Group files: удаление непустых каталогов** — 10.10.2026:
Один диалог подтверждает удаление всего выделения с содержимым папок. Сначала полный постраничный обход,
затем удаление детей раньше родителей; перед удалением папки запрашивается свежая версия после собственных изменений.
Проверено на настоящем mf_storage: дерево из 46 объектов, несколько корней, отмена, изменение файла/страницы,
появление нового файла перед удалением родителя. Новые файлы не подхватываются повторным обходом.
## Текущие задачи
[ ] **Member files: исчерпание кеша результатов массовых изменений** — 10.10.2026:
Воспроизведено test_member_files_browser::recursiveDeletion(server-busy): из 76 объектов удаляются 64,
@ -17,10 +12,6 @@
простое вытеснение старых записей нарушает гарантию повторного ответа. После исправления изменить регрессию
на успешное удаление более 64 объектов и проверить повторные запросы/потерю ответа.
[+] **Desktop: текущая вероятность речи на кнопке VAD** — 09.10.2026:
Зелёная полоса внизу кнопки отображает 0–100% из того же снимка ядра, что настройки; обновляется до 10Гц.
При недоступной оценке остаётся пустая дорожка, в режиме уровня полоса скрыта. Нажатия и удержание сохранены.
[ ] **Desktop: провалы вероятности VAD при компрессии** — диагностика 09.10.2026:
Воспроизведение пользователя: длительная гласная, затем временная нечувствительность; ближе к концу лога несколько повторов.
Новый лог 14:32:14–14:34:38: на 14:34:07–18 и 14:34:20–38 все 556 окон с RMS > -30dBFS имеют p < 0.5 ;
@ -41,107 +32,16 @@
Полного провала всей фразы почти до нуля стенд не воспроизвёл; нужен повтор живой речи с новыми логами для связи с AEC/AGC.
Алгоритмы обработки сигнала пока не менялись; исправление причины выбирается по результатам повтора.
[+] **Регрессия Silero с настоящей моделью доступна в desktop CMake** — 09.10.2026:
test_silero_vad переведён на актуальный radio_auto_fsm (в autotools ONNX здесь выключен, поэтому старый тест не собирался).
Опциональный --probe сравнивает повторения одной речи при разном фоне/усилении и печатает вероятность, gain и clips.
[+] **Desktop: общий интерфейс настроек активации рации** — 09.10.2026:
Удержание 500мс или правая кнопка на VAD/значке уровня открывает мини-блок; короткое нажатие переключает автопередачу.
В мини-блоке и вкладке используется один редактор: две кнопки режима, оба порога dBFS, порог/усиление VAD и задержка.
Одновременно отображаются RMS, вероятность речи и фактическое усиление VAD; снимок общего ядра обновляется с частотой до 10Гц.
Применение явное, закрытие отменяет несохранённый ввод; новая вкладка прокручивается на небольшом экране.
Проверки: radio_panel (удержание без toggle, повторное открытие, применение, индикаторы), gui_language, audio_recovery, voice_tx — PASS.
[+] **Android: текущая оценка VAD и постоянные настройки обоих детекторов** — 09.10.2026:
В мини-блоке и вкладке рации одновременно доступны пороги уровня, порог VAD, усиление перед VAD и задержка отключения.
Вероятность речи из работающего Silero выводится в процентах и на шкале с отметкой порога; обновление UI — 100мс.
Рядом с пределом усиления показан фактический gain отдельного компрессора VAD в dB; выключенный отмечен как 0 dB.
Снимок native/JNI помечает оценку недоступной при RX, ошибке inference, разрыве/остановке захвата или возрасте больше 200мс.
При выключенной автоматике или режиме уровня UI предлагает включить VAD для измерения вероятности речи.
Проверки: voice_tx (вероятность, свежесть, ошибки, RX, reset), radio_member_mute, radio_panel, gui_language — PASS.
voice_tx также сравнивает отображаемый gain с эталонным VAD-компрессором для mono/stereo и проверяет выключенный компрессор.
APK arm64-v8a/x86_64 собран; новый индикатор на физическом устройстве пока не проверен.
[+] **Android: автопередача рации по уровню сигнала** — 08.10.2026:
режим VAD/уровень, два порога dBFS, живой измеритель из общего radio_audio и prefetch 600мс.
Вкладка рации временно держит микрофон для измерения при выключенной автоматике; уход со вкладки/пауза освобождает этот захват.
Оба детектора используют прежнее окно foreground/screen/background; PTT имеет приоритет, RX-защита остаётся общей.
Настройки сохраняются одним DataStore edit и передаются одним заданием uasync с подтверждением до старта capture.
Кнопка панели показывает выбранный режим и сохранённое включение; JNI принимает явный OFF/VAD/LEVEL.
Для уровня на кнопке — значок эквалайзера; удержание VAD/уровня открывает мини-блок режима, измерителя и порогов прямо у кнопки.
Выбор режима — две кнопки VAD/уровень с областью нажатия от 48 dp, без вложенного выпадающего меню; выбор записывается в лог radio.
Добавлены русские/английские строки и диагностика. assembleDebug (arm64-v8a/x86_64), 21 JVM-тест — PASS.
На физическом устройстве новый режим пока не проверен.
[+] **ChatGUI: автопередача рации по уровню сигнала** — 08.10.2026:
выбор VAD/уровень, RMS 20мс после AEC до компрессоров, два порога dBFS (-35/-45 по умолчанию),
подтверждение 40мс и общая задержка отключения. Измеритель работает при включённой рации даже без автоматики.
Общие FSM, prefetch 600мс, приоритет PTT и RX-защита; уровень работает без Silero.
INI: radio_auto_enabled, radio_auto_mode (1=VAD, 2=уровень), radio_auto_hangover_ms,
radio_level_on_dbfs/radio_level_off_dbfs. Пара проверяется перед сохранением; ядро отключает автоматику
при некорректной паре из INI. Общие настройки/API переименованы в Android без изменения его VAD-интерфейса.
Проверки: FSM, voice_tx (PCM/Opus/prefetch/RX/AGC), radio_panel (настройки/индикатор/INI),
radio_member_mute, voice_audio_io, audio_recovery, gui_language — PASS; ChatGUI собран.
[+] **Индикация говорящих в списке участников Android** — 07.10.2026:
динамик без волн — молчит, с волнами — говорит; красное перечёркивание сохраняется при mute, включая речь.
Отдельный набор говорящих активной рации переживает обновления участников и просмотр другого канала;
STOP снимает только свой источник, выключение/смена активной рации и stop ядра очищают состояние.
Собственный значок использует фактический native TX (PTT/VAD) с привязкой к каналу, сброс после join capture.
Добавлены переводы и логи изменений radio. Чистые основная C-сборка и APK — PASS; 16 JVM-тестов — PASS.
APK установлен с сохранением данных на Q8 и SM-A525F; приложения запускаются без AndroidRuntime ошибок.
[ ] **Linux headless CMake: актуализировать зависимости общего ядра** — 07.10.2026:
чистая сборка tools/chatgui-android/build_headless останавливается на lib/speex_aec.c:
speex/speex_echo.h не найден. utun_setup_sources включает этот исходник, но UTUN_INCLUDE_DIRS не содержит
lib/speexdsp/include. Проверить полный состав Speex/voice зависимостей отдельного каркаса;
основная Autotools C-сборка и Android NDK-сборка проходят.
[+] **Динамик участника показывает речь в рации ChatGUI** — 07.10.2026:
без волн — молчит, с волнами — говорит, красное перечёркивание — локально заглушён; волны видны и при mute.
START/STOP обновляют строки через Qt bridge; собственная передача учитывает PTT, хоткеи и VAD.
Обновления списка/RTT сохраняют индикацию; смена канала, выключение рации и stop очищают её.
Исправлен сдвиг индексов строк при удалении участника. Добавлены переводы подсказок и логи radio.
Чистая сборка desktop, 4 Qt-теста и сетевой test_radio с FIN/таймаутом/отпиской — PASS;
полный check.sh — 138 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS. Формы значков проверены визуально.
[+] **Случайный зашифрованный padding ETCP** — 07.10.2026:
keepalive UDP/TCP, UDP PING/PONG и STCP-handshake используют нулевой padding переменной длины.
Каждый ETCP_CONN хранит свой диапазон и таймер; отдельный диапазон instance обслуживает пинги без соединения.
min=rand(0,30), max=min+rand(32,128), обновление каждые rand(50,300) секунд.
Добавлены проверки границ 0/158, эхо-данных, sleep/ACK, независимости и отмены таймеров, ошибок MTU/формата.
TCP-пинг не удерживает instance. Полный check.sh: 134 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS.
[+] **STCP: исключить повтор CTR-потока между подключениями** — 07.10.2026:
CTR IV теперь зависит от session_key, направления и соли отправителя из существующего блока handshake.
Потоки инициализируются по мере получения солей; шифротекст Ed25519 и нулевого padding меняется.
Проверяются повторные и параллельные подключения одной пары, короткие send/recv и подмена соли при том же ECDH.
STCP — 14/14; полный check.sh — 134 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS.
[ ] **REALITY: исправить формат ClientHello и продолжение TLS-потока** — 07.10.2026:
supported_groups и signature_algorithms не содержат внутренние двухбайтовые длины списков.
После ClientHello/ServerHello идёт STCP без TLS record framing; это распознаётся пассивным наблюдателем.
[+] **Одна реакция автора и горизонтальный выбор смайликов** — 07.10.2026:
выбор в Android и desktop отображается одной горизонтальной прокручиваемой строкой с выделением своего смайлика.
Ядро допускает одну реакцию на сообщение от автора; другой смайлик атомарно заменяет прежний, повторный клик отменяет.
Многосмайловые состояния отклоняются кодеком, в том числе корректно подписанные; ver и подпись защищают изменения/отмены.
По выбору пользователя прежнее состояние 👍+😮 заменено на 👍 новой подписанной версией 3; резервная копия и разовый
скрипт сохранены в `/home/user/utun2-backups/single-reaction-20261007` .
Local и Q8 обновлены. Тесты кодеков/API/Merkle/сетевого восстановления и 7 Qt-тестов — PASS; APK и desktop собраны.
[+] **Реакции в нижней строке пузыря в Android и desktop** — 07.10.2026:
слева внутри пузыря, включая голосовые/файлы/изображения/видео/опросы, добавлен прозрачный контур белого сердечка
с тенью в один физический пиксель во все стороны. Большие сообщения только из эмодзи показываются без кнопки.
Сердечко открывает выбор реакции; существующие реакции добавляют/убирают собственный выбор через общий API.
Сердечко уменьшено до 20; реакции находятся в общей строке с временем справа. При переносе реакций пузырь
увеличивается по высоте, время сохраняется справа в последней строке. Media-метаданные остаются выше неё.
Проверены входящие/исходящие сообщения шести типов в узком и широком desktop-окне: нажатия не запускают медиа,
drag/right-click не отправляют реакцию, внутри сердечка нет заливки. Проверено изменение высоты строки при
добавлении/удалении 16 реакций. Чистые Android/desktop-сборки и 7 Qt-тестов — PASS.
Local перезапущен; APK установлен и запущен на Q8, профиль сохранён в `/home/user/utun2-backups/inline-reactions-20261007` .
Визуальная проверка Q8 ждёт разблокировки экрана; установка на SM — подключения устройства к ADB.
[ ] **Завершить проверку реакций и голосов на SM** — 07.10.2026:
причина отказа установлена: у записей корней `signed_by=0/NULL` были нулевые/отсутствующие подписи дерева.
Проверка корня ключом канала остаётся обязательной; добавлены причины отказа и проверка доверенного Ed25519.
@ -153,49 +53,6 @@
Linux/Android/desktop собраны; `./check.sh` : 133 passed, 0 failed, 1 skipped; proxy/full, burst, load и 7 Qt-тестов — PASS.
Осталось установить APK на SM (`RZ8R72MG64B`) и проверить реакцию/голос на устройстве: SM отключился от ADB во время работы.
[+] **Восстановить отправку сообщений SM (Android) → local после обновления** — 07.10.2026:
существующие таблицы `msg_*` не имели нового поля `message_id` , INSERT и чтение истории завершались ошибкой.
При регистрации канала таблица атомарно перестраивается с вычислением ID из канала, timestamp и подписи;
прежние поля сообщений сохраняются, при ошибке вся операция откатывается. Список каналов читается до DDL.
`test_db_sync_schema` проверяет сохранение всех полей, одинаковые timestamp, ограничения ID, пустую историю,
повторный запуск, отправку после обновления и откат частичного копирования. Проверены копии БД SM и local.
Обновлены и запущены SM и local: все 89 сообщений каждого клиента и прежние поля сохранены, SQLite quick_check OK.
Резервные копии: `/home/user/utun2-backups/message-id-20261007` . Чистые сборки Linux и Android прошли;
`./check.sh` : 133 passed, 0 failed, 1 skipped; proxy/full, burst и load — PASS.
[+] **Голосования, опросы и реакции в чате** — 06.10.2026, копия `/home/user/utun3` , ветка `feature/chat-feedback` :
реализован базовый слой `src/chat/chat_feedback.c/h` : таблица каждого канала,
подписи участников, независимые ver для ответа и реакции, сохраняемая отмена,
детерминированный конфликт ver, атомарное применение страниц и восстановление индекса.
Merkle v3 разделяет MEMBERS/FEEDBACK, курсор записи — 32 байта.
Итоговый `./check.sh` : 132 passed, 0 failed, 1 skipped; proxy/full, burst и load также PASS.
Чистые сборки desktop и Android assembleDebug прошли; 7 Qt-тестов — PASS.
Подключены message_id к БД/API, общий каталог 16 эмодзи, канонические форматы
poll/answer/reactions, single/multiple, allow_custom_answer, подсчёт результатов,
отложенное получение опроса, CLI, desktop и Android. Собраны desktop и Android APK.
Сетевой тест A ↔ B ↔ C прошёл, включая restart C и сохранение отмены.
Исправлен запуск начального member_sync после restart чата поверх уже UP-транспорта.
Реализация и финальная проверка завершены. APK: app/build/outputs/apk/debug/app-debug.apk
внутри tools/chatgui-android; desktop: tools/chatgui/build/vibechat.
План продолжения:
1. Сохранить индексируемый message_id в msg_< channel > , вернуть его через API сообщений.
2. Добавить общий каталог эмодзи и канонические кодеки poll/answer/reactions;
single/multiple и allow_custom_answer проверяются ядром.
3. Добавить снимок результатов, список голосовавших и собственные состояния;
неизвестное сообщение откладывает учёт ответа, получение сообщения обновляет представление.
4. Подключить единый командный API к CLI, desktop и Android; команды выполняются в uasync.
5. Прогнать проверки кодеков и сетевые сценарии, собрать интерфейсы и выполнить check.sh.
[+] **tcp_proxy_full: проверить аномальные значения elapsed в отчёте** — 06.10.2026:
`/tmp/utun3-check.log` при успешной передаче показывает длительности вроде
`basic_1mb: 161949390ms` и `concurrent_20: -1612179507375916304ms` .
В `tests/tcp_proxy_full/run_test.sh` длительность считается через `date +%s%3N` ;
проверить фактический формат времени и арифметику в окружении запуска.
Причина установлена: date +%s%3N в окружении отдаёт 19 цифр наносекунд вместо миллисекунд.
run_test.sh переведён на Python time.monotonic_ns() // 1000000.
Подтверждено финальным check.sh: basic_1mb 179ms, concurrent_20 326ms, idle 1080ms;
все результаты передачи и проверки байтов — PASS. Лог: /tmp/utun3-polls-check.log.
[ ] **rlottie: разбор предупреждений GCC 15 в импортированных исходниках** — 05.10.2026:
при сборке `tools/chatgui/rlottie_src` GCC сообщает `-Wuninitialized` в
`VArenaAlloc::reset()` : fFirstBlock/fFirstSize/fFirstHeapAllocationSize читаются
@ -205,17 +62,6 @@
Лог: `/tmp/utun-rlottie-vendor-build.log` . Сборка библиотеки и подключение её
экспортированного CMake-пакета к ChatGUI на Linux прошли; Windows ещё не проверен.
[+] **ChatGUI Windows: проверка линковки ZXing без -Os** — 05.10.2026:
после удаления старых DLL и чистой сборки линкер не находит move-конструктор
`std::string` в `libZXing.a(Barcode.cpp.obj)` . ZXing задаёт этому файлу -Os;
такая ошибка описана в https://github.com/msys2/MINGW-packages/issues/31015.
Для MinGW GCC отключён пофайловый -Os; используются флаги выбранного типа сборки.
Проверена генерация команд: при MinGW+GNU Barcode.cpp получает -O3 без -Os,
Linux сохраняет прежний -Os. Компиляция Barcode.cpp на Linux прошла.
Windows-прогон `tools/chatgui/build.bat` прошёл линковку и дошёл до раскладки DLL;
это подтверждено следующим `1.log` . В списке системных DLL раскладки также добавлены
`imagehlp.dll` и `bcryptprimitives.dll` , ошибочно считавшиеся отсутствующими зависимостями.
[ ] **ChatGUI Windows: подтверждение обновления runtime компилятора** — 05.10.2026:
`cc-probe.log` : cc1 запускается из корня, но в `tools/chatgui/build` завершается
с `0xC0000139` ; локальные DLL GCC/math/runtime отличаются от UCRT64.
@ -231,124 +77,11 @@
Runtime GCC обновляется отдельно; для остальных DLL нужно продумать обновление
с сохранением приоритета собственной сборки FFmpeg и проверить запуск приложения.
[+] **Открытие HTTP/HTTPS в сообщениях Android и Qt** — 04.10.2026:
общая обработка ссылок по диапазонам текста; веб-адреса открываются системным
обработчиком, `utun://` сохраняет JOIN и короткое отображение PM-приглашений.
Учтены Unicode, IPv6, скобки, пунктуация, параметры, fragment и emoji в адресах.
Qt использует одинаковый layout для отрисовки, размеров, нажатия и выделения.
Ошибки открытия переведены и логируются. Общие 15 случаев прошли в Android и Qt;
проверки нажатия/drag/copy и приглашений прошли. Чистые сборки Qt и APK — PASS.
`check.sh` : 124 passed, 0 failed, 1 skipped; proxy/burst/load — PASS.
APK установлен на Q8, проверено системное открытие HTTP и HTTPS через ACTION_VIEW,
приложение запущено. Логи: `/tmp/message-links-check.log` ,
`/tmp/message-links-qt-final-tests.log` , `/tmp/message-links-android-tests.log` .
Контракт: `doc/message_links.md` .
[+] **Заголовок PM-приглашения и короткое отображение ссылки** — 04.10.2026:
`ct=invite` содержит зашифрованные название группы и полную ссылку; GUI получает
название через `tags.group_name` и добавляет заголовок из перевода интерфейса.
Android и Qt отображают ссылку одной строкой с многоточием. Нажатие и копирование
используют полный адрес: долгое нажатие в Android, выделение строки и Ctrl+C в Qt.
Срок действия ключа — 10 минут. Проверена доставка названия с Unicode, кавычками
и переносом строки, JOIN и история; Qt проверяет разные ширины, нажатие на конец
сокращённой ссылки и копирование. Чистые сборки ядра, Qt и APK прошли.
`check.sh` : 124 passed, 0 failed, 1 skipped; proxy/burst/load — PASS.
Qt `test_invite_links` и `test_emoji_playback` — PASS. APK установлен на Q8,
приложение запущено. Логи: `/tmp/pm-invite-title-check.log` ,
`/tmp/pm-invite-title-e2e-final.log` . Контракт: `doc/pm_invites.md` .
[+] **История группы после PM-приглашения** — 04.10.2026: регистрация `db_sync`
создаёт локальное состояние с блокировкой обмена; история запускается после
успешного member_sync через READY-пиров конкретной группы. Подписка на переходы
READY сбрасывает обмен при DOWN/REINIT/LEAVE и возобновляет его при восстановлении.
Обе роли имеют 15-секундный таймаут; ошибки отправки и отказ пира разрешают повтор.
JOIN_INFO_RESP применяет участника через транзакционный member_sync, обновляя
Merkle-индекс вместе с моделью. ASAN также выявил и помог исправить использование
временного content_type после выхода из области видимости при выдаче JSON истории.
Проверены пять сообщений до приглашения, оба порядка ID, потеря первого обмена,
восстановление READY на общем транспорте и последующая доставка новых сообщений.
Эти сценарии, ownership и services прошли под ASAN. Финальный check.sh:
124 passed, 0 failed, 1 skipped; интеграционные proxy/burst/load — PASS.
Чистые сборки ядра, Qt и APK прошли. На Q8 после обновления
существующий `g2` получил все 5 старых сообщений без повторного JOIN;
SYNC_DONE подтвердил совпадение хеша, SQLite quick_check — ok.
Диагностика: `/tmp/pm-history-q8-final.log` , `/tmp/pm-history-asan-invite.log` ,
`/tmp/pm-history-asan-timeout.log` , `/tmp/pm-history-final-check.log` .
Контракт: `src/chat/db_sync_arch.md` .
[+] **Android: диагностика несрабатывания VAD** — 03.10.2026: в `debug` добавлены
решения по каждому изменению состояния и сводки PCM/AGC/модели раз в секунду;
`debug=trace` показывает каждое окно 32мс. Логи различают сброс подтверждения,
чужой RX, закрытое окно Android, задержки inference и отложенный BEGIN.
Регрессии radio_vad, Silero, radio_audio и voice_tx прошли. На Samsung в 19:36:23
подтверждён отказ после истечения 15-минутного окна: `reason=window_expired` ,
`requested=true screen=true capture=false nativeVad=false` ; ручной PTT работает.
Кнопка на панели показывает сохранённое включение, а не фактическую активность VAD.
Лог: `/tmp/vad-sm-current.log` . Исправление таймера и индикации — следующая запись.
[+] **Android: индикация и фоновый таймер VAD** — 03.10.2026: кнопка показывает
фактическую активность захвата, гаснет при его остановке; повторное включение
открывает новое окно. 15-минутный таймер расходуется только в фоне, при видимом
приложении сбрасывается: после каждого сворачивания доступны полные 15 минут.
После истечения возврат в приложение возобновляет VAD; погасший экран по-прежнему
останавливает микрофон, включение экрана открывает новое окно.
`tests/test_radio_vad_window.kt` проверяет 2 часа в foreground, сброс таймера при
возврате, полное фоновое окно, границу истечения, экран и сохранение окна при перенастройке.
На Samsung проверены фактические lifecycle-переходы и выключенная кнопка после
остановки захвата: `/tmp/vad-background-sm-transitions.log` . Чистая сборка APK прошла,
Samsung SM-A525F и Q8 обновлены; на Samsung восстановлены прежний канал и активный VAD.
Уточнение: при возвращении в приложение таймер теперь полностью сбрасывается,
соответствующая регрессия прошла. Новая чистая сборка — `/tmp/vad-reset-android-build.log` ,
установлена на оба телефона. Повторная проверка lifecycle на Samsung после этого
обновления не выполнена: телефон заблокирован; правило сброса проверено тестом с искусственным временем.
[ ] **Android: время жизни config provider** — 03.10.2026: `utun_bridge_init()`
передаёт адрес локального `utun_config_provider_t` в `utun_config_set_provider()` ,
который хранит указатель. После возврата из init он недействителен; перед
использованием `cfg_get_*` обеспечить постоянное хранение provider.
[+] **DM: надёжная доставка сообщений, первый этап pm.txt** — 01.10.2026:
атомарные история/seq/outbox, подписанные точные квитанции после commit,
live BGP-маршрут и durable очередь на суперузле с проверкой подписанной роли.
Повторы, out-of-order, rollback БД, неверные ACK и поздний PUT проверены;
40 offline-сообщений доставлены после restart источника, первый offline DM —
после restart суперузла при выключенном источнике. Копии удалены по квитанции.
Подготовка ciphertext медиа использует ключ target и независимые nonce порций;
проверены границы 32 КиБ, пустые файлы, неверные ключи/UUID/hash и повреждения.
Clean-сборка ядра/Qt, test_dm, test_dm_e2e и полный check прошли:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load PASS.
Текущие гарантии и оставшаяся интеграция: `doc/dm_arch.md` .
[+] **DM: сетевая доставка и pending-медиа по pm.txt** — 01.10.2026:
отдельные owners dm_media/dm_mailbox_media и файловый transfer с CM handle,
подписанные метаданные и независимая квитанция файла после durable приёма.
Прямая доставка или S→M custody, restart S/M при выключенном A, доставка B,
повтор удаления до DELETE_ACK; поздние STORE/STORED не возрождают копии.
TTL M по умолчанию 7 дней, резервирование квоты, отмена transfer и stop workers.
Копирование, шифрование, хеширование, проверка и durable файловые операции —
в media_async; отправка по 8 КиБ через call_soon. Тест 8 МиБ/0,5 с сохраняет
heartbeat uasync (максимальный интервал 10,3 мс в проверенном прогоне).
Qt/Android: оригинальные файловые вложения, сохранение и статусы доставки;
PM-снимки из ядра асинхронные. Чистые сборки Qt и Android APK прошли.
test_dm_media/test_dm_worker, rollback/quota/cancel и полный check:
115 PASS, 0 FAIL, 1 SKIP; proxy/burst/load PASS. GUI на устройствах не проверялся.
Протокол, владение и проверки: `doc/dm_arch.md` .
[+] **DM: голосовые/видео и общий GUI вложений** — 01.10.2026:
тип FILE/VOICE/VIDEO и метаданные зашифрованы ключом target; в transport остаётся
подписанный descriptor, без новых ключей и канального протокола в DM.
Общие attachment/voice_file/attachment_send: Opus, waveform, копирование и видео
готовятся в workers; completion через reserved post вместо polling. PCM передаётся
без копии; адресат закреплён до записи/выбора. Отмена освобождает собственные файлы.
Android: ConversationComposer/ConversationMessages для каналов и PM, CameraX MP4,
IO для URI/метаданных; Qt: общий InputBar/delegate, FFmpeg, фоновые превью/Opus.
Подготовка адресована беседе и UUID задачи; статусы доставки показаны для всех типов.
test_attachment/test_attachment_send проверяют кодек, формат, владение и отмену;
test_dm_media — voice напрямую и encrypted video metadata после offline restart.
Чистые сборки ядра/Qt/APK, 117 PASS / 0 FAIL / 1 SKIP в check.sh; proxy/burst/load PASS.
Qt CTest: 7 PASS, включая повреждения Opus и отмену decoder. APK не устанавливался,
интерактивная проверка устройств не выполнялась. Подробности: `doc/dm_arch.md` .
[ ] **uasync: ложные ERROR об утечке перед cleanup** — 01.10.2026:
test_dm_e2e и существующий test_conn_mgr проходят, но uasync_destroy()
пишет Timer leak до освобождения отменённых heap/immediate таймеров.
@ -356,31 +89,6 @@
предварительные счётчики в DEBUG; ERROR оставить для реального расхождения
после cleanup, отдельно показывать активные и отменённые таймеры.
[+] **Статус подключения звонка в Qt и Android** — фактический путь router:
прямое / reverse / indirect / нет подключения. Учитывается BGP fallback;
события только при изменении, общий таймер 1с, сброс при завершении.
Проверены смена путей настоящего звонка, медиа, ASAN, Qt, Android APK и полный check.
[+] **Единицы RTT в окне звонка** — `call_post_stats()` переводит
`ETCP_CONN.rtt_last` из 0.1мс в миллисекунды до отправки CALL_STATS.
Исправление общее для Qt и Android. DEBUG-категория `debug` раз в секунду
показывает исходный RTT, RTT в мс, транспортного соседа и метрики аудио.
[+] **Восстановление путей conn_mgr на протяжении жизни handle** — начальный
TIMEOUT отделён от срока цикла проб. Поздний обрыв запускает новый цикл;
рабочий резерв и NCD сохраняются, при отсутствии резерва INDIRECT стартует сразу.
Циклы ограничены 15с, паузы 1→2→4→8→15с; новая версия узла или локальные сокеты
обходят паузу. Прямой READY останавливает поиск и получает приоритет.
Исходящие request_id и входящие сроки независимы; устаревшие ответы игнорируются.
UP/DOWN отражают общую доступность, TIMEOUT приходит один раз до первого UP.
Последний close отменяет восстановление, включая закрытие из callback.
Проверены десять сетевых сценариев с реальными данными: поздний обрыв,
повторные циклы, смена адресов/сокетов, потеря маршрута посредника, старый ACK,
начальный TIMEOUT с поздним UP и close. ASAN без ошибок; собраны Qt и Android APK.
Полный check: 113 PASS, 0 FAIL, 1 SKIP; пропущенный auto_socket_dynamic отдельно
прошёл с root в network namespace. Интеграционные proxy, burst и load прошли.
Алгоритм: `src/routing_layer/conn_mgr_doc.md` .
[ ] **Общий лимит check.sh после clean** — 01.10.2026: расчёт по времени предыдущего
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
до `test_uasync_socket_race` . Повторный запуск на готовой сборке с большим лимитом
@ -406,26 +114,6 @@
capture/playback, фактической частоты callback и PCM кадров за интервал.
Счётчики сбрасываются до старта устройства; нужна повторная запись с новой сборкой.
[+] **Ложный сигнал потери звука при запуске звонка** — 01.10.2026, звонок
`bc3efadcac629947` : Qt запускает аудиоколлбэки в 01:54:56.933, включает gap tone в
01:54:57.415 и выключает в 01:54:58.072 при первом воспроизведении PCM.
На SM между ACCEPT и первым захваченным кадром проходит около 1.15с;
часы SM отстают примерно на 1.23с. Qt стабильно обслуживает 100 callback/с,
RTT около 1мс. Исправлено: до первого PCM начальное ожидание проходит в тишине,
последующий перебой включает тон через 500мс. Стартовый сбой по-прежнему
ограничен существующим watchdog отсутствия медиа. Регрессия проверяет ожидание
1.5с, начальное накопление, настоящий перебой, неполный блок, новый звонок и завершение.
Ускорение запуска Android: router.start занимает около 775мс; система сообщает
длительное удержание AudioManager.updateAudioPortCache. Ещё около 370мс проходит
до запуска capture worker. Гудки теперь останавливаются до startAudio;
router использует снимок устройств с обновлением по событиям и явной переоценке,
capture запускается до подготовки/старта playback. Ошибка playback останавливает capture.
Добавлены длительности отдельных стадий, первого capture и PCM. Ядро, Qt и APK
собраны после clean; три аудиотеста прошли. Выигрыш на SM требует замера в новом звонке.
В звонке Qt `180f046ef22defc2` первый PCM пришёл через 1245мс без стартового gap tone.
Полный check вне песочницы: 113 PASS, 0 FAIL, 1 SKIP; auto_socket_dynamic отдельно
прошёл с root в private netns. Интеграционные proxy, burst и load прошли.
[ ] **Оставшаяся задержка запуска аудио SM** — после обновления 01.10.2026,
звонок `2b9ad4e21a54d8ba` : стартового gap tone нет на обеих сторонах; Qt
стабильно работает около 100 callback/с, max_idle=15.3мс, буфер 55мс, RTT=2мс.
@ -456,96 +144,6 @@
`stress_sessions` иногда печатают огромные положительные/отрицательные миллисекунды,
хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода.
[+] **SpeexDSP AEC (эхоподавление) в lib/** — акустическое эхоподавление звонка.
Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/` ,
обёртка `lib/speex_aec.c/h` (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних
зависимостей). Собственная линия задержки рендера на `delay_frames` кадров +
синхронный `speex_echo_cancellation` (встроенный буфер SpeexDSP фиксирован в
2 кадра и для Android не годится); дрейф потоков — через переполнение/недозаполнение
линии (drop/dup + passthrough). Категория лога `aec` .
Ядро: `call_audio_set_aec_enabled/delay_frames` , AEC в feed_pcm (захват→encode)
и pull_pcm (рендер-референс), lifecycle в start/release. Desktop — настройка GUI
`aec_enabled` (`[chatserver]`, читается в `call_audio_start` ). Android — включается
только на спикерфоне (`route==SPEAKER`, live через JNI `callAudioSetAec` ),
задержка меряется `AudioTrack/AudioRecord.getTimestamp` ; `CallAudioEngine.playLoop`
переведён на absolute-deadline (устранён систематический дрейф). Тест
`test_speex_aec` (подавление эха 25.5 дБ, сохранность речи 0.956, delay-line, NULL-гарды).
Сборки: autotools + chatgui CMake + Android CMake/APK.
[+] **Silero VAD в lib/** — добавлен детектор голосовой активности (Silero VAD v5,
стриминговая ONNX-модель + обёртка `lib/silero_vad.c/h` над ONNX Runtime C API).
API: `silero_vad_create/create_default/destroy/reset/process/process_pcm16`
(окно 512 сэмплов @16 кГц, вероятность речи [0..1], рекуррентное состояние).
Модель вшита в бинарник (`silero_vad_model.inc`, `create_default` ), файл не нужен.
Категория лога `vad` . Модуль под `#ifdef HAVE_SILERO_VAD` — без onnxruntime
компилируется в стабы, поэтому GLOB-сборки (chatgui/Android) не ломаются.
Сборка: `--with-silero-vad` (autotools), `-DHAVE_SILERO_VAD` + libonnxruntime
(chatgui CMake, Android NDK). `make check` : 111/0/1.
[+] **Авто-PTT рации по VAD** — сделано. Отдельный режим `radio_auto_enabled` (ручной
PTT не меняется, при зажатой кнопке — override). Сервисный слой `radio_audio.c` :
ресемплер 48k→16k моно (3-tap box + 3:1, стерео микшируется), накопитель окон
512, фильтр-гистерезис + машина состояний (`src/radio/radio_auto.h`, чистый,
юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога
`radio_vad_threshold` , стоп при тишине `radio_auto_hangover_ms` (200мс) или чужом
разговоре (busy = RX-кадр < 200мс ) . Перед BEGIN сохраняется до 600мс непереданного PCM ;
prefetch отправляется одной задачей перед live без повторов и задержки на стороне отправителя.
VAD использует отдельный компрессор с собственной историей усиления. Desktop `radio_audio_engine.cpp` и Android
`RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()` .
Тест `test_radio_vad` (ресемплер + FSM). Android: onnxruntime AAR 1.23.2 в
`jniLibs/` + `cpp/onnxruntime/include` + `-DHAVE_SILERO_VAD` в CMake.
Android: настройки VAD (включение, порог, задержка после тишины); микрофон в PTT
создаётся только для передачи. PTT запускается при касании, после отпускания
передаёт ещё 250мс; повторное касание отменяет завершение (двойной тап без FIN).
VAD работает 15 минут при горящем экране;
SCREEN_OFF освобождает захват, SCREEN_ON автоматически открывает новое окно.
RX остаётся активен; TX освобождается отдельно через `radio_audio_capture_stop()` .
[+] **Отдельный компрессор перед VAD рации** — 03.10.2026:
копия PCM 48кГц/20мс проходит через общий audio_compressor до ресемплера и Silero.
Настройки общие с TX, кроме `radio_vad_compressor_max_gain_db` (15дБ по умолчанию,
1..60дБ); включение также общее. Состояния TX/VAD независимы, prefetch хранит исходный PCM.
Выключенный компрессор не добавляет ожидания блока; разрыв/новый захват сбрасывает AGC и Silero.
Android: отдельный ползунок, DataStore/INI/runtime; диагностика `radio` показывает
параметры, input/output RMS, фактический gain и клиппинг. Диапазоны max_gain согласованы с библиотекой.
test_voice_tx: точный вход Silero, байтовое совпадение Opus/prefetch, mono/stereo,
6/30дБ, переменные PCM-чанки и reset. Настоящий Silero распознаёт речь при амплитуде
0.03 на тихом фоне, останавливается после речи; повторный reset воспроизводит результат.
Чистые сборки ядра/Qt/APK, целевые тесты и полный check.sh прошли:
124 PASS / 0 FAIL / 1 SKIP, proxy/burst/load PASS; Qt CTest 2/2, AGC 479/479.
APK с последующим переносом переключателя VAD установлен на Samsung SM_A525F.
[+] **Включение VAD на панели рации Android** — 03.10.2026:
кнопка VAD расположена слева от правой кнопки плавающего PTT; состояние читается из DataStore Flow.
Действие в ViewModel сохраняет настройку, обновляет ядро, запрашивает разрешения и перенастраивает захват.
Переключатель удалён из настроек; порог речи, задержка после тишины и max gain оставлены.
Чистые сборки ядра/Qt/APK прошли, APK установлен на Samsung; UI настроек проверен через uiautomator.
[+] **Верхняя панель рации и VAD в desktop ChatGUI** — 03.10.2026:
строка под шапкой канала: меню микрофона/выхода, состояние, переключатель VAD слева от PTT.
PTT перенесена с сообщений в панель; подсветка показывает фактический TX, включая автоматический VAD.
Настройки рации: вероятность речи, задержка тишины, включение AGC и независимые max gain TX/VAD.
Общий стек сохраняет prefetch 600мс; настройки применяются в uasync с подтверждением в GUI.
Перезапускается только capture, RX/output сохраняются; удерживаемые кнопка/хоткей восстанавливаются.
INI сохраняется атомарно; ошибка микрофона/детектора видна на панели, ручной PTT доступен без VAD.
Запрос настроек принадлежит reserved uasync task и освобождается также при shutdown до исполнения.
Исправлено сохранение Audio без посещения страницы: устройства и аудиопараметры предварительно загружаются.
Чистые сборки ядра/Qt и Qt CTest 16/16; test_radio_panel проверяет Qt-кнопки и настоящие audio workers
с null backend: auto-TX, 30 кадров prefetch, manual/hotkey, capture recovery, INI restart и отмену запроса.
Полный check.sh: 124 PASS / 0 FAIL / 1 SKIP, proxy/burst/load PASS; настоящий Silero распознаёт тихую речь.
Логи: `/tmp/desktop-vad-qt-verified-tests.log` , `/tmp/desktop-vad-core-check-unrestricted.log` .
[+] **PTT кнопками громкости на панели рации Android** — 03.10.2026:
переключатель слева от VAD, включённое состояние подсвечено; панель и настройки используют общий DataStore Flow.
Значок: динамик по центру, плюс сверху, минус снизу; проверен на панели Samsung после чистых сборок ядра/Qt/APK.
Сохранение обновляет быстрый кэш после записи; ошибки записи логируются без изменения состояния переключателя.
Экранная кнопка, оверлей и кнопки громкости имеют независимые владельцы ручной передачи.
Учёт отдельных volume-клавиш защищает от повторов и посторонних клавиш; потеря фокуса/выключение опции отменяет volume-PTT.
Чистые сборки ядра/Qt/APK прошли, 12 сценариев владельцев PTT проверены отдельным Kotlin-прогоном.
APK установлен на Samsung SM_A525F: обе volume-кнопки запускают запись, отпускание завершает её с хвостом 250мс.
В отдельном канале без других участников проверены переключатель и сохранение выключенного состояния после restart.
Логи: `/tmp/android-volume-ptt-final-device.log` , `/tmp/android-volume-ptt-apk-final-build.log` .
[ ] **Linux ONNX Runtime: повторная регистрация схем при запуске теста** — 03.10.2026:
test_silero_vad при загрузке модели печатает серии `Schema error: ... already registered` .
Инференс, речь, тишина и reset проходят; отдельно проверить состав и загрузку Linux-библиотек ONNX.
@ -556,35 +154,10 @@
огромные или отрицательные миллисекунды (например, -1611883348002232012ms).
Отдельно проверить формат/аргументы вывода длительности; `/tmp/vad-compressor-check.log` .
[+] **chatgui (Qt) не собирался: C++ void* в utun_node.cpp** — сделано. Причина:
`proxy_protocol.h` (C-заголовок, включается в C++ `utun_node.cpp` ) делал
`e->dgram = u_malloc(...)` и `struct proxy_flow* f = arg;` без явных приведений —
в C++ нет неявного `void*` →T*. Добавлены `(uint8_t*)` /`(struct proxy_flow*)`
(валидны и в C). `vibechat` собирается (47МБ, с вшитой моделью + onnxruntime).
[+] **test_auto_socket_dynamic: root-регрессия и нагрузка** — изолированный namespace,
корректные маршруты, двусторонний поток через backpressure, строгие seq/payload,
пределы TX/RX, остановка при обрыве и drain после восстановления, баланс ресурсов.
Исправлены привязка TCP к интерфейсу, удаление TCP моделей/линков/портов из БД,
sender receive window при selective ACK, timestamp overflow и нулевой jitter.
ASan/UBSan/LSan: PASS; дополнительно исправлены невыровненные метки аллокаторов
и PID-порты в test_etcp_connect. [Разбор и логи ](etcp_full_suite_findings.md ).
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:` , документация
описывает `=` ; преобразование битовой маски осталось после перехода enum на индексы.
[Разбор ](etcp_full_suite_findings.md ).
[+] **Q8/Android не подключается к локальному чатгуи (LAN)** — сделано. Причина: тип сокета
в Android-конфиге был захардкожен `type=public` , из-за чего `sock_match` ставил
`has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`).
Исправлено: (1) `ip_is_public()` вынесен в `lib/platform_compat` (единый классификатор
IPv4 с корректным ntohl); (2) в `auto_socket_reconcile_config` (Android-режим) тип сокета
выставляется по адресу (public→PUBLIC, приватный/link-local→NAT), конфиг `type` игнорируется;
(3) `sock_collect_views` больше не смотрит на `type` из конфига: `has_priv` — по классификации
локального адреса, `has_inet=1` (исходящие считаем доступными), `is_eim` — по NAT-детекции
(hairpin). `is_local_subnet` упрощён до `!ip_is_public()` . Тест `test_sock_match` обновлён
(+ `ip_is_public` кейсы). `check.sh` : 86 passed / 1 failed / 1 skipped.
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
ТЗ и архитектура: `/doc/dm_arch.md` . Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
@ -596,123 +169,11 @@
Android: переключение источника (трубка/громкая/гарнитура).
Архитектура: `/doc/chat_call_arch.md` .
[+] **Сигналинг + релей медиа** — сделано. `call_proto.h` (svc `ETCP_RT_ID_CALL
0x36`, subcmd INVITE/RINGING/ACCEPT/DECLINE/BUSY/CANCEL/HANGUP/MEDIA),
`call.c` (машина состояний, реестр сессий, conn_mgr, ring-timeout 45с,
watchdog no-traffic 20с, потеря соединения → end, события
CHAT_EVT_CALL_* 34–40), `DEBUG_CATEGORY_CALL 32` . `test_call` (2 инстанса,
happy path + decline + media-релей). Приём медиа — через `call_set_media_recv_cb` .
[+] **Headless API** — сделано. Команды control-сокета `call_start/accept/decline/
hangup` + события `call_incoming/ringing/accepted/ended/declined` (JSON);
аудио-сокет `call/call_headless.c` (TCP, бинарный фрейм
`[type:1][call_id:8][len:2][data]` , HELLO/FRAME), конфиг `call_audio_bind` .
Разрыв conn_mgr — отложенно 500мс (`CALL_TEARDOWN_DELAY_TB`) после ENDED,
чтобы HANGUP успел долететь; защита от stale-handle через conn-status.
Тесты: `test_call` (сигналинг+медиа), `test_call_headless` (полный e2e через
control+audio сокеты).
[+] **Аудио-контур (time-stretch в сервисном слое)** — сделано. `src/call/audio_jitter.cpp`
(SoundTouch, C API `ajb_*` : адаптивный джиттер-буфер ≤1000мс, target 60мс,
tempo 1.0→1.6x — догон live без потери данных при сетевой паузе),
`src/call/call_tones.c` (глитч при сталле >500мс + тон завершения),
`src/call/call_audio.c` (Opus 48kHz/20мс + jitter + тоны + pull API финального PCM).
`call.c` : `call_audio_ops` (on_media c seq/ts, get_stats) + событие
`CHAT_EVT_CALL_STATS` раз в 1с [rtt_ms, buffer_ms, tempo_x100].
Desktop `call_audio_engine.cpp` — только miniaudio I/O (получает финальный PCM).
Тест `test_audio_jitter` (ёмкость/догон/стационар). Демон остаётся C-only.
[+] **Android** — сделано. JNI (`utun_bridge_call_*`/`nativeCall*`), AudioRecord/Track
(CallAudioEngine), роутинг источника (CallAudioRouter: EARPIECE/SPEAKER/HEADSET,
BT SCO), CallScreen + IncomingCallDialog + RingtonePlayer + IncomingCallManager,
`libutun_lite/call_audio.c` (собственный аудио-контур Opus 48kHz/20мс).
[+] **Единый голосовой стек (voice stack) + SoundTouch на Android** — сделано.
Унификация аудио-контура звонка: `libutun_voice` (статическая C++-либа с
C-интерфейсом `call_audio.h` = SoundTouch + `voice_jitter.cpp` + `call_audio.c`
+ `call_tones.c` ), собирается через `src/call/voice_sources.cmake` и линкуется
desktop/Android/headless-CLI. Джиттер-буфер держит КОДИРОВАННЫЕ (Opus) кадры,
декодирует только на pull (непосредственно перед воспроизведением) — память ~48x
меньше и uasync-поток разгружен. Android переведён с push-FIFO (Kotlin ArrayDeque)
на pull-модель (`nativeCallAudioPull`), своего джиттера больше нет → убрана
асимметрия задержки (desktop↔Android). Headless-аудио-сокет отдаёт готовый PCM
(декод Opus→PCM, без тактирования; клиент сам задаёт темп). Диагностика:
`CALL_STATS` расширен [dropped, underruns] (раз в 1с), счётчики headless.
Удалены `audio_jitter.cpp` и `libutun_lite/call_audio.c` . Тест `test_voice_jitter`
(ёмкость/догон/стационар). Android — `ANDROID_STL=c++_static` .
[ ] **DM-звонки** (fallback `group_id==0` ) — не сделано: `call_is_online` /`call_start`
требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет.
## Звонок: две фазы исходящего вызова (индикация + гудки)
[+] **Две фазы вызова** — сделано. Исходящий звонок разбит на 2 фазы с индикацией
в окне вызова и разными гудками:
- фаза 1 «Пытаемся связаться…» (`OUTGOING` — INVITE/маршрут) — гудок «соединение»;
- фаза 2 «Звоним…» (`RINGING` — у пира идёт вызов) — длинные гудки (ringback).
Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting` /`call_ringing`
(MP3 в qrc), переключение по `onCallRinging` , остановка по accepted/ended/error;
добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок
зависали навсегда. Android: `CallRingback` (MediaPlayer loop из res/raw),
подписи `OUTGOING` /`RINGING` в CallScreen + ActiveCallBanner.
Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing).
[+] **Надёжность отмены вызова (кодограмма + watchdog)** — сделано. Отмена вызова
(HANGUP/CANCEL) шлётся один раз, без ретрансмита, и при флапе маршрута (RST/epoch,
прямой TCP к мобиле не поднимается — только релей) терялась: вызываемый висел в
RINGING вечно и отвечал BUSY на все новые INVITE. Исправлено моделью «сердцебиения»:
- вызывающий шлёт кодограмму INVITE каждые 300мс весь период вызова (OUTGOING+RINGING);
- у вызываемого watchdog 2с — нет кодограммы → вызов сорван (`CALL_REASON_CANCEL`);
- завершающие кодограммы без ретрансмиссий (потерю покрывает watchdog);
- запоздалые кодограммы вызова при разговоре (CONNECTING/ACTIVE) игнорируются.
`CALL_INVITE_RETRY_TB=3000` , `CALL_RING_DEAD_TB=20000` , `CALL_NO_ROUTE_MAX_STRIKES=20` .
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE` ):
- `chat_setting` → `inst->chat_settings` (`struct chat_setting_state`, state-level API для парсера конфига + instance-level для runtime).
- `chat_event` → `inst->chat_event_handler` (обработчик теперь получает `inst` ).
- `chat_core` (`g_cc`) → `inst->chat_core` (`struct chat_core_ctx*`).
- `chat_sync` (`g_cs`) → `inst->chat_sync` .
- `member_sync` callback-списки (`g_props_cbks`/`g_apply_cbks`) → в `chat_core_ctx` .
- `chat_join` (`g_keys`) → в `chat_core_ctx` .
- `chat_msg` media-счётчики/backfill → в `chat_core_ctx` .
- `dm_core` (`g_dm`) → `inst->dm` ; `dm_mailbox` (`g_mb`) → `inst->dm_mailbox` .
- `chat_headless_control` (`g_hc`) → `inst->headless` .
- `chat_whisper` — сигнатуры `init/available/trigger` принимают `inst` .
- API протащено через `inst` (chat_core_*/dm_*), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser).
- `test_dm_e2e.c` переписан на однопоточный: один `uasync` , три `utun_instance` (A,B,C), master state-machine, без fork. Фаза 2 (B offline) — `utun_instance_destroy(B1)` + пересоздание B2.
- Попутно исправлен 1-байтовый overflow в `dm_mailbox.c: mb_route_send` (`u_malloc(1+body_len)` → `1+1+body_len` ).
Статус сборки: `make -C src` (libutun.a + бинарник utun) и `make -C tests` — OK.
### Осталось (по порядку)
[+] **test_dm_e2e: зависание в utun_instance_destroy(B1)** (фаза P_B_OFFLINE) — сделано.
Причин было несколько (не одна):
1. `lib/u_async.c` : если ближайший таймер уже истёк на входе в `uasync_poll` ,
`get_next_timeout` возвращал {0,0}, `timeout_ms=-1` → `epoll_wait(-1)` блокировался
навсегда, а `process_timeouts` (вызывается только после epoll_wait) не запускался.
Фикс: `if (timeout_ms == -1 && heap не пуст) timeout_ms = 0` .
2. `routing_layer/etcp_router.c` : `router_send_conn` с group_id=0 (глобальная
маршрутизация DM/mailbox) не находил маршрут (`topo_groups_find(0)==NULL`).
Фикс: fallback на `instance_find_conn(inst, remote)` при group==NULL.
3. `dm/dm_core.c` : `dm_on_conn_status` PULL-ил только беседы с подключившимся пиром;
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения
PULL для всех бесед, чей пир не подключён напрямую.
4. `dm/dm_mailbox.c` : не было ACK после PULL_RESP → storage не чистил dm_mail.
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK.
5. `utun_instance.c` : `dm_core_init` ставил deliver-cb до `dm_mailbox_init` →
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core.
`test_dm_e2e` проходит (несколько прогонов), `test_dm` тоже.
[+] **chatgui-android: переезд на per-instance API** — сделано.
`instance_lite.c/h` (+`instance_lite_get_instance()`), `jni_bridge.c` , `standby.c` ,
`headless_control.c` , `voice_recorder.c` , `attachment_sender.c` , `video_sender.c` ,
`photo_sender.c` — все call-сайты переведены на `inst` . `libutun_lite` компилируется,
`jni_bridge.c` — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.)
[+] **test_chat_join: segfault** (регресс рефакторинга) — сделано.
`chat_join` хранил `join_keys` в `chat_core_ctx` (CC(inst)), а тест использует chat_join
без chat_core → NULL-deref. Фикс: `join_keys` вынесен в `UTUN_INSTANCE.join_keys`
(у chat_join собственный init/destroy).
[ ] **BGP-гонка: дропнутый REQUEST_TABLE** (отдельная задача, как договорились).
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571` . Дедуп-ветка
@ -759,44 +220,12 @@
(не пытаться соединяться с узлами, для которых нет `[client]` /ncd-конфигурации,
либо обрабатывать неудачу без спама).
[+] **Обновить GUI call-сайты `tools/chatgui` (Qt, desktop)** — сделано.
- В `chat_core.h` вынесены trampoline-структуры из `.c` : `update_my_name_arg` ,
`save_ui_state_arg` , `chat_setting_arg` (были в chat_profile.c/chat_core.c).
- `gui_bridge.h/impl` : `gui_bridge_set_inst()` /`gui_bridge_get_inst()` (+`g_inst`).
- `utun_node.cpp` : `gui_bridge_set_inst(m_instance)` , `chat_event_set_handler(m_instance,…)` ,
`chat_core_sync_my_addresses(m_instance)` .
- Обновлены ~45 call-сайтов в 16 файлах (`mainwindow`, `messagelist` , `messagedelegate` ,
`memberlistmodel` , `memberpropsdialog` , `accountlist` , `invitedialog` , `inviteby` ,
`joindialog` , `settingsdialog` , `channelsettingsdialog` , `soundsettingspage` ,
`audiodevicesettingspage` , `storagesettingspage` , `statuspage` , `connmonitorwindow` ).
- Сборка `cmake --build . --target vibechat` — OK.
Попутно: `chat_sync_connect_from_invite` писал `cs->pending_*` на вызывающем (GUI) потоке —
перенёс в `cm_invite_trampoline` (uasync-поток), убрав гонку; заодно исправил утечку
`inv->addrs_data` и добавил null-check `cs` в трамплине.
[+] **Прогон всех тестов** — сделано. `make clean && make -j4` OK; все тесты проходят
(кроме `test_auto_socket_dynamic` — пропуск «requires root»). `test_dm_e2e` стабилен
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов,
`router_no_route` ), не влияет на результат — стоит разобрать отдельно.
[ ] **lwip/proxy: утечка pcb в FIN_WAIT_2** — `tcp_shutdown(pcb,0,1)` (half-close по FIN от
exit) не ставит `TF_RXCLOSED` , поэтому таймаут FIN_WAIT_2 в `tcp_slowtmr` не срабатывает:
если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash).
Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).
## Открытые флаки
[+] **test_chat_join_e2e** — сделано. Причина: ложный критерий успеха джойнера J.
`wait_group_started()` проверял `senders_list` непуст в предположении «senders_list
непуст ⟺ J получил JOIN_READY». После коммита b483335c `tgi_to_channel_cb`
(topo_group_invite.c) при `NCD_EVENT_UP` стал вызывать `topo_group_new_conn` →
conn попадает в `senders_list` сразу после установки TCP/UDP-соединения, ДО
завершения хендшейка (JOIN_INFO_REQ→RESP→REQUEST→READY). J выходил «OK» раньше,
чем A получал JOIN_REQUEST → A не подписывал J → `wait_signed` на A/C падал
(`get_sign_rc=-1`). Фикс: критерий J — событие `CHAT_EVT_CONNECT_RESULT(result=0)`
(JOIN_READY), ловится через `chat_event_set_handler` ; `wait_group_started` удалён.
Это также убрало гонку «C вышел раньше, чем J синкает свой рекорд» (C выходит сразу
после `wait_signed` , не дожидаясь merkle-синка J). 5 прогонов стабильно 3/3.
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4` :
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
запуске стабильно `TEST PASSED` . Наш код (chat_join/chat_sync) его не трогает.