← Commit history

v2.0.660 (insiders): per-profile extension lanes: isolated:true and profile:<name> without CDP (pup-bridge#113/#112); throwaway lanes fully cleaned up

John Lauer ·b73445f4c3 ·1mo ago ·parent a4afb47
8 files changed +192−126
dev-skills/pup-bridge-dev/SKILL.md+13
@@ -1754,3 +1754,16 @@ John's radar for a misbehaving pup. The replacement is mandatory and already wir   do not know `reload`, so the very first upgrade to that line still needs one lane cycle. - **Orphan `about:blank` windows** from a lane relaunch are swept after every ext open and on the tick. - **Rule of the day (John)**: pup is the extension lane. CDP opens only on an explicit `lane:"cdp"`.++## The extension lane is PER PROFILE (2.0.659, pup-bridge#113): how it is wired++`extdrive::LaneState` holds everything that used to be a static: lane token, pids, command queue, results, taken,+last poll, removed windows, worker version, current window, the DevTools handle. The default lane (adom-you) is+`default_lane()`; every other profile gets a state in `LANES`. Which lane a call means is a THREAD-LOCAL profile:+`dispatch` clears it per request, `transport::lane_for` sets it from the session's profile, `open_window_ext`+sets it from `_mintedProfile` / `profile`, so the 40-odd `extdrive::run` call sites are unchanged. The+worker side needed nothing: each lane's browser reads its own `extlane-<profile>/pup-extension/port.json` and+polls `/extcmd?lane=<its token>`; `take_command_for` finds the lane by token, `put_result` by command id.+Rules: a thread spawned from a verb (birth watcher, z-watch) does NOT inherit the thread-local, so capture the+lane's pids before spawning. A throwaway lane ends with its last window (`close_window`) and is swept at boot.+The CDP lane is off by default (`cdpLane`); `profile` and `isolated` no longer refer to it.
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.658"+version = "2.0.660" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.658"+version = "2.0.660" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.658"+version = "2.0.660" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.658"+version = "2.0.660" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.658"+version = "2.0.660" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.658"+version = "2.0.660" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.658"+version = "2.0.660" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.658"+version = "2.0.660" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.658"+version = "2.0.660" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.658"+version = "2.0.660" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.658"+version = "2.0.660" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.658"+version = "2.0.660" 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.658"+version = "2.0.660" edition = "2021" license = "MIT" 
rust/browser/src/lib.rs+2−1
@@ -379,7 +379,8 @@ pub fn stage_extensions(host: &dyn Host, port: u16, profile: &str, toolbar_on: b /// answered /extcmd, and pup ended up driving a DIFFERENT thread's window (measured 2026-09-13). pub fn stage_extensions_lane(host: &dyn Host, port: u16, profile: &str, toolbar_on: bool, lane: Option<&str>, identity_shim: Option<String>) -> Vec<PathBuf> {     if !toolbar_on { return Vec::new(); }-    let root = if lane.is_some() { host.state_dir().join("extlane") } else { host.state_dir() };+    // 2.0.659: one staging folder per lane profile, so each lane browser reads its OWN port.json (lane token)+    let root = if lane.is_some() { host.state_dir().join(if profile == "adom-you" { "extlane".to_string() } else { format!("extlane-{profile}") }) } else { host.state_dir() };     let _ = fs::create_dir_all(&root);     // Remove any staged extension folder that is no longer one of ours (a retired placeholder like     // pup-lane-extension left a second browser-window icon in the toolbar until it was deleted by hand,
rust/verbs/src/extdrive.rs+109−84
@@ -24,51 +24,87 @@ use crate::Ctx; /// and a verb) overwrote each other, one of them timed out and the annotate watcher died on its first /// read. Commands queue by id, results are keyed by id, the worker drains the queue on every poll, and /// everything the worker had taken is re-queued when a fresh hello says it restarted.-static PENDING: Mutex<std::collections::VecDeque<Value>> = Mutex::new(std::collections::VecDeque::new());-static RESULTS: Mutex<Option<std::collections::HashMap<u64, Value>>> = Mutex::new(None);-/// Taken by the worker, not yet answered: id -> (command, taken_at_ms).-static TAKEN: Mutex<Option<std::collections::HashMap<u64, (Value, u64)>>> = Mutex::new(None);+/// 2.0.659 (pup-bridge#113): the lane is PER PROFILE. The default lane (adom-you, the one every user runs)+/// keeps exactly the state it always had; every other profile (a throwaway iso-<id> for isolated:true, or a+/// named profile) gets its own browser, its own staged extension with its own lane token, its own queue.+/// Which lane a call means comes from a thread-local set by `enter_profile` (the dispatcher resets it per+/// request; `transport::lane_for` sets it from the session's profile), so every existing call site is unchanged.+pub struct LaneState {+    pub profile: String,+    pub token: Mutex<String>,+    pub pids: Mutex<Vec<u32>>,+    pub pending: Mutex<std::collections::VecDeque<Value>>,+    pub results: Mutex<Option<std::collections::HashMap<u64, Value>>>,+    pub taken: Mutex<Option<std::collections::HashMap<u64, (Value, u64)>>>,+    pub last_poll: Mutex<u64>,+    pub removed: Mutex<Vec<i64>>,+    pub worker: Mutex<String>,+    pub cur: Mutex<(i64, i64)>,+    pub handle: std::sync::Mutex<Option<std::sync::Arc<crate::BrowserHandle>>>,+}+impl LaneState {+    pub fn new(profile: &str) -> LaneState { LaneState { profile: profile.to_string(), token: Mutex::new(String::new()), pids: Mutex::new(Vec::new()), pending: Mutex::new(std::collections::VecDeque::new()), results: Mutex::new(None), taken: Mutex::new(None), last_poll: Mutex::new(0), removed: Mutex::new(Vec::new()), worker: Mutex::new(String::new()), cur: Mutex::new((0, 0)), handle: std::sync::Mutex::new(None) } }+}+static DEFAULT_LANE: std::sync::OnceLock<std::sync::Arc<LaneState>> = std::sync::OnceLock::new();+static LANES: Mutex<Vec<std::sync::Arc<LaneState>>> = Mutex::new(Vec::new());+thread_local! { static CUR_PROFILE: std::cell::RefCell<String> = std::cell::RefCell::new(String::new()); }+pub fn default_lane() -> std::sync::Arc<LaneState> { DEFAULT_LANE.get_or_init(|| std::sync::Arc::new(LaneState::new("adom-you"))).clone() }+pub fn st() -> std::sync::Arc<LaneState> {+    let p = CUR_PROFILE.with(|c| c.borrow().clone());+    if p.is_empty() || p == "adom-you" { return default_lane(); }+    lane_for_profile(&p).unwrap_or_else(default_lane)+}+pub fn cur_profile() -> String { let p = CUR_PROFILE.with(|c| c.borrow().clone()); if p.is_empty() { "adom-you".into() } else { p } }+pub fn enter_profile(profile: &str) {+    if profile.is_empty() || profile == "adom-you" { CUR_PROFILE.with(|c| c.borrow_mut().clear()); return; }+    if lane_for_profile(profile).is_none() { LANES.lock().unwrap().push(std::sync::Arc::new(LaneState::new(profile))); }+    CUR_PROFILE.with(|c| *c.borrow_mut() = profile.to_string());+}+pub fn leave_profile() { CUR_PROFILE.with(|c| c.borrow_mut().clear()); }+pub fn lane_for_profile(profile: &str) -> Option<std::sync::Arc<LaneState>> {+    if profile.is_empty() || profile == "adom-you" { return Some(default_lane()); }+    LANES.lock().unwrap().iter().find(|l| l.profile == profile).cloned()+}+pub fn lane_for_token(token: &str) -> Option<std::sync::Arc<LaneState>> {+    if token.is_empty() { return None; }+    if *default_lane().token.lock().unwrap() == token { return Some(default_lane()); }+    LANES.lock().unwrap().iter().find(|l| *l.token.lock().unwrap() == token).cloned()+}+pub fn drop_lane_for_profile(profile: &str) { LANES.lock().unwrap().retain(|l| l.profile != profile); }+pub fn secondary_lanes() -> Vec<std::sync::Arc<LaneState>> { LANES.lock().unwrap().clone() } static SEQ: Mutex<u64> = Mutex::new(0);-/// Last time the extension asked for work — how we know an extension-driven browser is alive.-static LAST_POLL: Mutex<u64> = Mutex::new(0);-/// 2.0.595: Chrome window ids the worker saw removed (chrome.windows.onRemoved), reported on its next poll.-/// A session whose window is in here was closed by the USER (pup's own close removes the session first).-static REMOVED: Mutex<Vec<i64>> = Mutex::new(Vec::new());-pub fn take_removed() -> Vec<i64> { std::mem::take(&mut *REMOVED.lock().unwrap()) }+pub fn take_removed() -> Vec<i64> { std::mem::take(&mut *st().removed.lock().unwrap()) }  /// GET /extcmd — the extension asks for work. Returns `{}` when there is nothing to do. pub fn take_command_for(lane: &str, removed: &[i64]) -> Value {-    // 2.0.502: only the extension-driven browser (the one carrying this launch's lane token) may take work.-    // Every pup browser loads the same extension, so without this a NORMAL window answered and pup drove-    // another thread's window.-    let want = crate::EXTDRIVE_LANE.lock().unwrap().clone();-    if want.is_empty() || lane != want { return json!({}); }-    if !removed.is_empty() { REMOVED.lock().unwrap().extend_from_slice(removed); }-    take_command()-}--pub fn take_command() -> Value {-    let first = *LAST_POLL.lock().unwrap() == 0;-    *LAST_POLL.lock().unwrap() = pup_core::now_ms();-    // 2.0.495: log the FIRST poll so "the extension never called home" is answerable from the log instead of-    // guessed at (ConfRoomROG: the channel stayed dead and there was no way to tell reach from load).-    if first { pup_core::log("[extdrive] the pup extension reached /extcmd — the control channel is live".to_string()); }-    let cmd = PENDING.lock().unwrap().pop_front().unwrap_or(json!({}));-    if let Some(id) = cmd["id"].as_u64() { TAKEN.lock().unwrap().get_or_insert_with(Default::default).insert(id, (cmd.clone(), pup_core::now_ms())); }+    // 2.0.502: only a browser carrying a lane token pup issued may take work. 2.0.659: the token names WHICH+    // lane (default adom-you, or a per-profile lane), so several browsers can be driven at once.+    let Some(l) = lane_for_token(lane) else { return json!({}) };+    if !removed.is_empty() { l.removed.lock().unwrap().extend_from_slice(removed); }+    take_command_on(&l)+}++pub fn take_command() -> Value { take_command_on(&st()) }+fn take_command_on(l: &LaneState) -> Value {+    let first = *l.last_poll.lock().unwrap() == 0;+    *l.last_poll.lock().unwrap() = pup_core::now_ms();+    if first { pup_core::log(format!("[extdrive] the pup extension reached /extcmd (lane profile {}) — the control channel is live", l.profile)); }+    let cmd = l.pending.lock().unwrap().pop_front().unwrap_or(json!({}));+    if let Some(id) = cmd["id"].as_u64() { l.taken.lock().unwrap().get_or_insert_with(Default::default).insert(id, (cmd.clone(), pup_core::now_ms())); }     cmd }  /// Called on a matched hello: if the previous worker instance took a command and never answered, hand it /// to this one. Returns how many were re-queued.-fn requeue_taken(reason: &str) -> usize {+fn requeue_taken_on(l: &LaneState, reason: &str) -> usize {     let now = pup_core::now_ms();-    let mut taken = TAKEN.lock().unwrap();+    let mut taken = l.taken.lock().unwrap();     let Some(m) = taken.as_mut() else { return 0 };     let mut ids: Vec<u64> = m.iter().filter(|(_, (_, at))| now.saturating_sub(*at) >= 1500).map(|(id, _)| *id).collect();     ids.sort();     let mut n = 0;-    let mut pending = PENDING.lock().unwrap();-    let results = RESULTS.lock().unwrap();+    let mut pending = l.pending.lock().unwrap();+    let results = l.results.lock().unwrap();     for id in ids.into_iter().rev() {   // oldest ends up at the front         if results.as_ref().map(|r| r.contains_key(&id)).unwrap_or(false) { m.remove(&id); continue; }         if let Some((cmd, _)) = m.remove(&id) {@@ -78,11 +114,13 @@ fn requeue_taken(reason: &str) -> usize {     }     n }+#[allow(dead_code)]+fn requeue_taken(reason: &str) -> usize { requeue_taken_on(&st(), reason) }  /// POST /exthello — an extension announces itself, whatever lane it holds. Throttled to one line per /// distinct (lane, match) per 30s so a 1.2s poll loop cannot flood the log. /// The worker version the running lane browser reported in its last hello ("2-0-541" from /worker.2-0-541.js).-pub static LANE_WORKER: Mutex<String> = Mutex::new(String::new());+// LANE_WORKER lives in LaneState.worker since 2.0.659 /// 2.0.569: after a bridge_install the running extension browser keeps the OLD worker until it relaunches, /// which only happens when no thread has an ext window open (measured: a 2.0.565 bridge driving a 2.0.562 /// worker for an hour). On each health tick, if the lane's worker is behind, re-stage the extension folder@@ -94,7 +132,7 @@ pub fn reload_stale_worker(ctx: &Ctx) {     let Some((have, want)) = lane_worker_stale() else { return };     if ASKED.lock().unwrap().as_deref() == Some(want.as_str()) { return; }     *ASKED.lock().unwrap() = Some(want.clone());-    let lane = crate::EXTDRIVE_LANE.lock().unwrap().clone();+    let lane = st().token.lock().unwrap().clone();     if lane.is_empty() { return; }     let shim = crate::lane_shim(ctx);     let staged = pup_browser::stage_extensions_lane(&*ctx.host, ctx.port, "adom-you", true, Some(&lane), shim);@@ -121,10 +159,10 @@ pub fn reload_stale_worker(ctx: &Ctx) { }  /// 2.0.647: a lane that ends takes its worker version with it; a fresh browser reports its own at hello.-pub fn reset_worker_version() { LANE_WORKER.lock().unwrap().clear(); }+pub fn reset_worker_version() { st().worker.lock().unwrap().clear(); }  pub fn lane_worker_stale() -> Option<(String, String)> {-    let w = LANE_WORKER.lock().unwrap().clone();+    let w = st().worker.lock().unwrap().clone();     if w.is_empty() { return None; }     let want = pup_core::version().replace('.', "-");     if w == want { None } else { Some((w, want)) }@@ -133,14 +171,13 @@ pub fn lane_worker_stale() -> Option<(String, String)> { pub fn hello(v: Value) {     static LAST: Mutex<Option<std::collections::HashMap<String, u64>>> = Mutex::new(None);     let lane = v["lane"].as_str().unwrap_or("").to_string();-    if !lane.is_empty() {-        if let Some(w) = v["worker"].as_str().and_then(|p| p.rsplit('/').next()).and_then(|f| f.strip_prefix("worker.")).and_then(|f| f.strip_suffix(".js")) {-            *LANE_WORKER.lock().unwrap() = w.to_string();-        }+    let target = lane_for_token(&lane);+    if let (Some(l), Some(w)) = (target.as_ref(), v["worker"].as_str().and_then(|p| p.rsplit('/').next()).and_then(|f| f.strip_prefix("worker.")).and_then(|f| f.strip_suffix(".js"))) {+        *l.worker.lock().unwrap() = w.to_string();     }-    let want = crate::EXTDRIVE_LANE.lock().unwrap().clone();-    let matched = !lane.is_empty() && lane == want;-    if matched { requeue_taken("the lane worker restarted (fresh hello)"); }+    let want = default_lane().token.lock().unwrap().clone();+    let matched = target.is_some();+    if let Some(l) = target.as_ref() { requeue_taken_on(l, "the lane worker restarted (fresh hello)"); }     let key = format!("{lane}|{matched}");     {         let mut g = LAST.lock().unwrap();@@ -149,45 +186,33 @@ pub fn hello(v: Value) {         if let Some(t) = m.get(&key) { if now.saturating_sub(*t) < 30_000 { return; } }         m.insert(key, now);     }-    if !matched && !lane.is_empty() && *LAST_POLL.lock().unwrap() == 0 {-        // 2.0.513: ADOPT. Nothing has ever been served since this pup launched, and a browser on our own-        // profile is asking for work with a lane token. That is an extension browser we started and then-        // lost track of (a pup restart, or a close whose kill missed the main process and took lane.json-        // with it), and it is unkillable because pup no longer knows its pids. Fighting it just spawns a-        // second Chrome that forwards to it and exits. Adopting costs nothing and cannot hijack a working-        // lane, because a working lane has served at least one poll.-        *crate::EXTDRIVE_LANE.lock().unwrap() = lane.clone();+    let dl = default_lane();+    if !matched && !lane.is_empty() && *dl.last_poll.lock().unwrap() == 0 {+        *dl.token.lock().unwrap() = lane.clone();         pup_core::log(format!("[extdrive] adopted the running extension browser (lane {lane}); pup had lost track of it"));         return;     }-    // 2.0.599: record the token-less hello whether or not pup holds a token. It used to require pup to hold-    // NONE, so a pup whose token belonged to a killed browser refused this live one on every hello and never-    // recovered (ConfRoomROG, 2026-09-16: "carrying lane \"\" (pup wants ext-1789600049390) NOT served", for-    // 20 minutes, until a pup restart). adopt_tokenless still refuses while pup's own browser is alive.-    if !matched && lane.is_empty() && *LAST_POLL.lock().unwrap() == 0 {+    if !matched && lane.is_empty() && *dl.last_poll.lock().unwrap() == 0 {         *TOKENLESS_HELLO_AT.lock().unwrap() = pup_core::now_ms();-        pup_core::log(format!("[extdrive] a pup extension is polling with no lane token{}: the next health tick adopts it if pup's own browser is gone", if want.is_empty() { String::new() } else { format!(" while pup wants \"{want}\"") }));+        pup_core::log(format!("[extdrive] a pup extension is polling with no lane token{}: the next health tick adopts it if pup's own browser is gone", if want.is_empty() { String::new() } else { format!(" while pup wants {want}") }));         return;     }-    if matched {-        pup_core::log(format!("[extdrive] hello from the lane browser (lane {lane}) — this one may take work"));-    } else {-        pup_core::log(format!("[extdrive] hello from a pup extension carrying lane {:?} (pup wants {:?}), port.json read={} pkg={} — NOT served",-            lane, want, v["read"], v["pkg"]));-    }+    if let Some(l) = target { pup_core::log(format!("[extdrive] hello from the lane browser (lane {lane}, profile {}) — this one may take work", l.profile)); }+    else { pup_core::log(format!("[extdrive] hello from a pup extension carrying lane {:?} (pup wants {:?}), port.json read={} pkg={} — NOT served", lane, want, v["read"], v["pkg"])); } } -/// POST /extresult — the extension reports what happened. pub fn put_result(v: Value) {     let Some(id) = v["id"].as_u64() else { return };-    if let Some(m) = TAKEN.lock().unwrap().as_mut() { m.remove(&id); }-    RESULTS.lock().unwrap().get_or_insert_with(Default::default).insert(id, v);+    let mut lanes = vec![default_lane()]; lanes.extend(secondary_lanes());+    let owner = lanes.iter().find(|l| l.taken.lock().unwrap().as_ref().map(|m| m.contains_key(&id)).unwrap_or(false)).cloned().unwrap_or_else(|| st());+    if let Some(m) = owner.taken.lock().unwrap().as_mut() { m.remove(&id); }+    owner.results.lock().unwrap().get_or_insert_with(Default::default).insert(id, v); }  /// A killed lane must read as dead at once. `alive()` is heartbeat-based, and a browser killed 3 s after its /// last poll still counted as alive for 7 more seconds: the next open took the WARM path and waited 60 s on a /// dead channel until ab timed out (2026-09-13, the first cold open of the 20-site proof).-pub(crate) fn mark_dead() { *LAST_POLL.lock().unwrap() = 0; }+pub(crate) fn mark_dead() { *st().last_poll.lock().unwrap() = 0; }  /// The lane as pup_status reports it. There are no lane verbs: a lane window is a pup window. pub fn lane_health(ctx: &Ctx) -> Value {@@ -198,14 +223,14 @@ pub fn lane_health(ctx: &Ctx) -> Value {     let live_windows = { let p = crate::extdrive_pids(); if p.is_empty() { 0 } else { ctx.host.windows_for_pids(&p, Some("Chrome_WidgetWin_1")).into_iter().filter(|w| w.visible && w.rect.2 - w.rect.0 > 200).count() } };     json!({ "connected": alive(ctx), "lastPollMsAgo": if ago > 86_400_000 { Value::Null } else { json!(ago) },             "processes": crate::extdrive_pids().len(), "profile": "adom-you", "debugPort": false,-            "workerVersion": LANE_WORKER.lock().unwrap().replace('-', "."), "stale": stale.is_some(),+            "workerVersion": st().worker.lock().unwrap().replace('-', "."), "stale": stale.is_some(),             "_staleHint": stale.map(|(w, want)| format!("the lane browser still runs extension {} while pup is {}; it cycles by itself the moment no extension-lane window is open, or pup_close_window {{all:true}} from every thread cycles it now", w.replace('-', "."), want.replace('-', "."))),             "sessions": ctx.sessions.all().iter().filter(|s| crate::transport::is_ext(s)).count(),             "windows": live_windows }) } -pub fn connected_ms_ago() -> u64 { pup_core::now_ms().saturating_sub(*LAST_POLL.lock().unwrap()) }-pub fn last_poll_ms() -> u64 { *LAST_POLL.lock().unwrap() }+pub fn connected_ms_ago() -> u64 { pup_core::now_ms().saturating_sub(*st().last_poll.lock().unwrap()) }+pub fn last_poll_ms() -> u64 { *st().last_poll.lock().unwrap() } /// 2.0.580: a pup extension polled with NO lane token while pup holds none either (both sides empty after a /// restart whose lane.json did not survive). Refusing it forever left an orphan browser (2026-09-14 20:57). /// The health tick adopts it: mint a token, stamp it into port.json (the worker re-reads it every poll),@@ -222,7 +247,7 @@ pub fn adopt_tokenless(ctx: &Ctx) {     if last_poll_ms() != 0 { return; }     let mine = crate::extdrive_pids();     if !mine.is_empty() && mine.iter().any(|p| ctx.host.alive(*p)) { return; }-    let had = crate::EXTDRIVE_LANE.lock().unwrap().clone();+    let had = st().token.lock().unwrap().clone();     let lane = format!("ext-{}", pup_core::now_ms());     let staged = pup_browser::stage_extensions_lane(&*ctx.host, ctx.port, "adom-you", true, Some(&lane), crate::lane_shim(ctx));     if staged.is_empty() { return; }@@ -231,7 +256,7 @@ pub fn adopt_tokenless(ctx: &Ctx) {     let cdp: Vec<u32> = ctx.browsers.lock().unwrap().values().map(|b| b.launched.pid).filter(|p| *p > 0).collect();     let pids: Vec<u32> = { let mut v: Vec<u32> = ctx.host.frame_windows_all(Some("Chrome_WidgetWin_1")).into_iter().filter(|w| w.title.ends_with("Google Chrome for Testing") && !cdp.contains(&w.pid)).map(|w| w.pid).collect(); v.sort(); v.dedup(); v };     if pids.is_empty() { return; }-    *crate::EXTDRIVE_LANE.lock().unwrap() = lane.clone();+    *st().token.lock().unwrap() = lane.clone();     crate::set_extdrive_pids(pids.clone());     crate::persist_lane(ctx);     *TOKENLESS_HELLO_AT.lock().unwrap() = 0;@@ -249,18 +274,18 @@ fn run_inner(cmd: Value, wait: Duration) -> Result<Value, Value> {     let id = { let mut s = SEQ.lock().unwrap(); *s += 1; *s };     let mut c = cmd;     c["id"] = json!(id);-    PENDING.lock().unwrap().push_back(c);+    st().pending.lock().unwrap().push_back(c);     let t0 = Instant::now();     while t0.elapsed() < wait {-        let hit = RESULTS.lock().unwrap().as_mut().and_then(|m| m.remove(&id));+        let hit = st().results.lock().unwrap().as_mut().and_then(|m| m.remove(&id));         if let Some(r) = hit {             if r["ok"] == json!(true) { return Ok(r); }             return Err(err("ext_command_failed", r["error"].as_str().unwrap_or("the extension could not run it").to_string(), None));         }         std::thread::sleep(Duration::from_millis(60));     }-    PENDING.lock().unwrap().retain(|c| c["id"].as_u64() != Some(id));-    if let Some(m) = TAKEN.lock().unwrap().as_mut() { m.remove(&id); }+    st().pending.lock().unwrap().retain(|c| c["id"].as_u64() != Some(id));+    if let Some(m) = st().taken.lock().unwrap().as_mut() { m.remove(&id); }     Err(pup_core::merge(         err("ext_no_answer", format!("the pup extension did not answer within {}s", wait.as_secs()),             Some("Is an extension-driven window open? pup_open_window starts one. The extension only answers while its browser is running; a normal CDP window does not serve this channel.")),@@ -279,14 +304,14 @@ static IN_FLIGHT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsiz pub(crate) fn alive(_ctx: &Ctx) -> bool { connected_ms_ago() < 20_000 || (IN_FLIGHT.load(std::sync::atomic::Ordering::Relaxed) > 0) }  /// The window/tab pup is driving, so navigate/eval do not wander to another window.-static CUR: Mutex<(i64, i64)> = Mutex::new((0, 0));   // (windowId, tabId)+// CUR lives in LaneState.cur since 2.0.659 fn pin(r: &Value) {     let w = r["windowId"].as_i64().unwrap_or(0);     let t = r["tabId"].as_i64().unwrap_or(0);-    if w > 0 || t > 0 { *CUR.lock().unwrap() = (w, t); }+    if w > 0 || t > 0 { *st().cur.lock().unwrap() = (w, t); } } fn aim(c: &mut Value) {-    let (w, t) = *CUR.lock().unwrap();+    let (w, t) = *st().cur.lock().unwrap();     if t > 0 { c["tabId"] = json!(t); }     if w > 0 { c["windowId"] = json!(w); } }@@ -380,7 +405,7 @@ pub fn close(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value {         return match run(json!({ "action": "close", "tabId": t }), Duration::from_secs(20)) { Ok(_) => ok(json!({ "closed": true })), Err(e) => e };     }     let via_ext = run(json!({ "action": "closeAll" }), Duration::from_secs(20)).is_ok();-    *CUR.lock().unwrap() = (0, 0);+    *st().cur.lock().unwrap() = (0, 0);     let n = crate::kill_extdrive(ctx);     ok(json!({ "closed": true, "viaExtension": via_ext, "processesEnded": n, "_hint": "The extension-driven browser is closed. pup_open_window starts a fresh one." })) }@@ -392,7 +417,7 @@ pub fn status(ctx: &Ctx, _caller: &Caller, _args: &Value) -> Value {     ok(json!({         "browserRunning": a,         "extensionConnected": a && ago < 8000,-        "lastPollMsAgo": if *LAST_POLL.lock().unwrap() == 0 { Value::Null } else { json!(ago) },+        "lastPollMsAgo": if *st().last_poll.lock().unwrap() == 0 { Value::Null } else { json!(ago) },         "profile": "adom-you",         "_hint": "Extension-driven mode carries NO CDP debug port. Measured 2026-09-12: st.com, mouser and arrow load here and fail over CDP on the same box. analog.com stays denied (that one is profile trust, and denies the real extension on a cold profile too).",         "_next": ["pup_close_window"],@@ -443,7 +468,7 @@ fn tab_inner(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value {     let mut c = json!({ "action": "newtab", "url": url });     aim(&mut c);     match run(c, Duration::from_secs(70)) {-        Ok(r) => { let w = r["windowId"].as_i64().unwrap_or(0); if w > 0 { CUR.lock().unwrap().0 = w; }+        Ok(r) => { let w = r["windowId"].as_i64().unwrap_or(0); if w > 0 { st().cur.lock().unwrap().0 = w; }             ok(json!({ "opened": true, "url": r["url"], "title": r["title"], "tabId": r["tabId"], "windowId": r["windowId"],                        "_hint": "Tab opened in the extension-driven window (no CDP debug port). pup_eval {tabId} reads it." })) }         Err(e) => e,@@ -669,7 +694,7 @@ pub fn fit_now() { pub fn apply_consumer_brands(ctx: &Ctx) {     // 6 s, not 10: on a cold start this runs before the first page and every second here is on the open's clock.     static DONE_LANE: Mutex<String> = Mutex::new(String::new());-    let lane = crate::EXTDRIVE_LANE.lock().unwrap().clone();+    let lane = st().token.lock().unwrap().clone();     if lane.is_empty() || *DONE_LANE.lock().unwrap() == lane { return; }     let on = ctx.settings.get("consumerBrands", "on") != "off";     let mut cmd = json!({ "action": "setBrands", "on": on });@@ -712,9 +737,9 @@ pub fn dress_window(ctx: &Ctx, caller: &Caller) { /// 2.0.578: a browser handle for the extension lane whose Cdp forwards every command to the extension's /// DevTools driver (nbe's cdp.js, via the worker's `cdp` action). Verbs written against BrowserHandle /// (click, hover, type, press_key, scroll, fill, element/full-res screenshots) run unchanged on this lane.-static EXT_HANDLE: std::sync::Mutex<Option<std::sync::Arc<crate::BrowserHandle>>> = std::sync::Mutex::new(None);+// EXT_HANDLE lives in LaneState.handle since 2.0.659 pub fn ext_handle() -> std::sync::Arc<crate::BrowserHandle> {-    let mut g = EXT_HANDLE.lock().unwrap();+    let lane_st = st(); let mut g = lane_st.handle.lock().unwrap();     if let Some(h) = g.as_ref() { return h.clone(); }     let via: pup_cdp::Via = Box::new(|session: Option<&str>, method: &str, params: Value, timeout: Duration| {         let tab = session.and_then(|s| s.strip_prefix("ext:")).and_then(|x| x.parse::<i64>().ok());
rust/verbs/src/lib.rs+39−27
@@ -83,24 +83,24 @@ pub(crate) static BIRTH_LOCK: Mutex<()> = Mutex::new(()); pub(crate) fn taken_hwnds(ctx: &Ctx, except: &str) -> std::collections::HashSet<i64> {     ctx.sessions.all().iter().filter(|s| s.id != except).filter_map(|s| s.hwnd).collect() }-static EXTDRIVE_PIDS: Mutex<Vec<u32>> = Mutex::new(Vec::new());+// EXTDRIVE_PIDS lives in extdrive::LaneState.pids since 2.0.659 (per profile) /// The token only the extension-driven browser carries (see stage_extensions_lane).-pub static EXTDRIVE_LANE: Mutex<String> = Mutex::new(String::new());-pub fn extdrive_pids() -> Vec<u32> { EXTDRIVE_PIDS.lock().unwrap().clone() }-pub(crate) fn set_extdrive_pids(p: Vec<u32>) { *EXTDRIVE_PIDS.lock().unwrap() = p; }+// EXTDRIVE_LANE lives in extdrive::LaneState.token since 2.0.659 (per profile)+pub fn extdrive_pids() -> Vec<u32> { extdrive::st().pids.lock().unwrap().clone() }+pub(crate) fn set_extdrive_pids(p: Vec<u32>) { *extdrive::st().pids.lock().unwrap() = p; } /// Write lane.json (token + pids) so the next pup can re-attach (restore_extdrive). pub(crate) fn persist_lane(ctx: &Ctx) {     let p = lane_file(ctx);     if let Some(d) = p.parent() { let _ = std::fs::create_dir_all(d); }-    let _ = std::fs::write(&p, json!({ "lane": EXTDRIVE_LANE.lock().unwrap().clone(), "pids": EXTDRIVE_PIDS.lock().unwrap().clone() }).to_string());+    let _ = std::fs::write(&p, json!({ "lane": extdrive::st().token.lock().unwrap().clone(), "pids": extdrive::st().pids.lock().unwrap().clone() }).to_string()); }-pub fn extdrive_browser_alive(ctx: &Ctx) -> bool { EXTDRIVE_PIDS.lock().unwrap().iter().any(|p| ctx.host.alive(*p)) }+pub fn extdrive_browser_alive(ctx: &Ctx) -> bool { extdrive::st().pids.lock().unwrap().iter().any(|p| ctx.host.alive(*p)) } pub fn kill_extdrive(ctx: &Ctx) -> u32 {-    let pids: Vec<u32> = EXTDRIVE_PIDS.lock().unwrap().clone();+    let pids: Vec<u32> = extdrive::st().pids.lock().unwrap().clone();     let mut n = 0;     for p in pids { if ctx.host.alive(p) { ctx.host.kill(p); n += 1; } }-    EXTDRIVE_PIDS.lock().unwrap().clear();-    EXTDRIVE_LANE.lock().unwrap().clear();+    extdrive::st().pids.lock().unwrap().clear();+    extdrive::st().token.lock().unwrap().clear();     extdrive::mark_dead();     extdrive::reset_worker_version();   // 2.0.647: the next hello is the truth, not the last lane's     let _ = std::fs::remove_file(lane_file(ctx));@@ -115,11 +115,11 @@ pub fn kill_extdrive(ctx: &Ctx) -> u32 { /// Chrome that forwarded to the running one and exited at once ("1 process(es)" in the log), and /// pup_close_window killed nothing because it knew no pids. Measured on AdomLapper 2026-09-13: pup said /// browserRunning:false while two of its own Chrome for Testing windows sat on John's screen.-fn lane_file(ctx: &Ctx) -> std::path::PathBuf { ctx.host.state_dir().join("extlane").join("lane.json") }+fn lane_file(ctx: &Ctx) -> std::path::PathBuf { let p = extdrive::cur_profile(); ctx.host.state_dir().join(if p == "adom-you" { "extlane".to_string() } else { format!("extlane-{p}") }).join("lane.json") }  fn save_lane(ctx: &Ctx) {-    let lane = EXTDRIVE_LANE.lock().unwrap().clone();-    let pids = EXTDRIVE_PIDS.lock().unwrap().clone();+    let lane = extdrive::st().token.lock().unwrap().clone();+    let pids = extdrive::st().pids.lock().unwrap().clone();     let p = lane_file(ctx);     if let Some(d) = p.parent() { let _ = std::fs::create_dir_all(d); }     let _ = std::fs::write(&p, json!({ "lane": lane, "pids": pids }).to_string());@@ -137,8 +137,8 @@ pub fn restore_extdrive(ctx: &Ctx) {         let _ = std::fs::remove_file(&p);   // nothing survived; do not hold a token no browser carries         return;     }-    *EXTDRIVE_LANE.lock().unwrap() = lane.clone();-    *EXTDRIVE_PIDS.lock().unwrap() = alive.clone();+    *extdrive::st().token.lock().unwrap() = lane.clone();+    *extdrive::st().pids.lock().unwrap() = alive.clone();     pup_core::log(format!("[extdrive] re-attached to the extension browser that outlived this pup ({} process(es), lane {lane})", alive.len())); } /// The consumer-identity JS shim staged into the lane extension (None when consumerBrands is off).@@ -197,7 +197,8 @@ pub fn launch_extdrive(ctx: &Ctx, url: &str) -> Result<u32, Value> {     // cannot coexist with a CDP browser on the same directory. The resolution is that the lane TAKES the     // profile: any CDP browser holding adom-you is ended first, and CDP re-launches on it are refused     // while the lane owns it (see ext_lane_owns_profile).-    let profile = "adom-you";+    let profile_owned = extdrive::cur_profile();   // 2.0.659: the lane this thread entered (adom-you unless isolated/profile)+    let profile = profile_owned.as_str();     let dir = pup_browser::profiles_dir(&*ctx.host).join(profile);     // 2.0.513: publish the token AFTER kill_extdrive below, never before. 2.0.510 taught kill_extdrive to     // clear EXTDRIVE_LANE (right, so a closed lane holds no token), but this line ran FIRST and the kill@@ -266,7 +267,7 @@ pub fn launch_extdrive(ctx: &Ctx, url: &str) -> Result<u32, Value> {         pup_core::log(format!("[lane] ended {} orphaned browser process(es) holding profile {} (no live lane owned them); launching fresh", orphans.len(), dir.display()));     }     for f in ["SingletonLock", "SingletonCookie", "SingletonSocket", "lockfile"] { let _ = std::fs::remove_file(dir.join(f)); }-    *EXTDRIVE_LANE.lock().unwrap() = lane.clone();   // the kill is done; this token is now the live one+    *extdrive::st().token.lock().unwrap() = lane.clone();   // the kill is done; this token is now the live one     let before: Vec<u32> = ctx.host.pids_for_image(&image);     let stock = ctx.settings.get("laneStockFlags", "off") == "on";     if stock { pup_core::log("[lane] laneStockFlags is ON: launching with only the firewall guards, everything else stock".to_string()); }@@ -292,7 +293,7 @@ pub fn launch_extdrive(ctx: &Ctx, url: &str) -> Result<u32, Value> {             }             pup_core::log(format!("[extdrive] {} up on profile \"{profile}\" with NO debug port ({} process(es))", cand.kind, owned.len()));             for p in &owned { ctx.host.watch_process(*p); }   // 2.0.595: the exit code decides restore vs forget-            *EXTDRIVE_PIDS.lock().unwrap() = owned;+            *extdrive::st().pids.lock().unwrap() = owned;             save_lane(ctx);             Ok(pid)         }@@ -538,6 +539,7 @@ pub const PHASE1_VERBS: &[&str] = &["pup_describe", "pup_readiness", "pup_prewar  pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value {     let t0 = Instant::now();+    extdrive::leave_profile();   // 2.0.659: every request starts on the default lane; lane_for / open enter another     let mut r = match verb {         "pup_describe" => describe(ctx),         "pup_readiness" => readiness(ctx),@@ -833,7 +835,7 @@ fn close_orphan_blank_targets(ctx: &Ctx, cdp: &Cdp, profile: &str) { /// Does the extension lane currently hold this profile directory? True only while a lane browser is /// actually alive, so a dead lane never blocks a CDP open. pub fn ext_lane_owns_profile(ctx: &Ctx, profile: &str) -> bool {-    profile == "adom-you" && !EXTDRIVE_LANE.lock().unwrap().is_empty() && extdrive_browser_alive(ctx)+    profile == "adom-you" && !extdrive::st().token.lock().unwrap().is_empty() && extdrive_browser_alive(ctx) }  pub fn browser_for(ctx: &Ctx, profile: &str) -> Option<Arc<BrowserHandle>> { ctx.browsers.lock().unwrap().get(profile).cloned().filter(|b| b.cdp.is_alive()) }@@ -1185,9 +1187,8 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {         return err("cdp_lane_disabled", "the CDP lane is off on this box; every pup window runs on the extension lane", Some("Use the extension lane (omit lane). Trusted input, HTTP auth, fetch, screenshots and recording all work there. If a feature genuinely needs CDP, file it on adom/pup-bridge as an extension-lane gap; the user can enable the lane for one box with pup_configure {cdpLane:\"on\"}."));     }     if lane != "cdp" {-        let needs_cdp = if profile != "adom-you" { Some(format!("profile:\"{profile}\" (a separate profile runs on the CDP lane; the extension lane is the shared adom-you browser)")) }-            else if isolated { Some("isolated:true (a throwaway jar is a CDP-lane feature; see pup-bridge#112 for what it does today)".to_string()) }-            else if high_fps { Some("highFps:true (anti-throttle flags are a CDP-lane feature)".to_string()) }+        // 2.0.659 (pup-bridge#112/#113): profile and isolated run on the EXTENSION lane now (a lane per profile)+        let needs_cdp = if high_fps { Some("highFps:true (anti-throttle flags are a CDP-lane feature)".to_string()) }             else if insecure { Some("webSecurity:false (a CDP-lane feature)".to_string()) }             else { None };         if let Some(why) = needs_cdp {@@ -1196,7 +1197,8 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {                 Some("The extension lane is pup's default: it is the lane every vendor site loads on, with no --remote-debugging-port for edges to detect. Pass lane:\"cdp\" ONLY if you specifically need what CDP alone can do (iframes, trusted input events, element/full-res screenshots, a separate profile, highFps, webSecurity:false); read the pup-lanes skill first. Note that lane:\"cdp\" on adom-you takes the profile away from the extension lane and closes its windows."));         }         let owner = args.get("owner").and_then(|o| o.as_str()).map(|o| o.to_string()).or_else(|| if caller.thread.is_empty() { None } else { Some(caller.thread.clone()) });-        return open_window_ext(ctx, caller, args, &sid, owner.as_deref(), &url);+        let mut a2 = args.clone(); a2["_mintedProfile"] = json!(profile);+        return open_window_ext(ctx, caller, &a2, &sid, owner.as_deref(), &url);     }     // lane:cdp on adom-you while the extension lane holds it: hand the profile back. The two lanes cannot     // share one --user-data-dir (Chrome runs one process per profile), so this is symmetric with the ext@@ -1577,16 +1579,17 @@ type BirthRx = std::sync::mpsc::Receiver<(i64, bool, bool, u128, u128, Option<u1 fn birth_watch(ctx: &Ctx, caller: &Caller, sid: &str, plan_name: &str, url: &str, before: &std::collections::HashSet<i64>, taken: &std::collections::HashSet<i64>, ext: bool) -> (Option<(String, String, std::path::PathBuf)>, bool, Option<BirthRx>) {     let plan = identity::plan(ctx, sid, plan_name, url);     let early_ok = plan.as_ref().map(|(a, d, i)| identity::preregister(ctx, caller, a, d, i)).unwrap_or(false);   // used by the watcher thread below+    let lane_pids_now: Vec<u32> = if ext { crate::extdrive_pids() } else { Vec::new() };   // 2.0.659: THIS lane's pids, read on the verb thread     let watcher =     ctx.arc().map(|a| {         let (tx, rx) = std::sync::mpsc::channel::<(i64, bool, bool, u128, u128, Option<u128>)>();-        let before2 = before.clone(); let taken2 = taken.clone(); let plan2 = plan.clone(); let sid2 = sid.to_string(); let t_w = Instant::now();+        let before2 = before.clone(); let taken2 = taken.clone(); let plan2 = plan.clone(); let sid2 = sid.to_string(); let t_w = Instant::now(); let lane_pids = lane_pids_now.clone();         std::thread::Builder::new().name("birth-watch".into()).spawn(move || {             let mut found: Option<pup_host::HostWindow> = None;             let mut ambiguous = 0usize;             while t_w.elapsed() < Duration::from_secs(4) {                 // Class Chrome_WidgetWin_1 is ALSO Edge's: John's own Edge window is one, so an unfiltered                 // diff can stamp pup's tile onto his browser. Filter to the lane's own Chrome pids.-                let pids = if ext { crate::extdrive_pids() } else { Vec::new() };+                let pids = if ext { if lane_pids.is_empty() { crate::extdrive_pids() } else { lane_pids.clone() } } else { Vec::new() };                 let cands: Vec<pup_host::HostWindow> = a.host.frame_windows_all(Some("Chrome_WidgetWin_1")).into_iter()                     .filter(|w| !before2.contains(&w.hwnd) && !taken2.contains(&w.hwnd))                     .filter(|w| pids.is_empty() || pids.contains(&w.pid))@@ -1665,7 +1668,11 @@ fn spawn_zwatch(ctx: &Ctx, sid: &str, before: std::collections::HashSet<i64>) {     }).ok(); } pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owner: Option<&str>, url: &str) -> Value {-    let t_verb = Instant::now();   // 2.0.648 (pup-bridge#90): the early-return budget starts here+    let t_verb = Instant::now();+    // 2.0.659 (pup-bridge#113): the lane is per profile. isolated:true minted an iso-<id> profile (passed as+    // _mintedProfile); a named profile is honoured too. Everything below talks to THAT profile's browser and queue.+    let profile_arg = args.get("_mintedProfile").and_then(|p| p.as_str()).or_else(|| args.get("profile").and_then(|p| p.as_str())).unwrap_or("adom-you").to_string();+    extdrive::enter_profile(&profile_arg);   // 2.0.648 (pup-bridge#90): the early-return budget starts here     if let Some(s) = ctx.sessions.get(sid) {         if transport::is_ext(&s) {             if url != "about:blank" { return tabs::open_tab(ctx, caller, &json!({ "window": sid, "url": url })); }@@ -1769,7 +1776,7 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne             }         }     };-    let mut s = Session::new(sid, "adom-you", owner, Some(&caller.container), 0, None);+    let mut s = Session::new(sid, &profile_arg, owner, Some(&caller.container), 0, None);     s.transport = "ext".into(); s.window_id = chrome_win;     s.owner_from_caller = s.owner.as_deref() == Some(caller.thread.as_str()) && !caller.thread.is_empty() && args.get("owner").is_none();     let t = transport::ext_tab(&mut s, chrome_tab, &got_url, &got_title); s.tabs.push(t.clone()); s.active_tab = 0;@@ -2007,8 +2014,12 @@ pub fn close_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {         }         ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id); closed.push(s.id.clone());   // again: a late save loses         // last ext window: let the lane browser go, the same rule the CDP browsers follow below-        if transport::is_ext(&s) && !ctx.sessions.all().iter().any(|x| transport::is_ext(x)) {+        if transport::is_ext(&s) && !ctx.sessions.all().iter().any(|x| transport::is_ext(x) && x.profile == s.profile) {+            // 2.0.659: the LAST window of this profile's lane ends that lane's browser (the default lane's rule unchanged)+            extdrive::enter_profile(&s.profile);             let n = kill_extdrive(ctx); if n > 0 { chrome_killed += 1; }+            if s.profile != "adom-you" { extdrive::drop_lane_for_profile(&s.profile); }+            extdrive::leave_profile();         }         // last window on that profile: let the browser go (Node: killProfileBrowserIfUnused)         if ctx.sessions.for_profile(&s.profile).is_empty() {@@ -2016,6 +2027,7 @@ pub fn close_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {                 // 2.0.648 (pup-bridge#112): a throwaway profile dies with its last window; the browser is ended                 // just below, so remove the directory after it has let go (a few tries, then leave it for prewarm).                 let dir = pup_browser::profiles_dir(&*ctx.host).join(&s.profile); let sid = s.id.clone();+                let staging = ctx.host.state_dir().join(format!("extlane-{}", s.profile)); let _ = std::fs::remove_dir_all(&staging);   // 2.0.660: the lane's staged extension goes with it                 std::thread::spawn(move || { for _ in 0..10 { std::thread::sleep(Duration::from_millis(1500)); if std::fs::remove_dir_all(&dir).is_ok() || !dir.exists() { pup_core::log(format!("[profile] \"{sid}\": throwaway profile {} removed", dir.display())); return; } } pup_core::log(format!("[profile] \"{sid}\": throwaway profile {} still held; left on disk", dir.display())); });             }             if let Some(b) = ctx.browsers.lock().unwrap().remove(&s.profile) {
rust/verbs/src/recovery.rs+15−1
@@ -255,6 +255,20 @@ pub fn adopt_window_tabs(ctx: &Ctx, h: &Arc<BrowserHandle>) -> usize { }  pub fn recover_at_boot(ctx: &Ctx) {+    // 2.0.659 (pup-bridge#113): throwaway lanes do not survive a pup restart. End any browser still on an+    // iso-* profile and remove the profile; their windows were fleeting by definition.+    if let Ok(rd) = std::fs::read_dir(pup_browser::profiles_dir(&*ctx.host)) {+        for e in rd.flatten() {+            let name = e.file_name().to_string_lossy().to_string();+            if !name.starts_with("iso-") { continue; }+            let pids = crate::orphan_browser_pids(ctx, &e.path());+            for p in &pids { ctx.host.kill(*p); }+            std::thread::sleep(Duration::from_millis(if pids.is_empty() { 0 } else { 800 }));+            let removed = std::fs::remove_dir_all(e.path()).is_ok();+            pup_core::log(format!("[profile] boot: throwaway profile {name}: ended {} browser process(es), removed={removed}", pids.len()));+            for id in ctx.sessions.for_profile(&name).iter().map(|s| s.id.clone()).collect::<Vec<_>>() { ctx.sessions.remove(&id); ctx.sessions.delete_file(&id); }+        }+    }     let files = ctx.sessions.read_files();     if files.is_empty() { pup_core::log("Recovering 0 session(s) from disk..."); return; }     pup_core::log(format!("Recovering {} session(s) from disk...", files.len()));@@ -478,7 +492,7 @@ pub fn health_tick(ctx: &Ctx) {         if crate::extdrive_browser_alive(ctx) && !ctx.sessions.all().iter().any(|s| crate::transport::is_ext(s)) {             pup_core::log(format!("[lane] cycling the idle lane browser: it runs extension {} while pup is {}", w.replace('-', "."), want.replace('-', ".")));             crate::kill_extdrive(ctx);-            *crate::extdrive::LANE_WORKER.lock().unwrap() = String::new();+            crate::extdrive::reset_worker_version();         }     }     browser_liveness(ctx);
rust/verbs/src/transport.rs+1
@@ -18,6 +18,7 @@ pub fn is_ext(s: &Session) -> bool { s.transport == "ext" }  pub fn lane_for(ctx: &Ctx, s: &Session) -> Result<Lane, Value> {     if is_ext(s) {+        crate::extdrive::enter_profile(&s.profile);   // 2.0.659: this thread now talks to the session's own lane         if crate::extdrive::alive(ctx) { Ok(Lane::Ext) } else { Err(err("browser_gone", "the extension-driven browser behind this window is not running", Some("pup_close_window then pup_open_window."))) }     } else {         browser_for(ctx, &s.profile).map(Lane::Cdp).ok_or_else(|| err("browser_gone", "the browser behind this window is not connected", Some("pup_close_window then pup_open_window; Phase 2 reconnects automatically.")))