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.680 (insiders): The cache prune never races a browser install: it refuses while one is in flight (the boot pass waits for it) and never touches a folder written in the last 5 minutes, so a first install unpacking into <build>.partial cannot be deleted under it. Plus everything in 2.0.679.
4 files changed
+29−14
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.679"+version = "2.0.680" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.679"+version = "2.0.680" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.679"+version = "2.0.680" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.679"+version = "2.0.680" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.679"+version = "2.0.680" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.679"+version = "2.0.680" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.679"+version = "2.0.680" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.679"+version = "2.0.680" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.679"+version = "2.0.680" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.679"+version = "2.0.680" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.679"+version = "2.0.680" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.679"+version = "2.0.680" 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.679"+version = "2.0.680" edition = "2021" license = "MIT"
rust/bridge/src/main.rs+6−1@@ -118,7 +118,12 @@ fn main() { pup_verbs::highfps::start_loop(c.clone()); pup_verbs::extras::start_identity_audit(c.clone()); pup_verbs::extras::start_profile_janitor(c.clone());- { let c2 = c.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(60)); pup_verbs::identity::sweep_dead_identities(&c2, "boot"); let _ = pup_verbs::extras::cache_prune(&c2, false); }); }+ { let c2 = c.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(60)); pup_verbs::identity::sweep_dead_identities(&c2, "boot");+ // 2.0.680: a first-install download can still be unpacking at +60 s; wait for it (up to 20 min) so the+ // prune never deletes the build being written.+ let t0 = std::time::Instant::now();+ while c2.installer.installing() && t0.elapsed() < std::time::Duration::from_secs(1200) { std::thread::sleep(std::time::Duration::from_secs(10)); }+ let _ = pup_verbs::extras::cache_prune(&c2, false); }); } if let Ok(v) = c.ab.call("bridge_info", json!({}), &Caller::default(), Duration::from_secs(4)) { *c.ad_version.lock().unwrap() = v.get("version").and_then(|x| x.as_str()).map(|s| s.to_string()); } }); }
rust/verbs/src/extras.rs+10@@ -353,8 +353,17 @@ pub fn cache_prune(ctx: &Ctx, dry: bool) -> Value { if let Ok(rd) = std::fs::read_dir(p) { for e in rd.flatten() { let q = e.path(); if q.is_dir() { n += size_of(&q); } else if let Ok(m) = e.metadata() { n += m.len(); } } } n }+ // 2.0.680: never prune while a browser install is in flight, and never touch a directory written in the last+ // 5 minutes. The installer unpacks into "<build>.partial" and renames at the end; on a fresh box the boot prune+ // (60 s) would have deleted that folder mid-unpack on any connection slower than the test VM's (measured there:+ // download+unpack finished at 20 s, so the race was invisible).+ if !dry && ctx.installer.installing() {+ pup_core::log("[cache] prune skipped: a browser install is in flight");+ return ok(json!({ "success": true, "skipped": "install_in_flight", "removed": [], "freedMB": 0, "_hint": "A browser download is unpacking right now; pruning waits for it. pup retries at the next boot, or call this again when pup_readiness says installing:false." }));+ } let root = ctx.host.cft_cache_dir(); let norm = |p: &std::path::Path| p.to_string_lossy().replace('\\', "/").to_lowercase();+ let fresh = |p: &std::path::Path| std::fs::metadata(p).and_then(|m| m.modified()).map(|t| t.elapsed().map(|e| e.as_secs() < 300).unwrap_or(false)).unwrap_or(false); // what must survive: running browser executables and the saved default let mut keep_paths: Vec<String> = ctx.host.run_shell_script("Get-CimInstance Win32_Process -Filter \"Name='chrome.exe' OR Name='chrome-headless-shell.exe'\" | ForEach-Object { $_.ExecutablePath }", Duration::from_secs(20)) .lines().map(|l| l.trim().replace('\\', "/").to_lowercase()).filter(|l| !l.is_empty()).collect();@@ -374,6 +383,7 @@ pub fn cache_prune(ctx: &Ctx, dry: bool) -> Value { let in_use = keep_paths.iter().any(|k| k.starts_with(&(bn.clone() + "/"))); let is_keep = pname == "chrome" && (b == pinned && pinned_ok || fallback.as_ref() == Some(&b)); let label = b.strip_prefix(&root).map(|x| x.to_string_lossy().replace('\\', "/")).unwrap_or_else(|_| bn.clone());+ if fresh(&b) { kept.push(json!({ "path": label, "why": "written in the last 5 minutes (an install may be unpacking into it)" })); continue; } if is_keep || in_use { kept.push(json!({ "path": label, "why": if in_use { "a running process or the default browser uses it" } else if b == pinned { "the pinned build" } else { "the newest complete build (the pinned one is missing)" } })); continue; } let sz = size_of(&b); if dry { removed.push(json!({ "path": label, "mb": sz / 1_048_576 })); freed += sz; continue; }