diff --git a/work/projects/cpm-web-extension-breakage-findings.md b/work/projects/cpm-web-extension-breakage-findings.md index 6ac5c00a..6356f5c7 100644 --- a/work/projects/cpm-web-extension-breakage-findings.md +++ b/work/projects/cpm-web-extension-breakage-findings.md @@ -661,3 +661,51 @@ No hook; `m_backgroundContentEventListeners` is not exposed. If everything else 6. `cpmBgSwRegistration` / `cpmBgSwState` via the SW-page probe — confirms 1 vs 2. SPI-adjacent (needs the background `WKWebView` reference). Items 1–4 are enough to attribute the current stuck pixels; 5–6 turn the health monitor into a classifier instead of a timer. + + +## Next steps (2026-09-09) + +Ordered so each step de-risks the next. Days are rough effort, not calendar. + +### Phase 0 — confirm trunk matches shipping WebKit (½ day, manual, no code) + +1. Debug sim persistence: enable "fail CPM background load", load one CMP page (fails), wait 35 s without navigating, load again. Expected: recovers. If it stays broken, shipping WebKit lacks the idle-eviction-on-wake-up path and chain 5/6 ranking changes. +2. Chain 1 repro: launch, wait 15 s, `sudo memory_pressure -S -l critical` for ~5 s, load a CMP page. Expected: no banner handling, `errors` empty, SW inspector target gone, recovery after 35 s idle. +3. Chain 2 repro: `kill -9` DDG's `com.apple.WebKit.Networking`, load a CMP page. Same expectation. +4. Chain 1 revive test: after step 2, in the background `WKWebView` (grab it from `didCreateBackgroundWebView` behind a debug flag) run `navigator.serviceWorker.getRegistration().then(r => r.active.postMessage({type:"ping"}))`, then load a CMP page. If CPM works again, we have a remediation that avoids the reload window. + +Outcome: which chains are real on macOS 26.x, and whether `postMessage` revive is viable. + +### Phase 1 — attribution in existing pixels (2–3 days) + +Add to `CPMMessagingHealthMonitor` failure/stuck/recovered pixels and to `BrokenSiteReport`: + +1. `cpmMemPressureCriticalRecently` / `cpmSecondsSinceCritical` — in-app `DispatchSource.makeMemoryPressureSource`. +2. `cpmNetworkProcessRestarted` — compare `WKWebsiteDataStore.default()._networkProcessIdentifier` against the value recorded at context load (SPI; fall back to OSLogStore crash-line count if SPI is off the table). +3. `cpmContextErrors` (`errors` codes + underlying NSURLError) and `cpmBgViewCreateCount` (count `didCreateBackgroundWebView`). +4. `cpmTabKnownToWebKit` (`context.openTabs.contains(tab)`) and `cpmTabNotFoundLogCount` (OSLogStore, own PID, `category == "Extensions" AND eventMessage CONTAINS "Tab not found"`). +5. `cpmContextLoadedAtCommit`, `cpmMsSinceLastReload`. + +Ship behind the existing pixel plumbing; no extension change needed. After one release the stuck pixels split into chains 1 / 2 / 3 / 4 / 5. + +### Phase 2 — probes that classify at failure time (2–3 days, needs `cpm.js` + app) + +1. `cpm.js`: publish `globalThis.__ddgCPM = { injectedAt, lifecycle, initSentAt, gotInitResp, lastSendError }`; wrap the `sendMessage` `await` in try/catch to capture `tab not found`. +2. App: on `cpmInitializationFailed`, evaluate `globalThis.__ddgCPM` in `WKContentWorld.world(name: "WebExtension-\(uniqueIdentifier)")` for the failing tab; attach `cpmContentScriptInjected`, `cpmContentScriptLifecycle`, `cpmLastSendError`. +3. App: on `cpmMessagingStuck`, run the SW-page probe on the background web view (`hasRegistration`, `active.state`) and `loadBackgroundContent` with a 2 s timeout; attach `cpmBgSwRegistration`, `cpmBgLoadProbe`. + +Outcome: health monitor becomes a classifier; every stuck pixel names its chain. + +### Phase 3 — fixes (in order of confidence) + +1. **Stable base URL** (½ day): `context.baseURL = URL(string: "webkit-extension://\(identifier)/")` in `WebExtensionLoader.makeContext`. Removes `_renameOrigin` from every load, shrinks the startup miss window (chain 3), and stops migrating extension web storage on every launch. Verify existing `storage.local` is unaffected (it is SQLite, not origin-keyed web storage) and that the first launch after the change performs one final rename from the last random URL. +2. **Self-heal on stuck** (1 day): on `cpmMessagingStuck`, if Phase 0 step 4 works → `postMessage` revive first; else `reloadExtension(identifier:, trigger: .healthRecovery)`. Gate by a 5-minute cooldown; fire `cpmMessagingRecoveredAfterExtensionReload` (already exists). +3. **Content-script retry** (½ day, `cpm.js`): if no `initResp` within ~3 s, resend `init` (max 2–3 times, backoff). Covers chain 6 and any transient drop, and gives chain 1/2 a natural retry after the 30 s eviction instead of waiting for the next navigation. +4. **Keep the worker alive** (research, 1 day): the SW server only refreshes `needsRunning()` on server-routed events. A background `chrome.alarms` tick does not go through the SW server either. Options: (a) from the SW page, periodic `registration.active.postMessage` keep-alive — would also count as "not idle" for `terminateIdleServiceWorkers`; (b) file a WebKit bug: extension service workers should be exempt from `terminateIdleServiceWorkers`/`terminateServiceWorkers`, or `WebExtensionContext` should observe worker termination and unload/reload the background view. (b) is the real fix; (a) is the interim. +5. **Tab registration audit** (only if Phase 1 shows chain 4 volume): log every `didOpenTab`/`didCloseTab` with tab id in debug builds and diff against `context.openTabs` on stuck. + +### WebKit bug reports to file (after Phase 0 confirms) + +1. Extension background service worker terminated by `terminateIdleServiceWorkers` / `terminateServiceWorkers` while `WebExtensionContext` keeps `m_backgroundContentIsLoaded == true`; messages resolve `undefined` with no error. Attach chain 1 and 2 repro steps. +2. `_loadServiceWorker` failure path does not schedule `scheduleBackgroundContentToUnload()`, so recovery depends on a second message arriving (minor). +3. Documentation/API request: a delegate callback or notification for background content load/unload and worker start/stop.