# Задачи по проекту Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ. ## Текущая задача [+] **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 таймеров. После cleanup счётчики сходятся, финальная проверка проходит. Перенести предварительные счётчики в 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`. Повторный запуск на готовой сборке с большим лимитом прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов либо учитывать clean при расчёте. [ ] **Паузы аудио Qt при звонке с SM** — 01.10.2026, звонок `939a3ea36f3eeac3`, прямой LAN TCP `192.168.40.250:44604 ↔ 192.168.40.247:44173`. Tcpdump: общие паузы входящего трафика до 645мс; ping до роутера в среднем 100мс, до SM 128мс. ПК на 2.4ГГц, SM на 5ГГц. Одновременно `CallAudio: cb` падает до 20–40/с вместо 100/с, буфер Qt растёт до 2с, на SM повторяется refill. В 8с strace аудиопоток продолжает обслуживать PulseAudio без долгих syscall; причина редких аудиоколлбэков пока не установлена. Добавить измерения интервалов и длительности callback, кадров capture/playback и сводку CALL_STATS; сравнить сетевые задержки после подключения ПК к 5ГГц или кабелю. Повторный звонок `b2a423ed2d36ed83` (01:25:31–01:27:39): DIRECT сохраняется, RTT 2–3мс; в начале на SM gap tone длится 1.8–2.0с примерно каждые 6с. На Qt частота callback падает вплоть до 4/с, входящий PCM продолжает приходить: буфер 65→465→1465→2079мс за 3с, затем растёт до нескольких секунд, dropped=0. Локализовано прекращение обслуживания duplex-аудио Qt; ожидание backend и задержка обработки PCM пока не разделены. Часы SM отстают от Qt примерно на 1.24с. Добавлена диагностика backend/устройств, времени между callback и длительности 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мс. На SM первый capture появляется через 1140мс от startAudio (1164мс от ACCEPT). router.start=781мс, получение снимка устройств=0мс, AudioRecord create=24мс, startRecording=7мс; от начала startRecording до первого кадра=328мс. Capture worker стартует раньше playback, но общий выигрыш пока не виден. AudioTrack create=21мс, play=327мс; система сообщает AudioPortCache contention=497мс и восстановление AudioTrack при setPreferredDevice. Разделить замеры регистрации callback, BT-профиля и применения маршрута; отдельно проверить первый AudioRecord.read. На SM входящий буфер за время подготовки вырос до 1149мс и к концу звонка снизился до 570мс; ускорение старта playback также сократит начальное накопление. Реализован независимый запуск AudioTrack и AudioRecord в play/capture потоках; частичный старт отменяется до освобождения устройств/native. AEC измеряется после готовности обоих. Добавлены callbackMs/btProfileMs/applyMs/refreshMs, длительность первого read и время первой записи playback. Резерв теперь зависит от изменения задержки доставки по timestamps CALL_MEDIA, отдельно от глубины очереди; стартовое накопление и догон не повышают запас. Сохранность FIFO и uint32 wrap проверяются регрессиями. Сборки ядра, Qt и Android после clean успешны; три аудиотеста прошли. Полный check: 113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load прошли. Регрессия refill учитывает остаток SoundTouch: два новых кадра могут уже заполнить запас 60мс. SM и Q8 обновлены 01.10.2026 в 03:03. Выигрыш по времени требует пробного звонка с новой версией. [ ] **Гонка invite-файла в test_chat_join_e2e** — `wait_file()` проверяет только существование файла, а `wf()` создаёт его до записи. Читатель иногда получает пустую ссылку и ошибку префикса `utun://`. Публиковать готовый файл атомарно. [ ] **Некорректное время в выводе check-proxy** — успешные `basic_1mb` и `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_vad_enabled` (ручной PTT не меняется, при зажатой кнопке — override). Сервисный слой `radio_audio.c`: ресемплер 48k→16k моно (3-tap box + 3:1, стерео микшируется), накопитель окон 512, фильтр-гистерезис + машина состояний (`src/radio/radio_vad.h`, чистый, юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога `radio_vad_threshold`, стоп при тишине `radio_vad_hangover_ms` (200мс) или чужом разговоре (busy = RX-кадр <200мс). История захвата (pre-roll) удалена; передаётся текущий PCM. Компрессор в 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()`. [+] **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` (интеграция). Остались задачи ниже. [ ] **Звонок (P2P аудио)** — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный каталог `src/call/`. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure (локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер (SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с. 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`. Дедуп-ветка «conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки `topo_group_send_table_request()`. **Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк, затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего переподключение не стартует. Поэтому `return` в дедупе — правильный. **В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу (NODEINFO/узлы группы). Сценарий «разнесённого старта»: 1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn` → `topo_group_send_table_request`, `topo_group.c:602`). 2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка). C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G → дропает запрос. 3. C наконец создаёт G (`topo_groups_create_group` → обход connections → `topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`. A отвечает своей таблицей → **C узнаёт A**. 4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G. Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость). **Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно идемпотентно `topo_group_send_table_request(group, conn)`: ```c if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) { topo_group_send_table_request(group, conn); /* повторно, идемпотентно */ DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name); return; } ``` `topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE, пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден. **Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE — поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный воспроизводимый тест для этой гонки пока не написан. [ ] **«packet undecryptable» на линках C** (вторичное). A, узнав B через BGP, через `topo_group_connect` пытается поднять прямой A↔B линк (`seskey 4777737b`), но B не настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением (не пытаться соединяться с узлами, для которых нет `[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) его не трогает.