Files
utun2/doc/call_audit_findings.txt
T

1039 lines
95 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Аудит src/call — 26.10.2026, начат с голосовых вызовов
Текущий статус: дефект допуска второго исходящего звонка исправлен после
согласования. Штатная регрессия, четыре C++ voice-набора и полный check202/202
PASS. Разделы1–4 сохраняют историю диагностики, внедрение — в разделе5.
При продолжении подтверждён и исправлен дефект: старые TX-задачи после destroy/init
call_audio уменьшали pending новой привязки и обходили лимит8. Внедрена отдельная
эпоха владельца, постоянные регрессии13/13 и голосовой профиль4/4 PASS.
Ложный отказ test_radio_mesh (dupC=0) также исправлен: место дедупликации
зависит от порядка прихода. После rebase сборки и voice17/17 PASS,
check202/202 PASS,proxy9/9,burst1/1,load8/8,176с. Результат этого этапа — раздел10;
история/план — разделы6–7, внедрение TX — раздел8, диагностика mesh — раздел9.
Полная проверка аудио, видео и платформенных разделов не завершена.
При продолжении подтверждён и по запросу пользователя исправлен дефект
исходящей PCM-очереди call_headless: header+PCM принимаются/отбрасываются
целиком, partial send сохраняет хвост, потери имеют отдельный учёт/диагностику.
Новая постоянная регрессия11/11 и штатный headless e2e1/1 PASS;
native build и check203/203 PASS,proxy9/9,burst1/1,load8/8,188с.
Диагностика/план — раздел11, внедрение/актуальная проверка — раздел12.
По запросу «пуш и продолжай» опубликованы6 коммитов до54a7522d. При продолжении
desktop-профиль25/25 PASS; подтверждён дефект Android mute до AEC:
занулённый Kotlin-вход превращается в ненулевой PCM перед Opus.
Компонентные контроли PASS, прежний mute ожидаемо FAIL,allocations0->0.
По следующему «ОК» внедрена передача raw PCM+mute через JNI в существующий core API.
Новая регрессия5/5, voice/desktop30/30, Android timing/trace14/14,
assembleDebug для двух ABI и check203/203 PASS,proxy9/9,burst1/1,load8/8,168с.
Диагностика/план — раздел13, внедрение/актуальная проверка — раздел14.
База проверки: f79b9261, чистое рабочее дерево после предыдущего push stream.
1. Подтверждённый дефект: call_start не проверяет обычную занятость
call_busy возвращает1 для любой сессии state!=ENDED. Входящий INVITE проверяет
этот же признак других сессий и отвечает BUSY. call_start проверяет только
radio_call_busy, затем создаёт новую call_session без проверки обычного звонка.
В результате один instance может иметь несколько незавершённых обычных сессий.
Точки кода на базе проверки:
- call.c:625–640: входящий INVITE отклоняется при другом незавершённом звонке;
- call.c:960–965: call_busy учитывает все состояния, кроме ENDED;
- call.c:1064–1137: call_start не использует call_busy, создаёт сессию/CM,
отправляет INVITE и запускает таймеры независимо от уже идущего звонка;
- chat_headless_control.c:750–763: headless-команда напрямую вызывает call_start,
без дополнительной проверки call_busy. Ограничение только в UI недостаточно.
Реестр может содержать завершённые записи для END_ACK/таймаута и новый звонок;
это штатно и не означает поддержку нескольких одновременно незавершённых.
call_audio_start_impl использует один g_call_id/codec/jitter-контур; start другого
ID освобождает прежний. Аппаратное воспроизведение/переключение в этом аудите
не запускалось, фактическая потеря звука не заявляется.
2. Изолированное воспроизведение на настоящих ядрах
/tmp/call-audit-busy.c использует существующий fixture tests/test_call.c:
два UTUN_INSTANCE на одном UASYNC, свои временные конфиги/ключи/SQLite,
настоящие CHAT/BGP/ETCP/router/CM. Сторонних узлов и пользовательской БД нет.
После штатного получения нужного состояния вызывается публичный call_start
с другим call_id для того же пира. Реестр и счётчики читаются в uasync.
Ни состояние первой сессии, ни результат call_busy вручную не подменяются.
У каждого сценария отдельный процесс. Перед вторым call_start:
call_busy=1, число незавершённых=1. Ожидается отказ без второй сессии/CM,
без изменения первой сессии и c_calls_started. Фактически:
Сценарий Первая сессия live Новый CM c_calls_started
outgoing caller OUTGOING, state1 1->2 да 1->2
ringing caller RINGING, state2 1->2 да 1->2
active обе стороны приняли, ACTIVE3 1->2 да 1->2
incoming callee RINGING, state2 1->2 да 0->1
wait_decision caller WAIT_DECISION, state5 1->2 да 1->2
Во всех пяти первая сессия/её состояние сохранены, вторая имеет OUTGOING1.
Это две незавершённые сессии, а не доказательство двух ACTIVE-разговоров:
стенд останавливается после проверки локального допуска, до обработки
второго INVITE другой стороной. Два CM handles не означают два физических ETCP.
Все5 отрицательных проверок завершаются exit1; после destroy/poll/uasync_destroy
счётчик tracked allocations возвращается0->0. Утечка этим тестом не обнаружена.
Диагностика: глобальный error, категория call=debug. В логах видны оба
call_start, ID/peer/group, отдельный CM, исходное состояние, live1->2,
счётчики и cleanup. Все логи пяти сценариев прочитаны полностью.
Пример ACTIVE: busy=1 live=1->2 original_state=3 preserved=1 second_created=1
second_state=1 second_CM=1 started=1->2 expected_live=1 result=FAIL.
Сборка через штатный Makefile/LIBUTUN/COMMON_LIBS, без ручного списка src/*.c:
make -C tests -f Makefile -f /tmp/call-audit-busy.mk /tmp/call-audit-busy
PASS: /tmp/call-audit-busy-build.log.
Логи /tmp/call-audit-busy-{outgoing,ringing,active,incoming,wait_decision}.log.
3. Предлагаемое исправление и проверки
Единая точка допуска — call_start в uasync. До создания сессии, CM, отправки
INVITE и публикации занятости проверять call_busy. Существующий звонок не
завершать и не заменять. Для нового ID при занятости использовать существующее
событие ENDED/BUSY, как при pending radio invitation, с понятным INFO-логом
нового/занятого ID, состояния и peer/group. Новые состояния/таймеры/wire не нужны.
До BUSY-события отдельно обработать уже зарегистрированный out_call_id:
повтор/коллизия не должны породить ENDED с ID действующего звонка или вставить
дубликат ключа реестра. Такой вызов безопасно отклонять с WARN без изменения
существующего звонка. Для новой попытки caller выбирает ненулевой уникальный ID.
Это требование к корректности будущего исправления; отдельная runtime-проверка
коллизии ID в текущем аудите не выполнялась.
Постоянная регрессия должна проверить:
- OUTGOING, caller/callee RINGING, ACTIVE и обе стороны WAIT_DECISION;
- второй вызов тому же/другому пиру и из другой группы/DM, защита на уровне instance;
- отсутствие второй session/CM/INVITE, новых started и изменений исходного звонка;
- BUSY ровно для нового ID, повтор существующего ID без ложного ENDED исходного;
- новый уникальный ID разрешён после ENDED, даже пока живёт старый tombstone;
- сохранение правил pending radio invitation, phone-policy и нормального start;
- исходный call/CM/аудиоприём продолжаются, cleanup без tracked leaks.
После исправления: сборка, test_call/test_call_headless, тесты голосового
стека соответствующей C++ сборки и общий check.sh. Затем продолжить аудит
call_audio/jitter/delay/timeline/tones/headless, а после них — видео.
4. Полнота аудита и текущие проверки
Полностью прочитаны call.c1230 строк, call.h134, call_priv.h43, call_proto.h111,
call_audio.h105 и voice_sources.cmake. Размеры заголовков указаны до правок.
В call_audio.c прочитана полная call_audio_start_impl141–258, остальной файл
целиком ещё не проверен. doc/chat_call_arch.md прочитан до строки320 из465.
Из теста call использован штатный fixture и просмотрены относящиеся к нему
setup/события/сценарии; полноценный аудит всех тестовых/video-функций не заявляется.
call.h, call_audio.h, call_priv.h отмечены[~]; прежняя отметка call_proto.h[~]
сохранена. Итого0[V]/4[~]/17[ ] для21 заголовка src/call. Остальные голосовые
заголовки пока[ ], видео по просьбе пользователя ещё не проверялось.
doc/chat_call_arch.md[~], doc/call_video.txt[ ] сохранены. В call.h и doc
добавлено предупреждение о подтверждённом дефекте, задача записана в tasks.md.
Штатный tests/test_call PASS:2/2,0 failed, exit0.
Лог /tmp/call-audit-baseline-test-call.log прочитан полностью; ERROR-сообщения
относятся к ожидаемым отрицательным media/video-проверкам самого теста.
Прохождение штатного набора не закрывает выявленный пробел допуска.
Новая диагностическая программа собрана PASS, пять отрицательных проверок
FAIL ожидаемо. C/C++ реализации и штатные тесты проекта не менялись.
Полная сборка/check.sh, sanitizers, отдельный C++ audio/GUI и Android runtime
в этом продолжении не запускались; прежние результаты stream сюда не переносятся.
Неизменность C-токенов всех21 call-заголовков относительно начала аудита,
отсутствие правок C/C++ реализаций/lib/tests от f79b9261, diff --check и
итоговые отметки чек-листа PASS: /tmp/call-audit-contract-check.log.
5. Исправление допуска и штатная регрессия — 26.10.2026
call_start до создания session, CM и INVITE проверяет:
- ненулевые peer/id и остальные обязательные аргументы, с ERROR контекста;
- уже зарегистрированный ID, включая ENDED: WARN без событий/изменений записи;
- любую обычную сессию state!=ENDED: ENDED/BUSY только для нового ID, INFO
с requested/busy ID, исходным ch, peer и состоянием занятого звонка;
- прежнюю pending radio invitation — по существующему правилу.
Для проверки обычной занятости используется тот же предикат, что в call_busy
и входящем INVITE. Один проход даёт также запись/состояние для точного лога;
дополнительного boolean-прохода и нового API поиска не добавлено. Всё выполняется
в одном uasync-потоке до первого изменения состояния, межпоточной гонки допуска нет.
Завершённые записи с таймерами END_ACK не блокируют новый уникальный ID.
Не изменены wire, таймеры/состояния, входящая политика и аудиопроцессинг.
Добавлен tests/test_call_admission.c и цель Autotools, включённая в общий check.
Fixture test_call используется в отдельном процессе: два реальных ядра,
CHAT/Merkle/BGP/ETCP/router/CM и DM. Для проверки рации дополнены настоящие
join/update/tree подписи участников через member_sync, без прямого изменения БД.
Тест получает все состояния штатным сигналингом, вручную их не присваивает.
Observers --wrap=conn_mgr_open/etcp_route_send считают реальные вызовы и передают
их исходным функциям; только END_ACK намеренно удерживается для проверки ENDED.
Покрыты caller OUTGOING и обе роли RINGING/ACTIVE/WAIT_DECISION (7 сочетаний):
повтор собственного ID, коллизия того же ID с другим tuple, новый ID через CHAT/DM
и другой ch/peer до проверки его доступности. Вторую существующую CHAT-группу
и третье физическое ядро тест не создаёт: проверяется общий допуск instance
до поиска группы/пира, а не маршрутизация к этим дополнительным назначениям.
Во всех отказах проверяются отсутствие новых session/CM/INVITE и started,
сохранение исходного состояния/роли/CM/очереди/таймеров/seq. BUSY приходит ровно
для нового ID; дубликат не даёт событий. Дополнительно проверены:
- нулевые peer/id без создания записей и ложных событий;
- двусторонние закодированные Opus-кадры после всех ACTIVE-отказов через реальный
router, прежний ETCP остаётся UP; это не запуск микрофона/PCM-вывода;
- повтор старого ID после ENDED без ложного события; новый звонок при ещё живом
старом tombstone, с последующим PHONE_BUSY/CONTINUE той же попытки;
- рация отклоняется при обычном звонке, обычный звонок — при pending radio invitation;
- нормальный новый звонок/decline после завершения приглашения и полная очистка.
Финальная регрессия:547 assertions,22 BUSY,18 отказов без события,
audio_RX1/1, allocations0->0, exit0. Лог /tmp/call-admission-fix-regression.log.
Сборка ./build.sh --no-gui -j4 PASS: /tmp/call-admission-fix-build.log;
после уточнения fixture целевая сборка PASS: /tmp/call-admission-regression-build.log.
Первый прогон остановился на рации: старый fixture не имел tree-подписей;
radio=info показал rc_group invalid member и UNAVAILABLE. Исправлен только
fixture, требования проверки членства в production не ослаблялись.
Первоначальный диагностический стенд пересобран с исправленной штатной libutun:
те же5 сценариев теперь PASS, exit0, live1->1, second_created0/CM0,
исходная сессия сохранена, started не меняется, allocations0->0.
Логи /tmp/call-admission-original-{outgoing,ringing,active,incoming,wait_decision}.log,
сборка /tmp/call-admission-original-build.log. Логи прочитаны полностью.
Отдельная штатная CMake-сборка /tmp/utun4-call-admission-gui:
test_call_jitter, test_call_delay, test_call_timeline и test_voice_tx4/4 PASS.
Это реальные Opus/SoundTouch/voice-компоненты с существующими тестовыми
обвязками, без аппаратного GUI/Android-звонка. Configure/build:
/tmp/call-admission-gui-configure.log, /tmp/call-admission-gui-build.log;
ctest: /tmp/call-admission-voice-tests.log (3,01с).
Полный check.sh PASS:202 passed,0 failed,0 skipped, включая новый admission,
test_call/test_call_headless и radio/radio_headless; proxy9/9,burst PASS,
load8/8,134217728 байт,полное время174с. Лог /tmp/call-admission-full-check.log.
Прогон от user в отдельном IPv6 network namespace: /tmp/call-admission-netns.log.
Настройки сети хоста сохранены. ASAN/UBSan/LSan, GUI/Android на устройстве
и реальный аппаратный звонок в этом исправлении не запускались.
Исправление285a60bf; тестcfc8d801 и уточнение fixture8eb3d102.
Полнота аудита пока прежняя:0[V]/4[~]/17[ ], архитектурный документ[~].
Итоговая проверка /tmp/call-admission-final-contract.log PASS: C-токены21/21
call-заголовков не менялись (API/ABI/wire прежние), source/lib/tests идентичны
проверенному8eb3d102, diff --check и оформление новых файлов в пределах150.
Отметки call и прежние stream18[V] сохранены. Основной namespace прежний,
all/default/lo.disable_ipv6=1. После runtime менялись только отчёты/задачи.
6. Аудиостек: устаревшие задачи уменьшают pending новой привязки — 26.10.2026
Полностью прочитаны call_audio.c652, call_jitter.cpp265, call_delay.cpp240,
call_timeline.cpp174 и call_tones.c165 (всего1496 строк), а также соответствующие
заголовки, call_clock.h и inline call_jitter_window.h. Уточнения контрактов ещё
не завершены: найдено расхождение учёта TX при повторной привязке к instance.
call_audio_init (строка110) задаёт g_inst=inst и g_tx_pending=0. У старых
posted tasks при этом нет отдельного поколения привязки. call_audio_send
(строка82) уменьшает g_tx_pending, даже когда tx->generation уже не совпадает
с g_tx_generation и отправка старого кадра запрещена. После init это уже
счётчик новых задач: он может стать0 при восьми ещё не исполненных новых кадрах.
Следующие восемь feed принимаются, хотя лимит должен остановить их.
Генерация звонка решает другую задачу: start/release меняют g_tx_generation,
поэтому старый Opus не отправляется. Она не является эпохой привязки init/destroy.
При обычной смене звонка без reinit старые задачи по-прежнему занимают pending,
и их исполнение должно уменьшать счётчик. Простое условие current перед
декрементом оставило бы эти слоты навсегда занятыми — это неверное исправление.
Детерминированное воспроизведение
/tmp/call-audit-tx-pending.cpp: минимальный UTUN_INSTANCE с настоящим UASYNC,
реальные call_audio/Opus/CallDelay/SoundTouch/tones из штатной CMake-библиотеки.
uasync и порядок posted callbacks настоящие; состояние/счётчики call_audio не
читаются и не подменяются. Нет сетевого звонка, микрофона или пользовательской БД.
Сетевую границу call_send_media заменяет наблюдатель числа кадров/ID;
chat_setting отключает compressor/AEC, get_time_tb фиксирован, чтобы исключить TTL.
reinit:
1) init/start_prepared41, принять8 PCM-кадров; их posted tasks пока не выполнять.
2) Поставить собственный uasync callback extra после этих8 задач.
3) stop/destroy/init того же instance/uasync, start_prepared42, принять8 новых кадров.
4) uasync исполняет8 старых задач: отправка запрещена, но pending новой привязки
уменьшается8->0. Штатный DEBUG-снимок показывает pending0 при восьми ожидающих новых кадрах.
5) extra пытается принять ещё8 кадров нового звонка, пока первые8 не исполнены:
принято8, ожидалось0. Одновременно ожидают исполнения16 кадров звонка42.
6) Два прохода uasync исполняют также задачи, опубликованные из extra:
sent_current16, sent_old0, allocations0->0; отрицательная проверка exit1.
Контроль fresh: extra поставлен перед8 кадрами нового звонка, старых задач нет.
pending8, extra_accepted0, sent_current8, sent_old0, allocations0->0, exit0.
Контроль same_owner: stop/start без destroy/init. Старые8 занимают лимит,
initial_new0, после их исполнения extra принимает8; sent_current8, old0,
allocations0->0, exit0. Это подтверждает необходимость различать привязку и звонок.
Сборка через готовые штатные CMake flags/utun_voice/utun/uasync/Opus,
без ручной компиляции C-реализаций; /tmp/call-audit-tx-pending.mk.
Build PASS: /tmp/call-audit-tx-pending-build.log.
Логи /tmp/call-audit-tx-pending-{fresh,same_owner,reinit}.log прочитаны целиком;
call=debug показывает pending, число posts и причины отказов. Первый вариант
стенда выполнял один poll и не исполнял задачи из extra до проверки доставки;
исправлен только стенд (два poll), после чего fresh/same_owner PASS, reinit FAIL.
Предлагаемое исправление
Использовать уже применённый в radio_audio шаблон: отдельная монотонная эпоха
привязки instance, сохранённая в каждой TX-задаче, помимо генерации звонка.
Менять её при фактическом init/destroy; повтор init того же живого владельца
оставлять идемпотентным. Обнуление pending относится к новой привязке.
В send вычислять owner по эпохе привязки и inst; уменьшать pending только
для своего owner, включая задачи старого звонка того же owner. Отправка
дополнительно требует текущего звонка/состояния/возраста. Статистика старой
привязки не должна попадать в новую. Radio-код прочитан в этих точках;
менять его или формат posted_task/uasync для этого исправления не требуется.
Предел8 относится к активной привязке; старые неотправляемые задачи могут
доживать в прежнем uasync до callback/destroy. Число физических старых+новых
аллокаций при нескольких последовательных reinit — отдельное свойство,
данный стенд не доказывает общего лимита8 для всех исторических привязок.
Постоянная проверка должна сохранить все три сценария, добавить разные
instance/uasync и повторное использование адреса instance, удаление старых
задач при uasync_destroy без callback, повтор init и обычную смену звонка.
Нельзя перенести старое уменьшение счётчика или просроченную статистику в новый owner.
Затем выполнить C++ voice-профиль и общий check; GUI/Android lifecycle проверять отдельно.
Существующий профиль call_jitter/delay/timeline/voice_tx повторён:4/4 PASS,
3,01с; /tmp/call-audit-voice-baseline.log. Он новый сценарий reinit не покрывает.
Desktop runLoop штатно снимает call_audio при остановке uasync; текущий GUI
не воспроизводился, сбой реального звонка не заявляется. Android/GUI и
ASAN/UBSan/LSan в этом продолжении не запускались. Полный check202 из раздела5
относится к прежнему исправлению допуска, а не к этому расхождению.
Аудит остановлен по AGENTS.md:10. Production C/C++ и штатные тесты не менялись;
добавлены предупреждение call_audio.h/статус doc и задача. Заголовки clock,
delay,jitter,jitter_window,timeline,tones теперь[~], без преждевременного[V].
Итог0[V]/10[~]/11[ ]; headless и видео ещё не проверялись. Архитектурный документ[~].
Проверка /tmp/call-audit-audio-contract-check.log PASS: C-токены21/21 заголовков
не менялись, production C/C++/lib/tests идентичны ab3d582a, diff --check чистый;
отметки call0/10/11 и прежние stream18[V] проверены. Исправление допуска из раздела5 сохранено.
7. Проверка корректности плана TX ownership — 26.10.2026
Вывод: отдельная эпоха привязки корректно решает выявленную ошибку. Уточнены
три обязательных условия: owner ограждает не только pending, но и отправку/
позднюю TX-статистику; старый звонок того же owner продолжает освобождать
слоты; предел8 относится к активной привязке, а не к сумме всех исторических tasks.
Это проверка плана, production C/C++ и штатные тесты проекта не менялись.
Временный минимальный прототип
/tmp/call-audio-owner-prototype.c — копия нынешнего call_audio.c с изменениями:
- static g_instance_generation и поле instance_generation в call_audio_tx;
- init: увеличить поколение только при фактической новой привязке, обнулить pending;
повтор init того же владельца сохраняет поколение/счётчик;
- destroy: инвалидировать поколение действующего владельца;
- feed: сохранить поколение привязки под тем же g_mtx, что увеличение pending;
- callback: owner=(поколение совпало && g_inst==tx->inst),
current=owner && совпало поколение звонка; decrement только owner,
запись post-time/age-drop и отправка только current с прежними active/ending/ID/TTL.
Все новые поля внутренние; wire, API/ABI заголовков, codec, PCM и таймеры не меняются.
Прототип не добавляет сброс DSP/часов/всех накопленных диагностик при init.
Изоляция статистики означает запрет позднему TX-callback дописывать post-time
и age-drop другой привязке; существующие правила start/get_stats сохраняются.
init/destroy и освобождение instance остаются сериализованным lifecycle;
эпоха не заменяет остановку producers и правила владения uasync.
Прочитаны полные process_posted_tasks, post_reserved/cancel_post и destroy:
uasync снимает текущую пачку tasks под mutex, вызывает callbacks без него,
затем u_free(task). При destroy ожидающие tasks удаляются без callback.
call_audio_tx содержит posted_task первым полем, payload в том же calloc-блоке;
добавление uint64_t не требует cleanup callback, отдельного owner-объекта,
отмены списка задач или правок uasync. Новая привязка обнуляет свой счётчик,
поэтому отсутствие callback удалённой старой задачи не блокирует новую очередь.
Прототип скомпилирован отдельно штатными CMake C-флагами voice; объект подставлен
перед libutun_voice.a без замены архивов/объектов проекта. Временные файлы:
/tmp/call-audio-owner-prototype.c/.o/.mk;
build /tmp/call-audio-owner-prototype-build.log и /tmp/call-audio-owner-plan-build.log.
diff с исходником проверен: только перечисленные ownership-поля/условия.
Сравнение на одинаковых стендах
Сценарий Исходный код Прототип Проверяемое свойство
fresh PASS PASS Лимит8 без старых задач
same_owner PASS PASS Смена звонка без reinit, старые слоты освобождаются
reinit FAIL PASS Старые callbacks не уменьшают новые8
idempotent PASS PASS Повтор init не открывает ещё8 слотов
different_instance FAIL PASS Новый pointer owner, поздний destroy старого
reused_address FAIL PASS Тот же адрес instance после переинициализации
detached_drain PASS PASS Старые задачи исполняются между destroy и init
discarded_loop PASS PASS Старый uasync уничтожен без исполнения задач
cross_loop FAIL PASS Поздние callbacks другого uasync после нового init
mixed_calls PASS PASS Четыре старые+четыре новые задачи одного owner
current_expiry PASS PASS Текущие задачи старше200мс освобождают лимит
rebind_storm FAIL PASS 16 reinit до poll, изоляция последней привязки
detached_engine FAIL PASS Encoder сохранён при detach/attach, поколение звонка то же
Итого исходный код7/13, шесть отрицательных проверок FAIL; прототип13/13 PASS.
Везде tracked allocations возвращаются к исходному значению, старый Opus
в прототипе не отправляется. cross_loop использует отдельный поток для каждого
uasync и явную синхронизацию: правило одного uasync на поток соблюдено.
Тесты наблюдают TX-границу и используют реальные Opus/uasync; полноценное
снятие audio_ops двух настоящих call_ctx, GUI и аппаратный захват не проверялись.
detached_engine дополнительно подтвердил необходимость owner в current:
исходный код отправил8 кадров прежней привязки и записал posts8 после reinit;
прототип не отправил их и не записал эту позднюю статистику. В этом сценарии
destroy лишь снимает регистрацию, engine освобождается позднее через stop.
Это не новое правило управления устройствами и не предложение сохранять
аппаратный звонок между перезапусками ядра.
Сводка /tmp/call-audio-owner-plan-results.log проверяет все результаты,
баланс аллокаций и отсутствие неожиданных ERROR стендов. Подробные логи:
/tmp/call-audio-owner-prototype-{fresh,same_owner,reinit}.log,
/tmp/call-audio-owner-plan-{baseline,prototype}-<scenario>.log.
Расширенный стенд /tmp/call-audio-owner-plan-check.cpp; состояния production
напрямую не читаются/не подменяются, все переключения — через public API.
Штатные C++ test_call_jitter/delay/timeline/voice_tx перелинкованы с тем же
временным объектом:4/4 PASS, exit0. Их собственные CHECK/REQUIRE активны
независимо от NDEBUG. Build /tmp/call-audio-owner-voice-build.log;
логи /tmp/call-audio-owner-test_call_{jitter,delay,timeline}.log и
/tmp/call-audio-owner-test_voice_tx.log. WARN/ERROR отрицательных проверок
clock/формата и AEC в этих тестах ожидаемы; runtime assertions пройдены.
Уточнённый план внедрения
1) Перенести только проверенные ownership-поля/условия в call_audio.c,
добавить редкие логи init/destroy с эпохой и TRACE решения старой задачи.
2) Добавить постоянные регрессии в штатную C++ voice-сборку: исходные три
сценария, повтор init, смена pointer/адреса/ua, удаление без callback,
mixed/current-expiry и изоляция отправки/поздней статистики.
3) Сохранить external lifecycle: один активный аудиоконтур, остановка producers
перед освобождением, init/destroy не гоняются между собой. Изменять
audio_ops/приём/сброс AEC и DSP в рамках этого исправления не требуется.
4) Сборка, C++ voice-профиль и полный check.sh; аппаратный GUI/Android lifecycle
остаётся отдельной проверкой. Новый полный check/санитайзеры здесь не запускались.
Отдельный общий предел8 физических аллокаций всех старых привязок план не обещает:
rebind_storm удерживает128 старых задач до poll/destroy и только8 действующих.
После drain всё освобождено. Это осознанная граница исправления учёта активного owner.
После внедрения нужен штатный regression, временные13/13 не заменяют его.
8. Внедрение TX ownership и постоянные регрессии — 26.10.2026
Проверенный план перенесён в call_audio.c: отдельная g_instance_generation,
снимок эпохи в embedded task, owner по эпохе+inst. Только owner уменьшает pending;
current=owner&&call-generation ограждает отправку и поздние post-time/age-drop.
Старый звонок того же owner освобождает слоты. init того же inst идемпотентен,
фактический init/destroy инвалидирует прежние задачи. uasync не менялся.
Заголовок описывает эти границы; C-токены API идентичны исходному заголовку.
Codec/DSP, приём, wire и внешние правила остановки producers не менялись.
Логи call=info показывают inst/epoch/pending при init/destroy, call=trace —
обе эпохи задачи/текущего звонка и решение owner; underflow логируется ERROR.
Основное исправление821b71e8; постоянные тестыda5817cf, уточнение проверок07aa7aa2.
Штатный tools/chatgui/tests/test_call_audio_tx.cpp зарегистрирован в CMake
для UNIX без Apple: GNU --wrap на сетевую границу, настройки и монотонный TB.
Каждый из13 сценариев — отдельный CTest-процесс с timeout15с; реальные
Opus/CallDelay/SoundTouch/uasync, без приватных полей/подмены состояния движка.
Инстансы минимальные: не создаются настоящие call_ctx/сеть/микрофон.
cross_loop использует отдельный поток для каждого uasync, явные promise/join;
init/destroy и захват сериализованы, одновременно работает один аудиодвижок.
REQUIRE вызывает abort независимо от NDEBUG; Release действительно проверяет условия.
Проверки: fresh,same_owner,reinit,idempotent,different_instance,reused_address,
detached_drain,discarded_loop,cross_loop,mixed_calls,current_expiry,
rebind_storm,detached_engine. Всегда sent_new8/sent_old0 и allocations0->0.
После drain принимаются ровно8 новых задач, девятая отклоняется; финальный
хвост освобождается через uasync_destroy без callbacks. mixed_calls проверяет
4 старые+4 новые задачи, затем освобождённые4 слота; old-call не блокирует очередь.
detached_engine сохраняет encoder/call generation при перепривязке: старые
свежие и просроченные tasks не отправляются и не добавляют post-time/age-drop.
current_expiry, напротив, проверяет posts8/age-drop8 действующей привязки.
Нативная сборка ./build.sh --no-gui -j4 PASS: /tmp/call-audio-tx-build.log.
CMake configure/build PASS: /tmp/call-audio-tx-voice-{configure,build}.log.
Профиль13 новых+4 прежних голосовых тестов17/17 PASS,3,45с:
/tmp/call-audio-tx-voice-tests.log. После уточнения проверок повторены13/13,
0,42с: /tmp/call-audio-tx-regression-{build,tests}.log.
Все13 логов прочитаны/проверены: /tmp/call-audio-tx-<scenario>.log;
сводка /tmp/call-audio-tx-diagnostics-review.log проверяет все строки,
owner/эпохи/pending, отсутствие ERROR и неожиданных WARN, баланс аллокаций.
rebind_storm:128 старых callbacks сохраняют pending8 активной привязки.
same_owner:8 старых callbacks с owner1 уменьшают pending7..0; mixed —7..4.
До пересборки сохранён прежний libutun_voice.a, новый test object отдельно
перелинкован с ним в /tmp/call-audio-tx-baseline; архивы проекта не подменялись.
Те же13 сценариев:7 PASS,6 ожидаемых FAIL (reinit,different_instance,
reused_address,cross_loop,rebind_storm,detached_engine). Первые пять принимают
лишние8 задач, detached_engine отправляет8 старых кадров. Это проверяемые
последствия, не только сравнение новых диагностических строк.
Логи /tmp/call-audio-tx-baseline-{build,results,<scenario>}.log.
Первый полный check:201 passed/1 failed/0 skipped из202. Единственный FAIL —
test_radio_mesh: все кадры/FIN/порядок получены, но dupC=0. Изменённый call_audio
не линкуется в этот бинарник (nm); radio/uasync не менялись. Одиночный запуск
того же бинарника с UTUN_TEST_DEBUG=1 PASS, dup drop seq0..5, итоговый C dup6.
Условие завершения P_WAIT_RX1 не ждёт дубликатов; это наблюдение по коду,
точная причина нестабильности не установлена. Создана отдельная открытая задача.
Логи /tmp/call-audio-tx-full-check-first.log,
/tmp/call-audio-tx-radio-mesh-{first,debug}.log. Повторный полный прогон также
дал201 passed/1 failed/0 skipped из202 с тем же test_radio_mesh/dupC=0;
/tmp/call-audio-tx-full-check.log, /tmp/call-audio-tx-netns-repeat.log,
/tmp/call-audio-tx-radio-mesh-repeat.log. Зелёный полный check не заявляется.
Повторы общего check остановлены; причина отдельного отказа требует своего аудита.
Так как check.sh при этом не дошёл до proxy-интеграций, они выполнены отдельно
в таком же изолированном IPv6 namespace: check-proxy9/9 PASS, check-burst1/1
PASS (8388608 байт), check-load8/8 PASS (134217728 байт); общий exit0.
Одиночный test_radio_mesh с диагностикой в этом namespace также PASS:
окружение само по себе отказ не воспроизвело, причина не объявляется установленной.
Логи /tmp/call-audio-tx-integrations.log, /tmp/call-audio-tx-check-{proxy,burst,load}.log,
/tmp/call-audio-tx-radio-mesh-netns.log. Сетевые настройки хоста не менялись.
Полный check запускается в отдельном sudo unshare --net: IPv6/lo включён
только там, запуск от user; IPv6 хоста остаётся отключён. Аппаратный GUI/Android,
ASAN/UBSan/LSan не запускались. Аудит headless/платформ/видео ещё не завершён;
call остаётся0[V]/10[~]/11[ ], архитектурный документ[~]. Push не выполнялся.
9. Проверка test_radio_mesh: место дедупликации зависит от порядка прихода — 26.10.2026
Вывод: dupC>0 не является инвариантом diamond. Дедупликация успешно срабатывает
на другом узле, а тест выдаёт ложный FAIL. Подтверждено наблюдением настоящих
raw ETCP отправок/приёма до radio_recv; production radio/uasync не менялись.
В tests/test_radio_mesh.c добавлены диагностические снимки фаз/узлов/рёбер,
а в Unix-сборке --wrap=etcp_bind/etcp_send. Наблюдатели вызывают реальные функции,
не меняют payload, результат, порядок или владение entry. DEBUG_CATEGORY_DEBUG
явно включена на INFO; события показывают self/peer/src/group/stream/seq/FIN/hops.
Счётчики TX означают переданные etcp_send задачи, RX — вход в raw callback.
Возврат к радио-обработчику и дедупликация остаются настоящими.
Опция UTUN_TEST_MESH_TAIL оставляет прежний loop работающим ещё500мс после
исходного P_DONE. Снимок dupC до хвоста сохраняется для итогового вердикта,
поэтому диагностический хвост не превращает старый FAIL в PASS. Обычный режим
сохраняет прежнее чтение счётчика после выхода из uasync_mainloop.
Таймер хвоста исполняется в работающем loop, без sleep/повторного запуска
остановленного mainloop; флаг stop автоматически не сбрасывается.
Нативная сборка ./build.sh --no-gui -j4 и цель make -C tests test_radio_mesh PASS:
/tmp/radio-mesh-audit-build.log, /tmp/radio-mesh-audit-target-build.log.
Запущен один обычный тест и16 с хвостом, по8 параллельно. Итог16 PASS/1 FAIL.
Сводка /tmp/radio-mesh-audit-runs.log, подробные ...-run-00.log..16.log.
Проверка всех логов /tmp/radio-mesh-audit-evidence.log сверяет кадры/FIN/seq,
порядок raw RX, счётчики рёбер и отсутствие runtime ERROR/неожиданных WARN.
Полный разбор воспроизведённого FAIL: /tmp/radio-mesh-audit-run-01.log.
1) В14:54:49.182 A отправляет seq0..5 обоим B и D; seq5 — FIN.
2) D принимает их и пересылает C. В14:54:49.183 C пересылает их B.
3) B принимает все6 от C в14:54:49.183607..183631, впервые видит ключи
(src=A,stream=64106,seq0..5), доставляет локально5 кадров и FIN.
4) Прямые A→B приходят лишь в14:54:49.185416..185444. radio_seen_dup
возвращает1, radio_recv увеличивает dupB и выходит ДО дальнейшего flood.
Поэтому B→C TX0, C←B RX0, dupB6 и dupC0.
5) В P_DONE все B/D/C имеют frames5/FIN1/seq_ok1. Через500мс показатели
остаются теми же: это не доказанный случай раннего завершения ожидания C.
Код согласован с логом: radio_seen_dup ведёт общий для узла кэш src/stream/seq,
независимо от соседа/хопов. Первый путь выигрывает; последующий дубликат
отбрасывается до radio_flood. Оба A→B и A→D существовали и передали пакет;
второй путь до C не обязан завершиться, если пути уже сошлись раньше на B.
Нормальный запуск run00, напротив: B получает A раньше, отправляет C;
C получает обе копии и имеет dupC6. Порядок UDP/dispatch не задан контрактом.
Предлагаемое исправление теста: ждать полную доставку/FIN и проверять
дедупликацию на допустимом узле B/C/D; закрепить оба порядка прихода.
Не добавлять фиксированную задержку и не ждать только dupC: хвост показал,
что C вообще не получит вторую копию в корректном варианте обхода.
Критерий и production пока не исправлялись; задача остаётся открытой.
Новый общий check был запущен с диагностическим тестом, но по запросу
пользователя pull/rebase корректно прерван SIGTERM runner до изменения дерева.
Процессы тестов остановлены; /tmp/radio-mesh-audit-full-check.log и ...-netns.log
содержат interrupted143, поэтому полным результатом не считаются.
В этом общем прогоне сам test_radio_mesh прошёл; tests/logs/test_radio_mesh.log.
Санитайзеры и аппаратный GUI/Android в этой проверке не запускались.
git pull --rebase успешно выполнен на origin/master aea8ca72: перенесены все23
локальных коммита. Конфликты были только в doc/tasks.md; записи feedback/member
сохранены побайтно, старые call-задачи обновлены до их итоговых состояний.
Исходники локальных изменений сохранены; tests/Makefile.am содержит также новые
upstream member_purge sources. Рабочее дерево после rebase чистое.
Сам radio.c и диагностический тест побайтно совпадают с проверенными до rebase;
новая объединённая версия целиком ещё не собиралась/не тестировалась.
Исторические ID коммитов разделов1–8 относятся к состоянию до rebase;
диагностика после него: c2908a6c/d943c0dd/21af2e0f/ff394c7a. Push не выполнялся.
10. Исправление критерия mesh и проверка объединённой версии — 26.10.2026
По запросу пользователя исправлен tests/test_radio_mesh.c; production radio,
uasync и протокол не менялись. Коммиты1bc27aa2/bf4d14c4.
Теперь каждый burst требует ровно5 кадров по порядку и один корректный FIN
на B/D/C, отсутствие локального эха A и суммарно не менее6 удалённых дублей
на B/D/C. Счётчики берутся относительно начала burst; прежний сценарий
не может дать ложное подтверждение следующему. Дополнительно проверяются
src/stream, FIN после всех кадров, повтор FIN и data после FIN.
Unix-wrapper считает отправки и входы в raw RX: завершение разрешено только
после совпадения TX[i][j] и RX[j][i] для всех рёбер и отсутствия удержанных
задач. Радио-forward из принятого пакета уже завершён к снимку на следующем
таймере. Поэтому отложенная копия тоже должна пройти настоящий radio_recv;
успех не объявляется между первыми данными и ещё ожидающим дубликатом.
Три последовательных burst на тех же4 настоящих ядрах и одном UASYNC:
0) обычный порядок доставки;
1) gate держит6 прямых A→B entry до получения B всех5 кадров+FIN через C;
затем передаёт entry реальному etcp_send. Обязательны dupB6/dupC0,
отсутствие B→C TX и C←B RX;
2) симметричный gate для A→D: обязательны dupD6/dupC0 и отсутствие D→C TX.
Payload, stream/seq и настоящий кэш radio_recv не подменяются. Возвращая0,
gate принимает владение entry; при release передаёт его real ETCP, при
незавершённом сценарии освобождает оставшийся хвост до destroy инстансов.
В исходящем пути после real etcp_send entry больше не читается.
Диагностический хвост UTUN_TEST_MESH_TAIL удалён; фиксированных задержек
для получения нужного порядка нет. Обычный clock/timer остаётся прежним.
Пакетные сообщения переведены в TRACE, INFO сохраняет фазы/итоги/gate/cleanup.
DEBUG_CATEGORY_DEBUG явно включена на INFO; UTUN_TEST_DEBUG включает её TRACE.
На платформе без Unix-wrapper исполняется обычный burst с общим критерием
дедупликации. Сборка/запуск этой ветки проверены на Linux с -UUTUN_TEST_MESH_OBSERVE;
это не проверка Windows или FreeBSD.
После rebase origin/master aea8ca72 выполнена нативная сборка
./build.sh --no-gui -j4 PASS и make -C tests test_radio_mesh PASS:
/tmp/radio-mesh-fix-build.log, /tmp/radio-mesh-fix-target-build.log.
Первый тест PASS, все3 сценария, tracked allocations0->0:
/tmp/radio-mesh-fix-test.log. Ещё16 процессов, по8 параллельно, каждый
с3 сценариями:16/16 PASS, всегда dupB6/dupD0/dupC0 в сценарии1 и
dupB0/dupD6/dupC0 в сценарии2, allocations0->0. Это48 дополнительных burst;
/tmp/radio-mesh-fix-repeat-00.log..15.log и ...-repeat-results.log.
Проверена чувствительность условий на временных копиях вне проекта:
- возврат старого условия dupC>0 даёт ожидаемый FAIL в сценарии1 после
настоящей обходной доставки на B; cleanup allocations0->0;
- искусственная лишняя локальная доставка даёт frames6 и ожидаемый FAIL
по FIN/числу/порядку; cleanup allocations0->0;
- ветка без Unix-wrapper компилируется и проходит обычный burst.
Ни production, ни архивы/объекты проекта для этого не заменялись.
Сборка использует переменные штатного tests/Makefile через временный overlay;
/tmp/radio-mesh-fix-negative.mk, ...-negative-build.log, ...-negative-results.log,
...-old-criterion.log, ...-extra-delivery.log, ...-no-wrap.log.
CMake-сборка всех голосовых целей после rebase PASS; CTest17/17 PASS,3,46с:
call_jitter/delay/timeline/voice_tx4 и call_audio_tx13.
Логи /tmp/radio-mesh-fix-voice-{configure,build,tests}.log.
Полный ./check.sh:202 passed,0 failed,0 skipped (202 total),exit0,176с.
Включены call_admission,radio_mesh,member_adapter/feedback и остальные тесты
объединённой версии; в общем mesh все3 сценария PASS,allocations0->0.
Интеграции того же запуска: proxy9/9,burst1/1 (8388608 байт),load8/8
(134217728 байт), обе стороны rc0. Логи /tmp/radio-mesh-fix-full-check.log,
/tmp/radio-mesh-fix-netns.log и tests/logs/test_radio_mesh.log.
Все проверки прошли после единственного текущего изменения исходников теста;
дальнейшие правки — только документация. Повтор общего набора не требуется.
Запуск из sudo unshare --net от user, IPv6/lo включены только там;
IPv6 хоста all/default/lo остаётся отключён. Санитайзеры, аппаратный GUI,
Android, Windows и FreeBSD в этом исправлении не запускались.
Аудит остальных контрактов call остаётся частичным; отметки0[V]/10[~]/11[ ] сохранены.
11. Headless PCM: разрыв фрейма при переполнении очереди — 26.10.2026
База продолжения03b05094, master совпадал с origin/master. Полностью прочитаны
call_headless.c/.h и относящиеся к TX/RX/cleanup функции test_call_headless;
запуск/остановка listener в chat_service_start/stop, media callback в call.c.
Сервис имеет собственные enc/dec на instance, отдельные от call_audio DSP.
Все его операции выполняются в uasync; stop снимает callback и освобождает
listener/клиентов/таймер/кодеки до call_destroy. Максимум16 клиентов,
единственный callback потребителя Opus; темп/джиттер готового PCM — у клиента.
Заголовки уточняют владение/потоки и существующий порядок байтов; API не меняется.
Полный аудит GUI/Android и всех ветвей headless ещё не завершён.
Подтверждённое расхождение
ca_write_frame формирует header11 байт и дважды вызывает ca_send_raw:
сначала header, затем PCM. При уже непустой очереди ca_send_raw проверяет
место отдельно для каждого фрагмента; если len>space, молча возвращается.
Поэтому возможен принятый header с отброшенным телом. После возобновления
записи клиент получает неполный фрейм, а очередной header попадает в его PCM.
Это нарушение границ протокола, а не только потеря одного20мс аудиокадра.
Штатный тест ищет один готовый PCM-фрейм и эту границу очереди не проверяет.
Локальная компонентная диагностика
/tmp/call-headless-audit-queue.c включает временную копию call_headless.c.
Единственное изменение этой копии — WARN перед существующим silent drop
с fd/call/fragment/queued/offset/space. Diff прочитан: логика исходной функции
не меняется. Файлы проекта и их архивы/объекты не подменяются.
Сборка через штатные test_call_headless_CFLAGS/LDADD в overlay Makefile:
/tmp/call-headless-audit-queue.mk; build ...-queue-build.log PASS.
Стенд создаёт один настоящий UASYNC и loopback TCP listener с port0 через
call_headless_init. Обычный клиент подключается и передаёт HELLO. В том же
потоке ca_media_recv получает реальные20мс Opus-пакеты от настоящего encoder;
decoder преобразует их в PCM. Отдельного настоящего call_ctx/звонка/роутера
в этом компонентном тесте нет: вызывается именно headless media callback.
Поля очереди/счётчики читаются для наблюдения, но вручную не заполняются.
--wrap=send задаёт EAGAIN только исходящему fd этого клиента; после снятия
backpressure настоящий TCP передаёт накопленные байты, строгий последовательный
парсер проверяет границы без сканирования/поиска следующего похожего header.
Контроль control:8 кадров, затем ещё один после drain. Получено8/9 полных
фреймов, remaining0, aligned1, allocations0->0,exit0.
Pressure:40 обычных вызовов callback при blocked send. Первые33 помещаются
целиком, queued63723. Следующий header увеличивает queued до63734, но
PCM1920 не помещается в оставшиеся1802 байта и отбрасывается. Таких orphan
header остаётся7, всего77 байт. Очередь63800 содержит33 полных фрейма и хвост77.
После drain/следующего настоящего кадра парсер встречает неверный header;
границы не восстановились. dec41/dec_drop0 в штатном destroy дополнительно
показывает отсутствие диагностики потери в исходном коде.
Pressure завершается ожидаемым exit1; allocations0->0. Логи control/pressure
прочитаны полностью: /tmp/call-headless-audit-queue-{control,pressure}.log.
Штатный tests/test_call_headless повторён:1/1 PASS,exit0;
/tmp/call-headless-audit-baseline.log. Он проверяет настоящий звонок двух ядер
через JSON control/TCP PCM, но только один media frame и не покрывает overflow.
Прежние202/202 и C++17/17 из раздела10 не являются тестом этого нового условия.
План исправления
1) Собрать полный ограниченный PCM-фрейм header+body и передавать его
ca_send_raw одним вызовом. При нехватке места принимать/отбрасывать
целый кадр; отдельно логировать/учитывать отбрасывание. Wire/API не меняются.
2) Сохранить корректную обработку partial send: уже переданный prefix
продолжается сохранённым tail; новый кадр не вклинивается в него.
Компактизация использованного места — отдельное улучшение, для целостности
не обязательна; рефакторинг всего сервиса на tcp_io не требуется.
3) Добавить постоянную регрессию на normal/EAGAIN/short-write, точную границу
очереди и следующий кадр после drain. Удерживать исходный e2e и cleanup-проверки.
4) Сборка/целевые проверки и общий check после внедрения. Аналогичный шаблон
rh_write_frame/rh_send_raw замечен при сравнении; в этом аудите radio overflow
отдельно не воспроизводился, полный radio-аудит не заявляется.
По AGENTS.md:10 реализация не исправляется без следующего согласованного шага.
Добавлены предупреждение/контракт header, заметка архитектуры и открытая задача;
call_headless.h теперь[~]. Call0[V]/11[~]/10[ ], архитектурный документ[~],
видео[ ] сохранены. Новый полный build/check, sanitizers, аппаратный GUI/Android,
Windows/FreeBSD здесь не запускались. Исправления предыдущих разделов сохранены.
12. Headless PCM: целый кадр в ограниченной очереди — 26.10.2026
Пользователь «исправляй» разрешил внедрение плана раздела11. Базаae9d3f01;
коммиты913ae531/7c4e6f99/0ad80f39. Изменены только call_headless.c/.h,
tests/test_call_headless_queue.c и его цель в tests/Makefile.am; остальные
изменения этого шага относятся к документации/чек-листу. Wire и публичный API
сохранены, реализация radio_headless и общий call_audio DSP не менялись.
Реализация
ca_write_frame собирает header11+PCM до единственного вызова ca_send_raw.
Буфер ограничен CA_PCM_FRAME_BYTES1920+CALL_AUDIO_HDR_SIZE11, находится
на стеке; oversize и ненулевой len с NULL отклоняются с ERROR до memcpy.
При ожидающем tail очередь проверяет место для целого нового кадра:
если его недостаточно, ни header, ни PCM не дописываются. Успешный partial
send сохраняет только неотправленный tail; следующие кадры стоят после него.
Нет новых динамических выделений, таймеров или дополнительных владельцев.
Переполнение увеличивает pcm_dropped клиента и c_pcm_dropped сервиса;
decode-drop остаётся отдельным показателем. WARN показывает call_id/fd,
размер кадра, send_len/offset/capacity и оба счётчика. Первый drop логируется
сразу, следующие WARN ограничены одним в секунду (10000 timebase units).
При закрытии клиента и destroy сервиса INFO выводят полные итоги pcm_drop,
включая потери между WARN. Использованный prefix буфера до полного drain
не компактизируется: это прежняя политика ёмкости, целостность не нарушается.
Постоянная регрессия
tests/test_call_headless_queue.c включён в check_PROGRAMS для !OS_WINDOWS;
нужен GNU --wrap=send. Стенд включает production .c, как другие компонентные
тесты проекта. Один настоящий uasync на поток, свежий loopback TCP listener
с port0/клиент/HELLO и настоящие Opus encoder/decoder для каждого сценария.
Минимальный instance не создаёт call_ctx/роутер: media callback вызывается
напрямую в том же потоке. Полноценный звонок двух ядер сохраняет штатный e2e.
Поля очереди только наблюдаются, она заполняется обычными media callbacks.
Wrapper задаёт EAGAIN/short write только серверному fd; успешный prefix
действительно передаётся через TCP. Независимый reference decoder обрабатывает
все Opus-пакеты, включая отброшенные для TCP, сохраняя такое же состояние.
Сравнивается весь ожидаемый wire header+PCM байт в байт, без поиска похожего
следующего header. CHECK действует и при NDEBUG.
11 сценариев PASS:
- normal:8 кадров без backpressure, затем следующий после drain;
- frame_boundary:34 попытки,33 принятых,1 целый drop;
- overflow:40 попыток,33 принятых,7 целых drops;
- partial_header:первые5 байт переданы TCP, далее EAGAIN;40/33/7;
- partial_pcm:первые113 байт переданы TCP, далее EAGAIN;40/33/7;
- exact_fit:первые118 байт переданы,34 кадра заполняют очередь ровно65536,
потерь нет; exact_overflow:35-я попытка отбрасывается целиком;
- chunked_write:prefix5, затем drain порциями не более127 байт;
- offset_overflow:после33 queued кадров отправлены118 байт, новая попытка
отбрасывается без изменения send_len/offset; оставшийся поток корректен;
- destroy_pending:8 queued кадров освобождаются без drain;
- invalid_output:oversize1921 и NULL+1920 дают ровно2 ожидаемых ERROR,
не изменяют очередь/переданные байты; следующий допустимый кадр проходит.
После каждого обычного drain проверен следующий кадр с непрерывным состоянием
Opus. В каждом сценарии проверяются точные PCM-счётчики закрытия/destroy,
наличие WARN, баланс uasync socket/timer и allocations0->0 после cleanup.
Лог /tmp/call-headless-fix-queue.log прочитан полностью:11/11 PASS;
6 первых overflow-WARN и2 намеренных ERROR, неожиданных ERROR нет.
Чувствительность теста проверена без замены production/архивов/объектов:
временная копия регрессии включает старый instrumented .c из раздела11.
Normal PASS, на frame_boundary старый код оставляет header, queued63734,
отбрасывает PCM1920 при space1802. Проверка ожидаемой длины сразу FAIL,
ожидаемый returncode=-6 (CHECK/abort), core dump отключён для этого запуска.
До abort первый fixture освобождён с allocations0->0; cleanup второго
прерван и его баланс этим негативным запуском не заявляется.
/tmp/call-headless-fix-old-test.c/.mk, ...-old-build.log, ...-old-test.log.
Проверка окончательного исходного кода
./build.sh --no-gui -j4 PASS; после финальной правки0ad80f39 целевая
make-сборка PASS, новая регрессия11/11 и штатный test_call_headless1/1 PASS.
Логи /tmp/call-headless-fix-{build,target-build,queue,e2e}.log.
Штатный e2e использует JSON control, два настоящих ядра и TCP PCM;
он дополняет компонентную проверку очереди и остаётся однокадровым.
Полный ./check.sh:203 passed,0 failed,0 skipped (203 total),exit0,188с.
Новая цель входит в этот запуск; proxy9/9,burst1/1 (8388608 байт),
load8/8 (134217728 байт), обе стороны rc0. Логи
/tmp/call-headless-fix-full-check.log, ...-netns.log,
tests/logs/test_call_headless_queue.log и test_call_headless.log.
Запуск в отдельном sudo unshare --net от user; IPv6/lo включены только там.
Настройки хоста all/default/lo disable_ipv6=1 сохранены и проверены.
После этих проверок менялись только документация/чек-лист.
Исправление фрейминга закрыто. Отдельная задача по похожему шаблону
radio_headless остаётся на диагностику, runtime-дефект там не заявляется.
Аудит остальных call-контрактов/платформ остаётся частичным:call0[V]/11[~]/10[ ],
call_headless.h[~], архитектура[~],видео[ ]. Новые sanitizer/аппаратные GUI,
Android, Windows и FreeBSD проверки этим шагом не выполнялись. Push не выполнялся.
13. Desktop I/O и Android mute до AEC — 26.10.2026
Публикация завершённого исправления
По запросу «пуш и продолжай» git push origin master PASS:
03b05094..54a7522d, все6 локальных коммитов отправлены. git ls-remote подтвердил
refs/heads/master=54a7522dd93934bd3f082990550e971339bdb4f7;
на момент push HEAD==origin/master, дерево чистое. Прежний полный check203/203
и headless11/11 из раздела12 относятся к уже опубликованной реализации.
Продолжение desktop-аудита
Полностью прочитаны CallAudioEngine.cpp/.h167+76 строк, VoiceAudioIo.cpp/.h525+76,
AudioDevice.cpp/.h225+68, AudioEventQueue, AudioCaptureLease, VoiceCaptureClock,
VoicePlaybackBuffer; относящиеся к start/end/stop handlers MainWindow и lifecycle
utun_node, attach/detach/render/recovery SoundManager. Остальные SoundManager
функции и все потребители общего I/O этим этапом целиком не проверены.
Повторно сверены call_audio feed/pull, call_jitter/reset/window, CallTimeline
и tones; их прежние частичные отметки не закрываются преждевременно.
Capture callback копирует PCM в ограниченную MPSC-очередь, capture worker
выполняет AEC и сохраняет границы mute. Независимый RX worker получает PCM;
SoundManager смешивает все источники того же выхода и возвращает итоговое
stereo как reference. Stop: закрыть/отменить capture, join capture worker,
detach output, join RX, release core. Async uninit сохраняет callback/backend
и microphone lease до завершения закрытия; отсутствие PCM не останавливает RX.
start подтверждает подготовленную сессию, recovery устройств может продолжаться.
Штатный CMake configure/build PASS, профиль25/25 PASS,20,89с:
- прежние call_jitter/delay/timeline/voice_tx и13 call_audio_tx —17 тестов;
- audio_event_queue,voice_audio_io,audio_timing,soundtouch_clock,
audio_recovery,microphone_watchdog,audio_buffer_tuning,audio_diagnostics —8.
Очередь проверена на80000 упорядоченных сообщениях; VoiceAudioIo проверяет
настоящие workers/stereo AEC с заменой сервисной PCM-границы наблюдателями.
Recovery/watchdog используют настоящий miniaudio null backend, Qt timers,
core voice и введённые отказы/задержки, без сетевого звонка/микрофона.
ERROR/WARN новых I/O-тестов соответствуют намеренным init/start/context/
PCM/watchdog faults, старвированию выхода при recovery и недостающему reference.
В recovery есть WARN о паузе uasync: fixture специально не выполняет poll
во время работы виртуальных устройств. Все25 завершились Passed.
Логи /tmp/call-desktop-audit-{configure,build,tests,last-test}.log;
вывод новых I/O-тестов прочитан полностью. Предупреждение компилятора из
существующего miniaudio.h не относится к новым исходникам этого аудита.
Это не проверка реального PulseAudio/ALSA, Windows/FreeBSD или акустики устройства.
Подтверждённый Android-дефект
В CallAudioEngine.kt прочитаны start/stop/runWorker/reportFailure и полный
captureLoop (строки1–454), а также syncAecForRoute/measureAecDelay и хвост
796–856. Kotlin playback/recovery середины файла и остальной Android lifecycle
целиком ещё не проверены. Сверена полная цепочка feed в NativeLib.kt,
nativeCallAudioFeed и utun_bridge_call_audio_feed, core call_audio_feed_impl.
captureLoop (строка414) делает out=if(muted) silence else buf, затем
NativeLib.callAudioFeed(callId,out,ptsUs). В NativeLib/JNI/C bridge флага mute
нет: bridge вызывает call_audio_feed_pcm. Core получает нулевой raw-вход,
после чего выполняет AEC, затем AGC и Opus. AEC с прогретым фильтром/ненулевым
reference может создать ненулевой результат даже из этого нулевого входа.
syncAecForRoute включает core AEC на SPEAKER; разрешённый delay1..20 кадров.
Таким образом раннее зануление не гарантирует нулевого PCM перед Opus.
Кроме того AEC/AGC лишаются настоящего микрофонного входа во время mute;
деградация на реальном устройстве отдельно не измерялась.
Core уже имеет call_audio_feed_pcm_muted: настоящий PCM проходит AEC/AGC,
muted применяется непосредственно перед encode. Desktop prepared path
применяет свою маску после AGC и этого дефекта не имеет.
Компонентная диагностика без замены production
/tmp/call-android-mute-audit.cpp и ...-build.py используют текущие CMake
libutun_voice/libutun/libuasync/Opus. Настоящий android_jni_bridge.c собран
отдельным host-объектом с function/data sections и gc-sections, как штатный
test_android_replies. __ANDROID__ выключен: реально выполняется C bridge,
JNI-маршалинг/Kotlin/AudioRecord/AudioTrack на устройстве не запускаются.
Проектные исходники, архивы и объекты не заменяются.
Один настоящий uasync/минимальный instance, call_audio_init/start, реальные
remote Opus encode → on_media → jitter/CallDelay → pull PCM → core AEC reference.
Синтетический микрофон = половина фактически полученного render; прогрев300
кадров, затем20 наблюдаемых. Через публичный API выбран AEC delay1; AGC
отключён настройкой для изоляции AEC. Начальный FIFO заполнен обычными
on_media, приватные поля/фильтр не подменяются. Синтетический get_time_tb
продвигается на20мс/кадр; настоящий uasync исполняет TX после каждого feed.
Сетевая граница call_send_media заменена счётчиком; соединение двух ядер
не создаётся. Wrapper AEC/encode только читает PCM до/после настоящих функций
и сохраняет их результат. Звук не записывается, используются искусственные данные.
Результаты (в20 кадрах всего19200 отсчётов):
- unmuted: bridge получает настоящий микрофон; AEC in19193/out18445 ненулевых,
Opus input18445,peak46,RMS10,500; control PASS,exit0;
- no_aec: текущий ранний mute, AEC отключён; Opus input0,peak0,RMS0;
control PASS,exit0;
- legacy: текущий ранний mute, AEC включён; AEC in0/out19191,
Opus input19191,peak7515,RMS1602,348. Проверка нулевого входа Opus ожидаемо
FAIL,exit1; наблюдение завершилось с полноценным cleanup;
- final_mute: существующий core API получает настоящий микрофон+muted1;
AEC in19193/out18445, но Opus input0,peak0,RMS0; control PASS,exit0.
Во всех4 запусках warm render RMS3878,247; все320 TX callbacks исполнены,
pending0 при destroy, u allocations0->0. Сырые входы AEC в unmuted/final_mute
совпадают; ошибочный legacy имеет именно нулевой вход и ненулевой выход.
Обычное последействие Opus-декодера здесь не измеряется: дефект доказан
до lossy encode. Прослушивание/утечка речи микрофона на устройстве не заявляются.
Логи /tmp/call-android-mute-audit-{build,results,unmuted,no_aec,legacy,final_mute}.log
прочитаны полностью. Build PASS; предупреждение snprintf относится к
существующей неиспользуемой здесь функции формирования списка каналов bridge.
План исправления для согласования
1) Убрать silence/out подмену в captureLoop: передавать настоящий buf и один
snapshot muted на готовый20мс кадр, сохранив capture PTS и порядок PCM.
2) Добавить флаг в NativeLib.callAudioFeed/nativeCallAudioFeed, JNI и объявление/
реализацию C bridge. Bridge вызывает существующий call_audio_feed_pcm_muted.
Сверены все production callers: один Kotlin captureLoop и один JNI вызов.
Не нужны новый глобальный mute, новый режим DSP или изменение wire.
3) Закрепить настоящий bridge→AEC/AGC→Opus постоянной регрессией: unmuted,
текущий ранний mute как негативный контроль, итоговый mute, AEC on/off,
AGC on/off, следующий unmute и cleanup. Наблюдать точный PCM перед encode;
raw-вход фильтра должен оставаться исходным, muted-вход Opus строго нулевым.
4) Собрать native/voice-профиль и Android assembleDebug для проверки общей
JNI/Kotlin сигнатуры; затем check. Реальное переключение mute на громкой
связи и AEC после unmute — отдельный аппаратный контроль.
По AGENTS.md:10 новый логический дефект требует остановки перед исправлением.
Изменены только комментарии call_audio.h и документация/внешний чек-лист.
Уточнены актуальные event IDs, состав C/C++ сборки, отдельный headless DSP,
desktop stop-порядок и reset FIFO; runtime/API/протокол/штатные тесты не менялись.
Call0[V]/11[~]/10[ ],chat_call_arch.md[~],видео[ ] сохранены;8 затронутых
desktop headers отмечены[~]. Общий голосовой/платформенный аудит ещё частичный.
Новый fullcheck/sanitizers/Gradle/аппаратный Android/desktop/Windows/FreeBSD
этим продолжением не выполнялись. Новый дефект не исправлен; задача открыта.
14. Android mute после AEC/AGC: внедрение и проверка — 26.10.2026
Следующее «ОК» разрешило план раздела13. База03be46c3; production-коммиты
a4435edb/4fcb2ab3, постоянная регрессия74375e23. Общий call_audio DSP,
wire, desktop prepared path и другие Android I/O-механизмы не менялись.
Реализация
В CallAudioEngine.captureLoop убран silence/out: на завершённый20мс кадр
берётся один snapshot frameMuted, в NativeLib передаются исходный buf,
этот флаг и прежний capture_pts_us. Флаг проходит через nativeCallAudioFeed
и jboolean JNI в C bridge; bridge вызывает call_audio_feed_pcm_muted.
Core AEC и AGC продолжают обрабатывать настоящий микрофон, а mute зануляет
только PCM непосредственно перед Opus. Обновлены все production callers,
объявление C bridge и комментарии API; совместимость со старой JNI-сигнатурой
не добавляется. JVM descriptor публичного и native feed теперь (J[SZJ)Z.
Нового глобального состояния mute, владельцев, блокировок, таймеров или
DSP-режимов нет. Метрики capture latency включают mutedFrames за интервал;
feed-отказ логирует call_id, snapshot mute и PTS. Эти кадры означают попытки
захвата/подачи, не подтверждённую доставку. Частота сводки остаётся раз в секунду.
Прежние INFO setMuted on/off сохраняются; логов на каждый успешный feed нет.
Постоянная регрессия реального bridge
tools/chatgui/tests/test_android_call_mute.cpp и цель CMake собирают настоящий
host android_jni_bridge.c с function/data sections и gc-sections. Нужен GNU
--wrap, поэтому цель в существующей ветке UNIX AND NOT APPLE. Проверяется
C feed bridge; JNI-маршалинг/AudioRecord/AudioTrack на устройстве не исполняются.
Настоящие call_audio/Opus/AEC/AGC/jitter/CallDelay/uasync, один uasync на поток.
Искусственный удалённый Opus проходит on_media/pull и становится настоящим
core AEC reference. Микрофон=render/2, начальный FIFO заполняется публичным
callback. Синтетический get_time_tb шагает20мс, delay1 задаётся публичным API;
network sink и настройки заменены наблюдателями. Wrapper AEC/AGC/encode
только сравнивает/читает PCM и вызывает настоящую функцию с прежними аргументами.
Приватные поля/кодеки/фильтры вручную не заполняются.
После300 кадров прогрева проверяются по20 before/muted/after, всего360 TX.
Перед каждым AEC подтверждается исходный raw PCM; AGC получает точный выход
AEC (либо raw при AEC off), его enabled соответствует настройке. Encoder
получает точный выход AGC до/после mute и строго нули во время mute.
Проверяются неизменность массива caller, PTS каждого posted callback,
capture epoch1/codec delay, пустая posted queue, баланс socket/timer и
u allocations0->0. CHECK действует при NDEBUG, PCM-несовпадения накапливаются
до полного cleanup, затем возвращается FAIL.
Пять CTest-сценариев PASS:
- четыре AEC0/1 × AGC0/1: в каждой muted-фазе Opus nonzero0,peak0,RMS0,
raw остаётся ненулевым; before/after Opus ненулевой и побайтно равен
наблюдаемому выходу DSP. AEC/AGC продолжают вызываться во время mute;
- legacy-контроль передаёт тишину с muted0, воспроизводя прежний Kotlin.
На320-м кадре после прогрева/before-фазы AEC снова создаёт19192/19200
ненулевых отсчётов,peak7676,RMS1591,958; контроль ожидает именно этот сигнал.
После unmute RMS528,486 против9,151 у правильного mute при AEC1/AGC0;
это искусственный компонентный сигнал, не акустический замер устройства.
Все5 cleanup:sent360,PTS_epoch1,allocations0->0; mismatch0 у правильного пути.
Лог новых сценариев полностью прочитан: /tmp/call-android-mute-fix-voice-details.log.
Чувствительность к потере флага подтверждена отдельно: неизменённый объект
постоянного теста с прежним телом bridge из03be46c3. Только сигнатура адаптирована
под новый muted-аргумент, который игнорируется; тело по-прежнему вызывает
call_audio_feed_pcm. Текущие production source/объекты/архивы не заменяются.
Сценарий AEC1/AGC0 даёт muted opus_nonzero18450,mismatches18450,
before/after PASS, затем полноценный cleanup allocations0->0 и ожидаемый exit1.
Это проверка утраченного флага в bridge; отдельный legacy-контроль выше
проверяет сам прежний порядок раннего зануления Kotlin.
/tmp/call-android-mute-fix-{old-bridge.c,negative-build.py,negative-build.log,
negative-result.log,negative.log}; отрицательный лог прочитан полностью.
Сборка и общий профиль
./build.sh --no-gui -j4 PASS. CMake configure/build PASS; профиль30/30 PASS,
20,67с: прежние25 voice/desktop тестов и5 новых mute-сценариев. Логи
/tmp/call-android-mute-fix-{native-build,voice-configure,voice-build,voice-tests,voice-details}.log.
Предупреждения/ERROR существующих I/O-тестов относятся к их намеренным
fault/watchdog проверкам; у5 новых положительных сценариев ERROR/WARN нет.
Android ./gradlew assembleDebug --max-workers=4 PASS: реальные NDK-сборки
arm64-v8a и x86_64, Kotlin/JNI, APK содержит оба libutun.so.
Первый запуск1м51с; финальный повтор после форматирования строки диагностики3с.
Это сборка APK без установки. javap подтвердил оба feed-descriptor (J[SZJ)Z.
Gradle :app:testDebugUnitTest для CallAudioTimingTest8 и CallAudioTraceTest6:
14/14,failures0,errors0,skipped0,BUILD SUCCESSFUL7с. Это тесты часов/trace,
не исполнение Android captureLoop или native-библиотеки на телефоне.
Логи /tmp/call-android-mute-fix-{android-build,android-final-build,android-unit}.log;
JUnit XML в tools/chatgui-android/app/build/test-results/testDebugUnitTest/.
Полный ./check.sh:203 passed,0 failed,0 skipped (203 total),exit0,168с.
Новая C++ voice-регрессия относится к CMake-профилю30, в C-only check203 не входит.
В общем наборе call/headless/Android replies и остальные тесты PASS;
proxy9/9,burst1/1 (8388608 байт),load8/8 (134217728 байт), обе стороны rc0.
/tmp/call-android-mute-fix-{full-check,netns}.log. Весь общий вывод прочитан.
IPv6/lo включены только в отдельном sudo unshare --net; check выполнялся от user.
Настройки хоста all/default/lo disable_ipv6=1 сохранены и проверены.
Исправление mute закрыто, задача и контракты обновлены. После проверок
менялись только документация/внешний чек-лист. Общий голосовой/платформенный
аудит остаётся частичным:call0[V]/11[~]/10[ ],desktop8[~],архитектура[~],видео[ ].
Новые sanitizer, аппаратный Android/desktop, Windows/FreeBSD проверки не
запускались. Реальная акустика/переключение mute на устройстве требуют отдельного
контроля; непосредственное последействие lossy Opus-декодера этим тестом не измеряется.
Новые коммиты локальные, push в этом шаге не выполнялся.