[2026-09-09] eagle: work/projects/cpm-web-extension-breakage-findings.md

This commit is contained in:
Alexey Martemyanov
2026-09-09 16:01:05 +06:00
parent 2e1633cea1
commit 1e67dd66ea
@@ -814,3 +814,39 @@ Debug → Web Extensions → **Break CPM Native Messaging (unregister handlers)*
### What this does and does not prove
Proves the class produces the symptom and that the code has a path (idempotent load after unregister) that leaves the live context without handlers. Does **not** yet prove that path fires in production — that is what `native_handler_registered` in the stuck pixels and Console `No handler for extension` lines on internal machines will tell.
## Correction and a traced production path (2026-09-09, night)
### What "Break CPM Native Messaging" is and is not
It only reproduces the **end state** (context loaded, no native handlers). It does not model Fire or any other real path. Traced every `unregisterHandlers` / `willLoad` pairing for the CPM context: Fire (`unloadAllExtensions``reloadInstalledExtensions`/`loadInstalledExtensions`), `syncEmbeddedExtensions` upgrade, launch `loadAndSync`, flag-off uninstall — all re-register or unload the context together. The idempotent-load race needs a concurrent `reloadExtension(identifier:)`, which in production only the scriptlet coordinator issues, and scriptlets exist **only for `adBlockingExtension`** (`ScriptletConfigProvider.swift:70-77`). **No production path to the no-handler state was found for CPM.** The class stays as a mechanism (native failure ⇒ background disables itself, no `initResp`) with only the "handler does not answer within 20 s" variant left, which is transient.
### Traced path that does reproduce "old tabs dead, new tabs fine": controller replacement
A tab's `WKWebViewConfiguration.webExtensionController` is set once, at tab creation, from `NSApp.delegateTyped.webExtensionManager?.controller` (`WKWebViewConfigurationExtensions.swift:74-76`). The manager — and with it the `WKWebExtensionController` — is created/destroyed by the `webExtensions` feature flag (`AppDelegate.swift:2063-2127`):
- flag **off**: `handleWebExtensionsFlagDisabled``uninstallAllExtensions()` (unloads contexts from controller A) → `webExtensionManager = nil`.
- flag **on**: `initializeWebExtensions()`**new** manager, **new** controller B → `loadAndSync` loads CPM into B.
Every tab created while the manager was nil has **no controller**; every tab created before an off→on cycle keeps **controller A with zero contexts**. In both cases nothing is ever injected into those tabs again: page reload does not help, the health monitor times out on every navigation (`extensionIsLoaded` reads the *new* manager → true → 4 s grace → `initialization_failed``stuck`), new tabs/windows work (`recovered_without_reload`). This is the only chain so far that reproduces the reported window-scoped observation.
The flag is `remoteReleasable(WebExtensionsSubfeature.featureEnabled)` (`FeatureFlag.swift:612`). It changes mid-session on any privacy-config refresh that flips it: **rollout ramp** (user enters the cohort while the app is running → all existing tabs, including the restored session, never get CPM), remote kill-switch off→on, internal-user overrides. `.removeDuplicates()` on the publisher means only real flips fire, but a single flip is enough.
Also covered by the same mechanism: tabs restored at launch before the manager exists (acknowledged in the code comment at `AppDelegate.swift:2088-2089`), when the flag is off at startup and turns on later.
### Reproduction (deterministic, uses existing internal debug menu)
1. Launch with the `webExtensions` flag ON. Open two tabs on CMP sites, confirm CPM works.
2. Debug → Feature Flags → turn `webExtensions` OFF, then ON again. (`uninstallAll` → manager nil → new manager/controller, extensions reinstalled.)
3. In the two old tabs navigate to CMP sites: banners not handled, reload does not help. `initialization_failed``stuck`.
4. Open a new tab/window with a CMP site: works; `recovered_without_reload`. Return to the old tabs: still broken, new failures → new episode.
Variant: start with the flag OFF, open tabs, turn ON — the tabs opened before never get CPM.
### Diagnostic added
`tab_controller_matches_context` = `tab.webView.configuration.webExtensionController === context.webExtensionController`. `false` in a stuck pixel is this chain, unambiguously. Combined with `native_handler_registered` and the WebKit probes this now separates: controller mismatch (this) / no native handler / registration failure / dead worker / routing.
### Open question that decides the fix design
Is CPM breakage in production reported for **old tabs only** (consistent with this chain) or for **new tabs too**? The stuck/recovered pixel sequence with `tab_controller_matches_context` answers it. Fix design deferred until that is known, per agreement.