← Commit history

Two fixes. (1) Close the set-enumeration gap I apologized for: RENDER_VERBS has four members and I had wired three, so browser_open_tab silently skipped the render check that browser_open_window performs. A tab is content on the user's screen exactly like a window, so it now captures its own HTTP status and runs probeRender. (2) Issue #22 (AdityaAngajala), the non-breaking half: sessions are no longer left unowned and ownership is settled at ADMISSION rather than at browser acquisition. The owner is the explicit owner arg when given, else the calling AI thread's own name from X-Adom-Caller-Thread, so 'unowned' stops being reachable for any caller that identifies itself; and a synchronous claim registry (no await between check and set) refuses a second differently-owned caller immediately with a COLD-START RACE explanation instead of handing it a provisioning hint that implies 'retry and it's yours'. Claims are released on close and close-all. Deliberately NOT included: refusing an anonymous grab of an owned session, which is a breaking contract change and is John's call.

John Lauer ·301315d2d4 ·3mo ago ·parent 5bf7900
4 files changed +57−7
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.109",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.109/adom-bridge-puppeteer-v1.9.109.zip",-  "sha256": "4197608726b1a25359e8197d82787b75d8f15c121d2e7374a13ed18357ce9e47",-  "size": 379403,+  "version": "1.9.110",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.110/adom-bridge-puppeteer-v1.9.110.zip",+  "sha256": "86d0aedbf1f584d547a49a7ef3e686a7b61bca6b94540a09c3373231f673b813",+  "size": 380830,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.109+1.9.110
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.109",+  "version": "1.9.110",   "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+51−1
@@ -250,6 +250,17 @@ let pupshotUrl = null; // looked wedged for minutes. This map lets concurrent callers JOIN the first launch instead of // racing a second Chrome onto the same dir. (Root cause of the 2026-07-19 cold-start wedge.) const _launchInFlight = new Map();+// v1.9.110 (issue #22, AdityaAngajala) — ADMISSION-TIME OWNERSHIP CLAIM.+// Two defects he measured: (a) `owner` is optional, so in practice sessions end up unowned and the guard+// that protects a busy window is opt-out by omission; (b) ownership was established when the BROWSER was+// acquired, not when the request was admitted, so two agents cold-starting the same sessionId were BOTH+// told to proceed and whichever launch landed first silently became the owner while the other believed it+// owned that window too. This map is claimed SYNCHRONOUSLY on the open path before any await, so the+// second differently-owned caller is refused immediately instead of receiving a provisioning hint that+// implies "retry and it's yours". Cleared on close. Ownership is still self-reported attribution, never+// authorization — it exists to stop threads trampling each other, not to secure anything.+const _sessionOwnerClaims = new Map();   // sessionId -> owner string+  // ── Tab helpers ──────────────────────────────────────────────────── function createSession(profileName) {@@ -5334,7 +5345,9 @@ async function addTabToSession(sessionId, url) {       } catch (e) {         console.log(`[creds] applyCredentialsToPage on new tab failed: ${e.message}`);       }-      await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });+      // v1.9.110: keep the tab's own HTTP status so probeRender can report a 404 with certainty.+      const _tResp = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });+      try { page.__pupHttpStatus = _tResp && typeof _tResp.status === 'function' ? _tResp.status() : null; } catch (e) {}     }   } catch {} @@ -6296,6 +6309,32 @@ const server = http.createServer(async (req, res) => {           if (args.wikiView === 'authed' && isAdomUrl(args.url)) args.profile = WIKI_AUTH_PROFILE;           if (!args.profile) args.profile = args.sessionId;           const sessionId = args.sessionId;+          // v1.9.110 (issue #22): NEVER LEAVE A SESSION UNOWNED, and settle ownership at ADMISSION.+          // The owner is the explicit `owner` arg when given, else the calling AI thread's own name+          // (AD hands us that via X-Adom-Caller-Thread), so "unowned" stops being reachable for any+          // caller that identifies itself. The claim below is taken synchronously — no await between+          // the check and the set — which is what closes the cold-start race where two agents were+          // both admitted for the same sessionId.+          {+            const _wantOwner = (typeof args.owner === 'string' && args.owner.trim())+              ? args.owner.trim()+              : (_callerThread ? String(_callerThread).trim() : null);+            const _held = _sessionOwnerClaims.get(sessionId) || null;+            if (_held && _wantOwner && _held !== _wantOwner && !args.takeover && !sessions.has(sessionId)) {+              sendJSON(res, {+                success: false,+                errorCode: 'session_owned_by_another_thread',+                error: `Session "${sessionId}" was claimed by another thread ("${_held}") moments ago and is still being provisioned.`,+                sessionId, owner: _held, requestedBy: _wantOwner,+                _hint: `STOP — thread "${_held}" claimed sessionId "${sessionId}" first and its window is still opening, so this is a COLD-START RACE, not an available id. Do NOT retry this call: the window will belong to "${_held}". Open your OWN window with a unique task-prefixed sessionId (e.g. "${_wantOwner}-web") and owner:"${_wantOwner}". Pass takeover:true only if the user EXPLICITLY told you to repurpose that thread's window.`,+              });+              return;+            }+            if (_wantOwner && !_held) _sessionOwnerClaims.set(sessionId, _wantOwner);+            // Make the synthesized owner explicit for the rest of the pipeline, so launchSession's+            // existing cross-owner refusal and browser_status both see a real owner.+            if (_wantOwner && !(typeof args.owner === 'string' && args.owner.trim())) args.owner = _wantOwner;+          }           // v1.6.3+: optional downloadPath override. Defaults to the user's           // home Downloads dir, which is what desktop_watch_files polls by           // default — the two ends meet without configuration. Caller can@@ -6744,6 +6783,7 @@ const server = http.createServer(async (req, res) => {           session.tabs.length = 0;           session.activeTabId = null;           sessions.delete(sessionId);+          _sessionOwnerClaims.delete(sessionId);             // v1.9.110: the id is free again           deleteSessionFile(sessionId);           clearSessionBusy(sessionId);                       // v1.9.57: drop any pending progress timer           unregisterPupAumid(_closingAppId).catch(() => {}); // v1.9.2/#207 cleanup (appId captured pre-delete)@@ -7107,6 +7147,15 @@ const server = http.createServer(async (req, res) => {                 brandPupWindow(sessionId).then(ok => { if (ok) session._badgedAt = Date.now(); }).catch(() => {});               }             }+            // v1.9.110 (closing the set-enumeration gap): RENDER_VERBS has four members and I had wired+            // three. A tab is content on the user's screen exactly like a window, so it gets the same+            // verdict — otherwise "open it in a tab" silently skips the check that "open it in a window"+            // performs. When a behavior belongs to a declared SET, every member gets it.+            try { res._renderCheck = await probeRender(tab.page, tab.page.__pupHttpStatus); } catch (e) {}+            try {+              const _rc = res._renderCheck;+              if (_rc && _rc.ok === false) console.log(`[render] "${sessionId}" tab ${tabId} BROKEN: ${_rc.signals.join(', ')}`);+            } catch (e) {}             const info = await describeTab(tab, session.activeTabId);             // v1.8.23: return tabId + tabCount + activeTabId at the TOP level (was only inside `info`).             sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId, tabId, tabCount: session.tabs.length, activeTabId: session.activeTabId, ...info }) });@@ -8653,6 +8702,7 @@ const server = http.createServer(async (req, res) => {             try { if (s && s._curAppId && s._curAppId !== PUP_APP_ID) unregisterPupAumid(s._curAppId).catch(() => {}); } catch (e) {}           }           sessions.clear();+          _sessionOwnerClaims.clear();                       // v1.9.110           activeSessionId = null;           // Belt-and-braces: sweep any orphaned Adom.Pup.* keys now that nothing is live.           try { pruneStaleAumids(); } catch (e) {}