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
v2.0.686 (insiders): No more 'Something went wrong when opening your profile': after pup ends a browser it waits until no process still runs that profile folder before starting another (the old wait watched a lock file Chrome only creates on Linux/macOS, so on Windows the next browser started 1 s after a kill), and pup_restart_browser now ends the extension-lane browser itself instead of ending nothing and leaving the next open to force-kill it (John, AdomLapper, 2026-10-03).
4 files changed
+51−20
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.685"+version = "2.0.686" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.685"+version = "2.0.686" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.685"+version = "2.0.686" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.685"+version = "2.0.686" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.685"+version = "2.0.686" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.685"+version = "2.0.686" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.685"+version = "2.0.686" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.685"+version = "2.0.686" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.685"+version = "2.0.686" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.685"+version = "2.0.686" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.685"+version = "2.0.686" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.685"+version = "2.0.686" dependencies = [ "base64", "png 0.17.16",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session", # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.685"+version = "2.0.686" edition = "2021" license = "MIT"
rust/verbs/src/extras.rs+12@@ -314,6 +314,18 @@ pub fn restart_browser(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let waited = t0.elapsed().as_millis() as u64; let _ = std::fs::remove_file(&port_file); pup_core::log(format!("[restart] profile \"{profile}\" released after {waited} ms (pid alive: {})", pid > 0 && ctx.host.alive(pid)));+ // 2.0.686: the EXTENSION lane has no CDP browser handle (pid 0 above), so this verb ended nothing on it, and the+ // next open then force-killed the still-running browser as "stale" and started another one second later: John+ // got Chrome's "Something went wrong when opening your profile" (AdomLapper, 2026-10-03, asked by buck-repro).+ // End the lane browser here, after reserving its windows, and wait until no process holds the profile.+ if sessions.iter().any(|s| crate::transport::is_ext(s)) {+ let ids: Vec<String> = sessions.iter().map(|s| s.id.clone()).collect();+ crate::recovery::reserve(&ids); crate::recovery::mark_lost(&ids);+ crate::extdrive::enter_profile(&profile);+ let n = crate::kill_extdrive(ctx);+ crate::extdrive::leave_profile();+ pup_core::log(format!("[restart] profile \"{profile}\": ended its extension-lane browser ({n} process(es)) and waited for the profile to be free"));+ } // 2.0.465: bring the windows back HERE, synchronously. Removing the browser handle above also removed the // only thing the health loop watches, so the sessions sat in memory pointing at a dead browser and nothing // relaunched them: pup_list_windows still listed them with their tabs, and every verb on them answered
rust/verbs/src/lib.rs+26−7@@ -95,10 +95,35 @@ pub(crate) fn persist_lane(ctx: &Ctx) { let _ = std::fs::write(&p, json!({ "lane": extdrive::st().token.lock().unwrap().clone(), "pids": extdrive::st().pids.lock().unwrap().clone() }).to_string()); } pub fn extdrive_browser_alive(ctx: &Ctx) -> bool { extdrive::st().pids.lock().unwrap().iter().any(|p| ctx.host.alive(*p)) }+/// 2.0.686 (John, AdomLapper, 2026-10-03: a "Something went wrong when opening your profile" dialog). After pup ends a+/// browser, wait until NO process still runs that profile folder before anything starts on it again. The old waits+/// looked for "SingletonLock", which Chrome only creates on Linux/macOS, so on Windows they returned at once and the+/// next browser started 1 s after a kill while the old one's helper processes still held the profile's files.+/// Stragglers are force-ended after 8 s; gives up (and says so) after `max`.+pub fn wait_profile_free(ctx: &Ctx, profile: &str, max: Duration) -> bool {+ let dir = pup_browser::profiles_dir(&*ctx.host).join(profile);+ let t0 = std::time::Instant::now(); let mut forced = false;+ loop {+ let left = orphan_browser_pids(ctx, &dir);+ if left.is_empty() {+ if t0.elapsed() > Duration::from_millis(300) { pup_core::log(format!("[extdrive] profile \"{profile}\" is free after {} ms", t0.elapsed().as_millis())); }+ return true;+ }+ if !forced && t0.elapsed() > Duration::from_secs(8) {+ for p in &left { ctx.host.kill(*p); }+ forced = true;+ pup_core::log(format!("[extdrive] profile \"{profile}\": {} process(es) still held it 8 s after the browser ended; force-ended them", left.len()));+ }+ if t0.elapsed() > max { pup_core::log(format!("[extdrive] profile \"{profile}\" still held by {} process(es) after {} s", left.len(), max.as_secs())); return false; }+ std::thread::sleep(Duration::from_millis(400));+ }+}+ pub fn kill_extdrive(ctx: &Ctx) -> u32 { let pids: Vec<u32> = extdrive::st().pids.lock().unwrap().clone(); let mut n = 0; for p in pids { if ctx.host.alive(p) { ctx.host.kill(p); n += 1; } }+ if n > 0 { let _ = wait_profile_free(ctx, &extdrive::cur_profile(), Duration::from_secs(20)); } extdrive::st().pids.lock().unwrap().clear(); extdrive::st().token.lock().unwrap().clear(); extdrive::mark_dead();@@ -275,14 +300,8 @@ pub fn launch_extdrive(ctx: &Ctx, url: &str) -> Result<u32, Value> { while t0.elapsed() < Duration::from_secs(10) { if !lock.exists() { break; } std::thread::sleep(Duration::from_millis(200)); } } }- let stale = kill_extdrive(ctx);+ let stale = kill_extdrive(ctx); // 2.0.686: kill_extdrive now waits until the profile is truly free if stale > 0 {- let lock = dir.join("SingletonLock");- let t0 = std::time::Instant::now();- while t0.elapsed() < Duration::from_secs(10) {- if !lock.exists() { break; }- std::thread::sleep(Duration::from_millis(200));- } pup_core::log(format!("[extdrive] ended {stale} stale browser process(es) holding profile \"{profile}\" before relaunching")); } // 2.0.646 (pup-bridge#121): an ORPHANED lane browser (launched by a pup instance that has since restarted,