diff --git a/work/projects/cpm-web-extension-breakage-findings.md b/work/projects/cpm-web-extension-breakage-findings.md index be2e3c45..71569297 100644 --- a/work/projects/cpm-web-extension-breakage-findings.md +++ b/work/projects/cpm-web-extension-breakage-findings.md @@ -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.