From 15e5d5e1e06b73f2b4bc0566c57f072438a02533 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 7 Oct 2026 15:39:22 +0600 Subject: [PATCH] [2026-10-07] eagle: personal/projects/virfield.md work/projects/cpm-web-extension-breakage-session-summary-2026-10-07.md --- personal/projects/virfield.md | 51 +++- ...ion-breakage-session-summary-2026-10-07.md | 257 ++++++++++++++++++ 2 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 work/projects/cpm-web-extension-breakage-session-summary-2026-10-07.md diff --git a/personal/projects/virfield.md b/personal/projects/virfield.md index b9b2be84..5e540dab 100755 --- a/personal/projects/virfield.md +++ b/personal/projects/virfield.md @@ -1,8 +1,8 @@ --- title: Virfield namespace: personal -updated: '2026-10-03' -status: candidate-clean-image-ready-clone-pending +updated: '2026-10-07' +status: virfield-3-stage-1-in-progress tags: - virfield - macos @@ -10,6 +10,53 @@ tags: - go - infrastructure --- +# Virfield 3.0 — handoff checkpoint — 2026-10-07 15:35 Bishkek + +**Текущий статус: Stage 1 (Tart/Orchard migration) в работе; продолжать в `/Users/admin/Developer/virfield-v3`, branch `codex/virfield-v3-stage1`.** Это актуальная запись состояния; ниже сохранена история v2 от 2026-10-03. + +## Что проверено сейчас + +- Worktree: HEAD `8830ad9` (`Add Tart and Orchard runtime providers`), родительский план `6013310`. Восемь файлов modified и не закоммичены: `cmd/virfieldd/main.go`, `internal/config/config.go`, `internal/httpapi/server.go`, `internal/tart/client.go`, `internal/tart/client_test.go`, `internal/web/app.js`, `internal/web/index.html`, `internal/web/style.css`. Не откатывать и не считать эти изменения заведомо своими без проверки. +- `http://127.0.0.1:7780` отвечает HTTP 200 и отдаёт v3 UI (Fleet / Images / Activity, Hosts & storage). Факт подтверждён запросом к live-сервису. +- Live `virfieldd`: launchd label `system/ai.virfield.virfieldd`, PID 54497 на момент проверки, пользователь `admin`; plist `/Library/LaunchDaemons/ai.virfield.virfieldd.plist` запускает `~/.virfield-v2/bin/virfieldd -config ~/.virfield-v2/config.json`. Имя `virfield-v2` — legacy путь развертывания, не доказательство версии исходников. +- Точная ревизия уже работающего процесса не установлена. Не заявлять, что бинарник на 7780 собран из HEAD `8830ad9`. +- Текущий dirty source успешно компилируется командой `go build -trimpath -o /private/tmp/virfieldd-v3-check ./cmd/virfieldd`. Полный `make check` после этих восьми изменений ещё не запускался. Предыдущий passing `make check` относится к исходной реализации до текущих правок. + +## Следующее действие + +1. Разобрать каждое из восьми незакоммиченных изменений и сохранить нужное; проверить владельца/намерение, не затирать. +2. Прогнать `make check` в этом worktree и устранить регрессии. +3. Собрать `make build`; сохранить копию текущего deployment binary для отката, заменить `~/.virfield-v2/bin/virfieldd` собранным бинарником. +4. Перезапуск текущего launchd job пока не проверен. Для контролируемой проверки после сборки: + ```sh + pgrep -x virfieldd + # Убедиться, что найден ровно один PID текущего сервиса. + kill -TERM PID + launchctl print system/ai.virfield.virfieldd | rg 'state =|pid =|runs =' + curl -fsS http://127.0.0.1:7780/ | rg 'v3|Fleet|Hosts & storage' + ``` + Причина ожидания автоматического старта: plist имеет `KeepAlive=true`, процесс работает от `admin`, а deployment directory принадлежит `admin`. Это ожидаемый способ без `sudo`, но пока не подтверждён живой проверкой. Если launchd не поднял новый PID, остановиться и диагностировать; не запускать второй daemon. + + Сборка/установка (после успешных проверок; перед заменой сохранить бинарник для rollback): + ```sh + cd /Users/admin/Developer/virfield-v3 + make check + make build + cp -p /Users/admin/.virfield-v2/bin/virfieldd /private/tmp/virfieldd-before-v3 + install -m 755 bin/virfieldd /Users/admin/.virfield-v2/bin/virfieldd + ``` + До live-проверки не объявлять обновление завершённым. Общий install/runbook находится в `docs/OPERATIONS.md`; он описывает регистрацию system LaunchDaemon с `sudo`, но не dev-перезапуск существующего job без `sudo`. +5. После подтверждения записать точный commit/build и результат live-проверки здесь. До этого не создавать второй listener и не менять порт. + +## План и границы + +- `docs/VIRFIELD-3-PLAN.md` — целевая архитектура и план, не журнал выполненных этапов. +- `docs/VERIFICATION.md` — acceptance evidence, не доказывает приёмку текущих Tart/Orchard dirty changes. +- В committed v3 уже есть провайдеры Tart и Orchard, host/storage/capacity UI и опциональный WoL по реализации базового коммита. Текущие незакоммиченные правки расширяют/исправляют это; перед claims сверить код и тесты. +- Источник кода — worktree `virfield-v3`; live deployment — `~/.virfield-v2`; это разные копии. Обновление source само по себе не обновляет процесс на 7780. + +--- + # Virfield — статус 2026-10-03 **Следующее действие:** проверить загрузку, работу и удаление временного клона из нового чистого образа Monterey/Xcode. diff --git a/work/projects/cpm-web-extension-breakage-session-summary-2026-10-07.md b/work/projects/cpm-web-extension-breakage-session-summary-2026-10-07.md new file mode 100644 index 00000000..8b11da12 --- /dev/null +++ b/work/projects/cpm-web-extension-breakage-session-summary-2026-10-07.md @@ -0,0 +1,257 @@ +# CPM Web Extension breakage — полное summary сессии + +**Дата:** 7 октября 2026 +**Охват:** public-attribution после релиза iOS 7.240 / macOS 1.210, исходный код Browser и WebKit, release bundle CPM, WebKit tracker и upstream regression tests. + +## Короткий вывод + +Главный production-сигнал — не доказанная потеря `initResp`, а отсутствие в течение 4 секунд native-вызова `refreshCpmDashboardState` после завершения навигации. Поэтому большая группа, где native-снимок выглядит здоровым, означает: WebKit controller/context/scripts могли существовать, но CPM не прислал или native не сопоставил ожидаемый dashboard refresh. Она не доказывает, что extension вообще не загрузился. + +Наиболее близкие к этому симптому WebKit-кандидаты — гонки при cold start service worker: + +1. **317981:** content script отправляет `runtime.sendMessage`, пока background ещё загружается; WebKit не должен терять сообщение. +2. **324743:** worker считается загруженным и получает сообщение до завершения activation/восстановления clients. + +Два других WebKit-багa могут объяснять ухудшение после перезагрузки/recovery, но пока не доказано, что они происходят в CPM: + +3. **322881:** после reload worker теряет существующие extension clients. +4. **324754:** при site isolation перезагруженный worker может оказаться в новом процессе без Web Extension bindings. + +M4 recovery даёт сильный сигнал восстановления измеряемого background → native пути, но не подтверждает восстановление `initResp` или всей CPM-функции. Следующая работа: воспроизвести upstream-сценарии на system WebKit и одновременно добавить event-level instrumentation, которая разделит content `init`, worker receive, `initResp` и dashboard refresh. + +--- + +## 1. Что измеряет current telemetry + +### Основной сигнал + +После `navigationDidFinish` `CPMMessagingHealthMonitor` ждёт до 4 секунд native `dashboardResponse`. Он приходит только когда background вызывает `refreshCpmDashboardState`, а native handler принимает и сопоставляет его с текущим документом. + +```text +navigationDidFinish + → ожидание dashboardResponse (4 s) + → нет ответа + → initialization.failed.after.*.daily + → при следующем qualifying timeout в пределах episode + → messaging.stuck.*.daily + → M4 reload embedded extension +``` + +Следствия: + +- `initialization.failed` не равно «content script не отправил init». +- `initialization.failed` не равно «background не получил init». +- `initialization.failed` не равно «initResp не дошёл до content script». +- `context_loaded`, живой background view, зарегистрированные scripts и native handler не подтверждают, что service worker уже активирован или обработал именно этот message. + +`context_loaded` в WebKit означает, что controller уже назначен. Это происходит раньше асинхронной подготовки storage, создания background view, регистрации scripts и injected content. `tab_has_ext_scripts` отражает зарегистрированные `WKUserScript`, а не факт их исполнения в этом документе. + +### Почему `initResp` и dashboard refresh не одно и то же + +В release bundle content script отправляет `init` в background. Успешная ветка background запускает `updateCpmDashboardState(...).then(refreshDashboardState(...))` без `await`, а затем отдельно отправляет `initResp` во вкладку. В части веток (`config_unavailable`, `settings_missing`, `setting_disabled`, `site_disabled`) отправляется dashboard refresh, но `initResp` не отправляется. + +Значит возможны оба расхождения: + +- `initResp` был, но refresh задержался, не пришёл или не сопоставился — monitor сообщает failure. +- refresh пришёл, но `initResp` не было — monitor считает путь здоровым. + +Дополнительные реальные задержки до refresh: remote config, native settings/site checks, последовательная async queue, session storage. JS native-request timeout в bundle — 20 секунд, тогда как monitor ждёт 4 секунды. URL сопоставляется по scheme, host, port, path и query; поздний refresh или другой query также дают failure при работающем JS. + +--- + +## 2. Public attribution: охват и контрольные суммы + +Окно: полный UTC-день 5 октября 2026, только события после release tag каждой платформы. `.daily` — максимум одно событие каждого имени в сутки на установку. Это не пользователи, не эпизоды и не transition rate. + +| После release tag | iOS 7.240 | macOS 1.210 | +|---|---:|---:| +| Первое `initialization.failed.after.*.daily` | 6 624 | 1 377 | +| Последующее `messaging.stuck.*.daily` | 1 440 | 487 | +| Первый неудачный CPM probe после reload | 670 | 232 | +| `recovered.after.extension.reload.daily` | 963 = 916 stuck + 47 init | 362 = 354 stuck + 8 init | +| Все CPM daily по именам | 11 252 | 2 804 | + +Контроль: 8 001 init + 1 927 stuck = основные первичные/вторичные строки после tag. До macOS tag в выгрузку попали 28 init, 2 stuck и 39 CPM daily; они вынесены из public-среза. До iOS tag строк этой версии нет. + +### Группы снимков на первом failure + +Каждая строка относится к одной группе по приоритету. Это описание состояния в момент timeout, не первопричина. + +| Группа | iOS init | macOS init | iOS stuck | macOS stuck | +|---|---:|---:|---:|---:| +| Native snapshot без видимой ошибки | 3 717 | 911 | 789 | 335 | +| Tab attachment / injection | 931 | 116 | 53 | 38 | +| Background state | 768 | 158 | 457 | 35 | +| Только история context errors | 343 | 158 | 87 | 79 | +| Недостаточно tab facts | 865 | 34 | 54 | 0 | + +Крупнейшая группа — **3 717 iOS / 911 macOS** — не является подтверждением «нормальной загрузки». Она только не содержит видимого native-признака поломки на момент снимка. + +### Главные окружающие цепочки + +- **Session restoration:** 952 iOS / 68 macOS init failures. На iOS у 604 controller совпадает, scripts отсутствуют, background view не создавался; 754 лежат в первых 30 секундах запуска. Это согласуется с временным startup miss, но 260 снимков выглядят здоровыми и требуют timings сообщений. +- **Обычные навигации iOS:** часты `load → view → proxy_on` и `load → view → proxy_on → dealloc → view → proxy_on`. `dealloc` означает освобождение background view, не process crash. Явный `died_*` есть лишь в 85 первичных iOS строках. +- **После extension reload:** 670 iOS / 232 macOS первых timeout. Типичный хвост содержит `load → base_url_changed → view → proxy_on`; `base_url_changed` сопутствует reload и не доказывает URL defect. +- **macOS controller/scripts mismatch:** после tag 77/1 377 init и 35/487 stuck имеют неверный controller и отсутствующие scripts; до tag таких строк нет. 45 из 77 совпали с `app_version_change=updated`, но update сам по себе не доказывает, что живая вкладка осталась на старом controller. Нужен отдельный event смены manager/controller при живой вкладке. +- **WebKit error 6:** в 364 iOS stuck снимках есть `WKWebExtensionContextErrorDomain:6`; в 363 перед ним есть `dealloc`. Это `BackgroundContentFailedToLoad`: service worker/background не удалось загрузить. Пиксель не содержит первичную причину registration/load failure. + +--- + +## 3. Связь init failure и stuck + +В коде связь прямая: первый timeout создаёт `Episode`; stuck появляется только после следующего qualifying timeout, обычно в пределах пяти минут. Причина stuck наследуется от первого failure, а stuck snapshot снимается со второй вкладки. Recorder хранит общую history background context, поэтому `stuck.bg_events` может содержать события до первого init failure. + +В public-срезе сравнивались длинные цепочки (`>=6` событий) в окне пяти минут: порядок токенов и возраста событий должны были совпадать. Контроль — те же правила, но stuck timestamps сдвинуты на ±10 минут. + +| Platform | Stuck | Длинная цепочка | Совпала с более ранним init | Контроль +10 / −10 min | Один кандидат | +|---|---:|---:|---:|---:|---:| +| iOS | 1 440 | 1 025 | 693 | 255 / 270 | 467 | +| macOS | 487 | 368 | 168 | 29 / 19 | 148 | + +Это подтверждает, что stuck чаще продолжает предшествующий timeout-эпизод, чем случайную соседнюю цепочку. Это не соединение одного пользователя: нет reporter ID, `.daily` дедуплицирует события, короткие хвосты недостаточно уникальны. + +**Важно:** один потерянный `init` сам по себе не доказывает global stuck. Для production stuck нужны два qualifying timeout в одном episode. Поэтому тест «один tab потерял init, второй сам сломался» не был честным доказательством и не получен. Проверять нужно: два timeout в одном стартовом окне → настоящий M4 recovery → третья вкладка без injection. + +--- + +## 4. Проверка релизов и mitigation + +### Session-restoration gate + +M7 gate уже ждёт initial `loadAndSync` перед первой main-frame HTTP(S) navigation restored tab с bounded timeout. По public data временные session-restoration failures **не исчезли**: 952 iOS / 68 macOS. Без связки событий нельзя посчитать, какая часть затем восстановилась. + +### Tab lifecycle fixes + +Новые controller/scripts поля появились только в новом атрибуционном релизе, поэтому старые версии нельзя сравнивать по tab bugs напрямую. Доступен только proxy `stuck.other / init.other` на macOS: + +| Версия | Proxy | +|---|---:| +| 1.208 | 39.52% | +| 1.209, содержит M1 tab identity fix | 38.81% | +| 1.210 public day | 39.81% | + +Разница 1.208 → 1.209 составляет 0.71 п.п. и не доказывает эффект или его отсутствие: различаются cohort и нагрузка. + +### M4 after-stuck recovery + +M4 делает reload embedded extension после подтверждённого hang. После tag `recovered.after.extension.reload.daily` с `recovery_from=messaging_stuck` есть у 916 iOS и 354 macOS. Отношение всех `recovered.after` к stuck daily: 66.9% iOS и 74.3% macOS; на предыдущих версиях proxy был 3.23% / 2.70%. + +Это сильный признак, что reload часто возвращает измеряемый background → native refresh. Это **не success rate** и не доказательство восстановленного CPM/initResp. После reload первый probe всё ещё timeout-ится 670 раз на iOS и 232 на macOS. + +--- + +## 5. Другие CPM/Web Extension сигналы + +После tag Web Extension продолжает работать на значимой части навигаций: + +| Signal | iOS | macOS | Что доказывает | +|---|---:|---:|---| +| `autoconsent_init.daily`, `fromExtension=1` | 56 906 | 2 825 | Background получил какой-то init и отправил native pixel; не delivery `initResp` | +| `web_extension_loaded.daily` | 61 902 | 2 870 | Manager загрузил хотя бы одно extension | +| `web_extension_state_checked.daily` | 62 004 | 2 805 | Lifecycle проверил loaded contexts | +| `web_extension_embedded_not_loaded.daily` | 3 389 | 1 | Embedded extension отсутствовал в manager во время проверки | +| `web_extension_load_error.daily` | 4 | 4 | Ошибка общей загрузки extension | + +Также есть extension-sourced `popup-found` и `done`, значит CPM functional path работает у части навигаций. Общие имена пикселей разделены с legacy user script: выводы допустимы только с `fromExtension=1`. + +На macOS лишь один `embedded_not_loaded.daily` при 77 init failures с controller mismatch/no scripts. Это согласуется с локальной проблемой вкладки при исправном глобальном manager. + +--- + +## 6. WebKit hypotheses + +### 317981 — сообщение в cold start worker + +Upstream regression test `SendMessageFromContentScriptWhileBackgroundIsLoading`: + +1. Загружает MV3 extension с non-persistent background. +2. Background синхронно регистрирует `runtime.onMessage`. +3. Сразу открывает web tab без ожидания background readiness. +4. Content script сразу вызывает `runtime.sendMessage`. +5. Ожидает ответ background. + +Правильное поведение — очередь до registration listener. Без фикса сообщение может быть потеряно. Это ближе всего к цепочке `scripts/context есть → init отправлен → нет response/refresh` и не требует искусственного JS timeout. + +Статус: fix 317981 присутствует в public Safari/WebKit ветке `safari-7625.1.29.*`. macOS 27 VM из проверки имела WebKit bundle `22625.1.29.11.27`; точное соответствие Apple OS build и WebKit commit нельзя доказать лишь CFBundleVersion. Нужен system-framework regression run на конкретной VM, а не предположение по номеру. + +### 324743 — worker получает event до activation + +Upstream test выгружает worker, будит его `runtime.sendMessage` из extension tab и прямо в message listener проверяет `clients.matchAll()` без polling. Offscreen document обязан быть уже controlled client. Без фикса worker обрабатывает message, пока activation/reconnect ещё не завершились. + +Это также похоже на primary telemetry: controller/view выглядят живыми, но реальная обработка message идёт в неправильный момент. + +### 322881 — reload потерял extension clients + +Upstream test создаёт offscreen document, проверяет его presence в `clients.matchAll()`, reload-ит background и проверяет, что новый worker вновь видит тот же client. В production relevance пока не установлена: CPM обычно работает с web tab/content script, а не обязательно с extension-owned offscreen page. + +### 324754 — worker в новом process без bindings + +Upstream test включает site isolation, создаёт offscreen document, reload-ит worker, проверяет смену PID и затем наличие `browser.runtime.getURL`. Без фикса JavaScript worker стартует в процессе без Web Extension API bindings. + +Это хороший кандидат для проблемы после reload/recovery, но не доказанный источник current CPM data. Сначала нужен upstream-shaped reproduction с offscreen/extension page, затем full CPM scenario. + +### Связь с error 6 + +Error 6 частично ложится на путь `refreshCpmDashboardState`: если background не загрузился, он не вызовет refresh. Но error 6 не говорит, где именно сломалось: service-worker registration, process, storage, bindings или другая внутренняя WebKit ошибка. Он не доказывает 317981, 324743, 322881 или 324754. + +--- + +## 7. Fix в browser code + +В рабочем дереве `alex/cpm-web-extension-ready-handshake` подготовлен некоммитнутый JS fix, ориентированный на потерянное/слишком раннее `init` сообщение: content side ждёт explicit readiness/ответ и повторяет handshake вместо того, чтобы считать один message окончательным. ZIP с extension — release artifact; в нормальной жизни он собирается из исходников расширения и попадает в bundle приложения на build/release pipeline, а не загружается пользователем. + +Смысл фикса: не менять WebKit и не «лечить» искусственной задержкой, а сделать CPM handshake идемпотентным. Если первый init попал в cold-start race, content script повторно инициирует состояние и получает актуальный dashboard path. Это покрывает потерю первого сообщения/ранний readiness contract, но не восстановит worker без `browser.runtime` и не исправит WebKit process/client bug сам по себе. + +Существующий fixture с `setTimeout` полезен как test retry protocol, но не доказывает настоящую WebKit race. Он показал важное ограничение: единичная потеря init не делает все tabs stuck автоматически. + +--- + +## 8. План точного воспроизведения без изменения WebKit logic + +### Почему не C++ `DYLD_INTERPOSE` + +Нужные C++ пути выполняются внутри отдельных WebContent/Network processes и используют internal hidden symbols. Вызовы внутри WebKit dylib обычно не проходят через interposable imported symbol stubs. Инъекция dylib в Apple WebContent subprocesses ненадёжна из-за hardened runtime, SIP и XPC environment. Это не практичный первый метод. + +### Рабочий harness + +1. Создать Swift integration harness с Objective-C runtime swizzle и `RaceGate`; тесты запускать serially, потому что swizzle process-global. +2. Для 317981 не ставить gate: портировать exact upstream cold-start race и многократно запускать на system WebKit. +3. Для 324743 swizzle `-[WKWebExtensionContext loadBackgroundContentWithCompletionHandler:]`: сообщить completion test coordinator раньше, а оригинальный вызов удержать до explicit release. Это контролирует scheduling на публичной ObjC boundary, не меняя extension logic или C++ WebKit code. +4. Для real lifecycle использовать доступные private test SPI динамически: `_terminateServiceWorkers`, `_backgroundWebView`, `_killWebContentProcessAndResetState`, `_webProcessIdentifier`, `_setSiteIsolationEnabled:`. +5. Логировать OS build, WebKit CFBundleVersion, timestamps init/response, background PID до/после, outcome каждой итерации. + +### Матрица + +| Сценарий | Реальный триггер | Успех/отказ | +|---|---|---| +| 317981 | Сразу открыть tab после extension load | Каждый init получает ответ/refresh | +| 324743 | Gate вокруг background load completion, tab уже навигировался | Message не должен исполняться до activation | +| 322881 | Создать extension client, `_terminateServiceWorkers`, разбудить message | Client есть в `clients.matchAll()` | +| 324754 | Site isolation, убить/restart background, дождаться нового PID | В новом worker есть `browser.runtime.*` | +| CPM amplification | Два timeout в одном shared start window, настоящий M4 reload, затем third tab без injection | Third tab работает; иначе доказан cascade/recovery defect | + +Нельзя подтверждать global stuck одним искусственно потерянным init на одной вкладке. Для честного test нужны два timeout, которые переводят health monitor в stuck, после чего injection выключается перед третьей вкладкой. + +--- + +## 9. Какие данные нужны дальше + +Приоритетная telemetry с общим, privacy-safe navigation/episode correlation ID: + +1. `content_init_sent`. +2. `background_init_received` и окончание async prerequisites. +3. `initResp_sent` / `initResp_received`. +4. `refreshCpmDashboardState_sent` / `received` / URL match result. +5. worker lifecycle: start, activation, PID/process generation, background load error code, recovery generation. + +Эти точки разделят пять разных случаев, которые сегодня попадают в один `init failure`: content injection miss, runtime message miss, worker activation race, delayed/mismatched dashboard refresh и native routing failure. + +--- + +## Источники + +- Public attribution: `cpm-web-extension-public-attribution-2026-10-05.md` и папка `cpm-public-attribution-2026-10-06/` в `apple-browsers.git`. +- Historical validation/methodology: `cpm-web-extension-breakage-validation.md`. +- WebKit 317981 regression: `Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm`, `SendMessageFromContentScriptWhileBackgroundIsLoading`. +- WebKit 322881/324743/324754 regressions: `Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIOffscreen.mm`. +- Browser implementation: `CPMMessagingHealthMonitor.swift`, `CPMMessagingDiagnosticsRecorder.swift`, `WebExtensionManager.swift`, `AutoconsentWebExtensionMessageHandler.swift`.