← Commit history

Jam-proof status verbs: browser_list_windows and browser_status now gather sessions in PARALLEL with a hard 2.5s per-session timeout and an in-memory fallback, so one busy window (Chrome under heavy load on a heavy SPA) can no longer hang the whole handler into a null response (the 'pup looks frozen' symptom). getSessionInfo caps its per-window CDP reads (title + focus eval) and caches last-known url/title; busy windows return marked degraded with last-known state instead of being dropped. Reproduced live: 6 heavy windows + memory pressure drove list_windows to null before; this returns the full list every time.

John Lauer ·af38094462 ·3mo ago ·parent 23442ed
4 files changed +66−18
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.171",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.171/adom-bridge-puppeteer-v1.9.171.zip",-  "sha256": "5f378f2be0d8d06145e1f1c20f4c38a7f89b65adbc64d05440a8cf05424fb9ea",-  "size": 409244,+  "version": "1.9.172",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.172/adom-bridge-puppeteer-v1.9.172.zip",+  "sha256": "235997b3a41e9b32935a4dc2e5ef02693010d788a4bb8669fbc136bb2b04dc4b",+  "size": 410565,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.171+1.9.172
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.171",+  "version": "1.9.172",   "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+60−12
@@ -6527,12 +6527,42 @@ function readBody(req) {   }); } +// v1.9.172 (jam fix): a pure IN-MEMORY snapshot of a session — NO CDP, so it can never hang.+// browser_list_windows falls back to this when a window is too busy (Chrome under load) to answer+// a live CDP read in time, so the list ALWAYS returns instead of stalling to null ("pup looks+// frozen"). Values come from the last successful read cached on the session.+function getSessionInfoFast(sessionId, session) {+  return {+    sessionId,+    url: session._lastUrl || null,+    title: session._lastTitle || null,+    active: sessionId === activeSessionId,+    errorCount: session.errors ? session.errors.length : 0,+    profile: session.profileName,+    tabCount: session.tabs ? session.tabs.length : 0,+    activeTabId: session.activeTabId,+    overlay: session._overlayDesc || (session._appOverlay ? 'favicon' : 'none'),+    flashCount: session._flashCount || 0,+    progressBarCount: session._progressCount || 0,+    owner: session.owner || null,+    ageMinutes: session.createdAt ? Math.round((Date.now() - session.createdAt) / 60000) : null,+    lastAgentUpdate: session._lastAgentUpdate || null,+    lastForeground: session._lastForeground || null,+    degraded: true,+    degradedReason: 'this window was busy (Chrome under heavy load) so pup returned its LAST-KNOWN state instead of stalling — the window is NOT crashed; retry shortly for live values',+  };+}+ async function getSessionInfo(sessionId, session) {   let url = null, title = null;   try {-    url = session.page?.url() || null;-    title = session.page ? await session.page.title() : null;+    url = session.page?.url() || null;   // synchronous (puppeteer-cached frame URL) — never hangs+    // page.title() is a CDP round-trip that HANGS on a jammed/busy page. Cap it hard and fall back+    // to the last-known title so a single busy window can never stall browser_list_windows (v1.9.172).+    if (session.page) title = await withTimeout(session.page.title(), 1200, 'title').catch(() => session._lastTitle || null);   } catch (e) {}+  if (url) session._lastUrl = url;         // cache for the degraded/fast path+  if (title) session._lastTitle = title;   const info = {     sessionId,     url,@@ -6581,7 +6611,10 @@ async function getSessionInfo(sessionId, session) {         const flashTs = _flashState.get(key) || 0;         if (!flashTs) return 'none';         let focusTs = 0;-        try { focusTs = await session.page.evaluate(() => window.__pupFocusTs || 0); } catch (e) {}+        // v1.9.172: cap this CDP eval too — on a jammed page it would hang getSessionInfo just like+        // title() did. On timeout we can't know if the flash was cleared, so report 'pending' (safe:+        // it just means "may still be flashing", never a false 'cleared').+        try { focusTs = await withTimeout(session.page.evaluate(() => window.__pupFocusTs || 0), 800, 'flash-focus').catch(() => 0); } catch (e) {}         return (focusTs && focusTs > flashTs) ? 'cleared' : 'pending';       } catch (e) { return null; }     })(),@@ -7273,11 +7306,22 @@ const server = http.createServer(async (req, res) => {           return;         }         case 'browser_list_windows': {-          const list = [];-          for (const [id, s] of sessions) {-            list.push(await getSessionInfo(id, s));-          }-          sendJSON(res, { success: true, output: JSON.stringify({ sessions: list, count: list.length, activeSession: activeSessionId }) });+          // v1.9.172 (jam fix): gather sessions IN PARALLEL, each with a hard per-session timeout that+          // falls back to a pure in-memory snapshot. Previously this awaited getSessionInfo() SERIALLY,+          // and getSessionInfo did uncapped CDP reads (title() + a focus eval) per window — so ONE busy+          // window (Chrome under load on a heavy SPA) hung the whole handler and the CLI returned null,+          // i.e. "pup looks frozen". Now the list ALWAYS returns within ~2.5s with every window present;+          // a window too busy to answer live is marked degraded with its last-known state, never dropped.+          const entries = [...sessions];+          const list = await Promise.all(entries.map(([id, s]) =>+            withTimeout(getSessionInfo(id, s), 2500, 'getSessionInfo').catch(() => getSessionInfoFast(id, s))+          ));+          const degradedCount = list.filter(x => x && x.degraded).length;+          const out = { sessions: list, count: list.length, activeSession: activeSessionId };+          if (degradedCount) {+            out._degraded = `${degradedCount} of ${list.length} window(s) were busy (Chrome under heavy load) so pup returned their last-known state rather than stalling. pup is NOT frozen and the windows are NOT crashed — they will report live values again shortly.`;+          }+          sendJSON(res, { success: true, output: JSON.stringify(out) });           return;         }         case 'browser_configure': {@@ -9352,10 +9396,13 @@ const server = http.createServer(async (req, res) => {           return;         }         case 'browser_status': {-          const list = [];-          for (const [id, s] of sessions) {-            list.push(await getSessionInfo(id, s));-          }+          // v1.9.172 (jam fix): same resilience as browser_list_windows — parallel + per-session+          // timeout + in-memory fallback, so a busy window can never hang browser_status into null.+          const _entries = [...sessions];+          const list = await Promise.all(_entries.map(([id, s]) =>+            withTimeout(getSessionInfo(id, s), 2500, 'getSessionInfo').catch(() => getSessionInfoFast(id, s))+          ));+          const _degraded = list.filter(x => x && x.degraded).length;           const statusResult = {             ok: true,             sessionCount: sessions.size,@@ -9363,6 +9410,7 @@ const server = http.createServer(async (req, res) => {             sessions: list,             pupshotUrl,           };+          if (_degraded) statusResult._degraded = `${_degraded} of ${list.length} window(s) were busy (Chrome under load) and show last-known state — pup is NOT frozen; retry shortly for live values.`;           // Add guidance when no sessions exist so the AI knows what to do next           if (sessions.size === 0) {             statusResult.hint = 'No browser sessions are open. Use browser_open_window with sessionId, profile, and url to launch a Chrome window on the desktop before using screenshot/eval/navigate.';