Три трека по честным хвостам. Каждый — шаги, файлы, критерий готовности.
Порядок внедрения: Трек 1 (release) → Трек 2 (сеть) → Трек 3 (turbo). Release-контур блокирует «дать человеку», сеть блокирует «мобильный opencode», турбо — перф.
Отдельно, вне этих трёх треков: Трек 4 — миграция на OpenCode 2,
план в docs/MIGRATION-V2.md. Приложение пакует бинарь serve внутрь APK.
Этапы 0–4 закрыты: в APK уже opencode 2.0.27, HTTP-слой перенесён на /api/*
(/session/status удалён — статус синтезируется из псевдо-сообщения type:"idle"),
вопросы и разрешения остались на v1-ветке через ApiPaths.active.legacy.
Открытые этапы — 5–7. Риск WebView снят: SPA v2 грузится и работает.
Боль: debug-APK 392MB, пакет .debug, isMinifyEnabled=false.
- Включить
isMinifyEnabled=true+shrinkResources=trueвreleaseблоке (app/build.gradle.kts, строчки ~20-26). - Критично:
libopencode.soи whisper.soлежат вjniLibs— R8 их не трогает (только dex), но проверить, чтоpackaging { jniLibs.useLegacyPackaging }не слетел (execve изnativeLibraryDir). - Добавить
keep-правила для рефлексии (opencode serve пути) вproguard-rules.pro. - Ожидание:
material-icons-extended(2000+ иконок) R8 выкинет неиспользуемые → экономия ~20-30MB dex.
- ✅ base вынесена из APK в lazy-скачивание (как turbo). APK: 374 → 274 (R8) → 147 MB.
- Реальный предел — ~147MB, т.к.
libopencode.so(184 MB) — это сам opencode, вынести нельзя. Дальнейшие варианты:- App Bundle / on-demand (Play требует AAB) — ещё ужимает загрузку.
- Опционально: убрать
libbun-musl.so(70MB) если opencode может работать без bun-браузинга (рискованно, требует проверки запуска serve без bun).
applicationIdSuffixубрать из release (сейчас только debug имеет.debug— ок, release будет чистыйorg.opencode.mobile).- Создать signing config (upload key), беймпинг
versionCode/versionName(0.1.x). - Собрать
assembleRelease, прогнать smoke-тест на устройстве: запуск, чат, STT.
Готово когда: assembleRelease даёт APK/AAB ~100MB (или lazy ~70MB),
пакет чистый, всё работает под smoke-тестами на телефоне.
Боль: на Android нет /etc/resolv.conf, musl шлёт на 127.0.0.1:53,
внешние сервисы не ходят без прокси.
✅ Реализовано. Встроенный Ipv4Proxy (CONNECT-туннель на 127.0.0.1:3128,
IPv4-first через InetAddress/Netd) + HTTPS_PROXY/HTTP_PROXY/NO_PROXY в
OpencodeRuntime.startServe. Подтверждено на устройстве: туннели к
mcp.context7.com, mcp.grep.app, registry.npmjs.org открываются автоматически.
Остатки (опционально):
- Прогнать явный
webfetch/gitиз чата на реальном телефоне как регресс-тест.
Готово: внешние вызовы opencode ходят в сеть без ручного прокси-конфига.
Боль: большие модели душатся до 1-5% (ColorOS/OPPO), turbo медленный.
- ✅ Внедрено (следовали
tools/ncnn-int8-plan.md; KV-cache уже был). int8-encoder (block-quant) грузится сразу в сеть (при неудаче — откат fp32), замерено: int8-CPU 8.82 s vs fp32-CPU 17.9 s (~2×), int8-Vulkan 6.45 s (эксперимент, в проде выключен — см.docs/EXPERIMENTS-STT-LATENCY.md§2). - Исполнитель:
ModelDownloaderкачает int8-варианты турбо;ncnn_jni.cpp::load()выбирает их автоматически.
WhisperTranscribeServiceуже держит приоритет (foreground).- ✅ Потоковый пайплайн/чанкинг внедрён (ЭКСП-5, сентябрь 2026):
SpeechSegmenter(VAD по RMS) +ChunkedTranscriber— длинная речь режется на сегменты, каждый распознаётся отдельно: первые слова видны на 1–2 s, хвост >30 с не теряется, галлюцинаций на тишине меньше. - ✅ Бенч turbo int8 на реальном OPPO прогнан (24.09.2026): int8 6.8–8.2 s
vs fp32 14.5–15.2 s (~2.1×); CSV:
docs/stt-bench-2026-09-24.csv. - ✅ WER-бенч работает (26.10.2026).
tools/stt-accuracy.ps1гоняет клипы на устройстве (SttBenchReceiver→logcat), эталон —C:\Projects\MyFLOW\data\eval_wispr(phrases.jsonl+ 624 wav). Первый прогон на 7 клипах (1,3–10,8 с): whispersmall— средний WER 10,47 %, corpus WER 13,85 % (9 правок / 65 слов); parakeetparakeet-tdt-0.6b-v3— corpus 9,52 % (6 / 63), но два односложных клипа не дали результата вовсе, поэтому сравнение пока нечестное. - Метрика исправлена: средний WER по клипам — шум на наборе с односложными фразами (одна буква в слове из одного слова даёт 100 %). Скрипт теперь печатает и corpus WER (правки на слово по всему набору), и число клипов ≤2 слов.
- Осталось: прогон по всему корпусу (624 клипа) и разбор, почему parakeet молчит на
односложных клипах; тюнинг
setThreadsна других устройствах. - ✅ VAD-пороги измерены, а не угаданы (26.09.2026): офлайн-свип
SpeechSegmenterBenchTestна реальных bench-wav, 6 порогов × 5 SNR = 30 замеров. Recall речи 90.7% на всём диапазоне 0.010–0.030; 0.015 и 0.025 дают идентичный результат во всех 30 замерах, то есть предложенный сдвиг порога — no-op.MIN_RMSостаётся 0.015. Отчёт:app/build/reports/vad-bench.txt. - ✅ Прошлый вывод «WER посчитать нельзя» снят (26.10.2026). Он опирался на то, что
эталона нет в репозитории: корпус лежит рядом, в
C:\Projects\MyFLOW\data\eval_wispr, и его достаточно. Считать WER по выводу модели по-прежнему нельзя (наjfk.wavint8 отдаёт текст, не являющийся цитатой) — но с внешним эталоном это и не нужно.
Готово когда: потоковый пайплайн с первым результатом 1–2 s + WER-прогон — ✅ пайплайн внедрён (1–2 s первые слова); пороги VAD измерены; WER-бенч работает и дал первые числа на 7 клипах. Полный прогон по корпусу — отдельная задача, не блокер.
- serve за паролем:
ServerAuth(генерация, зашифрованные prefs, заголовки). - Единый клиент
LocalOpenCodeClient(auth в одном месте). - Готово: все запросы к 4096 с Authorization; без — 401. Проверено e2e.
memory.jsна127.0.0.1:4199, регистрация в serve черезensureMcpConfig(фикс «0 MCP» — пустойopencode.jsonc), протокольная проверка вRuntimeValidation(GET /mcp→ 2xx).- ✅ Инструмент «забыть» из UI (
26.10.2026): секция «Локальная память» в диагностике — счётчики, последние 20 записей и кнопка «Забыть» на каждой с подтверждением. Вызовы идут из приложения напрямую в127.0.0.1:4199/mcp(MemoryTools+MemoryWire), без модели в цепочке: иначе удаление зависело бы от того, согласится ли агент вызвать инструмент. Проверено на устройстве сквозным прогоном: тестовая записьmem:rg9z6ilphsbmvohwvi3создана агентом, удалена кнопкой, в базе не осталось ни строки, ниterms, ни рёберgraph; «Всего» 54 → 53.
Хвост после сессии «холодный запуск Яндекс.Музыки» (69ae486, 994053b).
Проверено на устройстве: BAL обойдён через ручной SYSTEM_ALERT_WINDOW grant,
холодный старт при убитой ЯМ и фоне — 53 с и 81 с без BAL_BLOCK.
- ⛔
AppInstallBridgeTest.ktне существует вообще. На все роуты моста, разбор JSON и сериализацию тестов нет — то есть на весь код, который эта сессия добавила и который ломался дважды. - ⛔ В
ynison.jsнет ни одного экспорта. Все 48 существующих JS-кейсов (announce22,goaway7,search15,session4) работают приёмом «вырезать блок исходника по маркерам» (loadBlock+BLOCK_START/BLOCK_END).bridge(),opSessionStatus,opSessionControlпокрыты нулём. - ✅
play_pauseуже покрыт: вMediaControlContractsALIASESсодержит иplay_pause, иplay-pause, иtoggle, аMediaControlRequestValidatorTestсверяет список wire-имён. Пункт «добавить тест на play_pause» вычеркнут. - ✅
McpMusicConfigTestтеперь держит иTIMEOUT: тест сверяет"timeout": 180000в записи music и отсутствиеtimeoutу памяти и телефона (переименование вtimeoutMsopencode молча игнорирует и возвращается к дефолту — ловится здесь).
app/src/test/java/.../installer/AppInstallBridgeTest.kt — 10 тестов. Чтобы эти
поля вообще можно было проверить, JSON-формы вынесены из приватных обработчиков
моста в installer/AppInstallWire.kt (тело запроса, код ошибки, ответ на запуск)
и media/MediaWire.kt (формы медиа-ответов).
- ✅
jsonObject()на пустом теле →{}(раньше был 400, GET без body падал) - ✅ не-объектное тело →
IllegalArgumentException→ 400, не 500 - ✅
JSONException(нет обязательного поля) → 400: отдельная ветка, потому что он не наследникIllegalArgumentException. Тест заодно фиксирует это родство — если оно изменится, ветка перестанет быть нужной молча - ✅
okв/v1/apps/launch— литералtrue, не зависит отbackground_launch_allowed - ✅
toJsonотдаётbackground_launch_allowedи не отдаётrunning - ✅
media.verifiedвложен вmedia, в корне его нет - ✅ fallback-тексты ошибок, когда у исключения нет
message - ✅
bridge()не шлёт body на GET/HEAD — уже было вynison-bridge.test.mjs - ✅
clearTimeoutна всех путях, компактныйopSessionStatusс фильтром поcontrolable— тожеynison-bridge.test.mjs - ✅ сверка enum
session_controlвynison.jsсMediaCommand—SessionControlEnumContractTest.kt(читает исходник JS)
Проверено: build.ps1 -Task verify — 775 тестов, 0 падений, ktlint и detekt
чистые; jstest зелёный. Каждый тест проверен откатом фикса: с возвращёнными
регрессами краснеют ровно шесть тестов класса, после отката — зелено.
Effective-конфиг MCP — источник найден (25.10.2026), замеры ждут телефон.
Прежняя формулировка («/data/data/.../opencode.jsonc не содержит ynison,
источник не найден») описывала не тот файл. Конфиг целиком пишет приложение:
| Путь | Кто пишет | Роль |
|---|---|---|
<filesDir>/opencode-config/opencode/opencode.jsonc |
OpencodeRuntime.ensureMcpConfig |
действующий: serve стартует с XDG_CONFIG_HOME=<filesDir>/opencode-config (startServe) |
<base>/config-mirror/opencode.jsonc |
mirrorConfig после успешного ensureMcpConfig |
эталон для чтения по USB, односторонняя копия |
<base>/opencode/opencode.jsonc |
старый путь до переезда на ext4 | остаётся на диске после разовой миграции (migrateConfigFrom) и больше не обновляется — вероятно, именно его и смотрели |
Порядок записи: ensureMcpConfig → ensureMemorySection (создаёт управляемую
mcp-секцию) → syncMusicEntry (добавляет/убирает ynison по наличию
runtimeAccessToken()) → syncMoltbookEntry → syncModelEntry → mirrorConfig.
Отказ любого шага — отказ файла целиком (fail-closed), зеркало не пишется.
YNISON_MCP_TIMEOUT_MS = 180000 подтверждён как действующий: в схеме
opencode 2.0.27 (бинарь @opencode/cli-linux-arm64-musl@2.0.27, объект
McpRemoteConfig) есть поле timeout — «Timeout in ms for MCP server requests,
Defaults to 5000»; в документации opencode то же поле у remote-серверов. То есть
приложение пишет не мёртвый ключ. Имя и значение закреплены тестом
McpMusicConfigTest.«music получает timeout 180000 мс…»; проверено откатом:
timeout → timeoutMs краснит ровно этот тест.
Замер выполнен 25.10.2026 — 180 с не участвуют, вызов умирает на go-away 503.
music_play_query при убитой Яндекс.Музыке: 18,5 с, ошибка no player_state after handshake, приложение так и не стартует (pidof пуст) — то есть ensurePhone не
доходит, клиентский таймаут и холодный старт ни при чём. Причина в трассе
/storage/emulated/0/Documents/OpencodeTerminal/ynison-trace.log: сервер отвечает
{"error":{"details":{"ynison-error-code":"500140000","ynison-go-away-for-seconds":"3600"}, "http_code":503,"message":"Ynison service is temporary unavailable."}} — вложенная
форма, которую goAwayError не узнавал (искал код в корне). Кадр оставался обычным
кадром состояния, и вызов падал позже на «нет player_state».
Исправлено: код и интервал читаются из обеих форм, проверка бана стоит и после
handshake, интервал запоминается на процесс (пока бан идёт, сокет не открывается).
ynison-goaway.test.mjs — 14 тестов, оба отката краснят. Остаётся проверить на
устройстве после установки: сообщение должно называть 503 и остаток интервала, а
второй вызов подряд — не открывать соединение.
Проверено на устройстве 25.10.2026 — оба утверждения держатся. Первый вызов:
3,7 с, текст называет 500140000 и 3600s. Второй вызов подряд: 0,004 с, новых
соединений в трассе нет (сразу отказ по дедлайну). То есть вызов больше не висит
18,5 с и не открывает сокет внутри бана.
Второй бан пришёл 22:54, и это отдельная проблема — дедлайн жил только в памяти.
Разбор трассы показал две вещи. Первое: у строк трассы не было времени, поэтому
по ней нельзя сказать, когда случился бан и какой вызов его получил. Второе:
музыкальный MCP поднялся в 21:57, а бан от 21:44 к этому моменту был забыт — первый
вызов после рестарта открыл сокет и собрал свежие 3600 с (дедлайн до ~23:59).
Плюс handoffUntil продолжал делать три попытки записи внутри бана, давая три
одинаковые строки.
Исправлено 25.10.2026: trace() подписывает строки ISO-временем, дедлайн бана
кладётся рядом с кэшем состояния (ynison-goaway.json) и читается при старте,
handoffUntil прерывает повторы на permanent-ошибке. ynison-goaway.test.mjs —
21 тест, семь откатов краснят свои тесты. Что осталось непонятным: кто именно
вызвал музыку в 22:54 (в тот момент человек с телефоном не работал). Со временем в
трассе следующий такой случай будет виден по часам; гипотезы — фоновый агент
moltbook и music_status из панели диагностики.
Запуск музыки при этом работает: /v1/apps/launch для ru.yandex.music отвечает
за 0,45 с с background_launch_allowed: true (accessibility включён). Значит
недоступен именно Ynison, а не мост и не телефон.
Проверка выполнена 25.10.2026 — блока не было, причина найдена и исправлена.
Токен живой (files/opencode.log: ynison MCP http on 127.0.0.1:4200, порт слушает),
а конфиг не переписывался с 20.10. Предупреждения
Managed mcp block has unexpected formatting в logcat не было вовсе — приложение
пишет свои Log.* мимо logcat, поэтому искать надо было не там. Виноват
hasManagedMcpBlock: он узнавал секцию mcp целиком (mcpBlock()), а с
подключённым moltbook после телефона стоит запись moltbook, а не }. Отсюда
ensureMemorySection → false → memoryReady && syncMusicEntry(...) не выполнялся
вовсе, и music не регистрировалась никогда. planMusicEntry страдал тем же
сравнением и уходил в Refuse. Обе проверки переведены на уровень записей
(McpMusicConfigTest, 20 тестов, проверено откатом). Проверено на устройстве:
после установки записи music в конфиге нет причин не быть — "timeout": 180000
между mobile_tools и moltbook, GET /api/mcp отдаёт четыре сервера connected.
26.10.2026 — «кто вызвал музыку в 22:54» выяснено: никто не вызывал.
Разбор opencode.log показал 1632 строки mcp transport error server=music
между 21:35 и 04:28 по устройству — по одной каждые ~13 с, и ноль таких строк
у memory, mobile_tools и moltbook. Причина в ynison.js: GET-ветка отдавала
SSE-поток, который писал один кадр endpoint и дальше молчал, а Bun закрывает
простаивающий ответ через 10 с по умолчанию. opencode видел это как отказ
транспорта (а не как ответ инструмента) и переподключался; каждый повтор медленного
вызова (handshake, тикет, state-сокет) открывал новую сессию Ynison, а тот на каждый
заход отвечает stay away 3600s — то есть бан продлевался сам, пробой за пробой,
и не мог истечь. В memory.js это лечение было с самого начала
(srv.timeout(req, 0) + keepalive-кадр), в ynison.js — нет.
Исправлено 26.10.2026: server.timeout(req, 0) в обеих ветках (медленный POST
тоже рвался на 10 с) и keepalive каждые 15 с с очисткой по cancel/abort.
ynison-sse.test.mjs — 6 тестов, оба отката (снятый timeout и keepalive 30 с)
краснят свои. Проверено на устройстве: до правки ~1 обрыв в 13 с, после установки —
0 обрывов за 3,5 минуты, mcp connected server=music на старте. Дальше бан
должен истечь сам; сквозной прогон музыки — после 04:58 (последнее продление было
в 03:58).
Зацепка из прошлых сессий: RuntimeManager уже умеет fail-closed на конфиге —
ответ может лежать там. И отдельно: в mem:opencode-mobile-installer-final-20260925
замечено, что 15-минутный опрос установщика может превысить таймаут MCP-клиента.
Два независимых наблюдения одного класса — возможно, проблема не в ynison.
(Замером 25.10 это не подтвердилось: вызов рвётся за 18,5 с по go-away 503.)
Seed/mirror-цикл ensurePhone(). Ynison может ответить зеркалом собственных
announce-вызовов, и клиент примет это за live-state → риск потери состояния живой
сессии. Воспроизвести tools/ym-exp.ps1 -Variant nowrite / -Variant nohandback,
найти точку подмены, потом минимальный фикс. Правка без воспроизведения —
гадание, поэтому сначала эксперимент.
26.10.2026 — найдена настоящая причина «state open timeout»: сокет состояния шёл не туда.
Вызов музыки висел 97,7 с и падал state open timeout, buffered 0B. Диагностика (ярлык
state/redirect в каждой строке трассы, статусная строка ответа, отказ по закрытию
соединения вместо 10-секундного таймера) показала: редирект-сокет работает целиком — 101,
payload, тикет, закрытие; а state-сокет получает 0 байт за 10 с.
Причина: state-сервис отвечает только на адресе шарда. Приложение резолвит
ynison.music.yandex.ru (фронтенд) и передаёт этот IP в ynison.js (RESOLVED_YNISON_IP),
а Host/SNI у state-сокета — шардовый k3t3i6….ynison.music.yandex.ru. Фронтенд принимает
TLS и запрос и не отвечает вообще. Замер с PC при одинаковых Host, пути и токене:
фронтенд 77.88.21.101 — 0 байт, таймаут 6 с; шард 77.88.48.156 — 101 Switching Protocols за 0,16 с. DNS: фронтенд → 77.88.21.101, шард → 77.88.48.156.
Заодно снят миф, на котором стоял resolveIpv4 — «musl-Bun не умеет DNS на Android»: на
устройстве dns.lookup("k3t3i6….ynison.music.yandex.ru") → 77.88.48.156 (тот же bun
запущен под run-as). Поэтому резолв шарда делается в JS, а не приходит снаружи.
Исправлено: resolveStateHost() резолвит шард на месте и отдаёт его адрес в state-сокет;
при отказе резолва — именованная ошибка вместо тишины. ws.onclose до апгрейда теперь
отклоняет промис (state socket closed before upgrade): раньше любой ранний разрыв
выглядел как таймаут.
Проверено на устройстве 26.10.2026: state host: … -> 77.88.48.156 → state connect →
state head: HTTP/1.1 101 Switching Protocols; вызов вернулся за 7,86 с вместо 97,7 с.
Ответ сервера при этом 500140000 / «stay away 3600s» — Ynison сам просит подождать
(устройство насобирало сессий за отладку), второй вызов подряд отказывает за 0,07 с без
нового соединения. Сквозной прогон музыки — после интервала.
Пробный вызов продлевает бан на новый час. Через час после первого бана (дедлайн
02:56:58) один music_play_query вернулся за 7,2 с с текстом «token is valid — wait out the
interval» и перезапустил блокировку до 03:57:20. Значит «проверить, пустило ли» нельзя:
каждый замер стоит ещё час, и так можно не дождаться никогда. Сквозной прогон требует часа
полной тишины — ни music_play_query, ни music_status.
26.10.2026, 05:54–06:12 — чем блокировка НЕ является (замерено, а не предположено).
Четыре попытки подряд, каждая с новым дедлайном, и один контрольный опыт:
| время | что зашли | итог |
|---|---|---|
| 05:54 | id телефона (…LB5PAi) |
503 500140000, +3600 с |
| 05:58 | новый id (локальный пробник на ПК) | сессия открылась (уперлись в пустую очередь) |
| 06:00 | тот же новый id, второй заход | 503 500140000, +3600 с |
| 06:12 | ещё один новый id, первый заход | 503 500140000, +3600 с |
| — | Яндекс.Музыка на телефоне | играет, state=PLAYING, позиция растёт |
Выводы, которые из этого следуют:
- Это не бан по device id. Новый id, который ни разу не подключался, получил 503 с первого захода. Прежняя формулировка в коде («ban counted per device id») была неверна.
- Это не проблема аккаунта, токена и не авария сервиса: приложение на том же телефоне в это же время играет через тот же аккаунт.
- Значит троттлится токен OAuth, которым ходит наш клиент, и любой заход во время троттлинга снова взводит час. Смена id — не выход.
- Что троттлинг вызвало — уже известно: 1632 обрыва MCP-транспорта между 21:35 и 04:28, каждый обрыв = новый handshake (см. выше). Лечение транспорта сделано, но счётчик Yandex сам не обнулился.
Сделано, чтобы это не повторялось (26.10.2026).
- Одна сессия на все вызовы (
SESSION_TTL_MS= 10 мин). Раньше каждый тул-вызов открывал свой сокет, аmusic_play_query— ещё и второй на верификацию: один запрос пользователя стоил 2–3 захода. Теперь заход один, дальше сокет переиспользуется, кадры копятся в живом массиве (хвост 120 кадров, последнее состояние не теряется), аreadStateчитает последний кадр, а не первый — иначе на живой сессии он показывал бы трек, который играл при её открытии. - Параллельные вызовы не дублируют заход (
openingSession): два тула в одном батче ждут один диал, а не открывают два. - Бэкофф с удвоением (
GO_AWAY_MAX_MS= 12 ч): час, два, четыре, восемь, двенадцать. Счётчикattemptsлежит вynison-goaway.jsonрядом с дедлайном, поэтому рестарт процесса не обнуляет его и не возвращает часовой цикл. Успешная сессия счётчик сбрасывает. - Тесты:
ynison-session.test.mjs(5) переписан под новый контракт «сокет остаётся жить», новыйynison-session-cache.test.mjs(11) — переиспользование, TTL, мёртвый сокет, бан на живой сессии, удвоение, потолок, двойной кадр одного захода, сброс после успеха.-Task jstestзелёный.
Состояние прямо сейчас: троттлинг активен. Файл ynison-goaway.json на устройстве
вручную отодвинут на 6 часов (до ~12:08), чтобы старый код на телефоне не взвёл его
снова. Проверка «Playback >100 с» возможна не раньше этого срока — одним вызовом, без
предварительных «а пустило ли».
Переустановка (26.10.2026, 06:28): собран и поставлен adb install -r -d
APK 6fc15add — на телефоне теперь новый ynison.js (проверено по
run-as … grep throwIfBanned files/mem/ynison.js: 2 вхождения; ensureAssetScript
перезаписал копию из assets при старте). Музыкальный сервер поднялся
(ynison MCP http on 127.0.0.1:4200, порт отвечает), dial'а не было — новый код
прочитал {"until":1792998517901,"attempts":3} и не тронул троттлинг.
Гранты после переустановки (замер): appops get … SYSTEM_ALERT_WINDOW — allow
(time=+1d22h20m ago, то есть adb install -r его не сбросил). А accessibility-сервиса
в settings get secure enabled_accessibility_services нет — там только Microsoft
AppManager, наш MediaUiAutomationService отсутствует; включать заново руками.
Что при этом работает уже сейчас: управление воспроизведением через MediaSession
(session_control) — проверено на телефоне 26.10.2026: pause → paused, not playing,
play → playing, трек тот же, позиция та же. То есть пауза/плей/следующий/предыдущий
доступны без Ynison; недоступен только выбор произвольного трека — он идёт через
Ynison.
- ⛔ Playback >100 с после холодного старта не проверен: только короткие прогоны.
Живой вопрос — деградация позиции, потеря сессии, обрыв MCP-вызова. Блокер — троттлинг
токена (26.10.2026): пока он активен, любой замер стоит ещё час, а проверять надо одним
вызовом. Проверка сведена к одной команде —
pwsh -File tools\verify-playback-100s.ps1: до дедлайна изynison-goaway.jsonона отказывается звонить (иначе взведёт ещё час), после — делает ОДИНmusic_play_queryи 110 с читает MediaSession черезdumpsys, а неmusic_status(тот идёт в Ynison). Ближайший запуск — после ~12:08. - ✅ Мёртвые параметры моста (25.10.2026).
AppInstallBridge.mediaAppsчиталpackageиз тела GET-запроса (fetch тела на GET не шлёт → всегда null) и прибивалlimitкDEFAULT_LIST_LIMIT, хотяMediaControlRequestValidator.listи фильтр вMediaControlController.listAppsуже были. Теперьqueryиlimitчитаются из query-строки и проходят валидатор;MEDIA_ACTIONSизmemory.jsудалён —mobile_media_controlотозван, константу никто не читал. - ✅ Гигиена памяти проекта (26.10.2026). Правило доверия (
APPLY_RULE) теперь говорит и о свежести: запись о состоянии кода должна называть дату и коммит, а без них её положено перепроверить по коду. То же требование — в описанииlocal_memory_store, чтобы оно попадало агенту до записи, а не только при чтении. В ответахlist/recallпоявилось полеcreated_at(ISO): раньше дата была только эпохой в мс, и «свежая запись или нет» приходилось угадывать. Проверено прогономmemory.jsпод bun на ПК (store → list → forget) иjstest(29 тестов provenance).
Нужно от пользователя перед стартом P1: разблокированный телефон с выданным
SAW-грантом (SYSTEM_ALERT_WINDOW, «поверх других приложений»; замер 26.10.2026 —
adb install -r его не снимает, а вот accessibility-сервис после переустановки выключен),
разрешение убивать Яндекс.Музыку в экспериментах.
Реалистично за день — P0 плюс один из P1: разбор effective-конфига может
оказаться задачей на неделю.
Не «переизобретать» определение живости процессов. /proc на устройстве смонтирован
с hidepid=invisible, поэтому файл живого чужого процесса не читается
(Permission denied), и pidof отдаёт PID не того процесса. Признак жизни берётся
у цели — её MediaSession. Обходных путей нет: getRunningAppProcesses с API 21
отдаёт только своё приложение, UsageStatsManager требует второго grant.
Предупреждение есть комментарием в InstalledAppController.kt.
| Трек | Статус |
|---|---|
| 1. Release-контур | Выполнен — R8 + shrinkResources (374 → 274 → 147 MB), base вынесена в lazy, пакет чистый org.opencode.mobile, smoke OK. Подпись закрыта (26.09, 96bb02a): release подписан своим RSA-4096 ключом, keystore.properties в .gitignore, оффлайн-копия ключа сделана. Карточка Play готова (25.10): листинг в лимитах, 6 скриншотов 1272×2477, иконка 512×512, feature graphic 1024×500, политика конфиденциальности — docs/play/. Перепроверено 26.10.2026 после миграции на 2.0.27: сборка за 40 с, подпись release-ключом (CN=opencode-mobile, OU=Release), все нативы в APK (включая libopencode.so 2.0.27 и libbun-musl.so), движок и memory-MCP поднимаются за ~2 с, музыкальный MCP пропускается по делу (на чистой установке нет токена Яндекса), падений нет. Ограничение: не-debuggable сборку нельзя опросить по HTTP — пароль serve читается только через run-as. AAB теперь собирается штатной задачей -Task bundle (раньше — руками: -Task release даёт только APK, а Play APK не принимает); задача печатает versionCode, чтобы повторная загрузка с тем же номером не стала сюрпризом; targetSdk = 36, minSdk = 28, запас до лимита Play 200 МБ на base-модуль ~23 МБ. AAB пересобран 26.10.2026 после правок памяти и STT: app-release.aab 177 256 828 байт, versionCode=4, подпись на месте (META-INF/OPENCODE.RSA), ассеты сверены по размерам (иконка 512×512, feature 1024×500, шесть скриншотов 1272×2477), docs/privacy.md на месте. Следующая загрузка потребует versionCode > 4. Осталось: доступ к Play Console, URL политики и решение о номере следующей версии |
| 2. Сеть без прокси | Выполнен — встроенный Ipv4Proxy (CONNECT, IPv4-first); туннели к context7/grep/npm открываются автоматически. Legacy connect_proxy.py удалён (26.09) |
| 3. STT turbo на CPU | Почти готов — int8-encoder ~2× fp32, VAD-чанкинг внедрён (первые слова 1–2 s), бенч на устройстве прогнан (24.09), VAD-пороги измерены (26.09, 56034e9: MIN_RMS=0.015 подтверждён свипом 6 порогов × 5 SNR), WER-счётка готова и покрыта 13 тестами (26.09, c7256c1). Корпус найден и прогнан (26.10.2026): эталон лежит вне репозитория (C:\Projects\MyFLOW\data\eval_wispr), 7 клипов, whisper small — corpus WER 13,85 %; осталось прогнать весь корпус (624 клипа) |
| 4. Безопасность и память | Выполнен — Basic Auth (401 без пароля), локальная память MCP (ensureMcpConfig, фикс «0 MCP»), протокольная диагностика, e2e чистой установки пройден |
| 5. День 2: тесты + расследования | P0 закрыт (25.10.2026): AppInstallBridgeTest.kt — 10 тестов, JSON-формы вынесены в AppInstallWire/MediaWire; JS-часть уже была покрыта ynison-bridge.test.mjs и SessionControlEnumContractTest. P2-мелочи закрыты (25.10): /v1/media/apps читает query/limit из query-строки через валидатор, мёртвый MEDIA_ACTIONS удалён. P1 не закрыт: effective-конфиг MCP найден (25.10 — пишет ensureMcpConfig, поле timeout есть в схеме 2.0.27), блокировка регистрации music при подключённом moltbook найдена и исправлена (25.10, hasManagedMcpBlock/planMusicEntry — узнавали секцию целиком), замер таймаута закрыт (25.10: вызов умирает за 18,5 с на go-away 503, а не на 180 с; бан теперь узнаётся, соблюдается и переживает рестарт), seed/mirror-цикл ensurePhone() ждёт живого Ynison. Регистрация музыки по факту старта (26.10): ensureMcpConfig перенесён после startYnisonAndVerify и получает ynisonToken.takeIf { ynisonStarted } — неудачный старт снимает секцию, а не оставляет модели шесть инструментов, которые не отвечают. Шторм переподключений закрыт (26.10): 1632 обрыва server=music за 7 часов — это и был «неизвестный вызов в 22:54», лечится server.timeout(req, 0) + keepalive, см. выше |