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.684 (insiders): Windows survive an upgrade AND the user's close is final. pup relaunches a missing window only when it knows the browser died for this pass (it ended it for an upgrade or restart, it crashed, or it was already dead at boot); any other missing window was closed and is forgotten. Closing a window during a relaunch queue cancels the queued relaunches of that thread (its taskbar icon). 2.0.681 dropped every window after the first on an upgrade; 2.0.680 and earlier reopened windows the user closed while pup restarted (John, AdomLapper, 2026-09-23).
5 files changed
+53−20
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.681"+version = "2.0.684" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.681"+version = "2.0.684" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.681"+version = "2.0.684" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.681"+version = "2.0.684" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.681"+version = "2.0.684" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.681"+version = "2.0.684" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.681"+version = "2.0.684" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.681"+version = "2.0.684" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.681"+version = "2.0.684" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.681"+version = "2.0.684" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.681"+version = "2.0.684" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.681"+version = "2.0.684" 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.681"+version = "2.0.684" edition = "2021" license = "MIT"
rust/verbs/src/extdrive.rs+1−1@@ -153,7 +153,7 @@ pub fn reload_stale_worker(ctx: &Ctx) { let hwnds: Vec<i64> = ctx.sessions.all().into_iter().filter(|s| s.transport == "ext").filter_map(|s| s.hwnd).collect(); // 2.0.651: from the live sessions (the file has no hwnd) let files: Vec<Value> = ctx.sessions.all().into_iter().filter(|s| s.transport == "ext").map(|s| s.file_json()).collect(); let ids: Vec<String> = files.iter().filter_map(|f| f["sessionId"].as_str().map(|s| s.to_string())).collect();- crate::recovery::reserve(&ids);+ crate::recovery::reserve(&ids); crate::recovery::mark_lost(&ids); // 2.0.682: pup ends this browser itself for id in &ids { ctx.sessions.remove(id); } let n = crate::kill_extdrive(ctx); // 2.0.650: WAIT for the windows to be gone before relaunching. The kill is asynchronous; relaunch_from_file saw
rust/verbs/src/extras.rs+1@@ -327,6 +327,7 @@ pub fn restart_browser(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { // 2.0.468: reserve every id BEFORE dropping it from memory, so the health tick never re-attaches or relaunches // one of these files while the restore thread is still working on an earlier window. crate::recovery::reserve(&files.iter().map(|(id, _, _)| id.clone()).collect::<Vec<_>>());+ crate::recovery::mark_lost(&files.iter().map(|(id, _, _)| id.clone()).collect::<Vec<_>>()); // 2.0.682: pup restarts this browser itself for (id, _, _) in &files { ctx.sessions.remove(id); } let restoring: Vec<Value> = files.iter().map(|(id, n, _)| json!({ "window": id, "tabs": n })).collect(); if let Some(a) = ctx.arc() {
rust/verbs/src/recovery.rs+38−6@@ -59,14 +59,21 @@ static RELAUNCHED_AT: std::sync::Mutex<Vec<(String, u64)>> = std::sync::Mutex::n pub fn note_relaunched(id: &str) { let mut m = RELAUNCHED_AT.lock().unwrap(); let now = pup_core::now_ms(); m.retain(|(s, t)| s != id && now.saturating_sub(*t) < 120_000); m.push((id.to_string(), now)); } pub fn recently_relaunched(id: &str, within_ms: u64) -> bool { let now = pup_core::now_ms(); RELAUNCHED_AT.lock().unwrap().iter().any(|(s, t)| s == id && now.saturating_sub(*t) < within_ms) } static CANCELLED_RESTORE: std::sync::Mutex<Vec<(String, u64)>> = std::sync::Mutex::new(Vec::new());+/// 2.0.682: windows whose browser pup KNOWS is gone for this pass (pup ended it for an upgrade or restart, it crashed,+/// or it was already dead when this pup booted). Only these are relaunched when their window is missing; a missing+/// window that is not marked here was closed and is forgotten. 2.0.681 decided by "is a browser running NOW", and the+/// first relaunch of an upgrade restarts the browser, so every later window of that upgrade was dropped as "closed".+static LOST: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());+pub fn mark_lost(ids: &[String]) { let mut m = LOST.lock().unwrap(); for id in ids { if !m.contains(id) { m.push(id.clone()); } } }+fn is_lost(id: &str) -> bool { LOST.lock().unwrap().iter().any(|s| s == id) } /// 2.0.476: a bulk close CANCELS any in-flight restore of these windows instead of waiting for it. The restore /// thread checks this after it brings a window back and, if the window was cancelled, closes it again and drops /// the session — so "Close all" is instant and no window finishes relaunching behind the close and lingers /// (close-during-restore cycle). The mark expires in 45 s, and a deliberate reopen of the id clears it at once. pub fn cancel_restore(id: &str) { let mut m = CANCELLED_RESTORE.lock().unwrap(); let now = pup_core::now_ms(); m.retain(|(s, t)| s != id && now.saturating_sub(*t) < 300_000); m.push((id.to_string(), now)); }-pub fn restore_cancelled(id: &str) -> bool { let now = pup_core::now_ms(); CANCELLED_RESTORE.lock().unwrap().iter().any(|(s, t)| s == id && now.saturating_sub(*t) < 45_000) }+pub fn restore_cancelled(id: &str) -> bool { let now = pup_core::now_ms(); CANCELLED_RESTORE.lock().unwrap().iter().any(|(s, t)| s == id && now.saturating_sub(*t) < 300_000) } // 2.0.682: 45 s -> 5 min; an upgrade queue took 70 s pub fn clear_cancel(id: &str) { CANCELLED_RESTORE.lock().unwrap().retain(|(s, _)| s != id); }-impl Drop for RelaunchGuard { fn drop(&mut self) { note_relaunched(&self.0); IN_FLIGHT.lock().unwrap().retain(|s| *s != self.0); release(&self.0); } }+impl Drop for RelaunchGuard { fn drop(&mut self) { note_relaunched(&self.0); IN_FLIGHT.lock().unwrap().retain(|s| *s != self.0); LOST.lock().unwrap().retain(|s| *s != self.0); release(&self.0); } } /// 2.0.533: an EXT session is re-adopted, never relaunched, while its Chrome window is still in the lane /// browser. The liveness check here is CDP (reconnect_profile), which an ext window never satisfies, so a@@ -114,6 +121,7 @@ fn readopt_ext(ctx: &Ctx, f: &Value) -> bool { pub fn relaunch_from_file(ctx: &Ctx, f: &Value) { let id = f["sessionId"].as_str().unwrap_or("").to_string();+ forget_user_closed(ctx); // 2.0.682: a close reported during the previous relaunch cancels this one before it opens { let mut m = IN_FLIGHT.lock().unwrap(); if m.contains(&id) { pup_core::log(format!("[recovery] \"{id}\": a relaunch is already in flight; not starting a second")); return; } m.push(id.clone()); } reserve(std::slice::from_ref(&id)); let _guard = RelaunchGuard(id.clone());@@ -149,9 +157,9 @@ pub fn relaunch_from_file(ctx: &Ctx, f: &Value) { // and simply does not have this window, the window was CLOSED — by the user, or by the page — and pup must forget // it. Before, any missing window was treated as a crash to repair, so windows the user closed while pup was // restarting (an install landed in that exact second on his laptop) came back at the next boot, twice.- {+ if f["transport"].as_str() == Some("ext") && !is_lost(&id) { let _scope = ProfileScope::enter(&profile);- if crate::extdrive::alive(ctx) && !crate::extdrive_pids().is_empty() {+ if !crate::extdrive_pids().is_empty() { pup_core::log(format!("[recovery] \"{id}\": its browser is running and does not have this window, so it was CLOSED, not lost; forgetting it (never restored)")); ctx.sessions.delete_file(&id); RELAUNCHES.lock().unwrap().retain(|(s, _)| *s != id);@@ -318,6 +326,18 @@ pub fn recover_at_boot(ctx: &Ctx) { // 2.0.454: named window sessions first; hwnd-less "adopted-*" leftovers last, so they can never claim a // window's targets ahead of the session that owns the window. let mut files = files; files.sort_by_key(|f| f["sessionId"].as_str().unwrap_or("").starts_with("adopted-"));+ // 2.0.682: decide ONCE, before anything relaunches, which profiles' browsers were already gone at boot. Their+ // windows are lost (restore them); a missing window whose browser is still running was closed while pup was down.+ {+ let mut dead_profiles: std::collections::HashMap<String, bool> = std::collections::HashMap::new();+ let mut lost = Vec::new();+ for f in files.iter().filter(|f| f["transport"].as_str() == Some("ext")) {+ let prof = f["profile"].as_str().unwrap_or("adom-you").to_string();+ let dead = *dead_profiles.entry(prof.clone()).or_insert_with(|| { let _s = ProfileScope::enter(&prof); crate::extdrive_pids().is_empty() });+ if dead { if let Some(i) = f["sessionId"].as_str() { lost.push(i.to_string()); } }+ }+ mark_lost(&lost);+ } for f in &files { let id = f["sessionId"].as_str().unwrap_or("").to_string(); if id.is_empty() { continue; }@@ -443,6 +463,17 @@ fn idle_browser_reap(ctx: &Ctx) { fn forget_session(ctx: &Ctx, s: &Session, why: &str) { ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id); cancel_restore(&s.id);+ // 2.0.682 (John, 2026-09-23): closing a thread's taskbar icon while an upgrade was relaunching its windows one by+ // one brought the NEXT window of that thread back 13-28 s later, twice. A user close now cancels every queued+ // restore of the same thread, so the icon they closed stays closed.+ if let Some(owner) = s.owner.as_deref() {+ for f in ctx.sessions.read_files() {+ let Some(sid) = f["sessionId"].as_str() else { continue };+ if sid == s.id || f["owner"].as_str() != Some(owner) || ctx.sessions.exists(sid) || !restoring(sid) { continue; }+ cancel_restore(sid); ctx.sessions.delete_file(sid);+ pup_core::log(format!("[lane] \"{sid}\": the user closed \"{}\" of the same thread ({owner}) during a restore; its queued restore is cancelled", s.id));+ }+ } pup_core::log(format!("[lane] \"{}\": {why}; forgotten, nothing restored", s.id)); crate::push_cleanup_report(format!("the user closed the \"{}\" pup window ({why}). pup forgot it and will not bring it back; open it again with pup_open_window if you still need it.", s.id)); }@@ -506,6 +537,7 @@ fn lane_gone_pass(ctx: &Ctx) { let ids: Vec<String> = ext.iter().map(|s| s.id.clone()).collect(); for s in &ext { ctx.sessions.save_file(s); ctx.sessions.remove(&s.id); } crate::kill_extdrive(ctx);+ mark_lost(&ids); for f in ctx.sessions.read_files() { let id = f["sessionId"].as_str().unwrap_or(""); if ids.iter().any(|x| x == id) { relaunch_from_file(ctx, &f); }@@ -581,7 +613,7 @@ pub fn health_tick(ctx: &Ctx) { // the second window listed with its three dead tabs for the ~30 s the first window's relaunch took // (CRR crash drill: "restored 2x3" but 3 of 6 live); a listed window must be a live one. let ids: Vec<String> = files.iter().filter_map(|f| f["sessionId"].as_str().map(|s| s.to_string())).collect();- reserve(&ids);+ reserve(&ids); mark_lost(&ids); for id in &ids { ctx.sessions.remove(id); } for f in files { relaunch_from_file(ctx, &f); } } else {@@ -613,7 +645,7 @@ pub fn health_tick(ctx: &Ctx) { if f["transport"].as_str() == Some("ext") { if readopt_ext(ctx, &f) { continue; } let dead = { let _scope = ProfileScope::enter(f["profile"].as_str().unwrap_or("adom-you")); !crate::extdrive::alive(ctx) && crate::extdrive_pids().is_empty() };- if dead { relaunch_from_file(ctx, &f); }+ if dead { if let Some(i) = f["sessionId"].as_str() { mark_lost(&[i.to_string()]); } relaunch_from_file(ctx, &f); } continue; } let profile = f["profile"].as_str().unwrap_or("adom-you").to_string();