← 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).

John Lauer ·c855e9fa68 ·1mo ago ·parent 03c272a
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();