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.725 (insiders): A pup window stuck on its birth page now gets sent to its address. Testing 2.0.724 on ConfRoomROG showed the gap behind John's PUPBIRTH window: when the navigate after birth answered but the tab never left the birth page, pup recorded the birth page as the window's address, so nothing could ever send it on. The birth page is now never recorded over a real address (live tab sync, tab refresh, both open paths, and re-adoption after a restart), the open logs it, and the health check sends the window on. Proven end to end on ConfRoomROG: a window held on its birth page across a restart was re-adopted and the health check sent it to its address on the first try; on a restart that also ended the browser, it was relaunched at its saved address (both boxes). 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.724.
6 files changed
+36−18
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.724"+version = "2.0.725" dependencies = [ "pup-browser", "pup-cdp",@@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.724"+version = "2.0.725" dependencies = [ "pup-core", "pup-host",@@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.724"+version = "2.0.725" dependencies = [ "serde_json", "thiserror",@@ -803,7 +803,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.724"+version = "2.0.725" dependencies = [ "serde", "serde_json",@@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.724"+version = "2.0.725" dependencies = [ "pup-cdp", "pup-core",@@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.724"+version = "2.0.725" dependencies = [ "serde", "serde_json",@@ -835,7 +835,7 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.724"+version = "2.0.725" dependencies = [ "core-foundation", "core-graphics",@@ -851,7 +851,7 @@ dependencies = [ [[package]] name = "pup-host-win"-version = "2.0.724"+version = "2.0.725" dependencies = [ "pup-core", "pup-host",@@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.724"+version = "2.0.725" dependencies = [ "fontdue", "image",@@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.724"+version = "2.0.725" dependencies = [ "pup-core", "serde",@@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.724"+version = "2.0.725" dependencies = [ "pup-core", "pup-host",@@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.724"+version = "2.0.725" 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.724"+version = "2.0.725" edition = "2021" license = "MIT"
rust/verbs/src/extdrive.rs+14−1@@ -657,6 +657,12 @@ pub static STRAYS_WAITING: std::sync::atomic::AtomicUsize = std::sync::atomic::A /// 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.+/// pup's own birth page. Never recorded as a tab's address: it would replace the address the window was opened for,+/// and rescue_birth_tabs could then never send a stuck window on (found testing 2.0.724 on ConfRoomROG).+pub fn is_birth_url(u: &str) -> bool { u.starts_with("chrome-extension://") && u.contains("/birth.html") }+/// Record a live address for a tab, unless it is the birth page and the tab already holds a real address.+pub fn keep_url(cur: &mut String, live: &str) { if live.is_empty() || (is_birth_url(live) && !cur.is_empty() && !is_birth_url(cur)) { return; } *cur = live.to_string(); }+ 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; }@@ -669,7 +675,7 @@ pub fn rescue_birth_tabs(ctx: &Ctx) -> usize { 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; }+ if !is_birth_url(u) { 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; }@@ -942,6 +948,13 @@ pub fn ext_handle() -> std::sync::Arc<crate::BrowserHandle> { mod tests { // 2.0.723: a lane dropped while its thread still works in it (the idle sweep during a 20 s close) must never // resolve to the adom-you lane; that wiped the user's main browser record and ended it.+ #[test] fn birth_page_never_replaces_a_real_address() {+ let b = "chrome-extension://liomcaladmooagjiebkfgegciadapnpi/birth.html?token=x1";+ let mut u = "https://example.com/".to_string(); super::keep_url(&mut u, b); assert_eq!(u, "https://example.com/");+ super::keep_url(&mut u, "https://example.org/"); assert_eq!(u, "https://example.org/");+ let mut e = String::new(); super::keep_url(&mut e, b); assert_eq!(e, b);+ super::keep_url(&mut u, ""); assert_eq!(u, "https://example.org/");+ } #[test] fn dropped_lane_never_aliases_adom_you() { super::default_lane().token.lock().unwrap().clear(); *super::default_lane().token.lock().unwrap() = "keep-me".into();
rust/verbs/src/lib.rs+6−3@@ -1169,7 +1169,7 @@ pub fn inject_page_scripts_ext(ctx: &Ctx, s: &Session, lane: &transport::Lane, t pub fn refresh_tab(cdp: &Cdp, t: &mut Tab) { if let Ok(info) = cdp.target_info(&t.cdp_session) {- if let Some(u) = info.get("url").and_then(|u| u.as_str()) { t.url = u.into(); }+ if let Some(u) = info.get("url").and_then(|u| u.as_str()) { extdrive::keep_url(&mut t.url, u); } if let Some(ti) = info.get("title").and_then(|u| u.as_str()) { t.title = ti.into(); } } }@@ -2012,7 +2012,7 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne std::thread::Builder::new().name("open-finish".into()).spawn(move || { let ctx2: &Ctx = &a; if let Ok(nav) = extdrive::run(json!({ "action": "navigate", "url": url2, "tabId": tab2, "waitMs": 25000 }), Duration::from_secs(30)) {- ctx2.sessions.with_mut(&sid2, |x| if let Some(t0) = x.tabs.get_mut(0) { if let Some(u) = nav["url"].as_str() { if !u.is_empty() { t0.url = u.to_string(); } } if let Some(ti) = nav["title"].as_str() { if !ti.is_empty() { t0.title = ti.to_string(); } } });+ ctx2.sessions.with_mut(&sid2, |x| if let Some(t0) = x.tabs.get_mut(0) { if let Some(u) = nav["url"].as_str() { extdrive::keep_url(&mut t0.url, u); } if let Some(ti) = nav["title"].as_str() { if !ti.is_empty() { t0.title = ti.to_string(); } } }); if let Some(ss) = ctx2.sessions.get(&sid2) { ctx2.sessions.save_file(&ss); } } extdrive::fit_now();@@ -2040,7 +2040,10 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne } }; if let Some(t0) = s.tabs.get_mut(0) {- if let Some(u) = nav["url"].as_str() { if !u.is_empty() { t0.url = u.to_string(); } }+ if let Some(u) = nav["url"].as_str() {+ if extdrive::is_birth_url(u) { pup_core::log(format!("[window] \"{sid}\" the navigate to {} answered but the tab is still on the birth page; the health check will send it on", t0.url)); }+ extdrive::keep_url(&mut t0.url, u);+ } if let Some(ti) = nav["title"].as_str() { if !ti.is_empty() { t0.title = ti.to_string(); } } } }
rust/verbs/src/recovery.rs+2@@ -132,6 +132,8 @@ fn readopt_ext(ctx: &Ctx, f: &Value) -> bool { s.next_tab_seq = s.next_tab_seq.max(top + 1); for t in &live { let mut tab = crate::transport::ext_tab(&mut s, t["tabId"].as_i64().unwrap_or(0), t["url"].as_str().unwrap_or(""), t["title"].as_str().unwrap_or(""));+ // 2.0.724: a tab still on the birth page keeps the address the file recorded, so the rescue can send it on+ if let Some(saved_url) = f["tabs"].as_array().and_then(|a| a.iter().find(|x| x["browserTabId"].as_str() == Some(tab.target_id.as_str()))).and_then(|x| x["url"].as_str()) { let mut u = saved_url.to_string(); crate::extdrive::keep_url(&mut u, &tab.url); tab.url = u; } if let Some(keep) = saved.get(&tab.target_id) { if !s.tabs.iter().any(|x| &x.tab_id == keep) { tab.tab_id = keep.clone(); } } s.tabs.push(tab); }
rust/verbs/src/transport.rs+1−1@@ -174,7 +174,7 @@ pub fn refresh_all(lane: &Lane, s: &mut Session) { let live = r["tabs"].as_array().cloned().unwrap_or_default(); for t in s.tabs.iter_mut() { if let Some(l) = live.iter().find(|l| l["tabId"].as_i64().map(|i| i.to_string()) == Some(t.target_id.clone())) {- if let Some(u) = l["url"].as_str() { t.url = u.into(); }+ if let Some(u) = l["url"].as_str() { crate::extdrive::keep_url(&mut t.url, u); } if let Some(ti) = l["title"].as_str() { t.title = ti.into(); } } }