← Commit history

v2.0.261: A newborn window's self-activation is Chrome, never the user. John opened ti.com, it landed on top of his work, and pup then REFUSED to move it: the log read [park] ti-proof (post-nav re-assert): user-foreground - hands off, on a window he had never touched. Cause: every user-intent check attributes an OS-foreground pup window to the user via _userActiveAt or simply 'there was input in the last 5 seconds', and he was typing while the window opened. Both are wrong for a window that did not exist a second ago, because there was no taskbar button to click; the old off-screen birth hid this entirely, since Chrome does not activate a window that is nowhere. Adds session._bornAt plus an 8s BIRTH_GRACE_MS honored by osBottomUnlessForeground, userIsUsingWindow and the foreground observer, so a newborn is bottomed rather than credited to the user. An explicitly granted foreground open or raise sets _raiseGranted and is exempt, because that window is supposed to be in front.

John Lauer ·4eebe8787b ·2mo ago ·parent f3b566d
4 files changed +52−11
adom-bridge-puppeteer-manifest.json+5−5
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.260",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.260/adom-bridge-puppeteer-v2.0.260.zip",-  "sha256": "4c881020b6b5ff80d22c78f4bb5b132a2c66c719ae9293cf35fce1291617feda",-  "size": 956454,+  "version": "2.0.261",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.261/adom-bridge-puppeteer-v2.0.261.zip",+  "sha256": "f09bf7bd93dfd1d37256d58736e32663623cdb827072d5a95e30300bee4588f2",+  "size": 957681,   "verbPrefixes": [     "browser_",     "desktop_recorder_",@@ -12,5 +12,5 @@   ],   "healthEndpoint": "/status",   "statusVerb": "browser_readiness",-  "released_at": "2026-08-20T12:30:00.666330Z"+  "released_at": "2026-08-20T12:43:44.102914Z" }
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.260\ No newline at end of file+2.0.261\ 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.260",+  "version": "2.0.261",   "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+45−4
@@ -3317,6 +3317,10 @@ async function cachedSessionHwnd(sessionId) { // "pup windows still don't behave well"; root cause was exactly these unguarded demotions). function userIsUsingWindow(session) {   if (!session) return false;+  // v2.0.261: a newborn window cannot be one "the user is using" — its taskbar button did not exist+  // until a moment ago. Without this the flags set by Chrome's own self-activation make every+  // demotion path stand down and leave an unasked-for window on top of the user's work.+  if (inBirthGrace(session)) return false;   if (session._foreground) return true;   return !!(session._userActiveAt && Date.now() - session._userActiveAt < 10 * 60000); }@@ -8867,6 +8871,14 @@ async function badgePupTaskbar(sessionId) { // no direct API (CDP has no z-order; AD has no send-to-back verb yet — issue filed). Minimal // PS: one P/Invoke signature, no window-finding, no pid guessing, hwnd comes from AD's native // desktop_find_window. Everything else in the park is CDP/AD direct.+// v2.0.261: a window younger than this cannot have been raised by a human taskbar click, because+// its button did not exist yet. Inside the grace period, OS-foreground is attributed to CHROME+// (which self-activates a newly created on-screen window), never to the user. Generous enough to+// cover a slow cold start, short enough that a real click seconds later is still honored.+const BIRTH_GRACE_MS = 8000;+function inBirthGrace(sess) {+  return !!(sess && sess._bornAt && !sess._raiseGranted && Date.now() - sess._bornAt < BIRTH_GRACE_MS);+} async function osBottomUnlessForeground(hwnd, force, sess) {   // v2.0.27: NATIVE (was the last big PowerShell in the z-order path). Three ab calls at ~20ms each   // replace one ~300ms runtime-compiled PS spawn, and the user-protection doctrine rides along:@@ -8881,9 +8893,21 @@ async function osBottomUnlessForeground(hwnd, force, sess) {     const fg = abPayload(await chrome.abCommand('desktop_get_foreground', {}, { timeoutMs: 4000 }));     if (fg && Number(fg.hwnd) === Number(hwnd)) {       if (!force) return 'foreground';-      if (sess && sess._userActiveAt) { sess._userActiveAt = Date.now(); return 'user-foreground'; }-      const idle = await osIdleMs();-      if (idle != null && idle < 5000) { if (sess) sess._userActiveAt = Date.now(); return 'user-foreground'; }+      // v2.0.261 (John opened ti.com and it landed on top of his work, then pup refused to move it):+      // the checks below attribute an OS-foreground pup window to the USER — `_userActiveAt`, or+      // simply "there was input in the last 5s". For a window that was just born, both are wrong:+      // Chrome self-activates a newly created ON-SCREEN window (the old off-screen birth hid this),+      // and John typing while pup opens a window is not John clicking that window's taskbar button,+      // which did not exist yet. The measured result was `[park] "ti-proof" (post-nav re-assert):+      // user-foreground — hands off` on a window he had never touched. Inside the birth grace we+      // therefore bottom it, exactly as the launch park already does unconditionally.+      if (inBirthGrace(sess)) {+        console.log(`[park] "${sess._sessionId || '?'}" is foreground ${Math.round((Date.now() - sess._bornAt) / 1000)}s after birth — CHROME self-activating a new window, not the user (its taskbar button did not exist yet). Bottoming it.`);+      } else {+        if (sess && sess._userActiveAt) { sess._userActiveAt = Date.now(); return 'user-foreground'; }+        const idle = await osIdleMs();+        if (idle != null && idle < 5000) { if (sess) sess._userActiveAt = Date.now(); return 'user-foreground'; }+      }     }     const d = abPayload(await chrome.abCommand('desktop_set_window_state', { hwnd: Number(hwnd), state: 'bottom', force: true }, { timeoutMs: 5000 }));     return (d && (d.sentToBack || d.ok)) ? 'bottomed' : 'error';@@ -10549,6 +10573,11 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st   if (downloadPath) session._downloadPath = downloadPath;   attachTab(session, sessionId, page);   session._sessionId = sessionId; // v1.8.43: for AD taskbar/flash titleContains resolution+  // v2.0.261 (John, live on ti.com): BIRTH TIMESTAMP. A user CANNOT have raised a window that did+  // not exist a second ago — there was no taskbar button to click. Without this, pup's own newborn+  // window self-activating while the user happens to be typing reads as "the user raised it", and+  // every demotion path politely backs off and LEAVES IT ON TOP OF THEIR WORK. See BIRTH_GRACE_MS.+  session._bornAt = Date.now();   // v1.9.158 (flash-counter fix): the launch flash fires BEFORE this registration, so counting   // via sessions.get() at flash time always missed — single-tab windows read flash:0 while the   // orange really fired (John's audit column caught it). Credit it here, where the session exists.@@ -13775,7 +13804,13 @@ const server = http.createServer(async (req, res) => {           const fgReason = (typeof args.foregroundReason === 'string' && args.foregroundReason.trim().length >= 10) ? args.foregroundReason.trim() : null;           const fgGranted = fgRequested && !!fgReason;           const fgDenied = fgRequested && !fgReason;-          if (fgGranted) console.log(`[foreground] "${sessionId}" open GRANTED — reason: ${fgReason}`);+          if (fgGranted) {+            console.log(`[foreground] "${sessionId}" open GRANTED — reason: ${fgReason}`);+            // v2.0.261: exempt from the birth grace. The caller asked for a foreground window on+            // the user's behalf, so this one is SUPPOSED to be in front — the grace exists only to+            // stop Chrome's self-activation of an unasked-for window being read as user intent.+            try { const _s = sessions.get(sessionId); if (_s) _s._raiseGranted = true; } catch (e) {}+          }           // v2.0.165 THREAD TILES ROOT CAUSE: default the owner to the CALLING ai-thread when not           // explicitly declared, BEFORE launchSession — session.owner must exist at CREATE time so the           // FIRST identity stamp (the one that births the taskbar button) already computes the thread@@ -15613,6 +15648,7 @@ const server = http.createServer(async (req, res) => {             return;           }           console.log(`[foreground] "${sessionId}" raise GRANTED — reason: ${fgReason}`);+          try { const _rs = sessions.get(sessionId); if (_rs) _rs._raiseGranted = true; } catch (e) {}           session._lastForeground = { ts: Date.now(), verb: command, reason: fgReason };           toastForeground(sessionId, fgReason).catch(() => {});   // v1.9.166: ping the human before/as we reveal           // v1.9.150: the reveal redraws the taskbar button; nothing re-wore the favicon, so all@@ -18557,6 +18593,11 @@ setInterval(async () => {             try {               const hwnd = await cachedSessionHwnd(sid);               if (!hwnd || String(fg) !== String(hwnd)) continue;+              // v2.0.261: do NOT credit the user for a window that was just born. Chrome+              // self-activates a newly created on-screen window, and this observer running inside+              // that moment would stamp _userActiveAt/_foreground — permanently marking an+              // unasked-for window as "the user's", so nothing would ever move it off their work.+              if (inBirthGrace(sess)) continue;               sess._userActiveAt = Date.now();               if (!sess._foreground) {                 sess._foreground = true;