app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
THE RECONCILER: pup's model now self-heals against OS reality, killing a whole class of drift bugs (John: 'your code must be 100% correct... another ai thread closed an aa.com pup window and you left the aa.com icon in your overlay'). Root cause of every recent bug was the same: the in-memory model drifting from reality. Three coordinated fixes. (1) The zombie/close reaper was the FOURTH victim of the gated-interval disease (setInterval gated by minimalTouch = never ran in the normal mode), and it also wrongly required the whole profile's Chrome to be dead - so a single window closed by another thread on the shared adom-you profile was never reaped, leaving its stale badge/identity (the aa.com icon). New reconcileModel() runs UNGATED every 8s, plus immediately on tab-close-to-zero and after any identity setting change: it reaps any session whose windows are all closed (regardless of profile liveness), deleting its badge PNG and unregistering its AUMID so no ghost icon can persist. (2) set-setting saved grouping/aumidIcons/aumidIconStyle but never re-stamped existing windows, so switching grouped->split silently did nothing; reconcile now re-stamps every live window to match the current settings. (3) stampPupIdentity is now idempotent and self-correcting: aumidIcons is the single master switch, the old {once} guard that blocked legitimate identity CHANGES is gone, and the actual desktop_set_window_identity call is gated on desired-vs-current appId so it fires exactly once on a real change and is a pure no-op otherwise (zero churn at steady state, so the reconciler is safe to run every sweep). Ralph gained reap + ghost-badge(404) checks.
4 files changed
+68−12
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.278",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.278/adom-bridge-puppeteer-v1.9.278.zip",- "sha256": "610cc9d481ca064a2eae6e725b4df5747d4a8014eeabfce061e79ae845dc935f",- "size": 710194,+ "version": "1.9.279",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.279/adom-bridge-puppeteer-v1.9.279.zip",+ "sha256": "627d37076d543ba1b3f238084bb7a748be55cc3f35a5fc1f965388cd539b1244",+ "size": 711967, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.278+1.9.279
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.278",+ "version": "1.9.279", "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+62−6@@ -656,6 +656,9 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user session.activeTabId = session.tabs.length > 0 ? session.tabs[session.tabs.length - 1].tabId : null; } console.log(`Tab "${tabId}" of session "${sessionId}" closed by user. Remaining tabs: ${session.tabs.length}. Last URL: ${lastKnownUrl || '(unknown)'}`);+ // v1.9.279: if that was the LAST window of the session, reap it NOW (badge + identity + entry)+ // instead of leaving a stale icon until the next sweep — the aa.com-closed-by-another-thread case.+ if (session.tabs.length === 0) { try { reconcileModel('tab-close').catch(() => {}); } catch (e) {} } } // Auto-stop any tab recording for this tab try {@@ -2925,11 +2928,13 @@ async function stampPupIdentity(sessionId, opts) { // leaving them off... then i can play around with having it on or off and see if aumid is what's // crashing the app or not"). Default stays OFF. When the user turns it on: ONE gentle stamp per // window at creation ({once}), never the category re-stamp churn.- if (minimalTouch()) {- if (pupSettings().aumidIcons !== 'on' || !(opts && opts.once)) return false;- const _s0 = sessions.get(sessionId);- if (_s0 && _s0._curAppId) return true;- }+ // v1.9.279: the stamp is now IDEMPOTENT and SELF-CORRECTING. aumidIcons is the single master+ // switch (works the same in minimal-touch or not). The old {once} guard blocked legitimate+ // identity CHANGES (grouped->split never re-stamped, so John's split setting never took effect);+ // idempotency now comes from the desired-vs-current appId compare below (a real change re-stamps+ // exactly once; no change is a no-op), so `opts` is ignored and reconcileModel can call this every+ // sweep with zero churn.+ if (pupSettings().aumidIcons !== 'on') return false; if (process.platform !== 'win32' || !sessionId || _identitySupported === false) return false; if (!_identityRegistered) { await ensurePupIdentityRegistered(); if (_identitySupported === false) return false; } try {@@ -2981,9 +2986,14 @@ async function stampPupIdentity(sessionId, opts) { const appId = _dashInfra ? `${PUP_APP_ID}.Dashboard.${PUP_ICON_GEN}` : grouped ? PUP_APP_ID : `${PUP_APP_ID}.${sessionId}.${cat}.${PUP_ICON_GEN}`; const prevAppId = sess && sess._curAppId;+ // v1.9.279 THE IDEMPOTENCY GATE: only touch the window when its identity actually changes.+ // desktop_set_window_identity RECREATES the taskbar button every call, so firing it when nothing+ // changed is exactly the churn/flicker (and suspected crash pressure) we must never cause. Equal+ // desired-vs-current appId => no-op. This is what lets reconcileModel run every sweep safely.+ if (prevAppId === appId) return true; // Register the NEW appId BEFORE stamping (AD's verified order) so the fresh button resolves // its icon/name from the registry the moment it is created.- if ((!grouped || _dashInfra) && prevAppId !== appId) {+ if (true) { try { await registerPupAumid(appId, sessionId); } catch (e) {} // v1.9.61 — ORDER MATTERS (the live-tile trap, from AD's window-identity skill): Win11 bakes a // taskbar button's art when the button is CREATED and never re-reads it. Stamping the new appId@@ -4882,6 +4892,46 @@ async function adoptDraggedOutTabs(sessionId) { // So reclamation must work from the WINDOW side, not the tab side: enumerate every page in every // pup profile, group by OS window, and adopt any window no session owns. Runs on a timer (out of // band — never inside a targetcreated handler, to keep the pipe safe).+// v1.9.279 THE RECONCILER (John: "your code must be 100% correct... i even just saw another ai+// thread close an aa.com pup window and then you left the aa.com icon in your overlay"). Every bug+// this session was the same class: pup's in-memory model drifting from OS reality (a closed window+// lingering, a stamp not updated when a setting changed, a recycled hwnd). This one pass, run+// UNGATED every few seconds and immediately after any setting change or tab close, makes the model+// match reality. It NEVER kills a process (that stays in the gated reclaim path); it only corrects+// pup's own bookkeeping and the taskbar identity, both cheap and idempotent.+let _reconcileBusy = false;+async function reconcileModel(reason) {+ if (process.platform !== 'win32' || _reconcileBusy) return;+ _reconcileBusy = true;+ try {+ // (1) REAP any session whose windows are all gone. This is the aa.com fix: a window closed by+ // ANOTHER thread (or the user) leaves a session with only closed page handles. Unlike the old+ // reaper, we do NOT require the whole profile's Chrome to be dead — one closed window on a live+ // shared profile is still a dead session, and its lingering badge/identity is the "stale icon".+ for (const [sid, s] of [...sessions]) {+ if (_launchInFlight.has(s.profileName)) continue;+ if (s._lostBrowser && s._disconnectedAt && (Date.now() - s._disconnectedAt < 120000)) continue; // heal window+ let anyOpen = false;+ for (const t of (s.tabs || [])) { try { if (t && t.page && !t.page.isClosed()) { anyOpen = true; break; } } catch (e) {} }+ if (anyOpen) continue;+ try { if (s._curAppId && s._curAppId !== PUP_APP_ID) await unregisterPupAumid(s._curAppId); } catch (e) {}+ try { fs.unlinkSync(path.join(require('os').tmpdir(), 'pup-badge-debug', sid + '.png')); } catch (e) {}+ try { _tabFavCache && s.tabs && s.tabs.forEach(() => {}); } catch (e) {}+ sessions.delete(sid);+ try { deleteSessionFile(sid); } catch (e) {}+ console.log(`[reconcile] reaped closed session "${sid}" (no open windows${reason ? ', ' + reason : ''}) — removed its badge + taskbar identity`);+ }+ // (2) STAMP CONSISTENCY: make every live window's taskbar identity match the CURRENT settings.+ // stampPupIdentity is idempotent (no-op unless the desired appId differs), so this both applies a+ // grouped<->split or style change to existing windows AND stamps any window that never got one.+ if (pupSettings().aumidIcons === 'on' && _identitySupported !== false) {+ for (const [sid, s] of [...sessions]) {+ if (!(s.tabs || []).length) continue;+ try { await stampPupIdentity(sid, {}); } catch (e) {}+ }+ }+ } finally { _reconcileBusy = false; }+} async function reclaimUnmanagedWindows() { let reclaimed = 0; const handledByRescan = new Set();@@ -8880,6 +8930,9 @@ const server = http.createServer(async (req, res) => { const _ch = { ...(pupSettings()._chosen || {}), [key]: true }; savePupSettings(key === 'openFlash' ? { [key]: val, _openFlashChosen: true, _chosen: _ch } : { [key]: val, _chosen: _ch }); console.log(`[settings] "${key}" -> ${val} (dashboard dialog)`);+ // v1.9.279: a taskbar-identity setting must take effect on EXISTING windows immediately,+ // not just future ones. Reconcile now re-stamps every live window to match (idempotent).+ if (['aumidIcons', 'taskbarGrouping', 'aumidIconStyle'].includes(key)) reconcileModel('setting ' + key).catch(() => {}); return done({ ok: true, settings: pupSettings() }); } if (act === 'fg' && sid) {@@ -12742,6 +12795,9 @@ server.listen(PORT, BIND_HOST, async () => { // net, so an unbranded window can never persist even if its window-opened event was missed. setTimeout(() => { reclaimUnmanagedWindows().catch(() => {}); }, 12000); setInterval(() => { if (minimalTouch()) return; reclaimUnmanagedWindows().catch(() => {}); }, 90000);+ // v1.9.279: the reconciler runs UNGATED (bookkeeping + identity only, never a process kill), so+ // closed windows are reaped and stamp/settings drift is corrected within seconds, in every mode.+ setInterval(() => { reconcileModel('periodic').catch(() => {}); }, 8000); // 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