[2026-09-15] eagle: personal/plans/hermes-whale-system-prompt.md personal/projects/personal-os/hermes-agent-improvements.md personal/tech/hermes-git-repo.md work/projects/cpm-web-extension-breakage-validation.md
This commit is contained in:
@@ -1,166 +1,71 @@
|
||||
# CPM Web Extension Breakage — статус и план
|
||||
# CPM Web Extension Breakage — статус и PR-план
|
||||
|
||||
> Короткая актуальная версия на 2026-09-14. Полная трассировка остаётся в [[cpm-web-extension-breakage-findings]].
|
||||
> Актуально на 2026-09-15. Полная трассировка: [[cpm-web-extension-breakage-findings]].
|
||||
|
||||
## Итог
|
||||
|
||||
Есть две разные группы багов, их нельзя смешивать:
|
||||
Есть два независимых класса проблем:
|
||||
|
||||
1. **All-tabs:** смерть background WebContent process может сломать CPM во всех вкладках, включая новые. Это главный кандидат для telemetry → graveyard → A/B.
|
||||
2. **Per-tab:** приложение сообщает WebKit, что ещё живая вкладка закрыта. Ломается только конкретная вкладка или shared pinned tabs. Это отдельные фиксы и отдельные PR.
|
||||
- **All-tabs:** после смерти background WebContent process CPM может перестать работать во всех вкладках. Нужна последовательность telemetry → production validation → bounded graveyard → A/B.
|
||||
- **Per-tab:** ошибки lifecycle заставляют WebKit забыть ещё живую вкладку. Исправляются независимо от graveyard.
|
||||
|
||||
## Статус проблем
|
||||
## Статус гипотез
|
||||
|
||||
| Проблема | Статус | Что известно / что делать |
|
||||
| Проблема | Статус | Результат |
|
||||
|---|---|---|
|
||||
| Смерть background WebContent process → page-less service-worker process → BackgroundContentFailedToLoad loop | **Подтверждено воспроизведением** | 100% repro через kill -9 background PID. CPM не работает во всех вкладках, reload/new tab не помогают, Fire помогает. Естественная частота и реальные production reasons пока неизвестны. Это scope graveyard experiment. |
|
||||
| Закрытие окна забывает shared pinned tabs в WebKit | **Подтверждено вручную** | didCloseWindow закрывает/forget-ит shared pinned Tab, хотя он остаётся в других окнах. Нужен отдельный per-tab lifecycle PR. |
|
||||
| Close Other Tabs forget-ит оставленную вкладку | **Подтверждено по коду** | removeAll(andAppend:) посылает didCloseTab и для retained tab, затем возвращает тот же объект без didOpenTab. Нужен ручной regression repro и отдельный fix PR. |
|
||||
| Duplicate Tab из popup переносит оригинальный Tab вместо копии | **Подтверждено по коду** | При закрытии popup WebKit забывает вкладку, оставшуюся в main window. Нужен ручной repro и отдельный fix PR. |
|
||||
| Пропуск CPM content script при startup/session restore | **Подтверждено как transient** | Только текущий document; следующая навигация восстанавливает работу. Не объясняет устойчивый all-tabs outage. |
|
||||
| Однократный failed background load остаётся в m_backgroundWebView | **Механизм подтверждён, исходный вывод отвергнут** | Самовосстановление есть: второй wake-up ставит 30 s eviction, затем возможна повторная загрузка. Устойчивый outage требует повторяемой причины или постоянного traffic. |
|
||||
| Natural background-process death в production (crash / memory / CPU / jetsam) | **Не проверено в поле** | Forced crash доказывает цепочку, но не частоту и не распределение termination reasons. Нужна телеметрия до фикса. |
|
||||
| Event starvation / JSC hang / stale listener bookkeeping | **Не проверено, низкий приоритет** | Состояния возможны, но стабильного repro и production transition нет. Оставить как residual diagnostic buckets. |
|
||||
| Native messaging stall / malformed CPM config | **Не проверено, низкий приоритет** | Возможный all-tabs симптом, но подтверждённого production path нет. |
|
||||
| Critical memory pressure как конкретный триггер | **Не воспроизведено** | Ручной memory_pressure CPM не сломал. Не использовать как доказанный root cause. |
|
||||
| Kill/restart Network process как триггер | **Отвергнуто на проверенной сборке** | WebKit восстановился, CPM продолжил работать. |
|
||||
| removeAllUserScripts() удаляет WebExtension scripts | **Отвергнуто** | WebKit использует RemoveWebExtensions::No. |
|
||||
| Feature-flag transition, scriptlet reload, обычный long-running JS страницы | **Отвергнуто как объяснение all-tabs outage** | Другой extension context или tab-local execution. |
|
||||
| Safari SW registration DB fix | **Нерелевантно** | Extension SW registration не хранится как обычная persistent registration. |
|
||||
| Forced death background process → persistent all-tabs CPM outage | **Подтверждено воспроизведением** | Kill background PID ломает CPM; reload и новая вкладка не помогают, Fire восстанавливает. |
|
||||
| Natural crash / memory / CPU termination в production | **Не проверено** | Механизм доказан, реальная частота и связь с outage требуют M3/M4. |
|
||||
| Close Other Tabs forget-ит retained tab | **Подтверждено и исправлено в M1** | Сохраняется тот же экземпляр Tab без ложного didCloseTab. |
|
||||
| Duplicate Tab из popup переносит original | **Подтверждено и исправлено в M1** | В main window уходит копия, source остаётся отдельным объектом. |
|
||||
| Закрытие окна forget-ит shared pinned/other-owned tabs | **Подтверждено и исправлено в M1** | Закрываются только tabs, реально принадлежащие закрываемому окну. |
|
||||
| Нестабильный WebExtension base URL | **Исправлено и локально проверено в M2** | URL детерминирован по identifier и сохраняется между load/reload; production migration/storage validation остаётся rollout-задачей. |
|
||||
| Startup/session-restore content-script miss | **Подтверждено как transient** | Не объясняет устойчивый all-tabs outage. |
|
||||
| Failed background load навсегда застревает в m_backgroundWebView | **Исходный вывод отвергнут** | WebKit способен повторно загрузить background; устойчивый outage требует повторяемой причины/traffic. |
|
||||
| Event starvation / JSC hang / native messaging stall | **Не проверено** | Оставлено в diagnostic buckets M3. |
|
||||
| Critical memory pressure как конкретный trigger | **Не воспроизведено** | Не считать root cause без production correlation. |
|
||||
| Network process restart как trigger | **Отвергнуто** | На проверенной сборке CPM восстановился. |
|
||||
| removeAllUserScripts удаляет WebExtension scripts | **Отвергнуто** | WebKit использует RemoveWebExtensions::No. |
|
||||
| Feature-flag transition / scriptlet reload / обычный page JS как all-tabs cause | **Отвергнуто** | Это другой context или tab-local execution. |
|
||||
|
||||
## Текущий worktree
|
||||
## Worktrees
|
||||
|
||||
Сейчас в одной незавершённой пачке смешаны:
|
||||
Все созданы от локального `origin/main` `570c2f8ef4`. Коммитов нет. Исходный `cpm-breakage-pixel` не изменялся.
|
||||
|
||||
- diagnostic model/recorder, pixel parameters и hooks в health monitor/manager;
|
||||
- background WKNavigationDelegate proxy, process-death/hang timeline;
|
||||
- graveyard treatment и два feature flags/cohorts;
|
||||
- debug notifications/menu, crash simulations, большие UI tests и локальная repro HTML;
|
||||
- посторонние/временные изменения (NSObject deallocation assertion выключен, Package.resolved bump, правило UI tests, accessibility refactor).
|
||||
|
||||
**Состояние:** код не считать готовым к PR; в этой сессии он не собирался и тесты не запускались. Отдельный blocker: proxy уже сам менял lifetime/timing и в наблюдении предотвращал repro. В текущем коде install сохраняет associated object на original delegate, а isInstalled/uninstall ищут его на webView — это надо исправить и покрыть тестом до PR. Поэтому текущий control не является доказанно нейтральным baseline.
|
||||
|
||||
## Milestones
|
||||
|
||||
Два трека:
|
||||
|
||||
- **Correctness:** M1 tabs и M2 stable base URL независимы друг от друга и от graveyard.
|
||||
- **All-tabs:** M3 telemetry → M4 production validation → M5 graveyard → M6 A/B выполняются последовательно; M5/M6 лучше делать stacked на M3.
|
||||
|
||||
### M0 — Разобрать worktree
|
||||
|
||||
**Цель:** получить чистые группы изменений без изменения поведения.
|
||||
|
||||
- Убрать из feature stack выключенный NSObject assertion, случайный Package.resolved bump и изменение repo rule.
|
||||
- Debug menu/notifications/repro HTML оставить локально или вынести в отдельный debug-tooling PR.
|
||||
- Большие AutoconsentUITests, accessibility cleanup и restart(beforeLaunch:) вынести в test-infrastructure PR; оставить только детерминированные regression tests.
|
||||
|
||||
**Done:** каждый оставшийся diff однозначно относится к одному milestone; нет смешанных production/debug/test-infrastructure изменений.
|
||||
|
||||
### M1 — Исправить tab lifecycle
|
||||
|
||||
Один общий PR допустим только при введении общего lifecycle API; иначе лучше три небольших feature PR.
|
||||
|
||||
1. **Close Other Tabs:** заменить перегруженный removeAll(andAppend:) на removeAll(except:); retained Tab не получает didCloseTab.
|
||||
2. **Duplicate Tab from popup:** создавать новый Tab/tabCopy и переносить копию; один Tab/WKWebView не должен принадлежать двум коллекциям.
|
||||
3. **Shared pinned tabs on window close / Merge All Windows:** didCloseWindow не должен закрывать shared pinned Tab, который остаётся видимым в другом окне.
|
||||
|
||||
**Validation:**
|
||||
|
||||
- Unit tests проверяют identity и точную последовательность didOpenTab/didCloseTab/didCloseWindow, а не только массив и selection.
|
||||
- Close Other Tabs: retained loaded tab остаётся известен WebKit; CMP работает после последующей navigation/reload.
|
||||
- Popup Duplicate: source и duplicate — разные объекты; закрытие popup не ломает CPM в main window.
|
||||
- Shared pinned: закрытие второго окна и Merge All Windows не дают tab not found; pinned tab работает без открытия нового окна.
|
||||
- Negative coverage: реально закрытые tabs по-прежнему получают ровно один didCloseTab.
|
||||
- Diagnostic signature после фикса: tab_known_to_webkit=true, tab_controller_matches_context=true, нет content_script_send_error=tab_not_found.
|
||||
|
||||
**Done:** три repro проходят; обычные open/close/move/Fire сценарии не меняют баланс lifecycle events.
|
||||
|
||||
### M2 — Stable WebExtension base URL
|
||||
|
||||
**Цель:** задать context.baseURL = webkit-extension://<uniqueIdentifier>/ до WKWebExtensionController.load(context), чтобы после первой миграции WebKit больше не запускал rename origin на каждом load.
|
||||
|
||||
**Validation matrix:**
|
||||
|
||||
- Fresh install: сразу используется stable base URL; CPM/background/content scripts загружаются.
|
||||
- Existing install с предыдущим random base URL: ровно одна migration/rename, затем stable URL сохраняется между relaunch/reload.
|
||||
- Повторные cold launches: rename origin больше не вызывается; context достигает safe-to-inject раньше.
|
||||
- storage.local и extension preferences переживают миграцию; отдельно проверить, что web-origin storage либо переносится, либо осознанно не используется.
|
||||
- Extension update/reinstall, explicit reload и Fire: identifier/base URL соответствуют ожидаемой semantics и не сталкиваются между extension contexts.
|
||||
- Session restoration: детерминированный lifecycle test подтверждает, что load завершён до addInjectedContent; небольшой manual stress run подтверждает отсутствие пропуска CPM на restored document. Не делать flaky тест с десятками случайных relaunch обязательным CI gate.
|
||||
|
||||
**Telemetry/validation:** временно логировать или считать reason/duration base-URL migration только в internal build; сравнить cold-load timing до/после. Не отправлять URL или identifier в pixel.
|
||||
|
||||
**Done:** one-time migration подтверждена, повторных rename нет, storage не потерян, restored CMP page работает без ручного reload.
|
||||
|
||||
### M3 — CPM failure-attribution telemetry
|
||||
|
||||
Самостоятельный production PR без graveyard и без recovery behavior.
|
||||
|
||||
- CPMMessagingDiagnostics с короткими PII-free/bucketed параметрами.
|
||||
- Diagnostics на cpmInitializationFailed и cpmMessagingStuck.
|
||||
- Passive facts: context errors, background view count/alive, WebProcess/NetworkProcess state, tab known to WebKit, native handler и controller/context match.
|
||||
- Ordered background_events только если observation не меняет lifetime.
|
||||
- Manager hooks, macOS wiring, pixel definitions/privacy review и unit tests на bucketing/sanitization.
|
||||
|
||||
**Критический design constraint:** delegate proxy не должен сам удерживать WKWebView/WebPageProxy. В текущем WIP proxy уже менял timing и скрывал repro; кроме того, associated object сохраняется на original delegate, а ищется на webView.
|
||||
|
||||
**Done:** forced crash классифицируется как died_* → error_background_failed_to_load при network_process_restarted=false; tab repro классифицируется отдельно как tab_not_found.
|
||||
|
||||
### M4 — Production validation milestone
|
||||
|
||||
Без feature fix. Дождаться достаточного объёма одного релиза telemetry.
|
||||
|
||||
Ответить данными:
|
||||
|
||||
- встречаются ли реальные died_memory_limit / died_cpu_limit / died_crash;
|
||||
- следует ли за ними BackgroundContentFailedToLoad/stuck signature;
|
||||
- какова доля per-tab tab_not_found относительно all-tabs failures;
|
||||
- есть ли другие dominant buckets, которые graveyard не исправит.
|
||||
|
||||
**Go/No-Go:** переходить к graveyard rollout только если в production видна связка process death → all-tabs failure. Forced kill подтверждает механизм, но сам по себе не доказывает ценность rollout.
|
||||
|
||||
### M5 — Background WebView graveyard mitigation
|
||||
|
||||
Stacked на M3; implementation/failsafe PR без включённого массового эксперимента.
|
||||
|
||||
- Forwarding delegate proxy для termination callback.
|
||||
- Bounded hold dead WKWebView, затем гарантированный release.
|
||||
- Независимый runtime kill switch, сначала выключен.
|
||||
- Не более одного retained view на context; cleanup при unload/reload; защита от повторных deaths и retain cycles.
|
||||
- Tests: public/private termination callback forwarding, reason mapping, immediate control release, treatment timeout, unload cleanup, repeated termination.
|
||||
|
||||
**Обязательная валидация:** proxy-only control должен стабильно воспроизводить исходный kill -9 failure. Если proxy сам лечит race, control не нейтрален и реализацию нельзя использовать для A/B.
|
||||
|
||||
**Done:** с одинаковым observation path control ломается, treatment восстанавливается; разница создаётся только hold duration.
|
||||
|
||||
### M6 — Graveyard A/B experiment
|
||||
|
||||
Отдельный stacked PR: cohorts, outcome measurement и rollout.
|
||||
|
||||
- control: тот же proxy/observation path, немедленный release.
|
||||
- treatment: bounded graveyard hold.
|
||||
- Stable cohort assignment; kill switch не зависит от cohort.
|
||||
- Denominator pixel на каждую background-process termination: cohort + bucketed reason.
|
||||
- Outcome после следующей CPM navigation или ограниченного окна: healthy, background_failed_to_load, stuck, recovered.
|
||||
- Primary metric: доля background_failed_to_load|stuck после termination по cohort.
|
||||
- Guardrails: app/WebKit crashes, retained-view count, recovery latency, repeated termination loops.
|
||||
|
||||
background_events на stuck pixel полезен для диагностики, но не является A/B denominator: успешный treatment иначе не попадёт в выборку.
|
||||
|
||||
**Rollout:** internal/review → небольшой 50/50 production sample → проверить denominator и cohort balance → расширять только после достаточного числа termination events.
|
||||
|
||||
**Done:** статистически и технически подтверждено снижение post-termination failures без ухудшения guardrails; после решения о rollout эксперимент удаляется или treatment превращается в обычный remotely controlled fix.
|
||||
|
||||
## Предлагаемая PR-карта
|
||||
|
||||
| PR | Milestone | Зависимость |
|
||||
| Milestone | Branch / worktree | Статус |
|
||||
|---|---|---|
|
||||
| Tab lifecycle API + Close Other Tabs | M1 | independent |
|
||||
| Popup Duplicate Tab identity fix | M1 | возможно stacked на lifecycle API |
|
||||
| Shared pinned window-close fix | M1 | возможно stacked на lifecycle API |
|
||||
| Stable WebExtension base URL | M2 | independent |
|
||||
| CPM attribution telemetry | M3 | independent |
|
||||
| Graveyard mitigation/failsafe | M5 | stacked на telemetry |
|
||||
| Graveyard A/B wiring | M6 | stacked на mitigation |
|
||||
| Debug/repro tooling | M0 | отдельный optional PR или local-only |
|
||||
| M0 debug/repro | `codex/cpm-debug-tooling` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-debug-tooling` | **Готово:** local-only HTTP iframe churn fixture; localhost HTTP 200. |
|
||||
| M1 tab lifecycle | `codex/cpm-tab-lifecycle-fixes` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-tab-lifecycle-fixes` | **Готово:** три lifecycle fix; 4 targeted tests и macOS build проходят. |
|
||||
| M2 stable base URL | `codex/cpm-stable-base-url` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-stable-base-url` | **Готово:** 11/11 tests; iOS/macOS builds проходят. |
|
||||
| M3 attribution telemetry | `codex/cpm-failure-telemetry` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-failure-telemetry` | **Готово к PR:** passive PII-free facts, forwarding proxy, runtime kill switch; 23/23 tests; schemas iOS/macOS валидны. |
|
||||
| M4 production validation | `codex/cpm-production-validation` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-production-validation` | **Ожидает production data:** runbook готов; начинать после одного telemetry release. |
|
||||
| M5 graveyard mitigation | `codex/cpm-graveyard-mitigation` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-graveyard-mitigation` | **Готово к PR, выключено по умолчанию:** максимум один WKWebView, bounded 3 s hold, unload/reload cleanup, runtime disable; 18/18 tests; iOS/macOS builds проходят. |
|
||||
| M6 graveyard A/B | `codex/cpm-graveyard-ab` — `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-graveyard-ab` | **Готово к PR:** control/treatment, stable cohort, denominator/outcome pixels, bounded pending queue; 30/30 tests; iOS/macOS builds и pixel validators проходят. |
|
||||
|
||||
Во всех семи worktree проходит `git diff --check`; случайный `Package.resolved` churn удалён.
|
||||
|
||||
## PR topology
|
||||
|
||||
Независимые PR от `origin/main`:
|
||||
|
||||
1. M0 — optional/local tooling.
|
||||
2. M1 — tab lifecycle fixes.
|
||||
3. M2 — stable base URL.
|
||||
4. M3 — production telemetry.
|
||||
|
||||
Последовательный feature stack:
|
||||
|
||||
```
|
||||
M3 telemetry
|
||||
├── M4 production validation gate
|
||||
└── M5 graveyard mitigation
|
||||
└── M6 A/B experiment
|
||||
```
|
||||
|
||||
Поскольку коммитов пока нет, refs зависимых branches указывают на тот же base SHA, а зависимость представлена накопленным dirty diff. При оформлении: commit M3 → перенести/перебазировать M5 на M3 → M6 на M5. M4 фиксирует Go/No-Go после production данных.
|
||||
|
||||
## Rollout gates
|
||||
|
||||
- M3: privacy review, schema review, затем один production release.
|
||||
- M4: отдельно по iOS/macOS проверить termination reason → initialization failure/stuck и долю per-tab failures.
|
||||
- M5: включать только если M4 подтверждает process-death hypothesis; kill switch и graveyard по умолчанию выключены.
|
||||
- M6: internal/review → небольшой 50/50 sample → проверить cohort balance, denominator/outcome coverage и crash/retention guardrails.
|
||||
|
||||
Reference in New Issue
Block a user