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.669 (insiders): Windows survive an upgrade: recovery no longer counts a window that is still on screen (waiting for the extension to reconnect) as a failed relaunch, so it never deletes a live window's record. Before, three quiet checks after a restart forgot the window, leaving it ownerless with no taskbar badge and making its thread open a duplicate. The identity audit also reads ab's nested reply now instead of reporting every window as unstamped.
4 files changed
+30−20
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.668"+version = "2.0.669" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.668"+version = "2.0.669" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.668"+version = "2.0.669" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.668"+version = "2.0.669" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.668"+version = "2.0.669" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.668"+version = "2.0.669" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.668"+version = "2.0.669" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.668"+version = "2.0.669" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.668"+version = "2.0.669" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.668"+version = "2.0.669" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.668"+version = "2.0.669" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.668"+version = "2.0.669" 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.668"+version = "2.0.669" edition = "2021" license = "MIT"
rust/verbs/src/extras.rs+7−1@@ -171,7 +171,13 @@ pub fn start_identity_audit(ctx: Arc<Ctx>) { if wins.is_empty() { continue; } let caller = crate::identity::internal_caller(); let Ok(r) = ctx.ab.call("desktop_get_window_identity", json!({ "hwnds": wins.iter().map(|w| w.1).collect::<Vec<_>>() }), &caller, Duration::from_secs(10)) else { continue };- let rows = r["rows"].as_array().or(r["windows"].as_array()).cloned().unwrap_or_default();+ // 2.0.669: ab answers {output:"{..., data:{windows:[...]}}"}; reading only the top level found no rows, so+ // every window was reported "the OS says None" every 5 minutes while Windows held the right identity+ // (AdomLapper, 2026-09-21). Accept every shape, and treat a reply with no rows as no reading at all.+ let body = match &r["output"] { Value::String(t) => serde_json::from_str::<Value>(t).unwrap_or(Value::Null), v if v.is_object() => v.clone(), _ => r.clone() };+ let pick = |v: &Value| v["rows"].as_array().or(v["windows"].as_array()).or(v["data"]["windows"].as_array()).or(v["data"]["rows"].as_array()).cloned();+ let rows = pick(&body).or_else(|| pick(&r)).unwrap_or_default();+ if rows.is_empty() { pup_core::log("[identity-audit] ab returned no rows; no reading this tick (not a mismatch)"); continue; } let mut mismatched = Vec::new(); for (sid, hwnd, want) in wins { let actual = rows.iter().find(|x| x["hwnd"].as_i64() == Some(hwnd)).and_then(|x| x["appId"].as_str().map(|s| s.to_string()));
rust/verbs/src/recovery.rs+10−6@@ -113,15 +113,19 @@ pub fn relaunch_from_file(ctx: &Ctx, f: &Value) { let url = f["url"].as_str().unwrap_or("about:blank").to_string(); let profile = f["profile"].as_str().unwrap_or("adom-you").to_string(); let owner = f["owner"].as_str().unwrap_or("").to_string();- let attempts = { let mut m = RELAUNCHES.lock().unwrap(); match m.iter_mut().find(|(s, _)| *s == id) { Some(e) => { e.1 += 1; e.1 } None => { m.push((id.clone(), 1)); 1 } } };- if attempts > 2 { pup_core::log(format!("[recovery] \"{id}\" failed to come back twice — giving up on it (the page itself may be crashing Chrome); removing its file")); ctx.sessions.delete_file(&id); crate::push_cleanup_report(format!("pup could NOT auto-recover the \"{id}\" window: it crashed again on relaunch ({url}). Open it again yourself with pup_open_window if you still need it.")); return; }+ // 2.0.669: a window still on glass is NOT a failed relaunch. The "alive, will re-adopt" path below used to+ // run after this counter, so three quiet 15 s ticks read as "failed to come back twice" and the file of a+ // LIVE window was deleted: after the 2.0.668 upgrade on AdomLapper (2026-09-20 23:57) three windows stayed on+ // screen with no session, no badge and no owner, and the adom/insiders thread opened a duplicate the next+ // morning. Re-adopt first, then decline while the window lives, and count only real relaunches. if restore_cancelled(&id) { pup_core::log(format!("[recovery] \"{id}\": its restore was cancelled by a close; not relaunching")); ctx.sessions.delete_file(&id); return; }- if readopt_ext(ctx, f) { return; }- // 2.0.588: the window this file describes is still on glass: nothing is gone. Relaunching here replaced a- // window the user had just raised. Re-adopt failed only because the worker was quiet; the next tick retries.+ if readopt_ext(ctx, f) { RELAUNCHES.lock().unwrap().retain(|(s, _)| *s != id); return; } if let Some(h) = f["_hwnd"].as_i64().filter(|&h| h > 0) {- if ctx.host.is_alive(h) { pup_core::log(format!("[recovery] \"{id}\": its window {h} is alive on glass; NOT relaunching (worker quiet, will re-adopt)")); return; }+ if ctx.host.is_alive(h) { pup_core::log(format!("[recovery] \"{id}\": its window {h} is alive on glass; NOT relaunching (worker quiet, will re-adopt on the next tick; its file is kept)")); return; } }+ let attempts = { let mut m = RELAUNCHES.lock().unwrap(); match m.iter_mut().find(|(s, _)| *s == id) { Some(e) => { e.1 += 1; e.1 } None => { m.push((id.clone(), 1)); 1 } } };+ if attempts > 2 { pup_core::log(format!("[recovery] \"{id}\" failed to come back twice — giving up on it (the page itself may be crashing Chrome); removing its file")); ctx.sessions.delete_file(&id); crate::push_cleanup_report(format!("pup could NOT auto-recover the \"{id}\" window: it crashed again on relaunch ({url}). Open it again yourself with pup_open_window if you still need it.")); return; }+ // (2.0.588: a window still on glass is never relaunched; that check now runs before the attempt counter.) pup_core::log(format!("[recovery] \"{id}\": its Chrome is gone (profile {profile}) — relaunching at {url} in the background (attempt {attempts})")); let caller = pup_core::Caller { thread: owner.clone(), reason: "auto-recovery: the window's Chrome was gone".into(), ..Default::default() }; let open_args = json!({ "window": id, "url": url, "profile": profile, "takeover": true, "lane": f["transport"].as_str().map(|x| x.to_string()).unwrap_or_else(|| ctx.settings.get("defaultLane", "ext")), "owner": if owner.is_empty() { Value::Null } else { json!(owner) } });