← Commit history

Stop doing the extra things to Chrome, instead of monitoring harder (John: 'you crash chrome with the way you control it ... what are you doing that's so different trying to control chrome that you cause crashes and freezes in the first place? that's where you should be looking'). TWO changes. (1) REAL LEAK FIXED, unconditional: the v1.9.78 crash detector opened a SECOND CDP session per tab and never detached it, so every tab held an extra attached debugger session for its entire life, accumulating in the browser process; puppeteer's page 'error' event already covers renderer crashes, so it is removed. (2) New minimalTouch setting (browser_configure {minimalTouch:'on'}) strips pup to open-a-window + drive-it-over-CDP and disables EVERY cosmetic/janitor action pup performs ON Chrome: taskbar identity re-stamps (which destroy+recreate the taskbar button), tab-count badges, favicon overlays, jump-list rebuilds, progress bars, auto-flash, foreground toasts, the in-page title MutationObserver (which re-fired on every SPA title write, a pup-created in-page write loop on exactly the Slides/Sheets windows that froze), and the periodic reclaim/placement/reconcile/unstick sweeps that eval into parked pages and force wakes. In minimal mode pup also NEVER kills or relaunches Chrome behind the user's back. This makes the cause measurable: if freezes persist with it on, pup's window machinery is not the cause and I look elsewhere; if they stop, it was.

John Lauer ·cbaee454e2 ·3mo ago ·parent 9ffd4e3
4 files changed +76−19
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.184",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.184/adom-bridge-puppeteer-v1.9.184.zip",-  "sha256": "2a602cbb59e9b31db91d30276f925b678854396f3206cb1ce56d699e716c03ab",-  "size": 420729,+  "version": "1.9.185",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.185/adom-bridge-puppeteer-v1.9.185.zip",+  "sha256": "326a2f6ffe00cdbc3db5bfe2c0ee41c334a5ebce3345f03b9d5379e41eadfdeb",+  "size": 422512,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.184+1.9.185
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.184",+  "version": "1.9.185",   "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
src/server.js+70−13
@@ -453,14 +453,14 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user     } catch (e) {}   };   try { page.on('error', (err) => _markCrashed((err && err.message) || 'Page crashed!')); } catch (e) {}-  try {-    // Belt-and-braces at the protocol level — Inspector.targetCrashed fires even when the higher-    // level 'error' event does not (e.g. an OOM-killed renderer).-    page.target().createCDPSession().then((cdp) => {-      try { cdp.on('Inspector.targetCrashed', () => _markCrashed('Inspector.targetCrashed')); } catch (e) {}-      try { cdp.send('Inspector.enable').catch(() => {}); } catch (e) {}-    }).catch(() => {});-  } catch (e) {}+  // v1.9.185 (John: "what are you doing that's so different controlling Chrome that you cause+  // crashes?" — this was one real answer): the v1.9.78 crash detector opened a SECOND CDP session+  // per tab and NEVER detached it. Every tab therefore held an extra attached debugger session for+  // its whole life, accumulating in the browser process — cross-process state your normal Chrome+  // never carries. puppeteer's page 'error' event already fires on renderer crash (it is driven by+  // the same Inspector.targetCrashed on the page's OWN session), so the extra session bought+  // nothing. Removed: one less permanent CDP attachment per tab, and one less thing pup does TO+  // Chrome that Chrome was not designed to have done to it continuously.   // v1.8.17: PERSISTENT title-suffix injector. AD's browser_raise/lower_os_window   // (AD-core verbs, not this bridge — they shadow our fallback handlers) locate the   // OS window by its " (session: <id>)" title suffix. Post-goto page.evaluate re-adds@@ -470,7 +470,10 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user   // enforcer at document-start on EVERY navigation (before page scripts), so the   // suffix reappears the instant the page sets its title — closing that gap.   try {-    page.evaluateOnNewDocument((sid) => {+    page.evaluateOnNewDocument((sid, minimal) => {+      // v1.9.185: carry minimal-touch into the page so the title observer below can skip installing+      // itself (see the note at `observe`). Read at document-start, before any page script runs.+      try { if (minimal) window.__pupMinimalTouch = true; } catch (e) {}       // TOLERANT marker check (v1.8.19): a tab's own suffix may be       // " (session: <sid>)" OR " (session: <sid> | <tabId>)". This injector must       // only ADD a marker when NONE is present — if it enforced an exact@@ -512,12 +515,19 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user       const observe = () => {         const t = document.querySelector('title') || document.head || document.documentElement;         if (!t) return false;+        // v1.9.185 minimal-touch: in MINIMAL mode we stamp the title ONCE and do NOT install a+        // MutationObserver. On a heavy SPA (Google Slides/Sheets rewrite document.title constantly)+        // this observer re-fired on every one of those writes, each time writing the title BACK —+        // an in-page write loop pup itself created, on the exact windows John saw freeze. The+        // window still carries the tag from the initial stamp; only the continuous re-enforcement+        // is dropped. (window.name, set below, is the durable identity anyway.)+        if (window.__pupMinimalTouch) return true;         try { new MutationObserver(ensure).observe(t, { childList: true, subtree: true, characterData: true }); } catch (e) {}         return true;       };       ensure();       if (!observe()) document.addEventListener('DOMContentLoaded', () => { observe(); ensure(); });-    }, sessionId).catch(() => {});+    }, sessionId, minimalTouch()).catch(() => {});   } catch (e) {}   // v1.8.21: install the shadow-DOM + scroll helpers on every document so browser_eval   // can use $deep / $$deep / deepText without hand-rolling a shadow-root walk.@@ -2506,6 +2516,30 @@ function savePupSettings(patch) {   return cur; } +// v1.9.185 — MINIMAL-TOUCH MODE (John, 2026-07-29: "you crash chrome with the way you control it …+// what are you doing that's so different trying to control chrome that you cause crashes and freezes+// in the first place? that's where you should be looking").+//+// He is right that the honest experiment is to STOP DOING THE EXTRA THINGS, not to monitor harder.+// Everything pup does to a window BEYOND opening it and driving CDP is cosmetic/管理 machinery that a+// normal Chrome window never experiences, and all of it is cross-process traffic that synchronizes+// with Chrome's UI thread or forces throttled renderers awake:+//   • taskbar identity re-stamps (AUMID changes DESTROY + RECREATE the taskbar button)+//   • tab-count badges + favicon overlays (per-window art pushed through AD → shell)+//   • jump-list rebuilds (every CommitList re-creates the menu)+//   • progress bars, auto-flash, foreground toasts+//   • title re-stamping via an in-page MutationObserver that re-fires on EVERY SPA title change+//   • periodic sweeps that eval into parked/throttled pages (forcing wakes Chrome wanted to avoid)+//   • the unstick real-click, watchdog kills, adoption re-parks+// minimalTouch:'on' keeps ONLY what the user actually asked pup for — windows open in the background,+// drivable over CDP, logins accumulating in the shared profile — and disables every item above.+// If freezes/crashes persist with it on, the cause is NOT pup's window machinery and I must look+// elsewhere; if they stop, this machinery WAS the cause. Either answer is worth more than more+// monitoring. Default OFF (existing behaviour) until measured.+function minimalTouch() {+  try { return pupSettings().minimalTouch === 'on'; } catch (e) { return false; }+}+ const PUP_APP_ID = 'Adom.Pup'; const PUP_DISPLAY_NAME = 'Adom Pup'; // v1.9.111 (John's ask) — SAY WHICH MODE the taskbar icon is showing, in the jump-list header row.@@ -2912,6 +2946,7 @@ async function setWikiAuthedState(session, sessionId, authed, opts = {}) { // per-window menu would be ambiguous) and only for Adom (adom.inc) URLs; any other URL clears the // menu. Self-gates off if AD lacks the verb. Idempotent per (session, view) via _wikiJumplistView. async function updateWikiJumplist(session, sessionId, opts) {+  if (minimalTouch()) return;   // v1.9.185 minimal-touch: no cosmetic/shell machinery pushed at Chrome   if (process.platform !== 'win32' || _identitySupported !== true || _jumplistSupported === false) return;   if (!session || pupSettings().taskbarGrouping === 'grouped') return;   // v1.9.61: callers may pass the appId EXPLICITLY. stampPupIdentity brands the header BEFORE it@@ -3102,6 +3137,7 @@ const BUSY_IDLE_MS = 2500; // on mac" — it does; it just cannot address what pup needs to address. The mac equivalent of this // cue is browser_alert_window's toast-and-unhide path, which IS per-window. async function setSessionProgress(sessionId, busy) {+  if (minimalTouch()) return;   // v1.9.185 minimal-touch: no taskbar progress bars   // v1.9.152 (John's audit columns): count progress-bar triggers + total ms it ran, per session.   try {     const _ps = sessions.get(sessionId);@@ -3123,6 +3159,7 @@ async function setSessionProgress(sessionId, busy) { // just appeared — never silently push a window onto their screen. Best-effort; debounced per session. const _fgToastAt = new Map(); async function toastForeground(sessionId, reason) {+  if (minimalTouch()) return;   // v1.9.185 minimal-touch: no cosmetic/shell machinery pushed at Chrome   try {     if (process.platform !== 'win32') return;     const now = Date.now();@@ -3269,6 +3306,7 @@ async function updateThumbnailTooltip(session, sessionId) { let _thumbTooltipSupported = null;  async function updateTabCountBadge(session, sessionId) {+  if (minimalTouch()) return;   // v1.9.185 minimal-touch: no cosmetic/shell machinery pushed at Chrome   if (process.platform !== 'win32' || _identitySupported !== true) return; // overlay busy with brand badge on old AD   // v1.9.31: a tab verb just ran — cheap moment to notice a drag-out (event-driven, not a scan).   drainWindowOpenedEvents().catch(() => {});@@ -3353,6 +3391,7 @@ async function updateTabCountBadge(session, sessionId) { // strand a window un-resolvable) then re-stamps it. Runs at startup (post-reattach) and after // every rescan. async function brandSweep(reason) {+  if (minimalTouch()) return;   // v1.9.185 minimal-touch: no cosmetic/shell machinery pushed at Chrome   // v1.9.41 (audit): re-stamp CATEGORY icons for every live session. Windows that predate the   // category work, or that were recovered across a bridge restart, kept the plain default tile   // forever because nothing re-evaluated them. Found by auditing my own taskbar: a long-lived@@ -4224,6 +4263,7 @@ async function retagSessionTitle(page, sessionId, profileName) { // refresh silently no-ops and leaves a STALE overlay: after a drag-out the remaining window kept // showing the dragged site's favicon even though that tab was gone. async function refreshWindowChrome(session, sessionId, tries = 5) {+  if (minimalTouch()) return false;   // v1.9.185 minimal-touch: no identity/badge re-stamps   if (process.platform !== 'win32' || !session) return false;   for (let i = 0; i < tries; i++) {     let ok = false;@@ -5129,6 +5169,7 @@ function osClearFlashByActivate(pid, cdpPort) { // {stop:true} clears it; otherwise a finite 4-count flash. Returns AD's real result so // callers can report truthfully (no more cleared:true lies). async function flashViaAD(sessionId, opts) {+  if (minimalTouch()) return;   // v1.9.185 minimal-touch: no cosmetic/shell machinery pushed at Chrome   opts = opts || {};   if (process.platform !== 'win32' || !sessionId) return null;   const args = opts.stop@@ -7719,6 +7760,16 @@ const server = http.createServer(async (req, res) => {             sendJSON(res, { success: false, error: `autoLogin must be one of on|off` });             return;           }+          // v1.9.185: minimalTouch on|off — see the minimalTouch() note. 'on' strips pup down to+          // open-a-window + drive-it-over-CDP, disabling every cosmetic/janitor action pup performs+          // ON Chrome (taskbar identity/badges/jump lists/progress/flash/toasts, the in-page title+          // observer, and the periodic reclaim/placement/reconcile/unstick sweeps).+          const mt = args.minimalTouch;+          if (mt !== undefined && !['on', 'off'].includes(mt)) {+            sendJSON(res, { success: false, error: `minimalTouch must be one of on|off` });+            return;+          }+          if (mt) { savePupSettings({ minimalTouch: mt }); console.log(`[configure] minimalTouch -> ${mt}`); }           if (ri) { savePupSettings({ reportIssues: ri }); console.log(`[configure] reportIssues -> ${ri}`); }           if (al) { savePupSettings({ autoLogin: al }); console.log(`[configure] autoLogin -> ${al}`); }           if (aa) {@@ -10448,7 +10499,7 @@ server.listen(PORT, BIND_HOST, async () => {   // v1.9.47: reclaim on boot (catches windows orphaned by a crash/restart) and then as a slow safety   // net, so an unbranded window can never persist even if its window-opened event was missed.   setTimeout(() => { reclaimUnmanagedWindows().catch(() => {}); }, 12000);-  setInterval(() => { reclaimUnmanagedWindows().catch(() => {}); }, 90000);+  setInterval(() => { if (minimalTouch()) return; reclaimUnmanagedWindows().catch(() => {}); }, 90000);   // v1.9.179 — TAB RECONCILER. Prune dead/detached and duplicate tab entries so the taskbar count   // badge reflects the REAL live tabs (rapid reconnects had inflated 1-tab windows to "4") and the   // favicon overlay reads from a LIVE active tab instead of a detached phantom. Runs on a gentle@@ -10457,7 +10508,7 @@ server.listen(PORT, BIND_HOST, async () => {   // reconnects, and every rescan already reconciles inline — the patrol is just a backstop, and a   // hot one multiplies any probe-judgement bug across every window (today's lesson).   setTimeout(() => { for (const [sid, s] of [...sessions]) reconcileSessionTabs(s, sid).catch(() => {}); }, 8000);-  setInterval(() => { for (const [sid, s] of [...sessions]) reconcileSessionTabs(s, sid).catch(() => {}); }, 300000);+  setInterval(() => { if (minimalTouch()) return; for (const [sid, s] of [...sessions]) reconcileSessionTabs(s, sid).catch(() => {}); }, 300000);   // v1.9.171 — CRASH SELF-HEAL. The shared Chrome dies two ways on heavy/hostile pages (measured   // repeatedly on the Autodesk sign-in flow), and pup must recover from BOTH without a human:   //@@ -10483,6 +10534,10 @@ server.listen(PORT, BIND_HOST, async () => {   const HEAL_COOLDOWN_MS = 5 * 60 * 1000;   setInterval(async () => {     if (process.platform !== 'win32') return;+    // v1.9.185 minimal-touch: NEVER kill or relaunch the user's Chrome behind their back. John:+    // "you're just slapping monitoring on top of it so you can fucking restart it on me which is+    // infuriating." In minimal mode pup reports problems and lets the user decide.+    if (minimalTouch()) return;     // (2) HUNG detection — probe each LIVE browser entry; on 2 misses, tear it down so its sessions     // become `_lostBrowser` and fall into the relaunch path below.     for (const [profileName, entry] of [...browsers]) {@@ -10545,6 +10600,8 @@ server.listen(PORT, BIND_HOST, async () => {     // missing primitive behind "pup keeps yanking my windows" (PLACEMENT-INVARIANT item 1, John's     // design, finally implemented); (b) run the stuck-occlusion unstick on that ONE window only.     try {+      // v1.9.185 minimal-touch: no foreground polling, no unstick, no autonomous clicks.+      if (minimalTouch()) throw new Error('minimal-touch');       if (sessions.size > 0) {         const fg = osGetForegroundWindow();         if (fg) {@@ -10597,7 +10654,7 @@ server.listen(PORT, BIND_HOST, async () => {     }   };   setTimeout(() => { sweepPlacement('startup').catch(() => {}); }, 15000);-  setInterval(() => { sweepPlacement('periodic').catch(() => {}); }, 120000);+  setInterval(() => { if (minimalTouch()) return; sweepPlacement('periodic').catch(() => {}); }, 120000);    // v1.9.89: reap ORPHANED pup chrome/renderers — any process under a pup-profiles dir whose profile   // has no live session (a previous bridge crash left them, or renderers that outlived a close and are