← Commit history

v2.0.251: Fix a pup window that a human click could not open (John's report). The taskbar button nudge HIDES the window for 80ms to force the shell to rebuild its button; the re-show was a separate statement in a PowerShell process node kills at 20s, so any interruption in that gap left the window IsWindowVisible=false with a live taskbar button that revealed nothing, until some later audit happened to nudge again. Reproduced live on AdomLapper. The re-show is now in a finally, the caller verifies it landed (a killed process runs no finally), and a new 120s VISIBILITY SWEEP asserts the invariant for every standing window: a human can minimize a window but never hide one, so a hidden pup window is always pup's to repair. Second bug in the same line: the re-show was always SW_SHOWNOACTIVATE, which RESTORES a minimized window, so pup silently un-minimized windows the user had put away; it now re-shows in the state it found (SW_SHOWMINNOACTIVE when minimized).

John Lauer ·ceb1f413b5 ·2mo ago ·parent e5fd22b
4 files changed +108−13
adom-bridge-puppeteer-manifest.json+5−5
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.250",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.250/adom-bridge-puppeteer-v2.0.250.zip",-  "sha256": "ef89c72355a1eef88ec11d27787f0e59bc1121765db45204d95c8fba82dfbc60",-  "size": 948180,+  "version": "2.0.251",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.251/adom-bridge-puppeteer-v2.0.251.zip",+  "sha256": "0193677b624f6cfb5c52e2cb4ffeb4a6a7197c230eb4bd31e3b1db58365a3a1e",+  "size": 950332,   "verbPrefixes": [     "browser_",     "desktop_recorder_",@@ -12,5 +12,5 @@   ],   "healthEndpoint": "/status",   "statusVerb": "browser_readiness",-  "released_at": "2026-08-19T19:28:16.909737Z"+  "released_at": "2026-08-20T10:20:56.401874Z" }
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.250\ No newline at end of file+2.0.251\ No newline at end of file
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "2.0.250",+  "version": "2.0.251",   "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. pup_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. pup_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, pup_alert_window posts a native toast whose click focuses the window, pup_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-bridge",   "author": "Adom Inc.",
src/server.js+101−6
@@ -4117,7 +4117,8 @@ async function stampPupIdentity(sessionId, opts) {           // shell does NOT recreate a taskbar button when a live window's AUMID changes — the button           // stays latched to the birth identity (default CfT) no matter how cleanly the stamp applied           // (AD replied applied:[appId,relaunchIcon,displayName,icon] and the button never moved).-          // A hide + re-show (SW_HIDE -> SW_SHOWNOACTIVATE, never activates) forces the shell to+          // A hide + re-show (SW_HIDE -> SW_SHOWNOACTIVATE, or SW_SHOWMINNOACTIVE when the user had+          // it minimized; never activates, never changes what the user chose to see) forces the shell to           // destroy and recreate the button, which reads the CURRENT thread AUMID -> the name tile           // appears (verified: the stuck "Google Chrome for Testing" button became "kicad" on nudge).           // Once per (session, appId); skipped when the window is foreground (a visible 600ms blink@@ -4686,6 +4687,40 @@ let _nudgeUntil = 0;   // v2.0.172: while set, ignore window-opened events cause // usually clicks away within seconds), then patient — a window focused for 10 straight minutes is // being actively used, and blinking it the moment they look away is the whole point of the skip. const NUDGE_RETRY_DELAYS = [6000, 15000, 45000, 120000, 300000];+// v2.0.251: THE VISIBILITY INVARIANT — "a live pup window is never invisible".+// A human can MINIMIZE a window; a human can never HIDE one. So IsWindowVisible=false on a window+// pup owns is ALWAYS pup's own doing, and re-showing it can never fight the user. That makes this+// repair unconditionally safe, which is why it can run from the periodic sweep as well as from the+// nudge callback. Re-shows with SW_SHOWNOACTIVATE (no focus steal, no z-order pop).+async function ensureWindowShown(hwnd, sessionId, why) {+  if (process.platform !== 'win32' || !hwnd) return null;+  const ps = `$s=@'+using System; using System.Runtime.InteropServices;+public static class EV {+  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);+  [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);+  [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h);+}+'@; Add-Type -TypeDefinition $s; $h=[IntPtr]${hwnd}+if(-not [EV]::IsWindow($h)){ 'GONE' }+elseif([EV]::IsWindowVisible($h)){ 'OK' }+else { [EV]::ShowWindow($h,4)|Out-Null; if([EV]::IsWindowVisible($h)){ 'RESHOWN' } else { 'RESHOW-FAILED' } }`;+  return await new Promise((resolve) => {+    try {+      execFile('powershell', ['-NoProfile', '-Command', ps], { windowsHide: true, timeout: 20000 }, (err, stdout) => {+        const r = String(stdout || '').trim();+        if (r === 'RESHOWN') {+          console.log(`[visibility] "${sessionId}" window was HIDDEN (${why}) — re-shown. A hidden window's taskbar button reveals nothing to a human click; pup must never leave one that way.`);+          try { recordCleanup(`re-showed session "${sessionId}"'s window (it had been left HIDDEN by ${why} — its taskbar button would have revealed nothing when clicked)`); } catch (e) {}+        } else if (r === 'RESHOW-FAILED') {+          console.log(`[visibility] "${sessionId}" window is hidden and would NOT re-show (${why}) — reporting rather than pretending it is fine`);+        }+        resolve(r || null);+      });+    } catch (e) { resolve(null); }+  });+}+ function nudgeTaskbarButton(hwnd, sessionId) {   if (process.platform !== 'win32' || !hwnd) return;   _nudgeUntil = Date.now() + 6000;@@ -4695,12 +4730,41 @@ public static class SW {   [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);   [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();   [DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr h, uint m, IntPtr w, IntPtr l);+  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);+  [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h); }-'@; Add-Type -TypeDefinition $s; if([SW]::GetForegroundWindow() -eq [IntPtr]${hwnd}){ 'SKIP-FG' } else { [SW]::ShowWindow([IntPtr]${hwnd},0)|Out-Null; Start-Sleep -Milliseconds 80; [SW]::ShowWindow([IntPtr]${hwnd},4)|Out-Null; 'NUDGED' }`;+'@; Add-Type -TypeDefinition $s; $h=[IntPtr]${hwnd}+if([SW]::GetForegroundWindow() -eq $h){ 'SKIP-FG' } else {+  # v2.0.251 (John: "why can't i open this pup window? ... when i go to click on it as a human you+  # fuck me over"). ROOT CAUSE: this nudge HIDES the user's window, and the re-show was a separate+  # statement in a process node kills at 20s. Reproduced live: after the HIDE half alone the window+  # is IsWindowVisible=false — its taskbar button reveals NOTHING to a human click, for minutes,+  # until some later audit cycle happened to nudge again. So the re-show is now in a FINALLY: the+  # window comes back even if this script throws, and (belt and braces) the caller verifies too.+  # SECOND BUG, same line: the old re-show was always SW_SHOWNOACTIVATE (4), which RESTORES a+  # minimized window — so pup silently un-minimized windows the user had put away (John's window+  # went minimized -> normal on its own while I was probing it). Re-show in the state we found it:+  # 7 = SW_SHOWMINNOACTIVE for a window the user had minimized, 4 otherwise. pup's taskbar+  # bookkeeping must never change what the user chose to see.+  $wasMin = [SW]::IsIconic($h)+  try { [SW]::ShowWindow($h,0)|Out-Null; Start-Sleep -Milliseconds 80 }+  finally { [SW]::ShowWindow($h, $(if($wasMin){7}else{4}))|Out-Null }+  if([SW]::IsWindowVisible($h)){ 'NUDGED' } else { 'NUDGED-STILL-HIDDEN' }+}`;   try {     execFile('powershell', ['-NoProfile', '-Command', ps], { windowsHide: true, timeout: 20000 }, (err, stdout) => {       const r = String(stdout || '').trim();       console.log(`[thread-tile] "${sessionId}" button nudge: ${err ? 'ERR ' + err.message.slice(0, 80) : r}`);+      // v2.0.251: the nudge hid the user's window; PROVE it came back. The PS finally clause covers a+      // throw, but node KILLS this process at 20s (an Add-Type compile on a loaded box can reach that)+      // and a killed process runs no finally — that is the path that stranded John's window invisible+      // behind a live taskbar button. Verify from here, where the kill cannot reach us.+      if (err || r === 'NUDGED-STILL-HIDDEN') {+        ensureWindowShown(hwnd, sessionId, err ? 'a nudge that was killed or errored mid-sequence' : 'a nudge whose re-show did not take').catch(() => {});+      }+      // The button WAS rebuilt in the still-hidden case (the hide/show ran) — only the re-show+      // failed, and we just repaired that. So downstream bookkeeping treats it as a normal NUDGED.+      const rN = (r === 'NUDGED-STILL-HIDDEN') ? 'NUDGED' : r;       // v2.0.227 (John's icon audit found fusion-dashboard grouped under the stock "Google Chrome for       // Testing" button while wearing stale teal art): a SKIP-FG was a DEAD END. Windows never rebuilds       // a taskbar button on an in-place AUMID change without this hide/show, so a window that merely@@ -4709,7 +4773,7 @@ public static class SW {       // that change. Now the skip is a PENDING state with a bounded backoff retry; the PS itself       // re-checks foreground, so a retry while still focused simply skips again and re-queues.       const _sk = sessions.get(sessionId);-      if (!err && r === 'SKIP-FG' && _sk) {+      if (!err && rN === 'SKIP-FG' && _sk) {         const at = (_sk._nudgePending && _sk._nudgePending.attempts || 0) + 1;         if (at <= NUDGE_RETRY_DELAYS.length) {           _sk._nudgePending = { since: (_sk._nudgePending && _sk._nudgePending.since) || Date.now(), attempts: at, hwnd };@@ -4725,8 +4789,8 @@ public static class SW {           console.log(`[thread-tile] "${sessionId}" nudge still skipped after ${NUDGE_RETRY_DELAYS.length} retries — the window has been focused throughout; leaving it pending for the next stamp`);         }       }-      if (!err && r === 'NUDGED' && _sk) { _sk._buttonBuiltAt = Date.now(); }   // v2.0.228: stale-art anchor-      if (!err && r === 'NUDGED' && _sk && _sk._nudgePending) {+      if (!err && rN === 'NUDGED' && _sk) { _sk._buttonBuiltAt = Date.now(); }   // v2.0.228: stale-art anchor+      if (!err && rN === 'NUDGED' && _sk && _sk._nudgePending) {         console.log(`[thread-tile] "${sessionId}" deferred nudge LANDED after ${_sk._nudgePending.attempts} retr(ies) — button rebuilt with its real identity`);         _sk._nudgePending = null;         if (_sk._nudgeTimer) { clearTimeout(_sk._nudgeTimer); _sk._nudgeTimer = null; }@@ -4734,7 +4798,7 @@ public static class SW {       // v2.0.171: the nudge DESTROYS and recreates the button, so any overlay badge already painted       // is gone from glass while pup's model still says painted — mark bare so the enforcer repaints       // the favicon badge onto the recreated thread-tile button (John wants tile + overlay together).-      if (!err && r === 'NUDGED') {+      if (!err && rN === 'NUDGED') {         const s = sessions.get(sessionId);         if (s) {           s._appOverlay = false; s._overlayEnforceAt = 0; s._lastPaintSig = null;@@ -18443,6 +18507,37 @@ setInterval(async () => {   setTimeout(() => { sweepPlacement('startup').catch(() => {}); }, 15000);   setInterval(() => { if (minimalTouch()) return; sweepPlacement('periodic').catch(() => {}); }, 120000); +  // v2.0.251 (John: "why can't i open this pup window? its like you did that off-screen thing again+  // and now when i go to click on it as a human you fuck me over"). The placement sweep above asserts+  // GEOMETRY — it catches a window stranded off-screen, and it explicitly does NOT touch a healthy or+  // minimized one. But a window can be invisible without being mis-placed: pup's taskbar nudge HIDES+  // the window for 80ms to force the shell to rebuild its button, and any interruption in that gap+  // (the 20s execFile kill, a crash, a sleep) left it hidden with nothing anywhere in this codebase+  // watching for it. Reproduced live on AdomLapper: after the hide half alone, IsWindowVisible=false+  // and the taskbar button reveals nothing to a human click — exactly John's report.+  // So placement is no longer the only invariant: VISIBILITY is asserted too, for the whole standing+  // population. Safe by construction — a human can minimize a window but can never hide one, so a+  // hidden pup window is always pup's own mess to clean up (see ensureWindowShown).+  // Deliberately NOT minimalTouch-gated, unlike every other sweep here: minimal-touch exists to keep+  // pup's automatic machinery off the user's taskbar, and a window the user cannot open is not+  // "minimal touch", it is a broken window. Repairing it is the one automatic act that only ever+  // gives the user back something pup took.+  const sweepVisibility = async (cause) => {+    // Never race a nudge's own 80ms hide gap — un-hiding mid-nudge would silently defeat the button+    // rebuild. _nudgeUntil covers the in-flight window; a nudge that STRANDED a window has long since+    // passed it, so the repair still lands (at worst one sweep later).+    if (Date.now() < _nudgeUntil) return;+    for (const [sid, sess] of [...sessions]) {+      try {+        if (!sess || !(sess.tabs || []).length) continue;+        const hw = await windowHwnd(sess, sid);+        if (hw) await ensureWindowShown(hw, sid, `the ${cause} visibility sweep`);+      } catch (e) {}+    }+  };+  setTimeout(() => { sweepVisibility('startup').catch(() => {}); }, 20000);+  setInterval(() => { sweepVisibility('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   // pegging a core). Boot sweep runs AFTER recovery has settled so it never kills a window recovery