← Commit history

v2.0.734 (insiders): pup keeps its own browser on the current Chrome Stable, silently (John, 2026-10-25: 'New Chrome available' showed in a pup window; pup had pinned one Chrome for Testing build, which has no updater, and it had fallen four major versions behind). A few minutes after start and every 6 hours pup checks Google's Chrome for Testing Stable channel and downloads a newer build in the background; the next browser start uses it. If pup's browser is still on the old build, pup restarts it once when no AI has used pup for 10 minutes and the user is not looking at a pup window; every window comes back at its page. The cache pruner keeps the newest build instead of the pinned one, and readiness counts a newer build as ready. Setting browserAutoUpdate (on by default). Tested on ConfRoomROG and arav-rog: the updater fetched and unpacked 156.0.8078.47 by itself, the restarted browser ran from it, and every gate passed on 156 (smoke all pass, foreground pass, jump list at baseline, isolated-close replay kept every window, parity 48/56 per pup-bridge#131, login tip and auto-fill). Includes 2.0.733.

John Lauer ·f278a89590 ·16d ago ·parent 9958332
7 files changed +97−20
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.733"+version = "2.0.734" dependencies = [  "pup-browser",  "pup-cdp",@@ -781,7 +781,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.733"+version = "2.0.734" dependencies = [  "pup-core",  "pup-host",@@ -793,7 +793,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.733"+version = "2.0.734" dependencies = [  "serde_json",  "thiserror",@@ -803,7 +803,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.733"+version = "2.0.734" dependencies = [  "serde",  "serde_json",@@ -813,7 +813,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.733"+version = "2.0.734" dependencies = [  "pup-cdp",  "pup-core",@@ -826,7 +826,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.733"+version = "2.0.734" dependencies = [  "serde",  "serde_json",@@ -835,7 +835,7 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.733"+version = "2.0.734" dependencies = [  "core-foundation",  "core-graphics",@@ -851,7 +851,7 @@ dependencies = [  [[package]] name = "pup-host-win"-version = "2.0.733"+version = "2.0.734" dependencies = [  "pup-core",  "pup-host",@@ -861,7 +861,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.733"+version = "2.0.734" dependencies = [  "fontdue",  "image",@@ -871,7 +871,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.733"+version = "2.0.734" dependencies = [  "pup-core",  "serde",@@ -881,7 +881,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.733"+version = "2.0.734" dependencies = [  "pup-core",  "pup-host",@@ -892,7 +892,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.733"+version = "2.0.734" 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.733"+version = "2.0.734" edition = "2021" license = "MIT" 
rust/browser/src/lib.rs+20−5
@@ -46,9 +46,9 @@ pub fn cft_versioned_dir(host: &dyn Host, build_id: &str) -> PathBuf { host.cft_ pub fn cft_exe_path(host: &dyn Host, build_id: &str) -> PathBuf { cft_versioned_dir(host, build_id).join(host.cft_exe_relative()) }  pub fn detect_cft(host: &dyn Host) -> Option<Candidate> {-    let exe = cft_exe_path(host, CFT_BUILD_ID);-    if exe.is_file() { return Some(Candidate { kind: "chrome-for-testing".into(), source: "cache".into(), executable_path: exe, build_id: Some(CFT_BUILD_ID.into()) }); }-    // any other complete build in the cache (newest wins), so an older cache still drives+    // 2.0.734 (John, 2026-10-25: "New Chrome available" in a pup window): the NEWEST complete build in the cache+    // wins, not the pinned one, so a build the auto-updater (cft_auto_update) unpacked takes over at the next launch.+    // CFT_BUILD_ID is now only the first-install build and the floor.     let root = host.cft_cache_dir().join("chrome");     let mut builds: Vec<(String, PathBuf)> = fs::read_dir(&root).ok()?.flatten()         .filter_map(|e| { let n = e.file_name().to_string_lossy().to_string(); let prefix = format!("{}-", host.cft_cache_platform()); n.strip_prefix(&prefix).map(|b| (b.to_string(), e.path().join(host.cft_exe_relative()))) })@@ -136,6 +136,9 @@ impl Installer {         // upgrades on its own.         let pinned = cft_exe_path(&*host, CFT_BUILD_ID);         if pinned.is_file() { let mut s = self.state.lock().unwrap(); s.phase = "ready".into(); s.executable_path = Some(pinned); return; }+        // 2.0.734: a NEWER build the auto-updater installed counts as ready too; otherwise pruning the old pinned build+        // after an upgrade would make every start download it again.+        if let Some(c) = detect_cft(&*host) { if c.build_id.as_deref().map(|b| !build_newer(CFT_BUILD_ID, b)).unwrap_or(false) { let mut s = self.state.lock().unwrap(); s.phase = "ready".into(); s.executable_path = Some(c.executable_path); return; } }         if self.inflight.swap(true, Ordering::SeqCst) { return; }         let state = self.state.clone(); let inflight = self.inflight.clone();         std::thread::Builder::new().name("cft-install".into()).spawn(move || {@@ -209,8 +212,20 @@ pub fn disk_free_mb(path: &Path) -> Option<u64> {  fn set(state: &Arc<Mutex<InstallState>>, f: impl FnOnce(&mut InstallState)) { if let Ok(mut s) = state.lock() { f(&mut s); } } -pub fn install_cft(host: &dyn Host, state: &Arc<Mutex<InstallState>>) -> Result<PathBuf, (String, String)> {-    let build = CFT_BUILD_ID;+pub fn install_cft(host: &dyn Host, state: &Arc<Mutex<InstallState>>) -> Result<PathBuf, (String, String)> { install_cft_build(host, state, CFT_BUILD_ID) }++/// The current Stable Chrome for Testing version from Google's feed, e.g. "155.0.7811.92".+pub fn latest_stable_build() -> Option<String> {+    let r = ureq::AgentBuilder::new().timeout_connect(Duration::from_secs(15)).timeout(Duration::from_secs(30)).build()+        .get("https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json").call().ok()?;+    let v: Value = serde_json::from_str(&r.into_string().ok()?).ok()?;+    let b = v["channels"]["Stable"]["version"].as_str()?.to_string();+    if b.chars().all(|c| c.is_ascii_digit() || c == '.') && b.contains('.') { Some(b) } else { None }+}+/// Is version `a` newer than `b`?+pub fn build_newer(a: &str, b: &str) -> bool { version_key(a) > version_key(b) }++pub fn install_cft_build(host: &dyn Host, state: &Arc<Mutex<InstallState>>, build: &str) -> Result<PathBuf, (String, String)> {     set(state, |s| { *s = InstallState { phase: "installing".into(), started_at: Some(Instant::now()), build_id: Some(build.into()), ..Default::default() }; });     let cache = host.cft_cache_dir();     let root = cache.join("chrome");
rust/verbs/src/extras.rs+3−1
@@ -386,6 +386,7 @@ pub fn cache_prune(ctx: &Ctx, dry: bool) -> Value {     let pinned = pup_browser::cft_versioned_dir(&*ctx.host, pup_browser::CFT_BUILD_ID);     let pinned_ok = pup_browser::cft_exe_path(&*ctx.host, pup_browser::CFT_BUILD_ID).is_file();     let fallback = if pinned_ok { None } else { pup_browser::detect_cft(&*ctx.host).and_then(|c| c.executable_path.ancestors().find(|a| a.parent().map(|pp| pp.ends_with("chrome")).unwrap_or(false)).map(|a| a.to_path_buf())) };+    let newest = pup_browser::detect_cft(&*ctx.host).and_then(|c| c.executable_path.ancestors().find(|a| a.parent().map(|pp| pp.ends_with("chrome")).unwrap_or(false)).map(|a| a.to_path_buf()));     let mut removed = Vec::new(); let mut kept = Vec::new(); let mut freed = 0u64; let mut failed = Vec::new();     let Ok(top) = std::fs::read_dir(&root) else { return ok(json!({ "success": true, "cacheDir": norm(&root), "removed": [], "freedMB": 0, "_hint": "No browser download cache on this box." })) };     for product in top.flatten() {@@ -396,7 +397,8 @@ pub fn cache_prune(ctx: &Ctx, dry: bool) -> Value {         for b in builds {             let bn = norm(&b);             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));+            // 2.0.734: keep the build pup launches (the newest complete one), not a pinned build it has moved past+            let is_keep = pname == "chrome" && (newest.as_ref() == Some(&b) || (b == pinned && pinned_ok && newest.is_none()) || 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; }
rust/verbs/src/lib.rs+1
@@ -279,6 +279,7 @@ pub fn launch_extdrive(ctx: &Ctx, url: &str) -> Result<u32, Value> {     // never called home). Same profile-lock rule pup_restart_browser already follows: end the old one and wait     // for the lock to clear before launching.     let image = cand.executable_path.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_else(|| "chrome.exe".into());+    if profile == "adom-you" { *crate::recovery::LANE_BUILD.lock().unwrap() = cand.build_id.clone(); }   // 2.0.734: which build the main lane runs     // Hand the profile over from the CDP lane. Without this the portless launch FORWARDS to the running     // CDP browser and exits at once, which silently gives you a window that still carries a debug port -     // the exact thing this lane exists to avoid.
rust/verbs/src/recovery.rs+58
@@ -649,6 +649,63 @@ pub fn sweep_idle_isolated(ctx: &Ctx) -> usize { /// until the browser restarts. Probe WebGL in a pup window every 5 min (every minute once a probe fails); after two /// failures in a row, restart pup's browser when no AI has used a pup window for a minute (at most every 30 min), so /// every window comes back with 3D working, and tell the user.+/// The Chrome for Testing build the adom-you lane browser was launched from (set in launch_extdrive).+pub static LANE_BUILD: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);++/// 2.0.734 (John, 2026-10-25: "it sure seems like u should just be upgrading silently and always"). pup pinned one+/// Chrome for Testing build, which has no updater of its own, so once it aged Chrome showed "New Chrome available"+/// in pup windows. Now: a few minutes after start and every 6 h, check Google's Stable channel; download and unpack+/// a newer build in the background (detect_cft then prefers it). When the main lane still runs an older build, wait+/// until no AI has used pup for 10 minutes and no pup window is the one the user is looking at, then restart pup's+/// browser once; every window comes back at its page on the new build.+pub fn cft_auto_update(ctx: &Ctx) {+    static STATE: std::sync::Mutex<(u64, bool, Option<String>)> = std::sync::Mutex::new((0, false, None));   // last check, check running, restarted for build+    static BOOT: std::sync::OnceLock<u64> = std::sync::OnceLock::new();+    let now = pup_core::now_ms();+    let boot = *BOOT.get_or_init(|| now);+    if ctx.settings.get("browserAutoUpdate", "on") == "off" { return; }+    let (last, running, restarted_for) = STATE.lock().unwrap().clone();+    // 1. check + stage+    if !running && now.saturating_sub(boot) > 3 * 60_000 && (last == 0 || now.saturating_sub(last) > 6 * 3600_000) {+        STATE.lock().unwrap().0 = now; STATE.lock().unwrap().1 = true;+        let host = ctx.host.clone();+        std::thread::Builder::new().name("cft-update".into()).spawn(move || {+            let current = pup_browser::detect_cft(&*host).and_then(|c| c.build_id).unwrap_or_else(|| pup_browser::CFT_BUILD_ID.to_string());+            match pup_browser::latest_stable_build() {+                Some(latest) if pup_browser::build_newer(&latest, &current) => {+                    pup_core::log(format!("[chrome-update] Stable is {latest}, pup has {current}: downloading it in the background"));+                    let st = std::sync::Arc::new(std::sync::Mutex::new(pup_browser::InstallState::default()));+                    match pup_browser::install_cft_build(&*host, &st, &latest) {+                        Ok(_) => pup_core::log(format!("[chrome-update] {latest} is ready; pup's browser moves to it at its next start")),+                        Err((code, e)) => pup_core::log(format!("[chrome-update] could not install {latest} ({code}): {e}; pup keeps {current}")),+                    }+                }+                Some(latest) => pup_core::log(format!("[chrome-update] up to date ({current}; Stable is {latest})")),+                None => pup_core::log("[chrome-update] could not read the Chrome for Testing Stable version; will try again later"),+            }+            STATE.lock().unwrap().1 = false;+        }).ok();+        return;+    }+    // 2. switch the running lane to a newer staged build at a quiet moment+    let Some(newest) = pup_browser::detect_cft(&*ctx.host).and_then(|c| c.build_id) else { return };+    let Some(lane) = LANE_BUILD.lock().unwrap().clone() else { return };+    if !pup_browser::build_newer(&newest, &lane) || restarted_for.as_deref() == Some(newest.as_str()) { return; }+    let sessions = ctx.sessions.for_profile("adom-you");+    if sessions.is_empty() { return; }   // nothing open: the next open launches the new build anyway+    let busy = sessions.iter().map(|s| s.last_agent_update_ms.max(s.user_active_at_ms)).max().unwrap_or(0);+    if now.saturating_sub(busy) < 10 * 60_000 { return; }+    let fg = ctx.host.foreground();+    if fg != 0 && sessions.iter().any(|s| s.hwnd == Some(fg)) { return; }   // the user is looking at a pup window+    STATE.lock().unwrap().2 = Some(newest.clone());+    let Some(a) = ctx.arc() else { return };+    pup_core::log(format!("[chrome-update] moving pup's browser from {lane} to {newest} while it is quiet; every window comes back at its page"));+    std::thread::Builder::new().name("cft-switch".into()).spawn(move || {+        let ctx2: &Ctx = &a;+        let _ = crate::extras::restart_browser(ctx2, &crate::identity::internal_caller(), &json!({ "profile": "adom-you", "reason": format!("move pup's browser to Chrome {newest}") }));+    }).ok();+}+ pub fn webgl_watch(ctx: &Ctx) {     static STATE: std::sync::Mutex<(u64, u32, u64, u64)> = std::sync::Mutex::new((0, 0, 0, 0));   // last probe, failures, last restart, last wait log     let now = pup_core::now_ms();@@ -704,6 +761,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+    cft_auto_update(ctx);   // 2.0.734: keep pup's browser on the current Chrome Stable, silently     webgl_watch(ctx);   // 2.0.728: Chrome turned WebGL off after GPU crashes; restart when quiet     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)
rust/verbs/src/settings.rs+2−1
@@ -18,7 +18,8 @@ pub const KEYS: &[(&str, &str, &str)] = &[     ("consumerBrands", "on", "on|off"),     ("laneStockFlags", "on", "on|off"),     ("cdpLane", "off", "on|off"),-    ("importTipBelow", "5", "int"),   // 2.0.733: the first-window import tip shows while the vault has fewer saved logins than this   // 2.0.652 (John): the CDP lane is opt-in per box; the extension lane must solve every use case+    ("importTipBelow", "5", "int"),+    ("browserAutoUpdate", "on", "on|off"),   // 2.0.734: keep pup's browser on the current Chrome Stable   // 2.0.733: the first-window import tip shows while the vault has fewer saved logins than this   // 2.0.652 (John): the CDP lane is opt-in per box; the extension lane must solve every use case     ("laneFieldTrials", "on", "on|off"),     ("consumerBrandsHeader", "", "str"),     ("fgWatch", "on", "on|off"), ("birthSettleMs", "400", "int"), ("aumidShortcut", "off", "on|off"), ("idleBrowserQuitMin", "10", "int"), ("winAttentionFlash", "off", "on|off"), ("prePermissions", "off", "on|off"), ("cdpQuiet", "off", "on|off"), ("stockFlags", "off", "on|off"), ("fgCaption", "on", "on|off"), ("fgCaptionMs", "3500", "int"), ("notMineCaption", "off", "on|off"),