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.724 (insiders): A pup window can no longer sit on the taskbar titled PUPBIRTH-... A new window starts on pup's birth page and is then sent to its address; when that step failed, the failure was swallowed and the window stayed on the birth page for good (John's laptop, 2026-10-17). The open now retries the navigate once, and the health check sends any window still on its birth page 20 s after birth to the address its session recorded (up to 3 tries; this net could not be forced to fire in testing). Gates on arav-rog and ConfRoomROG: smoke all pass, foreground pass, parity 55/56, jump list 11/14 (baseline); the isolated-close replay kept every window on both. Includes 2.0.723.
5 files changed
+50−14
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.723"+version = "2.0.724" dependencies = [ "pup-browser", "pup-cdp",@@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.723"+version = "2.0.724" dependencies = [ "pup-core", "pup-host",@@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.723"+version = "2.0.724" dependencies = [ "serde_json", "thiserror",@@ -803,7 +803,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.723"+version = "2.0.724" dependencies = [ "serde", "serde_json",@@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.723"+version = "2.0.724" dependencies = [ "pup-cdp", "pup-core",@@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.723"+version = "2.0.724" dependencies = [ "serde", "serde_json",@@ -835,7 +835,7 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.723"+version = "2.0.724" dependencies = [ "core-foundation", "core-graphics",@@ -851,7 +851,7 @@ dependencies = [ [[package]] name = "pup-host-win"-version = "2.0.723"+version = "2.0.724" dependencies = [ "pup-core", "pup-host",@@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.723"+version = "2.0.724" dependencies = [ "fontdue", "image",@@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.723"+version = "2.0.724" dependencies = [ "pup-core", "serde",@@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.723"+version = "2.0.724" dependencies = [ "pup-core", "pup-host",@@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.723"+version = "2.0.724" 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.723"+version = "2.0.724" edition = "2021" license = "MIT"
rust/verbs/src/extdrive.rs+31@@ -654,6 +654,37 @@ pub fn rebind_unbound(ctx: &Ctx) -> usize { /// reload_stale_worker never cycles the browser while this is above zero: a window with no session has no file, /// so the cycle would close it for good (John's laptop, 2.0.694 install: an "AI Flow" page and a YouTube video). pub static STRAYS_WAITING: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(usize::MAX); // MAX = not scanned yet+/// 2.0.724: a pup window whose tab is still on the birth page 20 s after it was born never reached its address (the+/// navigate after birth failed or timed out). It sat on John's taskbar titled PUPBIRTH-... (2026-10-17). Send it to the+/// address its session recorded; at most 3 tries per tab, a minute apart.+pub fn rescue_birth_tabs(ctx: &Ctx) -> usize {+ static TRIED: Mutex<Option<std::collections::HashMap<i64, (u32, u64)>>> = Mutex::new(None);+ if !alive(ctx) { return 0; }+ let Ok(r) = run(json!({ "action": "tabs" }), Duration::from_secs(10)) else { return 0 };+ let tabs = r["tabs"].as_array().cloned().unwrap_or_default();+ let now = pup_core::now_ms();+ let sessions = ctx.sessions.all();+ let mut tried = TRIED.lock().unwrap();+ let tried = tried.get_or_insert_with(Default::default);+ let mut n = 0;+ for t in &tabs {+ let u = t["url"].as_str().unwrap_or("");+ if !(u.contains("/birth.html") && u.starts_with("chrome-extension://")) { continue; }+ let (Some(tab), Some(wid)) = (t["tabId"].as_i64(), t["windowId"].as_i64()) else { continue };+ let Some(s) = sessions.iter().find(|s| s.window_id == Some(wid)) else { continue };+ if now.saturating_sub(s.born_at_ms.max(s.created_ms)) < 20_000 { continue; }+ let target = s.tabs.iter().find(|x| x.target_id == tab.to_string()).or_else(|| s.tabs.get(0)).map(|x| x.url.clone()).unwrap_or_default();+ if target.is_empty() || is_blank_or_birth(&target) { continue; }+ let e = tried.entry(tab).or_insert((0, 0));+ if e.0 >= 3 || now.saturating_sub(e.1) < 60_000 { continue; }+ *e = (e.0 + 1, now);+ let ok = run(json!({ "action": "navigate", "url": target, "tabId": tab, "waitMs": 20000 }), Duration::from_secs(25)).is_ok();+ pup_core::log(format!("[birth] \"{}\" tab {tab} was still on the birth page {} s after birth; sent it to {target} (try {}): {}", s.id, now.saturating_sub(s.born_at_ms.max(s.created_ms)) / 1000, e.0, if ok { "ok" } else { "failed" }));+ if ok { n += 1; }+ }+ n+}+ pub fn adopt_stray_windows(ctx: &Ctx) -> usize { static SEEN: Mutex<Option<std::collections::HashMap<i64, u64>>> = Mutex::new(None); if !alive(ctx) { return 0; }
rust/verbs/src/lib.rs+5−1@@ -2024,7 +2024,11 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne "progress": { "phase": "navigating", "elapsedSec": t_verb.elapsed().as_secs(), "percent": 70 }, "_hint": "This box is slow: the window exists, is yours and is parked in the background, and the page is still loading. Poll pup_window_info {} (url/title fill in) or pup_readiness (active[]) rather than opening again; a second open with the same window label is refused as owned." })); }- let nav = match extdrive::run(json!({ "action": "navigate", "url": url, "tabId": chrome_tab, "waitMs": 25000 }), Duration::from_secs(30)) {+ // 2.0.724 (John, 2026-10-17: a window on his taskbar titled PUPBIRTH-...): a failed navigate here was+ // swallowed, the open answered success, and the window stayed on the birth page for good. Retry once; the+ // health tick's rescue_birth_tabs is the net for anything that still sticks.+ let nav_try = || extdrive::run(json!({ "action": "navigate", "url": url, "tabId": chrome_tab, "waitMs": 25000 }), Duration::from_secs(30));+ let nav = match nav_try().or_else(|e| { pup_core::log(format!("[window] \"{sid}\" first navigate off the birth page failed ({}); retrying once", e["error"].as_str().unwrap_or("?"))); std::thread::sleep(Duration::from_millis(1500)); nav_try() }) { Ok(v) => v, Err(e) => { if s.hwnd.map(|h| !ctx.host.is_alive(h)).unwrap_or(false) && !extdrive::alive(ctx) {
rust/verbs/src/recovery.rs+1@@ -646,6 +646,7 @@ pub fn health_tick(ctx: &Ctx) { crate::extdrive::adopt_tokenless(ctx); // 2.0.580: a token-less extension browser after a restart forget_user_closed(ctx); // 2.0.595: the user's close is final lane_gone_pass(ctx); // 2.0.595: a gone lane is forgotten unless it provably crashed+ crate::extdrive::rescue_birth_tabs(ctx); // 2.0.724: a window stuck on its birth page (PUPBIRTH-... on the taskbar) crate::extdrive::adopt_stray_windows(ctx); // 2.0.694: windows Chrome made outside pup (a dragged-out tab, a page's popup) crate::extdrive::rebind_unbound(ctx); // ext sessions whose birth outran the hwnd wait: bind + stamp now crate::extdrive::reload_stale_worker(ctx); // a bridge_install must not wait for every thread's window to close