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
v1.9.4: fix concurrent-launch wedge (_launchInFlight dedup) + liveness/recovery hints and skill troubleshooting coverage
8 files changed
+144−8
CHANGELOG.md+13@@ -1,5 +1,18 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.9.4 — 2026-07-19 — Fix the concurrent-launch wedge + liveness/recovery hints++Root-caused a live cold-start wedge: after a bridge restart, two near-simultaneous+browser_open_window calls for the SAME profile each spawned their own detached Chrome onto the+same --user-data-dir, collided on Chrome's SingletonLock, and mutually taskkilled each other's+process — so NONE came up and the bridge looked hung for minutes (verbs timed out, AD LED dim).+Fix: `_launchInFlight` dedup — the first launch per profile registers a Promise and concurrent+callers JOIN it instead of racing a second Chrome. Also added AI-facing recovery hints so other+threads don't relearn this: browser_readiness now explains that a status probe does NOT respawn a+down bridge (only a window/tab verb does) and warns against spamming opens during "bridge_starting";+the pup SKILL and dev-skill gained a liveness/restart/wedge troubleshooting section (the AD LED ++browser.bridgeRunning are truth; bridge_list status:"running" is not liveness).+ ## 1.9.3 — 2026-07-19 — Per-window wiki view toggle in the taskbar jump list (#208 shipped) AD 1.9.139+ shipped #208 (desktop_set_window_jumplist). Every WIKI pup window now carries a
SKILL.md+6@@ -107,4 +107,10 @@ the user asks — it's their taskbar. Details: `pup-windows-sessions-tabs`. the window, count before you eyeball. See `pup-screenshots-recording` → "Driving & verifying tricky pages." - **Never broadly kill Chrome** — `browser_close_window` for a specific session. - **Surface a verb's `_hint` verbatim** whenever it reports not-ready or failure.+- **If pup's verbs TIME OUT (or the AD LED is dim / `browser.bridgeRunning:false`) the bridge PROCESS is+ down** — usually right after a `restart_bridge` or a crash. To wake it, fire **one**+ `browser_open_window` (a status probe like `browser_readiness` does NOT respawn a down bridge — only a+ window/tab verb does), then poll `browser_list_windows` with backoff until it answers. **Do NOT spam+ opens** while AD says `bridge_starting` — one is enough; it's already starting. (`bridge_list`+ `status:"running"` is not liveness; the LED + `browser.bridgeRunning` + an actual verb reply are.) - Full machine-readable catalog: `adom-desktop browser_describe '{}'`.
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.3",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.3/adom-bridge-puppeteer-v1.9.3.zip",- "sha256": "e664508c4277458ee509e83cd58f8c70e5c99e73233a4741bee800d6606a8901",- "size": 212553,+ "version": "1.9.4",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.4/adom-bridge-puppeteer-v1.9.4.zip",+ "sha256": "bebf4d82b1afe8b2360b9505c4b215b097889bdc84b8d6a48137874320ea1ee4",+ "size": 213488, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-07-19T19:30:00Z"+ "released_at": "2026-07-19T20:00:00Z" }
dev-skills/pup-bridge-dev/SKILL.md+78@@ -588,6 +588,84 @@ mechanical requirement. (kept for explicit branded `browser_use` pins, where the base icon is genuinely Chrome/Edge). Rejected: patching CfT's exe icon resource (breaks signature/sha validation, SmartScreen/AV risk). +## Per-session AUMID registration + the wiki view-toggle jump list (#207/#208, v1.9.2–1.9.3)++Two AD verbs shipped that close the last gaps in the identity story. Both are AD-owned (the+long-lived native process that owns the HICONs, per the hard lesson above); pup just calls them.++**#207 — `desktop_register_app_identity {appId, displayName, iconPath, shortcut:false}` ++`desktop_unregister_app_identity {appId}` (AD ≥1.9.136).** Alt-Tab and the taskbar right-click+menu HEADER resolve a window's icon/name through its AUMID via a REGISTERED entry; an+UNREGISTERED per-session AUMID (`Adom.Pup.<sessionId>`, which split mode uses so each window is+its own taskbar button) fell back to the chrome.exe icon. `shortcut:false` registers the AUMID+**registry-only** — no Start Menu .lnk, so no launcher spam for these throwaway per-window ids.+pup registers on window create (`registerPerSessionAumid`, fired from `stampPupIdentity` in split+mode), tracks live ids in `_registeredSessionAumids`, and unregisters in `browser_close_window`.+The base `Adom.Pup` stays the shortcut-registered pinnable launcher (grouped mode uses it).++**#208 — `desktop_set_window_jumplist {appId, tasks:[{title, target?, args?, iconPath?,+description?}]}` (AD ≥1.9.139).** Attaches a jump list to an AUMID → a PER-WINDOW right-click+taskbar menu (because split mode gives each window its own per-session AUMID). Clicking a task+launches `target args` minimized; **target defaults to AD's bundled CLI, so `args` is simply an+AD/bridge verb + JSON** — the click routes back through the LOCAL AD to pup with zero cloud+round-trip. `tasks:[]` clears. The AUMID must be registered (from #207) or the shell ignores the+list. pup uses this for the **wiki view toggle**: `updateWikiJumplist(session, sessionId)` sets one+task on any Adom-URL window — "🔓 Switch to logged-in view" on a public window, "📖 Switch to+public view" on an authed one — with `args: browser_wiki_set_view {"sessionId":..,"view":..}`.+Called on open (900ms after brand, so the AUMID is registered) and on every navigate (adds on a+wiki page, clears when the window leaves Adom); split mode only; idempotent per (session,view) via+`session._wikiJumplistView`; self-gates via `_jumplistSupported`.++**The callback — `browser_wiki_set_view {sessionId, view}` (pup verb).** A view switch is a+cookie-jar (userDataDir) switch, and the profile is FIXED at browser launch, so there is NO+in-place cookie swap: it's a **close-and-relaunch** of the same sessionId under the other profile+(`WIKI_AUTH_PROFILE` for authed, `session._publicProfile || sessionId` for public) at the same+URL. It's a USER action (they clicked the taskbar task), so the relaunched window is brought+on-screen — via CDP place+maximize + `bringToFront` (same on-screen move as the raise handler, no+PowerShell), NOT the gated `browser_raise_os_window` path (which would demand a foregroundReason a+click can't supply). It re-checks auth, refreshes the in-tab glyph/brand/overlay/jumplist, and if+the authed jar hasn't done the one-time SSO yet returns `wikiLoginNeeded` so the user logs in once+in that window. AI threads should NOT call this verb — they pass `wikiView` on+`browser_open_window`; the verb exists for the taskbar affordance.++## Liveness, restart, and the concurrent-launch wedge (2026-07-19 — do NOT re-derive)++A live incident that ate an hour: after a `restart_bridge`, pup's verbs (`browser_readiness`,+`browser_describe`, `browser_open_window`) all timed out for MINUTES and the AD GUI LED was dim.+Three separate lessons, all now fixed/documented:++**1. The concurrent-launch wedge (pup's OWN bug, fixed v1.9.4 — the real root cause).** A fresh+profile is not in the `browsers` map until its Chrome launch FINISHES (~10-15s). So two+near-simultaneous `browser_open_window`s for the same profile (an AI retry/poll loop, OR two+threads sharing a profile) each entered `getOrLaunchBrowser`, saw no cached browser, and each+spawned its OWN detached Chrome onto the SAME `--user-data-dir`. They then collided on Chrome's+`SingletonLock`, and each launch's lock-recovery `taskkill`ed "the orphan holding the profile" —+which was the SIBLING launch. Net: they mutually killed each other, NONE ever came up, and the+bridge looked hung. **Fix:** `_launchInFlight` (Map profileName→Promise) — the first launch+registers a promise; concurrent callers JOIN it (`return await`) instead of racing a second+Chrome. If you ever see "Chrome process exited immediately" storms or a wedged cold-start under+load, check this dedup first. NEVER remove it. (Corollary for callers: do NOT spam+`browser_open_window` while AD says `bridge_starting` — fire ONE and poll.)++**2. LED / liveness — which signal is TRUTH.** The AD GUI bridge LED is ACCURATE: dim = the pup+PROCESS isn't actively serving. Corroborating truth signals: AD `status` → `browser.bridgeRunning`,+and simply whether a real pup verb answers. **`bridge_list` `status:"running"` is NOT liveness** —+it's desired/installed state and will say "running" even when the process is down. (Cost me a wrong+"pup is running" claim to John. Trust the LED + `browser.bridgeRunning` + an actual verb reply, and+`bridge_check_updates` `liveVersion`/`staleProcess` for version truth.)++**3. `restart_bridge` REAPS; only some verbs RESPAWN.** `restart_bridge` (and a crash) leaves pup+down until AD auto-spawns it on the next call — but AD spawns it for WINDOW/TAB verbs+(`browser_open_window` et al.), NOT for a bare status probe like `browser_readiness`/`status`. So+polling `browser_readiness` to "wait for pup to come back" waits FOREVER (the probe never triggers+a spawn and never reaches the down process). **Recovery playbook:** fire ONE `browser_open_window`+(it returns `bridge_starting` fast and triggers the spawn), THEN poll `browser_list_windows` (also+auto-spawning) with backoff until it answers. `refresh_bridges {name:"puppeteer"}` (AD ≥1.9.76)+downloads-and-restarts in one step for the UPDATE path; `restart_bridge` is the kill-and-respawn.++**Was it AD's fault?** No — AD's reap-on-restart is normal and correct. The wedge was pup's missing+launch dedup (#1); AD's 30s relay timeout + the dim LED just made a pup bug VISIBLE. Own it.+ ## The foreground gate (v1.8.70) — abe-parity dual key, and the AD interception hole Foregrounding (open `foreground:true`, `browser_raise_os_window`, `browser_focus_window`) requires a
skills/pup-adom-wiki/SKILL.md+8@@ -71,6 +71,14 @@ hydrogen.adom.inc SSO once; the session then persists in the authed profile and (Container-identity auto-login — no interactive step — is still tracked in `adom/wiki` #89; when it lands, this becomes zero-touch.) +**The user can flip the view themselves (v1.9.3).** Every wiki pup window carries a **taskbar+right-click menu** task — "🔓 Switch to logged-in view" on a public window, "📖 Switch to public+view" on an authed one. Clicking it relaunches THAT window under the other cookie jar and brings it+on-screen (no thread involvement — it's a pure user affordance). So if the user asks "how do I see+the logged-in version," you can tell them to right-click the pup taskbar button and pick "Switch to+logged-in view" (or just reopen with `wikiView:"authed"` yourself). This requires AD ≥1.9.139; on+older AD the menu simply isn't there.+ ## Historical note — pup used to be logged-out only pup drives a **fresh, isolated browser** with no cookies, so it views the wiki **logged out** — it sees
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.3+1.9.4
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.3",+ "version": "1.9.4", "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.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+32−1@@ -163,6 +163,13 @@ let browsers = new Map(); let sessions = new Map(); let activeSessionId = null; let pupshotUrl = null;+// v1.9.4: profileName -> in-flight launch Promise. A fresh profile isn't in `browsers` until its+// Chrome launch FINISHES (~10-15s), so two near-simultaneous opens for the same profile used to+// each spawn their OWN detached Chrome on the SAME userDataDir; they then collided on Chrome's+// SingletonLock and mutually taskkilled each other's process, so NONE came up and the bridge+// 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(); // ── Tab helpers ──────────────────────────────────────────────────── function createSession(profileName) {@@ -2868,6 +2875,20 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer browsers.delete(profileName); } + // v1.9.4: CONCURRENT-LAUNCH DEDUP — see `_launchInFlight` declaration. If a launch for this+ // profile is already running, JOIN it (return its Promise) rather than spawning a competing+ // Chrome onto the same userDataDir (which mutually lock-kills and wedges the bridge). The+ // reuse-of-a-live-browser fast path above already returned; only genuine (re)launches reach here.+ if (_launchInFlight.has(profileName)) {+ console.log(`[launch] profile "${profileName}" is already launching — joining the in-flight launch (no second Chrome)`);+ return await _launchInFlight.get(profileName);+ }+ let _resolveLaunch, _rejectLaunch;+ const _launchPromise = new Promise((res, rej) => { _resolveLaunch = res; _rejectLaunch = rej; });+ _launchPromise.catch(() => {}); // mark handled — joiners still observe rejection via their own await+ _launchInFlight.set(profileName, _launchPromise);+ try {+ // Prefer an ALREADY-INSTALLED browser (Chrome → Edge; every Windows box has Edge) // so the common case needs NO ~150 MB Chrome-for-Testing download. Falls back to a // cached CfT. A fresh --user-data-dir (below) keeps it CLEAN — it never touches the@@ -3263,7 +3284,17 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // hijacked unrelated windows on subsequent calls. browser.on('disconnected', () => handleBrowserDisconnect(browser, profileName)); + // v1.9.4: hand the launched browser to any concurrent joiners, then clear the in-flight slot.+ _launchInFlight.delete(profileName);+ _resolveLaunch(browser); return browser;+ } catch (_launchErr) {+ // v1.9.4: propagate the SAME failure to every concurrent joiner and clear the slot so a+ // later open can retry cleanly (a stuck slot would otherwise wedge all future opens).+ _launchInFlight.delete(profileName);+ _rejectLaunch(_launchErr);+ throw _launchErr;+ } } // Launch a session as a tab in a Chrome browser@@ -5956,7 +5987,7 @@ const server = http.createServer(async (req, res) => { output: JSON.stringify(rd), ...rd, _hint: rd.ready- ? `Ready — pup will drive ${rd.browserKind} (${rd.browserSource}) at ${rd.browserExecutablePath}. Available: ${availTxt}.${defTxt} No download needed. Proceed with browser_open_window.${switchTxt}`+ ? `Ready — pup will drive ${rd.browserKind} (${rd.browserSource}) at ${rd.browserExecutablePath}. Available: ${availTxt}.${defTxt} No download needed. Proceed with browser_open_window.${switchTxt} RECOVERY NOTE: if you were polling this because pup's verbs were TIMING OUT (dim LED in AD / browser.bridgeRunning:false), that means the bridge PROCESS was down — a status probe like this does NOT respawn it; only a window/tab verb (browser_open_window) auto-spawns the bridge. And never SPAM browser_open_window while it says "bridge_starting" — concurrent opens on one profile used to collide (fixed v1.9.4, but still: fire ONE open, then poll browser_list_windows with backoff).` : rd.installing ? `Fetching Chrome for Testing (${rd.installProgressPct}%) — this box had no Chrome/Edge. Poll this verb (or bridge_readiness) until ready:true, then browser_open_window. Faster: have the user install Chrome or Edge (Edge ships with Windows) and pup uses it instantly.` : rd.installPhase === 'failed'