[2026-10-07] eagle: personal/projects/virfield.md work/projects/cpm-web-extension-breakage-session-summary-2026-10-07.md

This commit is contained in:
Alexey Martemyanov committed 2026-10-07 15:39:22 +06:00
1 parent c23808e802
commit 15e5d5e1e0
2 files changed
+306 -2

No files matched your search

+49 -2
View File
@@ -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.
@@ -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`.