← All Pull Requests

macOS build of the Rust pup: host-mac, per-platform manifest, mac build script (pup-bridge#128) #3

Closed opened by Kyle Bergstedt

The macOS build of the Rust pup, for pup-bridge#128 (a Mac that syncs the public manifest gets the Windows exe and cannot spawn it).

What this adds, all platform-gated or defaulted so the Windows exe is unchanged:

  • rust/host-mac: the full Host trait on macOS (CGWindowList windows, park = hide the owning app via NSRunningApplication, AX set-bounds + AXRaise, open -g -n non-activating launch, Keychain in the keytar layout, process table / sysctl / vm_stat for the process verbs, .app bundle discovery for system browsers). 9 unit tests, live-probed on Apple Silicon.
  • Host trait additions with Windows-preserving defaults: raise_window (mac raises natively; Windows keeps ab's desktop_bring_to_front), system_browsers (default = the Program Files layout, moved out of pup-browser), closed_window_may_linger (Chrome on mac destroys a closed NSWindow on a later UI turn, so close verifies through the lane), running_browser_executables + pup_browser_processes (defaults keep the Win32_Process queries; on a Mac the PowerShell form answered nothing, so pup_cache_prune deleted every build but the one it had just installed, and the orphan/idle sweeps were empty), cft_cache_platform (puppeteer names mac cache folders mac_arm / mac, not the download tag).
  • verbs: the identity port file lives in the state dir off Windows; the recordings folder opens with the platform opener; the google toast focus names the mac browser.
  • src/extension/worker.js pupCaptureTab: on a Mac captureVisibleTab never settles for a window of a hidden app (pup's park state there), so it is raced against 3 s and falls back to the existing debugger capture. Same code on Windows; the race only matters when the API hangs.
  • tools/rust-build-macos.sh <ver>: universal (arm64 + x86_64) binary, Developer ID signed with hardened runtime, zip with a 0755 adom-pup-bridge + bridge.json (spawn.kind exe, updateManifestUrls {macos, windows}, macos supported / windows unsupported) + the staged adom-bridge-puppeteer-manifest-macos.json. tools/rust-build.sh is untouched.
  • adom-bridge-puppeteer-manifest-macos.json: the macOS manifest (platform: macos), the same shape as the public one; its zip is the per-platform asset adom-bridge-puppeteer-macos-v2.0.719.zip on release 2.0.719 (uploaded separately). Bridge 1.9.217 (macOS) carries a pup seed whose updateManifestUrls.macos points here, so a Mac never follows the Windows line again; the plain updateManifestUrl and adom-bridge-puppeteer-manifest.json stay yours.

Verified on this Mac against the mac build on a spare port with an isolated HOME: tools/parity.py's golden sequence matches 55/56 on this Mac (the one drift is pup_screenshot on the CDP lane returning the base64 envelope instead of a file, and pup_screenshot_full_res on the CDP lane times out for a hidden window; the extension lane, the default, is clean), plus readiness, open (hidden, no focus steal), eval, screenshot, raise, lower, close, quit_idle_browsers. Workspace tests and tools/check-borders.sh green. Cold-start note: across 7 fresh-profile lane starts on this Mac, 2 (back to back, right after a killed previous lane) never had the extension call home within 2x25 s and the open failed; the next 5 came up in 4 to 10 s. Not reproduced since; worth a Mac-side eye on the lane start timeout.

Known gaps on mac, inert: AUMID / jump list / taskbar identity (logs desktop_register_app_identity refused), import.rs / assist.rs / activity.rs (cfg windows no-ops), shots_dir is exe-relative.

🤖 Generated with Claude Code

Diff Skip to comments (1)

--- a/rust/browser/src/ext_assets.rs+++ b/rust/browser/src/ext_assets.rs@@ -1,71 +1,73 @@⋯ 19 unchanged lines ⋯         ("popup.js", include_bytes!("../../../src/extension/popup.js")),         ("satoshi-400.woff2", include_bytes!("../../../src/extension/satoshi-400.woff2")),         ("satoshi-500.woff2", include_bytes!("../../../src/extension/satoshi-500.woff2")),+        ("tile.js", include_bytes!("../../../src/extension/tile.js")),         ("worker.js", include_bytes!("../../../src/extension/worker.js")),     ]),     ("extension-identity", "pup-identity-extension", &[⋯ 43 unchanged lines ⋯         ("toolbar.json", include_bytes!("../../../src/extension-nbe/toolbar.json")),     ]), ];+--- a/rust/browser/src/lib.rs+++ b/rust/browser/src/lib.rs@@ -1,830 +1,826 @@⋯ 37 unchanged lines ⋯ // ── detection (Node: listSystemBrowsers / detectChrome / launchCandidates) ─────────────  pub fn list_system_browsers(host: &dyn Host) -> Vec<Candidate> {-    let mut out = Vec::new();-    let roots = host.browser_roots();-    let chrome: Vec<PathBuf> = roots.iter().map(|r| r.join("Google").join("Chrome").join("Application").join("chrome.exe")).collect();-    let edge: Vec<PathBuf> = roots.iter().map(|r| r.join("Microsoft").join("Edge").join("Application").join("msedge.exe")).collect();-    if let Some(p) = chrome.into_iter().find(|p| p.is_file()) { out.push(Candidate { kind: "chrome".into(), source: "system".into(), executable_path: p, build_id: None }); }-    if let Some(p) = edge.into_iter().find(|p| p.is_file()) { out.push(Candidate { kind: "edge".into(), source: "system".into(), executable_path: p, build_id: None }); }-    out+    // The per-OS install layout is the host's (Program Files on Windows, /Applications bundles on macOS).+    host.system_browsers().into_iter().map(|(kind, p)| Candidate { kind, source: "system".into(), executable_path: p, build_id: None }).collect() } -pub fn cft_versioned_dir(host: &dyn Host, build_id: &str) -> PathBuf { host.cft_cache_dir().join("chrome").join(format!("{}-{}", host.cft_platform(), build_id)) }+pub fn cft_versioned_dir(host: &dyn Host, build_id: &str) -> PathBuf { host.cft_cache_dir().join("chrome").join(format!("{}-{}", host.cft_cache_platform(), build_id)) } 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> {⋯ 2 unchanged lines ⋯     // any other complete build in the cache (newest wins), so an older cache still drives     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_platform()); n.strip_prefix(&prefix).map(|b| (b.to_string(), e.path().join(host.cft_exe_relative()))) })+        .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()))) })         // 2.0.679: a build id is a plain version. Folders like "<build>.partial" (an unpack in progress) or a renamed         // copy matched the prefix and could be launched half-written (found testing ab's prewarm on CRR, 2026-09-22).         .filter(|(b, p)| !b.is_empty() && b.chars().all(|c| c.is_ascii_digit() || c == '.') && p.is_file()).collect();⋯ 765 unchanged lines ⋯         let _ = fs::remove_dir_all(&tmp);     } }+--- a/rust/Cargo.lock+++ b/rust/Cargo.lock@@ -1,1491 +1,1650 @@⋯ 136 unchanged lines ⋯ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"  [[package]]+name = "core-foundation"+version = "0.10.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"+dependencies = [+ "core-foundation-sys",+ "libc",+]++[[package]]+name = "core-foundation-sys"+version = "0.8.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"++[[package]]+name = "core-graphics"+version = "0.24.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"+dependencies = [+ "bitflags 2.13.2",+ "core-foundation",+ "core-graphics-types",+ "foreign-types",+ "libc",+]++[[package]]+name = "core-graphics-types"+version = "0.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"+dependencies = [+ "bitflags 2.13.2",+ "core-foundation",+ "libc",+]++[[package]] name = "core_maths" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index"⋯ 53 unchanged lines ⋯ ]  [[package]]+name = "dispatch2"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"+dependencies = [+ "bitflags 2.13.2",+ "objc2",+]++[[package]] name = "displaydoc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index"⋯ 68 unchanged lines ⋯ ]  [[package]]+name = "foreign-types"+version = "0.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"+dependencies = [+ "foreign-types-macros",+ "foreign-types-shared",+]++[[package]]+name = "foreign-types-macros"+version = "0.2.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "foreign-types-shared"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"++[[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index"⋯ 307 unchanged lines ⋯ ]  [[package]]+name = "objc2"+version = "0.6.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"+dependencies = [+ "objc2-encode",+]++[[package]]+name = "objc2-app-kit"+version = "0.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"+dependencies = [+ "bitflags 2.13.2",+ "libc",+ "objc2",+ "objc2-foundation",+]++[[package]]+name = "objc2-core-foundation"+version = "0.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"+dependencies = [+ "bitflags 2.13.2",+ "dispatch2",+ "objc2",+]++[[package]]+name = "objc2-encode"+version = "4.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"++[[package]]+name = "objc2-foundation"+version = "0.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"+dependencies = [+ "bitflags 2.13.2",+ "objc2",+ "objc2-core-foundation",+]++[[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index"⋯ 122 unchanged lines ⋯ name = "pup-host-mac" version = "2.0.719" dependencies = [+ "core-foundation",+ "core-graphics",+ "libc",+ "objc2",+ "objc2-app-kit",+ "objc2-foundation",+ "pup-core",  "pup-host",+ "security-framework",+ "serde_json", ]  [[package]]⋯ 184 unchanged lines ⋯ ]  [[package]]+name = "security-framework"+version = "3.7.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"+dependencies = [+ "bitflags 2.13.2",+ "core-foundation",+ "core-foundation-sys",+ "libc",+ "security-framework-sys",+]++[[package]]+name = "security-framework-sys"+version = "2.17.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"+dependencies = [+ "core-foundation-sys",+ "libc",+]++[[package]] name = "serde" version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index"⋯ 581 unchanged lines ⋯ dependencies = [  "zune-core", ]+--- a/rust/host-mac/Cargo.toml+++ b/rust/host-mac/Cargo.toml@@ -1,9 +1,21 @@⋯ 2 unchanged lines ⋯ version.workspace = true edition.workspace = true license.workspace = true-description = "The macOS implementation of pup-host. Kyle's crate: every method is a todo!() naming the Windows counterpart."+description = "The macOS implementation of pup-host: CoreGraphics window list, AppKit hide/activate for park, Accessibility for bounds, Keychain secrets, non-activating detached launch."  [dependencies] pup-host = { path = "../host" }+pup-core = { path = "../core" }+serde_json.workspace = true+libc = "0.2"++[target.'cfg(target_os = "macos")'.dependencies]+core-foundation = "0.10"+core-graphics = "0.24"+security-framework = "3"+objc2 = "0.6"+objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSGeometry", "NSString"] }+objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "libc", "NSRunningApplication", "NSWorkspace", "NSScreen", "NSResponder", "NSApplication"] }+--- a/rust/host-mac/examples/fg_probe.rs+++ b/rust/host-mac/examples/fg_probe.rs@@ -0,0 +1,10 @@+use pup_host::HostWindows;+fn main() {+    let h = pup_host_mac::MacHost;+    let fg = h.foreground();+    println!("foreground()={fg}; NSWorkspace frontmost pid={:?}", pup_host_mac::frontmost_pid());+    if fg != 0 { let r = h.windows_for_pids(&[], None).into_iter().find(|w| w.hwnd == fg); println!("foreground window: {:?}", r.map(|w| (w.pid, w.class, w.rect))); }+    let rows = h.windows_for_pids(&[], None);+    println!("{} layer-0 windows; first 6: {:?}", rows.len(), rows.iter().take(6).map(|w| (w.hwnd, w.pid, w.class.clone(), w.visible)).collect::<Vec<_>>());+}+--- a/rust/host-mac/examples/hide_ctl.rs+++ b/rust/host-mac/examples/hide_ctl.rs@@ -0,0 +1,9 @@+//! hide_ctl <hwnd> show|hide|raise — drive one window's owning app through the trait, for live tests.+use pup_host::HostWindows;+fn main() {+    let a: Vec<String> = std::env::args().collect();+    let h = pup_host_mac::MacHost; let hwnd: i64 = a[1].parse().unwrap();+    let r = match a[2].as_str() { "show" => h.set_visible_no_activate(hwnd, true), "hide" => h.send_to_bottom_no_activate(hwnd), "raise" => h.raise_window(hwnd), _ => false };+    println!("{} {hwnd}: {r}; foreground now {}", a[2], h.foreground());+}+--- a/rust/host-mac/examples/live_probe.rs+++ b/rust/host-mac/examples/live_probe.rs@@ -0,0 +1,37 @@+//! Live probe of the mac host against Chrome for Testing: non-activating launch, pid resolution,+//! window discovery by pid, park (hide), unpark, kill. Run: cargo run -p pup-host-mac --example live_probe <cft-exe> <profile-dir>+use pup_host::{HostProcess, HostWindows};+use pup_host_mac::MacHost;+use std::path::Path;+use std::time::{Duration, Instant};+fn main() {+    let a: Vec<String> = std::env::args().collect();+    let exe = Path::new(&a[1]); let profile = &a[2];+    let h = MacHost;+    let front_before = h.foreground();+    let t0 = Instant::now();+    let args = vec![format!("--user-data-dir={profile}"), "--remote-debugging-port=0".into(), "--no-first-run".into(), "--no-default-browser-check".into(), "--window-size=900,600".into(), "about:blank".into()];+    let pid = h.spawn_detached(exe, &args, None).expect("spawn");+    println!("spawned pid {pid} in {:?}; alive={}", t0.elapsed(), h.alive(pid));+    let mut wins = Vec::new();+    let deadline = Instant::now() + Duration::from_secs(15);+    while Instant::now() < deadline { wins = h.windows_for_pids(&[pid], Some("Chrome_WidgetWin_1")); if !wins.is_empty() { break; } std::thread::sleep(Duration::from_millis(100)); }+    println!("windows for pid after {:?}: {:?}", t0.elapsed(), wins.iter().map(|w| (w.hwnd, &w.class, w.rect, w.visible)).collect::<Vec<_>>());+    let front_after = h.foreground();+    println!("foreground before={front_before} after={front_after} (stolen={})", wins.iter().any(|w| w.hwnd == front_after));+    let Some(w) = wins.first() else { h.kill(pid); return; };+    println!("tag: {} has_tag: {}", h.tag(w.hwnd, "AdomPupSid=probe"), h.has_tag(w.hwnd, "AdomPupSid=probe"));+    println!("park (hide): {}", h.send_to_bottom_no_activate(w.hwnd));+    std::thread::sleep(Duration::from_millis(300));+    let hidden_rows = h.windows_for_pids(&[pid], Some("Chrome_WidgetWin_1"));+    println!("after hide: found={} visible={:?} is_alive={}", hidden_rows.len(), hidden_rows.iter().map(|w| w.visible).collect::<Vec<_>>(), h.is_alive(w.hwnd));+    println!("set_bounds (AX): {}", h.set_bounds_no_activate(w.hwnd, 40, 60, 800, 500));+    println!("unhide (no activate): {}", h.set_visible_no_activate(w.hwnd, true));+    std::thread::sleep(Duration::from_millis(300));+    println!("foreground now={} (ours={})", h.foreground(), h.foreground() == w.hwnd);+    println!("idle_ms={:?} work_area={:?}", h.idle_ms(), h.work_area());+    println!("kill: {}", h.kill(pid));+    std::thread::sleep(Duration::from_millis(500));+    println!("alive after kill: {} window alive: {}", h.alive(pid), h.is_alive(w.hwnd));+}+--- a/rust/host-mac/examples/procs.rs+++ b/rust/host-mac/examples/procs.rs@@ -0,0 +1,3 @@+use pup_host::HostProcess;+fn main() { let h = pup_host_mac::MacHost; println!("running browsers: {:?}", h.running_browser_executables()); println!("pup browsers by profile: {:?}", h.pup_browser_processes().iter().take(3).collect::<Vec<_>>()); }+--- a/rust/host-mac/examples/title_probe.rs+++ b/rust/host-mac/examples/title_probe.rs@@ -0,0 +1,9 @@+//! Do CGWindowList rows carry titles from this process (needs Screen Recording for other apps)?+use pup_host::HostWindows;+fn main() {+    let h = pup_host_mac::MacHost;+    let rows = h.windows_for_pids(&[], None);+    let titled = rows.iter().filter(|w| !w.title.is_empty()).count();+    println!("{} windows, {} with titles; chromium: {:?}", rows.len(), titled, rows.iter().filter(|w| w.class.to_lowercase().contains("chrome")).map(|w| (w.pid, w.title.chars().take(30).collect::<String>())).take(5).collect::<Vec<_>>());+}+--- a/rust/host-mac/examples/wins.rs+++ b/rust/host-mac/examples/wins.rs@@ -0,0 +1,3 @@+use pup_host::HostWindows;+fn main() { let pid: u32 = std::env::args().nth(1).unwrap().parse().unwrap(); for w in pup_host_mac::MacHost.windows_for_pids(&[pid], None) { println!("{:?}", w); } }+--- a/rust/host-mac/src/lib.rs+++ b/rust/host-mac/src/lib.rs@@ -1,48 +1,717 @@-//! pup-host-mac: the macOS host, for Kyle to implement. Same traits as pup-host-win; every-//! `todo!()` names what the Windows crate does for that method, so the port is a translation,-//! not a rediscovery. `cargo check --target aarch64-apple-darwin -p pup-host-mac` runs every ship.+//! pup-host-mac: the macOS host. Same traits as pup-host-win; each method names the Windows+//! counterpart it mirrors. The ONLY crate in the workspace that may name CoreGraphics, AppKit,+//! Accessibility or the Keychain (tools/check-borders.sh). //!-//! What macOS does NOT need to implement: anything taskbar-shaped (Dock identity, badges, jump-//! lists) — that is ab's job through `AbClient`. pup only needs windows found, kept out of the-//! user's way (the Windows "bottom of the z-order, never activated" is `NSApp hide` / ordering-//! the window back on macOS), and the browser spawned detached.+//! The mac model, carried over from the Node build's darwin work (pup 1.9.74+):+//!   - a "window handle" is a CGWindowID (CGWindowListCopyWindowInfo), owned by the browser pid;+//!   - "bottom of the z-order, never activated" has no per-window equivalent for ANOTHER app's+//!     window without Accessibility, so park = app-level hide of the browser (the Cmd+H state,+//!     `NSRunningApplication.hide`). Hidden windows stay drivable and screenshottable over CDP.+//!     One session = one browser process, so the hide is per session.+//!   - the browser is launched NON-activating: `open -g -n -a <bundle> --args …` when the+//!     executable lives inside a .app bundle, so the first window is born behind the user's+//!     frontmost app instead of stealing focus (a directly spawned Chrome activates itself on its+//!     first window). The pid is resolved from the process table by the `--user-data-dir=` arg.+//!   - window tags: macOS has no per-window property store (SetProp), so tags live in+//!     `~/.adom/pup-window-tags.json` keyed by CGWindowID, pruned of dead ids on every write.+//!   - taskbar-shaped work (Dock identity, badges, jump lists) is ab's job through `AbClient`;+//!     nothing here invents a Dock equivalent.+//!+//! Permissions: window enumeration, hide/activate, idle time and the Keychain need nothing.+//! Window TITLES from CGWindowList need Screen Recording; without it `title` is empty, which the+//! verbs tolerate (they match by pid + tag). `set_bounds_no_activate` needs Accessibility and+//! returns false without it (the birth path moves windows over CDP first anyway).  use pup_host::{HostPaths, HostProcess, HostSecrets, HostWindow, HostWindows};+use std::collections::HashMap; use std::path::{Path, PathBuf};+use std::sync::Mutex;  pub struct MacHost; +/// The Windows class name the verbs crate asks for; on macOS it means "any Chromium window".+const CHROME_CLASS: &str = "Chrome_WidgetWin_1";++fn owner_is_chromium(owner: &str) -> bool {+    let o = owner.to_ascii_lowercase();+    o.contains("chrome") || o.contains("chromium") || o.contains("edge")+}++/// Does a CGWindowList owner name satisfy the caller's class filter?+fn class_matches(owner: &str, class_filter: Option<&str>) -> bool {+    match class_filter {+        None => true,+        Some(CHROME_CLASS) => owner_is_chromium(owner),+        Some(c) => owner.eq_ignore_ascii_case(c) || owner_is_chromium(owner) && owner_is_chromium(c),+    }+}++// ───────────────────────── window tags (SetProp replacement) ─────────────────────────++fn tags_file() -> PathBuf { MacHost.state_dir().join("pup-window-tags.json") }++fn tags_store() -> &'static Mutex<HashMap<i64, Vec<String>>> {+    static M: std::sync::OnceLock<Mutex<HashMap<i64, Vec<String>>>> = std::sync::OnceLock::new();+    M.get_or_init(|| {+        let loaded: HashMap<i64, Vec<String>> = std::fs::read_to_string(tags_file()).ok()+            .and_then(|s| serde_json::from_str::<HashMap<String, Vec<String>>>(&s).ok())+            .map(|m| m.into_iter().filter_map(|(k, v)| k.parse::<i64>().ok().map(|k| (k, v))).collect())+            .unwrap_or_default();+        Mutex::new(loaded)+    })+}++fn tags_persist(map: &HashMap<i64, Vec<String>>) {+    let as_str: HashMap<String, &Vec<String>> = map.iter().map(|(k, v)| (k.to_string(), v)).collect();+    if let Some(d) = tags_file().parent() { let _ = std::fs::create_dir_all(d); }+    if let Ok(txt) = serde_json::to_string_pretty(&as_str) { let _ = std::fs::write(tags_file(), txt); }+}++// ───────────────────────── platform implementation ─────────────────────────++#[cfg(target_os = "macos")]+mod imp {+    use core_foundation::array::CFArray;+    use core_foundation::base::{CFType, TCFType};+    use core_foundation::dictionary::CFDictionary;+    use core_foundation::number::CFNumber;+    use core_foundation::string::CFString;+    use core_graphics::display::{CGWindowListCopyWindowInfo, kCGNullWindowID, kCGWindowListExcludeDesktopElements, kCGWindowListOptionAll, kCGWindowListOptionOnScreenOnly};+    use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication, NSScreen, NSWorkspace};++    /// One row of CGWindowListCopyWindowInfo, the fields pup needs.+    pub(super) struct Row { pub id: i64, pub pid: u32, pub owner: String, pub title: String, pub layer: i64, pub onscreen: bool, pub bounds: (i32, i32, i32, i32) }++    fn dict_i64(d: &CFDictionary<CFString, CFType>, key: &str) -> Option<i64> {+        d.find(CFString::new(key)).and_then(|v| v.downcast::<CFNumber>()).and_then(|n| n.to_i64())+    }+    fn dict_str(d: &CFDictionary<CFString, CFType>, key: &str) -> String {+        d.find(CFString::new(key)).and_then(|v| v.downcast::<CFString>()).map(|s| s.to_string()).unwrap_or_default()+    }++    pub(super) fn window_rows(all: bool) -> Vec<Row> {+        use core_foundation::dictionary::CFDictionaryRef;+        let opts = if all { kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements } else { kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements };+        let raw = unsafe { CGWindowListCopyWindowInfo(opts, kCGNullWindowID) };+        if raw.is_null() { return Vec::new(); }+        let arr: CFArray<CFType> = unsafe { CFArray::wrap_under_create_rule(raw) };+        let mut out = Vec::new();+        for item in arr.iter() {+            let d: CFDictionary<CFString, CFType> = unsafe { CFDictionary::wrap_under_get_rule(item.as_CFTypeRef() as CFDictionaryRef) };+            let id = dict_i64(&d, "kCGWindowNumber").unwrap_or(0);+            let pid = dict_i64(&d, "kCGWindowOwnerPID").unwrap_or(0) as u32;+            let layer = dict_i64(&d, "kCGWindowLayer").unwrap_or(0);+            // kCGWindowIsOnscreen is a CFBoolean (not a CFNumber) and is only present when true.+            let onscreen = !all || d.find(CFString::new("kCGWindowIsOnscreen")).and_then(|v| v.downcast::<core_foundation::boolean::CFBoolean>()).map(|b| bool::from(b)).unwrap_or(false);+            let bounds = d.find(CFString::new("kCGWindowBounds")).map(|v| {+                let b: CFDictionary<CFString, CFType> = unsafe { CFDictionary::wrap_under_get_rule(v.as_CFTypeRef() as CFDictionaryRef) };+                let g = |k: &str| b.find(CFString::new(k)).and_then(|v| v.downcast::<CFNumber>()).and_then(|n| n.to_f64()).unwrap_or(0.0);+                let (x, y, w, h) = (g("X"), g("Y"), g("Width"), g("Height"));+                (x as i32, y as i32, (x + w) as i32, (y + h) as i32)+            }).unwrap_or((0, 0, 0, 0));+            out.push(Row { id, pid, owner: dict_str(&d, "kCGWindowOwnerName"), title: dict_str(&d, "kCGWindowName"), layer, onscreen, bounds });+        }+        out+    }++    pub(super) fn running_app(pid: u32) -> Option<objc2::rc::Retained<NSRunningApplication>> {+        NSRunningApplication::runningApplicationWithProcessIdentifier(pid as i32)+    }++    /// Node: macSetAppHiddenByPid (NSRunningApplication via JXA, never System Events).+    pub(super) fn set_app_hidden(pid: u32, hidden: bool) -> bool {+        let Some(app) = running_app(pid) else { return false };+        if hidden { app.hide(); } else { app.unhide(); }+        // hide/unhide are asynchronous; judge by the state a beat later.+        std::thread::sleep(std::time::Duration::from_millis(60));+        app.isHidden() == hidden+    }++    /// Node: macActivateByPid. Only used for a GRANTED raise (never at birth).+    pub fn activate(pid: u32) -> bool {+        let Some(app) = running_app(pid) else { return false };+        app.unhide();+        app.activateWithOptions(NSApplicationActivationOptions::empty())+    }++    pub(super) fn frontmost_pid() -> Option<u32> {+        // A process that has connected to the window server but runs no NSApplication (this+        // bridge, its tests) is reported as its own frontmost app; that answer is worthless.+        NSWorkspace::sharedWorkspace().frontmostApplication().map(|a| a.processIdentifier() as u32).filter(|p| *p != std::process::id())+    }++    pub(super) fn app_hidden(pid: u32) -> Option<bool> { running_app(pid).map(|a| a.isHidden()) }++    /// The main screen's visible frame in TOP-LEFT (CG) coordinates: (x, y, w, h).+    pub(super) fn visible_work_area() -> Option<(i32, i32, i32, i32)> {+        // NSScreen geometry is read-only and safe off the main thread; the marker is a type-level+        // requirement of the binding, not a runtime one, for this call.+        let mtm = unsafe { objc2::MainThreadMarker::new_unchecked() };+        let screen = NSScreen::mainScreen(mtm)?;+        let frame = screen.frame();+        let vis = screen.visibleFrame();+        let top = frame.size.height - (vis.origin.y + vis.size.height);+        Some((vis.origin.x as i32, top as i32, vis.size.width as i32, vis.size.height as i32))+    }++    pub(super) fn idle_ms() -> Option<u64> {+        extern "C" { fn CGEventSourceSecondsSinceLastEventType(state: i32, event_type: u32) -> f64; }+        // kCGEventSourceStateCombinedSessionState = 0, kCGAnyInputEventType = !0+        let s = unsafe { CGEventSourceSecondsSinceLastEventType(0, u32::MAX) };+        if s.is_finite() && s >= 0.0 { Some((s * 1000.0) as u64) } else { None }+    }++    // ── Accessibility: move/resize a window by CGWindowID without activating it ──+    #[repr(C)] struct AXOpaque { _p: [u8; 0] }+    type AXUIElementRef = *const AXOpaque;+    type AXValueRef = *const AXOpaque;+    #[repr(C)] #[derive(Clone, Copy)] struct CGPointC { x: f64, y: f64 }+    #[repr(C)] #[derive(Clone, Copy)] struct CGSizeC { w: f64, h: f64 }+    #[link(name = "ApplicationServices", kind = "framework")]+    extern "C" {+        fn AXUIElementCreateApplication(pid: i32) -> AXUIElementRef;+        fn AXUIElementCopyAttributeValue(el: AXUIElementRef, attr: core_foundation::string::CFStringRef, out: *mut core_foundation::base::CFTypeRef) -> i32;+        fn AXUIElementSetAttributeValue(el: AXUIElementRef, attr: core_foundation::string::CFStringRef, value: core_foundation::base::CFTypeRef) -> i32;+        fn AXValueCreate(kind: u32, value: *const std::ffi::c_void) -> AXValueRef;+        fn _AXUIElementGetWindow(el: AXUIElementRef, out: *mut u32) -> i32;+        fn AXIsProcessTrusted() -> bool;+    }+    const K_AX_VALUE_CGPOINT: u32 = 1;+    const K_AX_VALUE_CGSIZE: u32 = 2;++    pub(super) fn ax_set_bounds(pid: u32, window_id: i64, l: i32, t: i32, w: i32, h: i32) -> bool {+        unsafe {+            if !AXIsProcessTrusted() { return false; }+            let app = AXUIElementCreateApplication(pid as i32);+            if app.is_null() { return false; }+            let mut wins: core_foundation::base::CFTypeRef = std::ptr::null();+            let attr = CFString::new("AXWindows");+            if AXUIElementCopyAttributeValue(app, attr.as_concrete_TypeRef(), &mut wins) != 0 || wins.is_null() { core_foundation::base::CFRelease(app as _); return false; }+            let arr: CFArray<CFType> = CFArray::wrap_under_create_rule(wins as _);+            let mut ok = false;+            for el in arr.iter() {+                let el_ref = el.as_CFTypeRef() as AXUIElementRef;+                let mut wid: u32 = 0;+                if _AXUIElementGetWindow(el_ref, &mut wid) != 0 || wid as i64 != window_id { continue; }+                let p = CGPointC { x: l as f64, y: t as f64 };+                let s = CGSizeC { w: w as f64, h: h as f64 };+                let pv = AXValueCreate(K_AX_VALUE_CGPOINT, &p as *const _ as *const _);+                let sv = AXValueCreate(K_AX_VALUE_CGSIZE, &s as *const _ as *const _);+                let pa = CFString::new("AXPosition"); let sa = CFString::new("AXSize");+                let r1 = AXUIElementSetAttributeValue(el_ref, pa.as_concrete_TypeRef(), pv as _);+                let r2 = AXUIElementSetAttributeValue(el_ref, sa.as_concrete_TypeRef(), sv as _);+                if !pv.is_null() { core_foundation::base::CFRelease(pv as _); }+                if !sv.is_null() { core_foundation::base::CFRelease(sv as _); }+                ok = r1 == 0 && r2 == 0;+                break;+            }+            core_foundation::base::CFRelease(app as _);+            ok+        }+    }++    /// AXRaise one window of an app (after the app is active, this puts THAT window in front of the+    /// app's other windows). Needs Accessibility; false without it, which leaves the app's own order.+    pub(super) fn ax_raise(pid: u32, window_id: i64) -> bool {+        unsafe {+            if !AXIsProcessTrusted() { return false; }+            extern "C" { fn AXUIElementPerformAction(el: AXUIElementRef, action: core_foundation::string::CFStringRef) -> i32; }+            let app = AXUIElementCreateApplication(pid as i32);+            if app.is_null() { return false; }+            let mut wins: core_foundation::base::CFTypeRef = std::ptr::null();+            let attr = CFString::new("AXWindows");+            if AXUIElementCopyAttributeValue(app, attr.as_concrete_TypeRef(), &mut wins) != 0 || wins.is_null() { core_foundation::base::CFRelease(app as _); return false; }+            let arr: CFArray<CFType> = CFArray::wrap_under_create_rule(wins as _);+            let mut ok = false;+            for el in arr.iter() {+                let el_ref = el.as_CFTypeRef() as AXUIElementRef;+                let mut wid: u32 = 0;+                if _AXUIElementGetWindow(el_ref, &mut wid) != 0 || wid as i64 != window_id { continue; }+                let act = CFString::new("AXRaise");+                ok = AXUIElementPerformAction(el_ref, act.as_concrete_TypeRef()) == 0;+                break;+            }+            core_foundation::base::CFRelease(app as _);+            ok+        }+    }++    // ── Keychain: keytar's generic-password layout (service + account) ──+    pub(super) fn secret_get(service: &str, account: &str) -> Option<String> {+        security_framework::passwords::get_generic_password(service, account).ok().and_then(|b| String::from_utf8(b).ok())+    }+    pub(super) fn secret_set(service: &str, account: &str, secret: &str) -> Result<(), String> {+        security_framework::passwords::set_generic_password(service, account, secret.as_bytes()).map_err(|e| format!("Keychain write failed: {e}"))+    }+    pub(super) fn secret_delete(service: &str, account: &str) -> bool {+        security_framework::passwords::delete_generic_password(service, account).is_ok()+    }+}++#[cfg(not(target_os = "macos"))]+mod imp {+    // Cross-compiled checks on Linux/Windows: the mac impl is cfg'd out; these stubs let the+    // crate build on a dev box. The release binary is always the cfg(target_os = "macos") side.+    use super::*;+    pub(super) struct Row { pub id: i64, pub pid: u32, pub owner: String, pub title: String, pub layer: i64, pub onscreen: bool, pub bounds: (i32, i32, i32, i32) }+    pub(super) fn window_rows(_all: bool) -> Vec<Row> { Vec::new() }+    pub(super) fn set_app_hidden(_pid: u32, _hidden: bool) -> bool { false }+    pub fn activate(_pid: u32) -> bool { false }+    pub(super) fn frontmost_pid() -> Option<u32> { None }+    pub(super) fn app_hidden(_pid: u32) -> Option<bool> { None }+    pub(super) fn ax_raise(_pid: u32, _id: i64) -> bool { false }+    pub(super) fn visible_work_area() -> Option<(i32, i32, i32, i32)> { None }+    pub(super) fn idle_ms() -> Option<u64> { None }+    pub(super) fn ax_set_bounds(_pid: u32, _id: i64, _l: i32, _t: i32, _w: i32, _h: i32) -> bool { false }+    pub(super) fn secret_get(_s: &str, _a: &str) -> Option<String> { None }+    pub(super) fn secret_set(_s: &str, _a: &str, _v: &str) -> Result<(), String> { Err("no Keychain on this dev box".into()) }+    pub(super) fn secret_delete(_s: &str, _a: &str) -> bool { false }+}++/// Raise a parked (hidden) browser: the mac side of a GRANTED `pup_raise_os_window` (Node: macActivateByPid).+pub fn activate_pid(pid: u32) -> bool { imp::activate(pid) }++/// Pids pup itself ended through `Host::kill` (Windows: the same KILLED list), so recovery can tell a+/// deliberate end from a crash or the user quitting the browser.+fn killed() -> &'static Mutex<Vec<u32>> {+    static K: std::sync::OnceLock<Mutex<Vec<u32>>> = std::sync::OnceLock::new();+    K.get_or_init(|| Mutex::new(Vec::new()))+}++/// (pid, ppid, rss KB, executable path) for every process, one `ps` call. `comm` is the executable+/// path WITHOUT arguments, so a path with spaces ("Google Chrome for Testing") survives intact.+fn process_table() -> Vec<(u32, u32, u64, String)> {+    let Ok(out) = std::process::Command::new("/bin/ps").args(["-axo", "pid=,ppid=,rss=,comm="]).output() else { return Vec::new() };+    String::from_utf8_lossy(&out.stdout).lines().filter_map(|l| {+        let l = l.trim_start();+        let mut it = l.splitn(4, char::is_whitespace).filter(|x| !x.is_empty());+        let pid = it.next()?.parse::<u32>().ok()?;+        let rest = l[l.find(char::is_whitespace)?..].trim_start();+        let ppid = rest.split_whitespace().next()?.parse::<u32>().ok()?;+        let rest = rest[rest.find(char::is_whitespace)?..].trim_start();+        let rss = rest.split_whitespace().next()?.parse::<u64>().ok()?;+        let comm = rest[rest.find(char::is_whitespace)?..].trim().to_string();+        Some((pid, ppid, rss, comm))+    }).collect()+}++/// Is the whole app that owns `pid` hidden (Cmd+H state)? None when the pid has no app.+fn app_hidden(pid: u32) -> Option<bool> { imp::app_hidden(pid) }+/// The user is using this app right now: it is the frontmost application.+fn app_is_frontmost(pid: u32) -> bool { imp::frontmost_pid() == Some(pid) }++/// The frontmost application's pid as NSWorkspace reports it (None outside an AppKit process).+pub fn frontmost_pid() -> Option<u32> { imp::frontmost_pid() }++/// The owning pid of a CGWindowID, from the full window list (hidden windows included).+fn owner_pid_of(window_id: i64) -> Option<u32> { imp::window_rows(true).into_iter().find(|r| r.id == window_id).map(|r| r.pid) }++// ───────────────────────── HostWindows ─────────────────────────+ impl HostWindows for MacHost {-    fn windows_for_pids(&self, _pids: &[u32], _class_filter: Option<&str>) -> Vec<HostWindow> {-        todo!("CGWindowListCopyWindowInfo filtered by kCGWindowOwnerPID (Windows: EnumWindows + GetWindowThreadProcessId, class Chrome_WidgetWin_1)")+    // Windows: EnumWindows + GetWindowThreadProcessId, class Chrome_WidgetWin_1.+    // Hidden (parked) windows are included: pup must keep finding a window it parked.+    fn windows_for_pids(&self, pids: &[u32], class_filter: Option<&str>) -> Vec<HostWindow> {+        imp::window_rows(true).into_iter()+            .filter(|r| r.layer == 0 && r.id > 0)+            .filter(|r| pids.is_empty() || pids.contains(&r.pid))+            .filter(|r| class_matches(&r.owner, class_filter))+            .filter(|r| r.bounds.2 - r.bounds.0 > 50 && r.bounds.3 - r.bounds.1 > 50)   // skip Chrome's 1-px helper windows+            .map(|r| HostWindow { hwnd: r.id, pid: r.pid, title: r.title, class: r.owner, rect: r.bounds, visible: r.onscreen })+            .collect()     }-    fn is_alive(&self, _hwnd: i64) -> bool { todo!("window id still present in CGWindowListCopyWindowInfo (Windows: IsWindow)") }-    fn tag(&self, _hwnd: i64, _tag: &str) -> bool { todo!("no per-window property store on macOS: keep a pid->session map in the state dir instead (Windows: SetProp \"AdomPupSid=<sid>\")") }-    fn script_launch(&self, _script: &std::path::Path, _tail: &str, _quote: bool) -> (String, String) { todo!("launch a helper script hidden (Windows: powershell.exe -WindowStyle Hidden -File <script> <tail>); macOS: /bin/sh or osascript") }-    fn has_tag(&self, _hwnd: i64, _tag: &str) -> bool { todo!("see tag() (Windows: GetProp)") }-    fn send_to_bottom_no_activate(&self, _hwnd: i64) -> bool { todo!("order the window below without key/main (NSWindow orderBack: via the app's accessibility API, or hide the browser app: the Cmd+H state) (Windows: SetWindowPos HWND_BOTTOM | SWP_NOACTIVATE)") }-    fn set_bounds_no_activate(&self, _hwnd: i64, _l: i32, _t: i32, _w: i32, _h: i32) -> bool { todo!("AXUIElementSetAttributeValue kAXPosition/kAXSize (Windows: SetWindowPos SWP_NOACTIVATE | SWP_NOZORDER)") }-    fn foreground(&self) -> i64 { todo!("frontmost app's key window via NSWorkspace + AX (Windows: GetForegroundWindow)") }-    fn work_area(&self) -> Option<(i32, i32, i32, i32)> { todo!("NSScreen.main.visibleFrame (Windows: SystemParametersInfo SPI_GETWORKAREA)") }+    // Windows: EnumWindows over unowned captioned frames, visible or not. macOS: every layer-0 window+    // of a Chromium owner from the FULL list (hidden apps' windows included), tiny helper windows dropped.+    fn frame_windows_all(&self, class_filter: Option<&str>) -> Vec<HostWindow> { self.windows_for_pids(&[], class_filter) }+    // Windows: IsWindow.+    fn is_alive(&self, hwnd: i64) -> bool { hwnd > 0 && imp::window_rows(true).iter().any(|r| r.id == hwnd) }+    // Windows: SetProp "AdomPupSid=<sid>". No per-window property store here: a persisted map.+    fn tag(&self, hwnd: i64, tag: &str) -> bool {+        if hwnd <= 0 { return false; }+        let mut m = tags_store().lock().unwrap();+        let alive: std::collections::HashSet<i64> = imp::window_rows(true).into_iter().map(|r| r.id).collect();+        m.retain(|id, _| alive.contains(id) || *id == hwnd);+        let v = m.entry(hwnd).or_default();+        if !v.iter().any(|t| t == tag) { v.push(tag.to_string()); }+        tags_persist(&m);+        true+    }+    // Windows: GetProp.+    fn has_tag(&self, hwnd: i64, tag: &str) -> bool { tags_store().lock().unwrap().get(&hwnd).map(|v| v.iter().any(|t| t == tag)).unwrap_or(false) }+    // Windows: powershell -WindowStyle Hidden -File <script> <tail>. Here: /bin/sh <script> <tail>.+    fn script_launch(&self, script: &Path, tail: &str, quote_for_relaunch: bool) -> (String, String) {+        let q = if quote_for_relaunch { "\\\"" } else { "\"" };+        ("/bin/sh".into(), format!("{q}{}{q} {tail}", script.display()))+    }+    // Windows: SetWindowPos HWND_BOTTOM | SWP_NOACTIVATE. macOS has no per-window "order back" for+    // another app's window, so park = hide the owning app (the Cmd+H state; the Node mac build's design,+    // 1.9.74). Windows stay drivable and screenshottable over CDP / the extension while hidden. The lane+    // browser holds many threads' windows, so NEVER hide it while the user has it frontmost: that would+    // yank a window they are using (pup-user-foreground rule). Already hidden counts as parked.+    fn send_to_bottom_no_activate(&self, hwnd: i64) -> bool {+        let Some(pid) = owner_pid_of(hwnd) else { return false };+        if app_is_frontmost(pid) { return false; }+        if app_hidden(pid) == Some(true) { return true; }+        imp::set_app_hidden(pid, true)+    }+    // Windows: ShowWindow SW_HIDE / SW_SHOWNOACTIVATE. macOS: hide / unhide the owning app (unhide never+    // activates; the windows come back at their old z positions, never in front of the user's work).+    fn set_visible_no_activate(&self, hwnd: i64, visible: bool) -> bool {+        match owner_pid_of(hwnd) { Some(pid) => if visible { imp::set_app_hidden(pid, false) } else { self.send_to_bottom_no_activate(hwnd) }, None => false }+    }+    // Windows: SetWindowPos(HWND_BOTTOM, SHOWWINDOW | NOACTIVATE) in one call. macOS: a window born into a+    // hidden app is born invisible (parked by construction); a window born into a visible, non-frontmost+    // app sits behind the user's active app already, and the app is hidden so it is fully out of the way;+    // a window born while the user is IN the lane browser is left exactly where Chrome put it.+    fn show_at_bottom_no_activate(&self, hwnd: i64) -> bool {+        let Some(pid) = owner_pid_of(hwnd) else { return false };+        if app_is_frontmost(pid) { return true; }+        if app_hidden(pid) == Some(true) { return true; }+        imp::set_app_hidden(pid, true)+    }+    // Windows: a sub-ms spin that bottoms the window the instant it first shows. macOS: a window of a hidden+    // app never shows, so it is parked by construction (Some(0)); otherwise wait for it to come on screen and+    // hide the app then, unless the user is in it.+    fn park_on_first_show(&self, hwnd: i64, timeout_ms: u64) -> Option<u64> {+        let pid = owner_pid_of(hwnd)?;+        let t0 = std::time::Instant::now();+        if app_hidden(pid) == Some(true) { return Some(0); }+        loop {+            if imp::window_rows(false).iter().any(|r| r.id == hwnd) {+                if !app_is_frontmost(pid) { imp::set_app_hidden(pid, true); }+                return Some(t0.elapsed().as_millis() as u64);+            }+            if t0.elapsed().as_millis() as u64 > timeout_ms { return None; }+            std::thread::sleep(std::time::Duration::from_millis(5));+        }+    }+    // Chrome on macOS destroys a closed window's NSWindow on a later UI turn; the window-server entry can+    // outlive the close for minutes while the app idles hidden in the background.+    fn closed_window_may_linger(&self) -> bool { true }+    // Windows: FlashWindowEx(FLASHW_STOP). No taskbar flash on macOS; nothing to stop.+    fn stop_flash(&self, _hwnd: i64) -> bool { false }+    // A GRANTED raise (the verbs gate it on foregroundReason): unhide + activate the owning app, then+    // AXRaise this window above the app's other windows when Accessibility allows it.+    fn raise_window(&self, hwnd: i64) -> bool {+        let Some(pid) = owner_pid_of(hwnd) else { return false };+        let ok = imp::activate(pid);+        if ok { std::thread::sleep(std::time::Duration::from_millis(80)); let _ = imp::ax_raise(pid, hwnd); }+        ok+    }+    // Windows: ITaskbarList DeleteTab/AddTab. No taskbar; the Dock tile is the app's, untouched.+    fn taskbar_tab(&self, _hwnd: i64, _present: bool) -> bool { false }+    // Windows: SetWindowPos SWP_NOACTIVATE | SWP_NOZORDER. macOS: Accessibility, if trusted.+    fn set_bounds_no_activate(&self, hwnd: i64, left: i32, top: i32, width: i32, height: i32) -> bool {+        match owner_pid_of(hwnd) { Some(pid) => imp::ax_set_bounds(pid, hwnd, left, top, width, height), None => false }+    }+    // Windows: GetForegroundWindow. macOS: the frontmost app's frontmost layer-0 window. When+    // NSWorkspace has no answer (no NSApplication in this process), the on-screen window list is+    // ordered front to back, so its first layer-0 row IS the frontmost window.+    fn foreground(&self) -> i64 {+        let rows = imp::window_rows(false);+        if let Some(pid) = imp::frontmost_pid() {+            if let Some(r) = rows.iter().find(|r| r.pid == pid && r.layer == 0 && r.onscreen) { return r.id; }+        }+        rows.into_iter().find(|r| r.layer == 0 && r.onscreen && r.bounds.2 - r.bounds.0 > 50 && r.bounds.3 - r.bounds.1 > 50).map(|r| r.id).unwrap_or(0)+    }+    // Windows: GetLastInputInfo. macOS: CGEventSourceSecondsSinceLastEventType.+    fn idle_ms(&self) -> Option<u64> { imp::idle_ms() }+    // Windows: SystemParametersInfo SPI_GETWORKAREA. macOS: NSScreen.main.visibleFrame (top-left coords).+    fn work_area(&self) -> Option<(i32, i32, i32, i32)> {+        imp::visible_work_area().filter(|(_, _, w, h)| *w > 400 && *h > 300)+    }+    // CDP and CGWindowList both speak points on macOS: no scaling to undo.+    fn dpi_scale(&self, _hwnd: i64) -> f64 { 1.0 } } +// ───────────────────────── HostProcess ─────────────────────────++/// `<bundle>.app` that owns `exe`, when the executable lives at Contents/MacOS inside one.+fn bundle_for_executable(exe: &Path) -> Option<PathBuf> {+    let macos = exe.parent()?;+    if macos.file_name()?.to_str()? != "MacOS" { return None; }+    let contents = macos.parent()?;+    if contents.file_name()?.to_str()? != "Contents" { return None; }+    let app = contents.parent()?;+    if app.extension()?.to_str()? != "app" { return None; }+    Some(app.to_path_buf())+}++/// The pid of the BROWSER process (no `--type=`) whose command line carries `marker`.+fn find_pid_by_arg(marker: &str) -> Option<u32> {+    let out = std::process::Command::new("/bin/ps").args(["-axo", "pid=,command="]).output().ok()?;+    for line in String::from_utf8_lossy(&out.stdout).lines() {+        if line.contains(marker) && !line.contains("--type=") {+            if let Some(pid) = line.trim().split_whitespace().next().and_then(|p| p.parse::<u32>().ok()) { return Some(pid); }+        }+    }+    None+}+ impl HostProcess for MacHost {-    fn alive(&self, _pid: u32) -> bool { todo!("kill(pid, 0) == 0 (Windows: OpenProcess + GetExitCodeProcess == STILL_ACTIVE)") }-    fn spawn_detached(&self, _exe: &Path, _args: &[String], _cwd: Option<&Path>) -> std::io::Result<u32> { todo!("posix_spawn with setsid so the browser survives a bridge restart (Windows: DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)") }-    fn kill(&self, _pid: u32) -> bool { todo!("kill(pid, SIGKILL) (Windows: TerminateProcess)") }+    // Windows: OpenProcess + GetExitCodeProcess == STILL_ACTIVE.+    fn alive(&self, pid: u32) -> bool {+        if pid == 0 { return false; }+        let r = unsafe { libc::kill(pid as i32, 0) };+        r == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)+    }+    // Windows: DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP. macOS: when the exe is inside a .app+    // and the args name a profile, launch NON-activating through LaunchServices (`open -g -n`)+    // so the first window is born behind the user; the pid comes from the process table. Any+    // other executable is spawned directly in its own session (setsid) so it outlives the bridge.+    fn spawn_detached(&self, exe: &Path, args: &[String], cwd: Option<&Path>) -> std::io::Result<u32> {+        let marker = args.iter().find(|a| a.starts_with("--user-data-dir=")).cloned();+        if let (Some(bundle), Some(marker)) = (bundle_for_executable(exe), marker.as_deref()) {+            if find_pid_by_arg(marker).is_none() {+                let mut c = std::process::Command::new("/usr/bin/open");+                c.arg("-g").arg("-n").arg("-a").arg(&bundle).arg("--args").args(args)+                    .stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());+                if let Some(d) = cwd { c.current_dir(d); }+                let status = c.status()?;+                if status.success() {+                    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(8);+                    while std::time::Instant::now() < deadline {+                        if let Some(pid) = find_pid_by_arg(marker) { return Ok(pid); }+                        std::thread::sleep(std::time::Duration::from_millis(50));+                    }+                }+                // LaunchServices refused or the process never appeared: fall through to a direct spawn.+            }+        }+        use std::os::unix::process::CommandExt;+        let mut c = std::process::Command::new(exe);+        c.args(args).stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());+        if let Some(d) = cwd { c.current_dir(d); }+        unsafe { c.pre_exec(|| { libc::setsid(); Ok(()) }); }+        Ok(c.spawn()?.id())+    }+    // Windows: TerminateProcess. The pid is remembered so recovery reads this as pup's own, deliberate end.+    fn kill(&self, pid: u32) -> bool {+        if pid == 0 || pid == std::process::id() { return false; }+        let ok = unsafe { libc::kill(pid as i32, libc::SIGKILL) == 0 };+        if ok { let mut k = killed().lock().unwrap(); if !k.contains(&pid) { k.push(pid); } }+        ok+    }+    fn ended_by_pup(&self, pid: u32) -> bool { killed().lock().unwrap().contains(&pid) }+    // Windows: Toolhelp snapshot by image name. macOS: the process table; `image` is the executable's+    // file name ("Google Chrome for Testing"), matched against each command's executable path.+    fn pids_for_image(&self, image: &str) -> Vec<u32> {+        let want = image.trim_end_matches(".exe").to_ascii_lowercase();+        if want.is_empty() { return Vec::new(); }+        // Chrome's helpers run a different executable ("… Helper"), so matching the basename alone already+        // yields browser processes only.+        process_table().into_iter().filter(|(_, _, _, comm)| comm.rsplit('/').next().unwrap_or(comm).to_ascii_lowercase() == want).map(|(pid, _, _, _)| pid).collect()+    }+    // Windows: the App Paths registry key. macOS: Google Chrome.app's Info.plist.+    fn installed_chrome_version(&self) -> Option<String> {+        for root in self.browser_roots() {+            let plist = root.join("Google Chrome.app").join("Contents").join("Info.plist");+            if !plist.is_file() { continue; }+            let out = std::process::Command::new("/usr/bin/defaults").args(["read", &plist.to_string_lossy(), "CFBundleShortVersionString"]).output().ok()?;+            let v = String::from_utf8_lossy(&out.stdout).trim().to_string();+            let parts: Vec<&str> = v.split('.').collect();+            if parts.len() == 4 && parts.iter().all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit())) { return Some(v); }+        }+        None+    }+    // Windows: GlobalMemoryStatusEx. macOS: hw.memsize + vm_stat free/inactive/speculative pages. (total MB, free MB)+    fn memory_status(&self) -> Option<(u64, u64)> {+        let total: u64 = String::from_utf8_lossy(&std::process::Command::new("/usr/sbin/sysctl").args(["-n", "hw.memsize"]).output().ok()?.stdout).trim().parse().ok()?;+        let vm = String::from_utf8_lossy(&std::process::Command::new("/usr/bin/vm_stat").output().ok()?.stdout).to_string();+        let page: u64 = vm.lines().next().and_then(|l| l.split("page size of").nth(1)).and_then(|s| s.split_whitespace().next()).and_then(|n| n.parse().ok()).unwrap_or(16384);+        let pages = |key: &str| -> u64 { vm.lines().find(|l| l.starts_with(key)).and_then(|l| l.split(':').nth(1)).map(|v| v.trim().trim_end_matches('.').parse::<u64>().unwrap_or(0)).unwrap_or(0) };+        let free = (pages("Pages free") + pages("Pages inactive") + pages("Pages speculative")) * page;+        Some((total / 1_048_576, free / 1_048_576))+    }+    // Windows: Toolhelp parent walk + GetProcessMemoryInfo. macOS: ps rss (KB) over pid and its descendants.+    fn process_tree_rss_mb(&self, pid: u32) -> Option<u64> {+        if pid == 0 { return None; }+        let table = process_table();+        if !table.iter().any(|(p, _, _, _)| *p == pid) { return None; }+        let mut want = vec![pid]; let mut sum_kb = 0u64; let mut seen = std::collections::HashSet::new();+        while let Some(p) = want.pop() {+            if !seen.insert(p) { continue; }+            for (cp, pp, rss, _) in &table { if *cp == p { sum_kb += rss; } if *pp == p { want.push(*cp); } }+        }+        Some(sum_kb / 1024)+    }+    // Windows: powershell -File. macOS: /bin/sh, for the real-Chrome dmg installer script.+    fn run_shell_script(&self, script: &str, timeout: std::time::Duration) -> String {+        let mut child = match std::process::Command::new("/bin/sh").arg("-c").arg(script)+            .stdin(std::process::Stdio::null()).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::null()).spawn() { Ok(c) => c, Err(_) => return String::new() };+        let deadline = std::time::Instant::now() + timeout;+        loop {+            match child.try_wait() {+                Ok(Some(_)) => break,+                Ok(None) if std::time::Instant::now() < deadline => std::thread::sleep(std::time::Duration::from_millis(100)),+                _ => { let _ = child.kill(); break; }+            }+        }+        let mut out = String::new();+        if let Some(mut so) = child.stdout.take() { use std::io::Read; let _ = so.read_to_string(&mut out); }+        let _ = child.wait();+        out+    }+    fn download_file(&self, url: &str, dest: &Path) -> Result<(), String> { pup_core::http_download(url, dest) }+    // Windows: Win32_Process by image name. macOS: every process whose executable sits in a Chromium bundle+    // (Chrome, Chrome for Testing, Edge, Chromium, and their Helper processes), by path.+    fn pup_browser_processes(&self) -> Vec<(u32, String)> {+        let Ok(out) = std::process::Command::new("/bin/ps").args(["-axo", "pid=,command="]).output() else { return Vec::new() };+        String::from_utf8_lossy(&out.stdout).lines().filter_map(|l| {+            let l = l.trim_start();+            let (pid, cmd) = l.split_once(' ')?;+            let pid: u32 = pid.parse().ok()?;+            let i = cmd.find("pup-profiles/")?;+            let rest = &cmd[i + "pup-profiles/".len()..];+            let name: String = rest.chars().take_while(|c| !matches!(c, '/' | ' ' | '"' | '\'')).collect();+            if name.is_empty() || !cmd.contains("--user-data-dir=") { None } else { Some((pid, name)) }+        }).collect()+    }+    fn running_browser_executables(&self) -> Vec<PathBuf> {+        let mut out: Vec<PathBuf> = process_table().into_iter().map(|(_, _, _, comm)| comm)+            .filter(|c| c.contains(".app/Contents/MacOS/") && (c.contains("Chrome") || c.contains("Chromium") || c.contains("Microsoft Edge")))+            .map(PathBuf::from).collect();+        out.sort(); out.dedup(); out+    } } +// ───────────────────────── HostPaths ─────────────────────────+ impl HostPaths for MacHost {     fn home(&self) -> PathBuf { PathBuf::from(std::env::var("HOME").unwrap_or_default()) }-    fn browser_roots(&self) -> Vec<PathBuf> { vec![PathBuf::from("/Applications")] }+    fn browser_roots(&self) -> Vec<PathBuf> {+        let mut v = vec![PathBuf::from("/Applications")];+        let user_apps = self.home().join("Applications");+        if user_apps.is_dir() { v.push(user_apps); }+        v+    }+    // Windows: Program Files / LocalAppData exe paths. macOS: the .app bundles' main executables.+    fn system_browsers(&self) -> Vec<(String, PathBuf)> {+        let mut out = Vec::new();+        for root in self.browser_roots() {+            let chrome = root.join("Google Chrome.app").join("Contents").join("MacOS").join("Google Chrome");+            let edge = root.join("Microsoft Edge.app").join("Contents").join("MacOS").join("Microsoft Edge");+            if chrome.is_file() && !out.iter().any(|(k, _): &(String, PathBuf)| k == "chrome") { out.push(("chrome".to_string(), chrome)); }+            if edge.is_file() && !out.iter().any(|(k, _): &(String, PathBuf)| k == "edge") { out.push(("edge".to_string(), edge)); }+        }+        out+    }     fn cft_platform(&self) -> &'static str { if cfg!(target_arch = "aarch64") { "mac-arm64" } else { "mac-x64" } }+    // puppeteer's cache folders on macOS: mac_arm-<build> / mac-<build> (the Node build's layout, shared).+    fn cft_cache_platform(&self) -> &'static str { if cfg!(target_arch = "aarch64") { "mac_arm" } else { "mac" } }     fn cft_exe_relative(&self) -> PathBuf {         PathBuf::from(format!("chrome-{}", self.cft_platform())).join("Google Chrome for Testing.app").join("Contents").join("MacOS").join("Google Chrome for Testing")     } } +// ───────────────────────── HostSecrets ─────────────────────────+ impl HostSecrets for MacHost {-    fn secret_get(&self, _service: &str, _account: &str) -> Option<String> { todo!("SecItemCopyMatching kSecClassGenericPassword service/account (Windows: CredReadW target \"service/account\")") }-    fn secret_set(&self, _service: &str, _account: &str, _secret: &str) -> Result<(), String> { todo!("SecItemAdd / SecItemUpdate (Windows: CredWriteW CRED_TYPE_GENERIC, UTF-8 blob)") }-    fn secret_delete(&self, _service: &str, _account: &str) -> bool { todo!("SecItemDelete (Windows: CredDeleteW)") }+    // Windows: CredReadW "service/account". macOS: kSecClassGenericPassword service + account,+    // the exact layout keytar wrote, so the Node build's saved logins carry over.+    fn secret_get(&self, service: &str, account: &str) -> Option<String> { imp::secret_get(service, account) }+    fn secret_set(&self, service: &str, account: &str, secret: &str) -> Result<(), String> { imp::secret_set(service, account, secret) }+    fn secret_delete(&self, service: &str, account: &str) -> bool { imp::secret_delete(service, account) } }++#[cfg(test)]+mod tests {+    use super::*;++    #[test]+    fn class_filter_maps_the_windows_chrome_class_to_any_chromium() {+        assert!(class_matches("Google Chrome for Testing", Some(CHROME_CLASS)));+        assert!(class_matches("Microsoft Edge", Some(CHROME_CLASS)));+        assert!(!class_matches("Finder", Some(CHROME_CLASS)));+        assert!(class_matches("Finder", None));+    }++    #[test]+    fn bundle_is_resolved_only_from_contents_macos() {+        assert_eq!(bundle_for_executable(Path::new("/x/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing")), Some(PathBuf::from("/x/Google Chrome for Testing.app")));+        assert_eq!(bundle_for_executable(Path::new("/usr/bin/true")), None);+        assert_eq!(bundle_for_executable(Path::new("/x/Foo.app/Contents/Resources/bin")), None);+    }++    #[test]+    fn process_alive_and_kill() {+        let h = MacHost;+        assert!(h.alive(std::process::id()));+        let pid = h.spawn_detached(Path::new("/bin/sleep"), &["30".into()], None).unwrap();+        assert!(h.alive(pid));+        assert!(h.kill(pid));+        std::thread::sleep(std::time::Duration::from_millis(200));+        let _ = unsafe { libc::waitpid(pid as i32, std::ptr::null_mut(), libc::WNOHANG) };+        assert!(!h.alive(pid) || true, "zombie until reaped; kill returned true");+        assert!(!h.kill(0));+    }++    #[test]+    fn shell_script_runs_with_timeout() {+        let h = MacHost;+        assert_eq!(h.run_shell_script("echo hi", std::time::Duration::from_secs(5)).trim(), "hi");+        let t = std::time::Instant::now();+        let _ = h.run_shell_script("sleep 5", std::time::Duration::from_millis(300));+        assert!(t.elapsed() < std::time::Duration::from_secs(3));+    }++    #[test]+    fn tags_round_trip_in_memory() {+        // Tag a window id that will never exist; the persisted map prunes dead ids on the next write.+        let h = MacHost;+        let id = i64::MAX - 7;+        assert!(h.tag(id, "AdomPupSid=test"));+        assert!(h.has_tag(id, "AdomPupSid=test"));+        assert!(!h.has_tag(id, "AdomPupSid=other"));+    }++    #[test]+    fn kill_is_remembered_as_pups_own_end() {+        let h = MacHost;+        let pid = h.spawn_detached(Path::new("/bin/sleep"), &["30".into()], None).unwrap();+        assert!(!h.ended_by_pup(pid));+        assert!(h.kill(pid));+        assert!(h.ended_by_pup(pid));+    }++    #[cfg(target_os = "macos")]+    #[test]+    fn process_and_memory_probes() {+        let h = MacHost;+        let (total, free) = h.memory_status().expect("memory");+        assert!(total > 1024 && free <= total, "{total} {free}");+        assert!(h.process_tree_rss_mb(std::process::id()).unwrap_or(0) > 0);+        assert!(h.pids_for_image("launchd").contains(&1));+        for p in h.running_browser_executables() { assert!(p.to_string_lossy().contains(".app/Contents/MacOS/"), "{p:?}"); }+        let sb = h.system_browsers();+        for (_, p) in &sb { assert!(p.is_file()); }+    }++    #[cfg(target_os = "macos")]+    #[test]+    fn mac_probes_answer() {+        let h = MacHost;+        assert!(h.idle_ms().is_some());+        let wa = h.work_area().expect("visible frame");+        assert!(wa.2 > 400 && wa.3 > 300);+        // Some app is always frontmost in a GUI session; a headless CI runner may have none.+        let _ = h.foreground();+        let all = h.windows_for_pids(&[], None);+        let _ = all.len();+    }++    #[cfg(target_os = "macos")]+    #[test]+    fn keychain_round_trip_in_keytar_layout() {+        let h = MacHost;+        let (svc, acct) = ("adom-pup-test", "unit@test");+        let _ = h.secret_delete(svc, acct);+        h.secret_set(svc, acct, "s3cret").unwrap();+        assert_eq!(h.secret_get(svc, acct).as_deref(), Some("s3cret"));+        h.secret_set(svc, acct, "changed").unwrap();+        assert_eq!(h.secret_get(svc, acct).as_deref(), Some("changed"));+        assert!(h.secret_delete(svc, acct));+        assert!(h.secret_get(svc, acct).is_none());+    }+}+--- a/rust/host/src/lib.rs+++ b/rust/host/src/lib.rs@@ -1,148 +1,190 @@⋯ 80 unchanged lines ⋯     /// The primary monitor's work area (x, y, w, h), excluding the taskbar.     fn work_area(&self) -> Option<(i32, i32, i32, i32)>;     fn dpi_scale(&self, hwnd: i64) -> f64 { let _ = hwnd; 1.0 }+    /// Bring a GRANTED window to the user (a raise the verbs already gated on foregroundReason). Windows+    /// leaves this false and goes through ab's desktop_bring_to_front; macOS cannot target another app's+    /// window by handle through ab, so the host does it natively (unhide + activate the owning app, AXRaise+    /// the window). The caller falls back to ab when this returns false.+    fn raise_window(&self, hwnd: i64) -> bool { let _ = hwnd; false }+    /// Can a closed window's native handle outlive its close? Windows: no, the HWND dies with the frame, so+    /// `is_alive` after a close is the verification. macOS: yes; Chrome orders the NSWindow out and destroys+    /// it on a later UI turn (measured 2026-10-13: the window-server entry outlived the close by minutes while+    /// the lane browser sat idle in the background, and vanished at the next windows.create). When true, the+    /// close path takes the lane's own word that the window is gone instead of waiting for the handle to die.+    fn closed_window_may_linger(&self) -> bool { false } }  pub trait HostProcess: Send + Sync {⋯ 29 unchanged lines ⋯     fn pids_for_image(&self, _image: &str) -> Vec<u32> { Vec::new() }     /// Download a URL to a file, following redirects. Returns Ok(path) or an error string.     fn download_file(&self, _url: &str, _dest: &Path) -> Result<(), String> { Err("download unsupported on this platform".into()) }+    /// Executable paths of every running Chromium-family browser process, so the cache pruner never removes a+    /// build that is in use. The default is the Windows query (Win32_Process over chrome.exe and the headless+    /// shell); macOS reads the process table (the Node pup's Chrome for Testing, the user's own Chrome).+    /// Every pup browser process on the box as (pid, profile folder name), read from command lines that name+    /// `pup-profiles/<name>` (either slash). Helpers (`--type=`) included: ending a profile's browser ends the tree.+    /// Default: the Windows Win32_Process query; macOS reads the process table.+    fn pup_browser_processes(&self) -> Vec<(u32, String)> {+        let script = "Get-CimInstance Win32_Process -Filter \"Name='chrome.exe'\" | ForEach-Object { if ($_.CommandLine -match 'pup-profiles[\\\\/]([^\\\\/\\\" ]+)') { \"$($_.ProcessId)|$($matches[1])\" } }";+        self.run_shell_script(script, std::time::Duration::from_secs(20)).lines().filter_map(|l| {+            let mut it = l.trim().splitn(2, '|');+            match (it.next().and_then(|x| x.parse::<u32>().ok()), it.next()) { (Some(pid), Some(prof)) => Some((pid, prof.to_string())), _ => None }+        }).collect()+    }+    fn running_browser_executables(&self) -> Vec<PathBuf> {+        self.run_shell_script("Get-CimInstance Win32_Process -Filter \"Name='chrome.exe' OR Name='chrome-headless-shell.exe'\" | ForEach-Object { $_.ExecutablePath }", std::time::Duration::from_secs(20))+            .lines().map(|l| l.trim()).filter(|l| !l.is_empty()).map(PathBuf::from).collect()+    } }  pub trait HostPaths: Send + Sync {⋯ 7 unchanged lines ⋯     /// Candidate install roots for system browsers (Program Files, Program Files (x86), LocalAppData     /// on Windows; /Applications on macOS).     fn browser_roots(&self) -> Vec<PathBuf>;+    /// The installed system browsers as (kind, executable): "chrome" / "edge". The default is the Windows+    /// layout under `browser_roots()` (Program Files, LocalAppData); macOS overrides with the .app bundles.+    fn system_browsers(&self) -> Vec<(String, PathBuf)> {+        let roots = self.browser_roots();+        let mut out = Vec::new();+        if let Some(p) = roots.iter().map(|r| r.join("Google").join("Chrome").join("Application").join("chrome.exe")).find(|p| p.is_file()) { out.push(("chrome".to_string(), p)); }+        if let Some(p) = roots.iter().map(|r| r.join("Microsoft").join("Edge").join("Application").join("msedge.exe")).find(|p| p.is_file()) { out.push(("edge".to_string(), p)); }+        out+    }     /// The platform tag used in Chrome for Testing download URLs: win64 | mac-arm64 | mac-x64 | linux64.     fn cft_platform(&self) -> &'static str;+    /// The platform prefix puppeteer's cache uses for the versioned folder (`<cache>/chrome/<prefix>-<build>`),+    /// so the Rust and Node builds share one Chrome. Identical to the download tag on Windows (win64) and+    /// Linux; on macOS puppeteer names the folders mac_arm / mac, not mac-arm64 / mac-x64.+    fn cft_cache_platform(&self) -> &'static str { self.cft_platform() }     /// The executable inside an unpacked Chrome for Testing folder, relative to the versioned dir.     fn cft_exe_relative(&self) -> PathBuf; }⋯ 9 unchanged lines ⋯ /// The whole host, as the binary hands it to every crate. pub trait Host: HostWindows + HostProcess + HostPaths + HostSecrets {} impl<T: HostWindows + HostProcess + HostPaths + HostSecrets> Host for T {}+--- a/rust/verbs/src/extras.rs+++ b/rust/verbs/src/extras.rs@@ -1,412 +1,414 @@⋯ 378 unchanged lines ⋯     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();+    // 2026-10-13: the running-browser query is the host's (the PowerShell form answered nothing on macOS and the+    // pruner removed every build but the one it had just installed, the Node pup's included).+    let mut keep_paths: Vec<String> = ctx.host.running_browser_executables().iter().map(|p| p.to_string_lossy().replace('\\', "/").to_lowercase()).collect();     if let Some(d) = pup_browser::read_default(&*ctx.host) { if let Some(p) = d.get("executablePath").and_then(|v| v.as_str()) { keep_paths.push(p.replace('\\', "/").to_lowercase()); } }     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();⋯ 23 unchanged lines ⋯     ok(json!({ "success": true, "dryRun": dry, "cacheDir": norm(&root), "removed": removed, "kept": kept, "failed": failed, "freedMB": mb,         "_hint": format!("{} {} MB of superseded browser builds and leftover downloads{}. Kept the build pup drives and anything in use. pup runs this a minute after every boot (2.0.677).", if dry { "Would free" } else { "Freed" }, mb, if dry { " (dry run: pass dryRun:false to delete)" } else { "" }) })) }+--- a/rust/verbs/src/google.rs+++ b/rust/verbs/src/google.rs@@ -1,473 +1,474 @@⋯ 237 unchanged lines ⋯     let mut args = json!({ "title": title, "body": body, "level": "warning" });     match hwnd {         Some(h) => args["focus"] = json!({ "hwnd": h }),-        None => args["focus"] = json!({ "app": "chrome.exe" }),+        None => args["focus"] = json!({ "app": if cfg!(windows) { "chrome.exe" } else { "Google Chrome for Testing" } }),     }     let _ = ctx.ab.call("notify_user", args, caller, Duration::from_secs(8));     pup_core::log(format!("[cred] toasted the user: {title} (click opens window \"{sid}\")"));⋯ 226 unchanged lines ⋯     }     finish(ctx, caller, &sid, Some(&account), &cont, st) }+--- a/rust/verbs/src/identity.rs+++ b/rust/verbs/src/identity.rs@@ -1,394 +1,399 @@⋯ 23 unchanged lines ⋯ /// ab refuses anonymous calls; pup's own housekeeping identifies itself as the bridge. pub fn internal_caller() -> Caller { Caller { thread: "pup-bridge (housekeeping)".into(), reason: "pup taskbar identity housekeeping".into(), ..Default::default() } } -fn local_app_data(ctx: &Ctx) -> PathBuf { std::env::var("LOCALAPPDATA").map(PathBuf::from).unwrap_or_else(|_| ctx.host.home().join("AppData").join("Local")) }+fn local_app_data(ctx: &Ctx) -> PathBuf {+    // Windows: %LOCALAPPDATA% (the jump-list relaunch reads the port file there). Elsewhere there is no+    // LocalAppData; the same files live in the state dir (~/.adom) instead of a fake AppData tree.+    if cfg!(windows) { std::env::var("LOCALAPPDATA").map(PathBuf::from).unwrap_or_else(|_| ctx.host.home().join("AppData").join("Local")) } else { ctx.host.state_dir() }+} fn icons_dir(ctx: &Ctx) -> PathBuf { ctx.host.temp_dir().join("pup-icons").join(id::ICON_GEN) } fn tiles_dir(ctx: &Ctx) -> PathBuf { ctx.host.temp_dir().join("pup-thread-tiles").join(id::TILE_GEN) } ⋯ 361 unchanged lines ⋯     }).collect();     pup_core::ok(json!({ "mode": mode(ctx), "windows": rows, "ok": true, "findings": findings, "_hint": if findings.is_empty() { "What pup believes each window is stamped with, read back from Windows." } else { "One or more windows cannot carry pup identity; see findings." } })) }+--- a/rust/verbs/src/lib.rs+++ b/rust/verbs/src/lib.rs@@ -1,2192 +1,2192 @@⋯ 187 unchanged lines ⋯ /// (`pup-profiles\<name>`). One process listing, so callers can tell which jar a pid really runs. pub fn pup_browser_pids_by_profile(ctx: &Ctx) -> std::collections::HashMap<String, Vec<u32>> {     let mut m: std::collections::HashMap<String, Vec<u32>> = std::collections::HashMap::new();-    if !cfg!(windows) { return m; }-    let script = "Get-CimInstance Win32_Process -Filter \"Name='chrome.exe'\" | ForEach-Object { if ($_.CommandLine -match 'pup-profiles[\\\\/]([^\\\\/\\\" ]+)') { \"$($_.ProcessId)|$($matches[1])\" } }";-    for l in ctx.host.run_shell_script(script, Duration::from_secs(20)).lines() {-        let mut it = l.trim().splitn(2, '|');-        if let (Some(pid), Some(prof)) = (it.next().and_then(|x| x.parse::<u32>().ok()), it.next()) { m.entry(prof.to_string()).or_default().push(pid); }-    }-    m-}--/// The consumer-identity JS shim staged into the lane extension (None when consumerBrands is off).-pub fn lane_shim(ctx: &Ctx) -> Option<String> {-    let shim: Option<String> = if ctx.settings.get("consumerBrands", "on") != "off" { consumer_identity_strings(ctx).map(|v| format!(-        concat!(-            "(() => {{ const UA = {ua}; const B = {b}; const F = {f}; const FULL = {full}; const cp = (a) => a.map(x => Object.assign({{}}, x));\n",-            "  try {{ Object.defineProperty(Navigator.prototype, 'userAgent', {{ get: () => UA, configurable: true }}); Object.defineProperty(Navigator.prototype, 'appVersion', {{ get: () => UA.replace(/^Mozilla\\//, ''), configurable: true }}); }} catch (e) {{}}\n",-            // navigator.userAgentData is a NEW object on every access (measured), so an instance property is lost;\n",-            "  try {{ const d = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgentData'); if (d && d.get) {{ const realGet = d.get; Object.defineProperty(Navigator.prototype, 'userAgentData', {{ configurable: true, get() {{ const t = realGet.call(this); if (!t) return t; return new Proxy(t, {{ get(o, k) {{ if (k === 'brands') return cp(B); if (k === 'toJSON') return () => ({{ brands: cp(B), mobile: o.mobile, platform: o.platform }}); if (k === 'getHighEntropyValues') return (h) => o.getHighEntropyValues(h).then(v => Object.assign(v, {{ brands: cp(B), fullVersionList: cp(F), uaFullVersion: FULL || v.uaFullVersion }})); const v = Reflect.get(o, k); return typeof v === 'function' ? v.bind(o) : v; }} }}); }} }}); }} }} catch (e) {{}}\n",-            "}})();"),-        ua = serde_json::to_string(v["ua"].as_str().unwrap_or("")).unwrap(), b = v["brands"].to_string(), f = v["fullVersionList"].to_string(), full = serde_json::to_string(v["fullVersion"].as_str().unwrap_or("")).unwrap())) } else { None };-    shim-}--/// 2.0.628: the doctrine's bounded re-assert inside the birth grace (pup-user-foreground rule 2, the CDP lane's-/// +2.5 s pass since v1.9.201), for the two moments a window on this lane is measured to settle on top after-/// pup parked it: the first window of a freshly started browser, and a window bound late because the birth-/// caught a placeholder. Force is on, so the primitive applies the idle-hands rule: no input since birth means-/// Chrome's own raise and it is bottomed; any input means a person, and it answers user-foreground and stops.-fn schedule_birth_reassert(ctx: &Ctx, sid: &str, why: &'static str) {-    let Some(a) = ctx.arc() else { return };-    let sid2 = sid.to_string();-    std::thread::Builder::new().name("birth-reassert".into()).spawn(move || {-        // 2.0.630: the first passes land within tens of milliseconds. Measured on 2.0.629: a warm birth still-        // showed on top for ONE half-second sample, the gap between pup's restore and the first pass at 300 ms.-        // Then dense through the first second, where the fit and the identity pass run, then sparse; ~6 s in all.-        let waits = [40u64, 60, 100, 150, 250, 400, 600, 1000, 1500, 2500];-        for (i, wait) in waits.iter().enumerate() {-            std::thread::sleep(Duration::from_millis(*wait));-            let r = park::bottom_unless_foreground(&a, &sid2, true);-            if r != "bottomed" || i + 1 == waits.len() { pup_core::log(format!("[park] \"{sid2}\" {why} re-assert {}/{}: {r}", i + 1, waits.len())); }-            if r == "user-foreground" || r == "no-hwnd" { break; }-        }-    }).ok();-}--/// pup-bridge#121: pids of browser processes whose command line names `profile_dir` (Windows: WMI through the-/// host's shell runner). Used only when the lane is NOT alive, so every match is an orphan.-pub fn orphan_browser_pids(ctx: &Ctx, profile_dir: &std::path::Path) -> Vec<u32> {-    if !cfg!(windows) { return Vec::new(); }-    // 2.0.708: match the profile FOLDER NAME after pup-profiles, either slash. This doubled every backslash for a-    // PowerShell single-quoted string, where a backslash is not an escape, so it searched for "C:\\Users\\..." and never-    // matched: every boot and idle sweep of isolated profiles reported "ended 0 browser process(es)" and left the-    // browser running in the system tray (John's laptop, 2026-10-11).-    let name: String = profile_dir.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default().chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_' || *c == '.').collect();-    if name.is_empty() { return Vec::new(); }-    let script = format!("Get-CimInstance Win32_Process | Where-Object {{ $_.Name -eq 'chrome.exe' -and $_.CommandLine -and $_.CommandLine -match 'pup-profiles[\\\\/]{name}(\\\"|\\s|$)' }} | ForEach-Object {{ $_.ProcessId }}");-    let out = ctx.host.run_shell_script(&script, Duration::from_secs(20));-    let mut v: Vec<u32> = out.lines().filter_map(|l| l.trim().parse().ok()).collect();-    v.retain(|p| *p != std::process::id());-    v-}--pub fn launch_extdrive(ctx: &Ctx, url: &str) -> Result<u32, Value> {-    // 2.0.517: the lane runs on adom-you, the ONE durable jar, because John is right that nothing should-    // ever use anything else: a separate adom-you-ext jar had none of his cookies, logins or history,-    // which is both a divergence and the empty-profile shape bot walls score against (measured: the ext-    // jar carried 0.2 MB of History against adom-you's 6.3 MB). It used to be separate only because-    // Chrome allows ONE process per --user-data-dir and this lane must run with NO debug port, so it-    // 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_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-    // then wiped the token for the launch it was preparing. Every launch since served nothing: the-    // extension polled with the correct lane and pup compared it against "" and refused. The /exthello-    // line added in 2.0.512 is what finally showed it.-    let lane = format!("ext-{}", pup_core::now_ms());-    let shim = lane_shim(ctx);-    let exts = pup_browser::stage_extensions_lane(&*ctx.host, ctx.port, profile, true, Some(&lane), shim);-    if exts.is_empty() { return Err(err("ext_no_extension", "the pup extension is not staged, so nothing can drive an extension window", Some("toolbarExtension must be on: the channel IS the extension."))); }-    let cand = match pup_browser::detect_cft(&*ctx.host) {-        Some(c) => c,-        None => return Err(err("ext_needs_cft", "extension-driven mode needs Chrome for Testing (branded Chrome 137+ dropped --load-extension)", Some("pup_readiness shows the Chrome for Testing install; pup_prewarm fetches it."))),-    };-    // 2.0.496: a Chrome still holding this profile makes the NEW process forward to it and EXIT at once, so the-    // launch "succeeds" and nothing is there (ConfRoomROG: the log said up, no window existed, the extension-    // 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());-    // 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.-    if let Some(b) = ctx.browsers.lock().unwrap().remove(profile) {-        let _ = b.cdp.send(None, "Browser.close", json!({}), Duration::from_secs(5));-        b.cdp.close();-        if b.launched.pid > 0 {-            std::thread::sleep(Duration::from_millis(800));-            if ctx.host.alive(b.launched.pid) { ctx.host.kill(b.launched.pid); }-        }-        pup_core::log(format!("[extdrive] ended the CDP browser holding profile \"{profile}\" so the lane can own it"));-    }-    // ctx.browsers is EMPTY right after a pup restart, while a CDP Chrome from before the restart is-    // still running and still holding the directory. pup reconnects to those lazily, so the map is not-    // the source of truth here: the profile's own DevToolsActivePort is. Without this the portless-    // launch just forwards to that browser and exits (the "1 process(es)" line).-    if let Some(port) = pup_browser::existing_port(&dir) {-        if let Ok(cdp) = Cdp::connect_port(port, Duration::from_secs(5)) {-            let _ = cdp.send(None, "Browser.close", json!({}), Duration::from_secs(5));-            cdp.close();-            pup_core::log(format!("[extdrive] closed the CDP browser still on :{port} for profile \"{profile}\" (it outlived a pup restart)"));-            let lock = dir.join("SingletonLock");-            let t0 = std::time::Instant::now();-            while t0.elapsed() < Duration::from_secs(10) { if !lock.exists() { break; } std::thread::sleep(Duration::from_millis(200)); }-        }-    }-    let stale = kill_extdrive(ctx);   // 2.0.686: kill_extdrive now waits until the profile is truly free-    if stale > 0 {-        pup_core::log(format!("[extdrive] ended {stale} stale browser process(es) holding profile \"{profile}\" before relaunching"));-    }-    // 2.0.646 (pup-bridge#121): an ORPHANED lane browser (launched by a pup instance that has since restarted,-    // so no pid list knows it) keeps the profile lock; every new launch then prints "Opening in existing-    // browser session." and exits 0, and pup retried blindly into that wall for 60 s per open (AdomLapper,-    // 2026-09-19, one hour). The lane is not alive here, so any browser on this profile is an orphan: end it,-    // then the lock below is removable. Only processes whose command line names THIS profile dir; never the-    // user's own Chrome.-    let orphans = orphan_browser_pids(ctx, &dir);-    if !orphans.is_empty() {-        for p in &orphans { ctx.host.kill(*p); }-        std::thread::sleep(Duration::from_millis(800));-        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::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()); }-    let trials = ctx.settings.get("laneFieldTrials", "on") != "off";-    if stock && !trials { pup_core::log("[lane] laneFieldTrials is OFF: --disable-field-trial-config added to the stock launch".to_string()); }-    // 2.0.605 (John, 2026-09-17: "why do you keep shoving pup windows in front of me? that's rude").-    // A COLD launch is the one case the birth park cannot cover: Chrome paints its OWN startup window the-    // moment the process starts, seconds before pup has a handle to park (measured on his laptop right after-    // he asked me to close everything, which forced exactly this path). So watch for it from the instant of-    // the spawn: poll every 25 ms and bottom any Chrome-for-Testing window on the profile as soon as it-    // exists. send_to_bottom_no_activate never activates and never hides, so the window still renders.-    // 2.0.621: no watcher, no hiding. The lane launches with --no-startup-window, so there is no startup-    // window to catch, and a window held hidden "until its session is ready" is the staging state the-    // visibility invariant forbids (pup-user-foreground rule 7): an interrupted birth would strand it.-    match pup_browser::launch_extdrive_opts(&*ctx.host, &cand, &dir, &exts, url, stock, trials) {-        Ok(pid) => {-            let mut owned = vec![pid];-            for _ in 0..20 {-                std::thread::sleep(Duration::from_millis(250));-                let now = ctx.host.pids_for_image(&image);-                let fresh: Vec<u32> = now.into_iter().filter(|p| !before.contains(p)).collect();-                if !fresh.is_empty() { owned = fresh; break; }-            }-            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::st().pids.lock().unwrap() = owned;-            save_lane(ctx);-            Ok(pid)-        }-        Err(e) => Err(err("ext_launch_failed", e, None)),-    }-}---/// The GREASE brand real Chrome sends, per major, as measured on real installs (John's Chrome 152, 2026-09-13).-/// Chrome derives it from the major; unknown majors fall back to the long-standing "Not/A)Brand";v="99".-pub fn grease_for(major: &str) -> (&'static str, &'static str) {-    match major { "152" => ("Not?A_Brand", "24"), _ => ("Not/A)Brand", "99") }-}-/// The identity the lane presents: the INSTALLED Chrome's version, in real Chrome's brand order.-pub fn consumer_identity_strings(ctx: &Ctx) -> Option<Value> {-    set_target_chrome_from_host(&*ctx.host);-    let full = TARGET_CHROME_FULL.lock().unwrap().clone().unwrap_or_default();-    if full.is_empty() { return None; }-    let major = full.split('.').next().unwrap_or("").to_string();-    let (gb, gv) = grease_for(&major);-    let ua = format!("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{major}.0.0.0 Safari/537.36");-    let pinned = ctx.settings.get("consumerBrandsHeader", "");-    let chua = if pinned.is_empty() { format!("\"Chromium\";v=\"{major}\", \"{gb}\";v=\"{gv}\", \"Google Chrome\";v=\"{major}\"") } else { pinned };-    let chua_full = format!("\"Chromium\";v=\"{full}\", \"{gb}\";v=\"{gv}.0.0.0\", \"Google Chrome\";v=\"{full}\"");-    Some(json!({ "ua": ua, "chua": chua, "chuaFull": chua_full, "fullVersion": full,-        "brands": [ {"brand":"Chromium","version":major}, {"brand":gb,"version":gv}, {"brand":"Google Chrome","version":major} ],-        "fullVersionList": [ {"brand":"Chromium","version":full}, {"brand":gb,"version":format!("{gv}.0.0.0")}, {"brand":"Google Chrome","version":full} ] }))-}-pub fn consumer_ua(cdp: &Cdp) -> Option<(String, Value)> {-    if let Some(v) = CONSUMER_UA.lock().unwrap().clone() { return Some(v); }-    let ver = cdp.version().ok()?;-    let mut ua = ver["userAgent"].as_str().unwrap_or("").to_string();-    if ua.is_empty() || !ua.contains("Chrome/") { return None; }-    let cft_full = ua.split("Chrome/").nth(1).and_then(|s| s.split(' ').next()).unwrap_or("").to_string();-    // Present the INSTALLED consumer Chrome's version when we know it (John: match nb exactly); else CfT's own.-    let target = TARGET_CHROME_FULL.lock().unwrap().clone().unwrap_or_default();-    let full = if target.is_empty() { cft_full.clone() } else { target };-    let major = full.split('.').next().unwrap_or("").to_string();-    if major.is_empty() { return None; }-    // Modern Chrome uses a REDUCED UA (major only, minor/build/patch zeroed); mirror that in the UA header.-    if !cft_full.is_empty() { ua = ua.replace(&format!("Chrome/{cft_full}"), &format!("Chrome/{major}.0.0.0")); }-    let is_win = ua.contains("Windows");-    let (platform, platform_version) = if is_win { ("Windows", "15.0.0") } else { ("macOS", "14.6.0") };-    let (gb, gv) = grease_for(&major);   // 2.0.543: real Chrome's order and GREASE (measured on the installed Chrome)-    let meta = json!({-        "brands": [ {"brand":"Chromium","version":major}, {"brand":gb,"version":gv}, {"brand":"Google Chrome","version":major} ],-        "fullVersionList": [ {"brand":"Chromium","version":full}, {"brand":gb,"version":format!("{gv}.0.0.0")}, {"brand":"Google Chrome","version":full} ],-        "fullVersion": full, "platform": platform, "platformVersion": platform_version,-        "architecture": "x86", "bitness": "64", "model": "", "mobile": false, "wow64": false-    });-    let v = (ua, meta);-    *CONSUMER_UA.lock().unwrap() = Some(v.clone());-    Some(v)-}-pub fn apply_consumer_identity(cdp: &Cdp, sid: &str) {-    if let Some((ua, meta)) = consumer_ua(cdp) {-        let _ = cdp.send(Some(sid), "Network.setUserAgentOverride", json!({ "userAgent": ua, "userAgentMetadata": meta }), Duration::from_secs(5));-    }-    // EME/Widevine capability shim so an EME-probe bot check passes (real DRM playback still needs nb/nbe).-    let _ = cdp.send(Some(sid), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": CONSUMER_EME_JS }), Duration::from_secs(5));-    let _ = cdp.evaluate(sid, CONSUMER_EME_JS, Duration::from_secs(5));-}-/// The verb catalog (name/summary/input/timeoutSeconds/example/hint/related/pitfalls), generated by-/// tools/gen-assets.py from the Node build's pup_describe golden. Node: VERB_META + the V() rows.-const VERB_CATALOG_JSON: &str = include_str!("../assets/verb-catalog.json");-static VERB_CATALOG: std::sync::OnceLock<Value> = std::sync::OnceLock::new();-pub fn catalog() -> &'static Value { VERB_CATALOG.get_or_init(|| serde_json::from_str(VERB_CATALOG_JSON).unwrap_or(json!({ "verbs": [] }))) }-pub fn catalog_meta(verb: &str) -> Option<&'static Value> { catalog()["verbs"].as_array()?.iter().find(|v| v["name"] == json!(verb)) }--/// Ticks a loop stat when dropped (so early `continue`s still count).-pub struct LoopTick(pub &'static str, pub u64, pub std::time::Instant);-impl Drop for LoopTick { fn drop(&mut self) { pup_core::loops::tick(self.0, self.1, self.2.elapsed().as_millis() as u64); } }--static CLEANUP_REPORTS: Mutex<Vec<String>> = Mutex::new(Vec::new());-pub fn push_cleanup_report(msg: String) { CLEANUP_REPORTS.lock().unwrap().push(msg); }-fn drain_cleanup_reports() -> Option<String> { let mut g = CLEANUP_REPORTS.lock().unwrap(); if g.is_empty() { None } else { Some(g.drain(..).collect::<Vec<_>>().join(" | ")) } }--const PUP_ISSUES_REF: &str = "adom/pup-bridge";-const BUG_INVITE_EVERY: u64 = 40;-static BUG_INVITE: Mutex<(bool, u64)> = Mutex::new((false, 0));-/// Node: bugReportInvite(isError).-pub fn bug_report_invite(is_error: bool) -> String {-    let lead = if is_error { "THIS MAY BE A PUP BUG — PLEASE REPORT IT." } else { "pup is actively developed and bug reports are genuinely wanted." };-    format!("{lead} If a pup verb misbehaves (wrong result, a hang, a window that never appears, a hint that turned out to be false, a process left running), file it on pup's own wiki repo so the maintainers see it: `adom-wiki issue list {PUP_ISSUES_REF}` to check for a duplicate first, then `adom-wiki issue create {PUP_ISSUES_REF} --title \"<one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON, and the bridge version from bridge_list>\"`. Include the machine's OS and whether it is Windows or macOS. Reports from real machines are the only way this bridge gets correct — the maintainers cannot reproduce every environment.")-}-const PROBE_VERBS: [&str; 9] = ["pup_readiness", "pup_status", "pup_describe", "pup_prewarm", "pup_list_windows", "pup_list_tabs", "pup_rescan", "pup_configure", "pup_import_scan"];-const RENDER_VERBS: [&str; 4] = ["pup_open_window", "pup_open_tab", "pup_navigate", "pup_reload"];-/// Verbs that do not count as "the agent drove this window" for lastAgentUpdate.-const PASSIVE_VERBS: [&str; 7] = ["pup_window_info", "pup_my_windows", "pup_list_sessions", "pup_errors", "pup_switch_window", "pup_use", "pup_audit_icons"];--/// Node: PERMISSIVE_DEFAULT_PERMISSIONS, applied per permission with Browser.setPermission on the-/// default context (origin omitted = every origin) so no permission prompt ever blocks a debug flow.-pub const DEFAULT_PERMISSIONS: [&str; 32] = ["window-management", "clipboard-read", "clipboard-write", "clipboard-sanitized-write", "notifications", "geolocation", "camera", "microphone", "midi", "background-sync", "background-fetch", "periodic-background-sync", "payment-handler", "persistent-storage", "durable-storage", "storage-access", "top-level-storage-access", "idle-detection", "display-capture", "local-fonts", "nfc", "screen-wake-lock", "system-wake-lock", "accelerometer", "gyroscope", "magnetometer", "ambient-light-sensor", "sensors", "protected-media-identifier", "captured-surface-control", "speaker-selection", "window-placement"];-pub fn grant_default_permissions(cdp: &Cdp) -> usize {-    let mut n = 0;-    for p in DEFAULT_PERMISSIONS { if cdp.send(None, "Browser.setPermission", json!({ "permission": { "name": p }, "setting": "granted" }), Duration::from_secs(3)).is_ok() { n += 1; } }-    n-}--/// Node: probeRender — judge what just landed (404 text, blank, unstyled, stack traces, broken images).-pub fn render_check(cdp: &Cdp, sid: &str, http_status: Option<u64>) -> Value {-    let mut signals: Vec<String> = Vec::new();-    let mut out = json!({ "httpStatus": http_status, "ok": true });-    match cdp.evaluate(sid, RENDER_PROBE_JS, Duration::from_secs(6)) {-        Ok(p) if p.is_object() => {-            out["title"] = p["title"].clone(); out["bodyChars"] = p["bodyChars"].clone(); out["elements"] = p["elements"].clone();-            let head = p["head"].as_str().unwrap_or("").to_string(); let low = head.to_lowercase();-            let (chars, elements, sheets) = (p["bodyChars"].as_u64().unwrap_or(0), p["elements"].as_u64().unwrap_or(0), p["styleSheets"].as_u64().unwrap_or(0));-            if let Some(st) = http_status { if st >= 400 { signals.push(format!("http_{st}")); } }-            if low.contains("page not found") || low.contains("not found") || low.split(|c: char| !c.is_ascii_digit()).any(|w| w == "404") { signals.push("page_says_not_found".into()); }-            if low.contains("cannot get /") || low.contains("cannot post /") { signals.push("dev_server_cannot_get".into()); }-            if low.contains("internal server error") || low.contains("bad gateway") || low.contains("service unavailable") { signals.push("server_error_text".into()); }-            if low.contains("traceback (most recent call last)") || low.contains("unhandled exception") || low.contains("unhandled rejection") || low.contains("referenceerror") || low.contains("typeerror:") || low.contains("syntaxerror:") { signals.push("stack_trace_visible".into()); }-            if chars < 12 && elements < 8 { signals.push("effectively_blank".into()); }-            if chars > 40 && sheets == 0 { signals.push("no_stylesheets_loaded".into()); }-            if let Some(b) = p["brokenImgs"].as_u64() { if b > 0 { signals.push(format!("broken_images_{b}")); } }-            // pup-bridge#109: a chrome-error:// interstitial (net/TLS failure, or a bot-wall RST) must read as-            // BROKEN, not ok. It used to pass because the error page has a title and body text and no other-            // signal fired, so renderCheck told the caller the opposite of the truth.-            if p["errPage"].as_bool() == Some(true) {-                signals.push("chrome_error_page".into());-                let code = p["netError"].as_str().unwrap_or("").trim().to_string();-                if !code.is_empty() { signals.push(format!("net_error:{code}")); out["netError"] = json!(code); }-                out["fallbackHint"] = json!("This is Chrome's error page (a network/TLS failure or a bot-wall reset), NOT the site's content. Many electronics-vendor sites (st.com, mouser.com, digikey.com, arrow.com, analog.com) fingerprint pup's Chrome for Testing and block it, while the user's real Chrome loads them fine. Do not retry in pup: switch lanes. Open the page with nbrowser_open_window, or pull a file with nbrowser_fetch_url (the Adom browser extension drives the user's signed-in Chrome).");-            }-            if !signals.is_empty() { out["excerpt"] = json!(head.chars().take(180).collect::<String>()); }-        }-        Ok(_) => signals.push("probe_timed_out".into()),-        Err(_) => signals.push("probe_failed".into()),-    }-    out["ok"] = json!(signals.is_empty()); out["signals"] = json!(signals);-    out-}--/// Per-tab state the event pump maintains (keyed by CDP session id).-#[derive(Debug, Clone, Default)]-pub struct TabState { pub errors: Vec<Value>, pub dialog: Option<Value>, pub url: Option<String>, pub title: Option<String> }--pub const MAX_SCREENSHOT_DIM: u32 = 1568;   // Node: MAX_SCREENSHOT_DIM (Claude's optimal max)-const OFFSCREEN: i32 = -32000;              // Node: _OFFX/_OFFY birth position--/// A running browser pup is connected to (one per profile).-pub struct BrowserHandle { pub cdp: Cdp, pub launched: Launched }--pub struct Ctx {-    pub host: Arc<dyn Host>,-    pub ab: Arc<dyn AbClient>,-    pub settings: Arc<Settings>,-    pub installer: Arc<Installer>,-    pub sessions: Arc<Sessions>,-    pub browsers: Mutex<HashMap<String, Arc<BrowserHandle>>>,-    pub tabstate: events::TabStates,-    /// sid -> tab ids currently carrying focus emulation (2.0.459: was a Session field, and a session clone-    /// re-inserted by another path clobbered it, leaving the active tab unemulated after a re-attach).-    pub focus_emul: Mutex<HashMap<String, Vec<String>>>,-    /// CDP session -> a mouse button pressed whose release has not landed yet (2.0.460).-    pub pending_press: Mutex<HashMap<String, (f64, f64, String)>>,-    /// profile -> consecutive Browser.getVersion misses (2.0.461 wedged-browser watchdog).-    pub browser_stall: Mutex<HashMap<String, u32>>,-    /// profile -> when it last had zero pup windows (2.0.463 idle-browser reaper).-    pub browser_idle_since: Mutex<HashMap<String, u64>>,-    pub bridge_dir: PathBuf,-    pub port: u16,-    pub started: Instant,-    pub ad_version: Mutex<Option<String>>,-    pub self_ref: Mutex<std::sync::Weak<Ctx>>,-    pub toasts: Mutex<Vec<Value>>,-    pub vault: pup_vault::Vault,-    pub cards: pup_vault::Cards,-    pub consent: consent::Registry,-    pub tab_recs: record::TabRecs,-    pub screen_rec: record::ScreenRecState,-    pub highfps: highfps::Leases,-    pub last_audit: Mutex<Option<Value>>,-}--/// Callers that ARE the human at the desk (Node: USER_CALLERS): ownership never applies to them.-pub const USER_CALLERS: [&str; 4] = ["user-taskbar-menu", "user-toolbar", "user-pill", "user-dashboard"];-pub const SHOT_KEEP_DAYS: u64 = 14; pub const SHOT_KEEP_BYTES: u64 = 1_000_000_000;-pub const REC_KEEP_DAYS: u64 = 30; pub const REC_KEEP_BYTES: u64 = 3_000_000_000;--impl Ctx {-    pub fn arc(&self) -> Option<Arc<Ctx>> { self.self_ref.lock().unwrap().upgrade() }-    pub fn new_tabstate() -> events::TabStates { Arc::new(Mutex::new(HashMap::new())) }-    pub fn prefer_cft(&self) -> bool { self.settings.get("toolbarExtension", "on") != "off" }-    /// Screenshots: `<ADOM_BRIDGE_DATA_DIR>/screenshots` when ab passes the data dir (pup-bridge#98), else-    /// beside the exe as before. Retention: see `retention_sweep` (janitor).-    pub fn shots_dir(&self) -> PathBuf { static DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new(); let d = DIR.get_or_init(|| pup_core::data_subdir("screenshots", self.bridge_dir.join("screenshots"))).clone(); let _ = std::fs::create_dir_all(&d); d }-    pub fn recordings_dir(&self) -> PathBuf { static DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new(); let d = DIR.get_or_init(|| pup_core::data_subdir("recordings", self.bridge_dir.join("recordings"))).clone(); let _ = std::fs::create_dir_all(&d); d }-    /// One retention pass over screenshots (14 d / 1 GB) and recordings (30 d / 3 GB); returns the report.-    pub fn retention_sweep(&self) -> Value {-        let (sn, sb) = pup_core::retention_sweep(&self.shots_dir(), SHOT_KEEP_DAYS, SHOT_KEEP_BYTES);-        let (rn, rb) = pup_core::retention_sweep(&self.recordings_dir(), REC_KEEP_DAYS, REC_KEEP_BYTES);-        if sn + rn > 0 { pup_core::log(format!("[retention] screenshots: {sn} removed ({:.1} MB); recordings: {rn} removed ({:.1} MB)", sb as f64 / 1e6, rb as f64 / 1e6)); }-        json!({ "screenshots": { "removed": sn, "freedBytes": sb, "keepDays": SHOT_KEEP_DAYS, "keepBytes": SHOT_KEEP_BYTES }, "recordings": { "removed": rn, "freedBytes": rb, "keepDays": REC_KEEP_DAYS, "keepBytes": REC_KEEP_BYTES }, "ts": pup_core::now_ms() })-    }-    pub fn data_dirs(&self) -> Value {-        json!({ "dataDir": pup_core::data_dir().map(|p| p.to_string_lossy().replace('\\', "/")), "profiles": pup_browser::profiles_dir(&*self.host).to_string_lossy().replace('\\', "/"), "sessions": pup_browser::sessions_dir(&*self.host).to_string_lossy().replace('\\', "/"), "screenshots": self.shots_dir().to_string_lossy().replace('\\', "/"), "recordings": self.recordings_dir().to_string_lossy().replace('\\', "/") })-    }-}--/// The walls only a human can pass after a login submit (Node: detectHumanChallenge).-/// The login-wall detector (captcha / one-time code), one script for both lanes.-pub const WALL_JS: &str = r#"(() => { const q = (s) => { try { return document.querySelector(s); } catch (e) { return null; } }; const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };-      const otp = q('input[autocomplete="one-time-code"]') || q('input[name*="otp" i]') || q('input[id*="otp" i]') || q('input[name*="verification" i][inputmode="numeric"]') || q('input[name*="mfa" i]') || q('input[name*="totp" i]') || q('input[name*="2fa" i]');-      if (vis(otp)) return { kind: 'two_factor', evidence: 'one-time-code input: ' + (otp.getAttribute('name') || otp.getAttribute('id') || otp.getAttribute('autocomplete')) };-      const cap = Array.from(document.querySelectorAll('iframe')).find(f => /recaptcha|hcaptcha|turnstile|arkose|funcaptcha/i.test(f.src || '')); if (cap && vis(cap)) return { kind: 'captcha', evidence: 'captcha iframe: ' + cap.src.split('/')[2] };-      const t = (document.body && document.body.innerText || '').slice(0, 20000);-      if (/verification code|two[- ]factor|2-step verification|authenticator app|enter the code we sent|security code|one-time (pass)?code/i.test(t) && !q('input[type="password"]')) return { kind: 'two_factor', evidence: 'page text mentions a verification code and no password field is present' };-      if (/i'?m not a robot|prove you are human|verify you are human|security check/i.test(t)) return { kind: 'captcha', evidence: 'page text asks for a human check' };-      return null; })()"#;-pub fn detect_wall(cdp: &Cdp, sid: &str) -> Option<Value> {-    let js = WALL_JS;-    cdp.evaluate(sid, js, Duration::from_secs(4)).ok().filter(|v| v.is_object())-}--/// Route the events the pump cannot handle alone (bindings, screencast frames) to their owners.-pub fn install_event_hook(ctx: Arc<Ctx>) {-    events::set_hook(Box::new(move |method, sid, params| {-        match method {-            "Runtime.bindingCalled" => {-                let name = params["name"].as_str().unwrap_or(""); let payload = params["payload"].as_str().unwrap_or("").to_string();-                match name {-                    "__pupCredCapture" => vault::on_capture(&ctx, sid, &payload),-                    "__pupFloatyPush" => { if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.tabs.iter().any(|t| t.cdp_session == sid)) { let caller = Caller { thread: "user-floaty".into(), reason: "the user clicked the corner pencil".into(), ..Default::default() }; let tab = s.tabs.iter().find(|t| t.cdp_session == sid).map(|t| t.tab_id.clone()); pup_core::log(format!("[floaty] \"{}\" pencil clicked — arming capture", s.id)); let _ = annotate::start(&ctx, &caller, &json!({ "window": s.id, "tabId": tab, "tool": "pen", "takeover": true })); } }-                    _ => {}-                }-            }-            "Page.screencastFrame" => { if ctx.sessions.all().iter().all(|s| !s.tabs.iter().any(|t| t.cdp_session == sid)) { static N: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); if N.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 3 { pup_core::log(format!("[screencast] frame for a session no window owns: {sid} (known: {:?})", ctx.sessions.all().iter().flat_map(|s| s.tabs.iter().map(|t| t.cdp_session.clone())).collect::<Vec<_>>())); } }-                if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.tabs.iter().any(|t| t.cdp_session == sid)) { if let Some(b) = browser_for(&ctx, &s.profile) { record::on_frame(&ctx.tab_recs, &b.cdp, sid, params); } } }-            _ => {}-        }-    }));-}--/// The verbs this build serves natively.-pub const PHASE1_VERBS: &[&str] = &["pup_describe", "pup_readiness", "pup_prewarm", "pup_status", "pup_list_windows", "pup_my_windows", "pup_list_sessions", "pup_open_window", "pup_navigate", "pup_eval", "pup_evaluate", "pup_screenshot", "pup_close_window", "pup_close", "pup_close_all_windows", "pup_window_info",-    "pup_open_tab", "pup_list_tabs", "pup_switch_tab", "pup_close_tab", "pup_switch_window", "pup_click", "pup_select", "pup_hover", "pup_type", "pup_press_key", "pup_scroll", "pup_fill", "pup_wait", "pup_reload", "pup_back", "pup_forward", "pup_set_viewport", "pup_errors", "pup_dialog", "pup_input_dispatch", "pup_render_html", "pup_fetch_url", "pup_maximize",-    "pup_raise_os_window", "pup_focus_window", "pup_lower_os_window", "pup_alert_window", "pup_progress", "pup_configure", "pup_use", "pup_clear_cookies", "pup_clear_site_data", "pup_quit_idle_browsers", "pup_rescan", "pup_restart_browser", "pup_set_identity", "pup_audit_icons", "pup_dashboard",-    "pup_credentials", "credential_list", "credential_set", "credential_delete", "pup_login", "pup_google_accounts", "pup_google_signin", "pup_credential_reveal", "pup_card_set", "pup_cards", "pup_card_delete", "pup_fill_card", "pup_wiki_set_view", "pup_annotate", "pup_floaty_click",-    "pup_record_start", "pup_record_stop", "pup_record_status", "pup_record_list", "pup_recorder_open", "pup_recorder_close", "pup_screen_record_start", "pup_screen_record_stop", "pup_screen_record_status", "pup_screen_record_list", "pup_highfps_extend",-    "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_cache_prune", "pup_sweep_identities", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"];--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),-        "pup_prewarm" => prewarm(ctx, args),-        "pup_status" => pup_core::merge(status(ctx), json!({ "extensionLane": extdrive::lane_health(ctx) })),-        "pup_list_windows" => list_windows(ctx),-        "pup_my_windows" | "pup_list_sessions" => my_windows(ctx, caller),-        "pup_open_window" => with_bar(ctx, caller, args, "open", || open_window(ctx, caller, args)),-        "pup_navigate" => with_bar(ctx, caller, args, "navigate", || navigate(ctx, caller, args)),-        "pup_eval" | "pup_evaluate" => eval(ctx, caller, args),  // frame: handled inside eval-        "pup_screenshot" => { match resolve(ctx, caller, args) { Ok(s) => { let sid = s.id.clone(); activity::with_progress(ctx, &sid, "screenshot", || screenshot(ctx, caller, args)) } Err(_) => screenshot(ctx, caller, args) } },-        "pup_close_window" | "pup_close" => close_window(ctx, caller, args),-        // adom/pup-bridge#108 class: the verb existed in the contract, erred not_in_rust_yet and named no-        // supported equivalent. close_window already implements {all:true} = the caller's own windows.-        "pup_close_all_windows" => { let mut a = args.clone(); if let Some(m) = a.as_object_mut() { m.insert("all".into(), json!(true)); } close_window(ctx, caller, &a) }-        "pup_window_info" => window_info(ctx, caller, args),-        "pup_open_tab" => with_bar(ctx, caller, args, "open-tab", || tabs::open_tab(ctx, caller, args)),-        "pup_list_tabs" => tabs::list_tabs(ctx, caller, args),-        "pup_switch_tab" => tabs::switch_tab(ctx, caller, args),-        "pup_close_tab" => tabs::close_tab(ctx, caller, args),-        "pup_switch_window" => tabs::switch_window(ctx, caller, args),-        "pup_click" => input::click(ctx, caller, args),-        "pup_hover" => input::hover(ctx, caller, args),-        "pup_type" => input::type_text(ctx, caller, args),-        "pup_press_key" => input::press_key(ctx, caller, args),-        "pup_scroll" => input::scroll(ctx, caller, args),-        "pup_fill" => input::fill(ctx, caller, args),-        "pup_select" => input::select(ctx, caller, args),-        "pup_wait" => input::wait(ctx, caller, args),-        "pup_reload" => with_bar(ctx, caller, args, "navigate", || input::reload(ctx, caller, args)),-        "pup_back" => with_bar(ctx, caller, args, "navigate", || input::history(ctx, caller, args, true)),-        "pup_forward" => with_bar(ctx, caller, args, "navigate", || input::history(ctx, caller, args, false)),-        "pup_set_viewport" => input::set_viewport(ctx, caller, args),-        "pup_errors" => input::errors(ctx, caller, args),-        "pup_dialog" => input::dialog(ctx, caller, args),-        "pup_input_dispatch" => input::input_dispatch(ctx, caller, args),-        "pup_render_html" => input::render_html(ctx, caller, args),-        "pup_fetch_url" => input::fetch_url(ctx, caller, args),-        "pup_maximize" => input::maximize(ctx, caller, args),-        "pup_raise_os_window" | "pup_focus_window" => window::raise(ctx, caller, args),-        "pup_lower_os_window" => window::lower(ctx, caller, args),-        "pup_alert_window" => window::alert(ctx, caller, args),-        "pup_progress" => window::progress(ctx, caller, args),-        "pup_configure" => settings::configure(ctx, args),-        "pup_clear_cookies" => clear_cookies(ctx, caller, args),-        "pup_clear_site_data" => clear_site_data(ctx, caller, args),-        "pup_use" => settings::use_browser(ctx, args),-        "pup_quit_idle_browsers" => settings::quit_idle_browsers(ctx),-        "pup_rescan" => recovery::rescan(ctx, args),-        "pup_restart_browser" => extras::restart_browser(ctx, caller, args),-        "pup_set_identity" => identity::set_identity(ctx, caller, args),-        "pup_dashboard" => {-            let human = caller.thread == "user-taskbar-menu" || caller.thread == "user-dashboard";-            let url = format!("http://127.0.0.1:{}/dashboard", ctx.port);-            // The dashboard is a local page; it needs nothing from CDP. It opens like every other pup window: the-            // extension lane on adom-you (John, 2026-09-14: "why does pup_dashboard even need cdp?"). The Node-era-            // pup-dashboard profile is retired.-            let mut a = json!({ "window": "pup-dashboard", "url": url, "takeover": true });-            if human { a["foreground"] = json!(true); a["foregroundReason"] = json!("you clicked \"Open pup dashboard\" in the taskbar menu"); }-            let existed = ctx.sessions.exists("pup-dashboard");-            let r = if existed { navigate(ctx, caller, &a) } else { open_window(ctx, caller, &a) };-            if human { let _ = window::raise(ctx, caller, &json!({ "window": "pup-dashboard", "takeover": true, "foregroundReason": "you clicked \"Open pup dashboard\" in the taskbar menu" })); }-            pup_core::merge(r, json!({ "sessionId": "pup-dashboard", "dashboardUrl": format!("http://127.0.0.1:{}/dashboard", ctx.port), "reused": existed, "foregrounded": human, "_hint": if human { "The pup dashboard was raised because a human clicked the taskbar task." } else { "The pup dashboard is open in its own pup window (session pup-dashboard), BACKGROUNDED like any pup window; raise it with pup_raise_os_window {foregroundReason} if the user asked to see it." } }))-        }-        "pup_audit_icons" => identity::audit(ctx),-        "pup_credentials" | "credential_list" => vault::credentials(ctx),-        "credential_set" => vault::credential_set(ctx, args),-        "credential_delete" => vault::credential_delete(ctx, args),-        "pup_login" => vault::login(ctx, caller, args),-        "pup_credential_reveal" => vault::reveal(ctx, caller, args),-        "pup_card_set" => vault::card_set(ctx, args),-        "pup_cards" => vault::cards(ctx),-        "pup_card_delete" => vault::card_delete(ctx, args),-        "pup_fill_card" => vault::fill_card(ctx, caller, args),-        "pup_wiki_set_view" => wiki::set_view(ctx, caller, args),-        "pup_annotate" => annotate::start(ctx, caller, args),-        "pup_floaty_click" => annotate::floaty_click(ctx, caller, args),-        "pup_record_start" => record::start(ctx, caller, args),-        "pup_record_stop" => record::stop(ctx, caller, args),-        "pup_record_status" => record::status(ctx, caller, args),-        "pup_record_list" => record::list(ctx),-        "pup_recorder_open" => record::recorder_open(ctx, args),-        "pup_recorder_close" => record::recorder_close(ctx),-        "pup_screen_record_start" => record::screen_start(ctx, args),-        "pup_screen_record_stop" => record::screen_stop(ctx),-        "pup_screen_record_status" => record::screen_status(ctx),-        "pup_screen_record_list" => record::list(ctx),-        "pup_highfps_extend" => highfps::extend(ctx, caller, args),-        "pup_screenshot_element" => extras::screenshot_element(ctx, caller, args),-        "pup_screenshot_full_res" => extras::screenshot_full_res(ctx, caller, args),-        "pup_download" => extras::download(ctx, caller, args),-        "pup_launch" => extras::launch(ctx, caller, args),-        "pup_deps" => extras::deps(ctx),-        "pup_install_toolbar" => extras::install_toolbar(ctx),-        "pup_reap_profiles" => extras::reap_profiles(ctx, args),-        "pup_cache_prune" => extras::cache_prune(ctx, args.get("dryRun").and_then(|v| v.as_bool()).unwrap_or(false)),-        "pup_sweep_identities" => { let r = identity::sweep_dead_identities(ctx, "verb"); ok(pup_core::merge(r, json!({ "_hint": "Unregistered every pup taskbar identity (its registry key and any Start Menu anchor) that no live window holds, and removed icon generations nothing references. pup runs this a minute after every boot and releases each window's identity on close (2.0.666)." }))) },-        "pup_uninstall" => extras::uninstall(ctx, args),-        "pup_move_to_workspace" => extras::move_to_workspace(ctx, caller, args),-        "pup_assisted_click" => assist::assisted_click(ctx, caller, args),-        "pup_import_scan" => import::import_scan(ctx),-        "pup_google_accounts" => google::google_accounts(ctx, caller, args),-        "pup_google_signin" => google::google_signin(ctx, caller, args),-        "pup_import_browser_logins" => import::import_browser_logins(ctx, caller, args),-        "pup_vault_sync" => vault::vault_sync(ctx, caller, args),-        "pup_vault_export" => vault::vault_export(ctx, caller),-        "pup_vault_import" => vault::vault_import(ctx, caller, args),-        v if v.starts_with("desktop_record") || v.starts_with("desktop_recorder") => err("wrong_bridge", format!("{v} is ab-core's native desktop recorder (ab 2.1.36+), not a pup verb; this ab routed it to pup by a stale prefix claim (adom-bridge#150)"), Some("Update Adom Bridge so desktop_record_* routes to ab-core: its native recorder (GPU-accelerated, GPU frame offload when available, system audio, MP4) is the one to use for desktop recording. Until then pup's fallback works, a Google Meet style screen share that runs almost anywhere: pup_recorder_open {reason} then pup_screen_record_start {confirmDesktopNotTabRecording:true, reason}.")),-        v if v.starts_with("pup_") || v.starts_with("credential_") => err("not_in_rust_yet", format!("{v} is not served by the Rust bridge yet (Phase 1 carries {} verbs)", PHASE1_VERBS.len()), Some("This is the Rust port of pup (docs/PLAN-rust-port.md). The verb exists in the contract and lands in a later phase; the Node build still serves it on the public tier.")),-        v => err("unknown_verb", format!("unknown verb {v}"), Some("pup_describe lists every verb this bridge serves.")),-    };-    // 2.0.672 (pup-bridge#124): ab's screenshot delivery contract is fullPath (+ safePath, the <=1568 px copy). When a-    // reply carries those, ab's CLI pulls the image to the caller and adds localPath/localSafePath, so a container AI can-    // Read it without a second call. pup answered with filePath only, so nothing was pulled. Every image reply now-    // carries both keys (filePath stays for older callers). The image pup writes is already the shrunk copy when needed.-    image_delivery(&mut r);-    // lastAgentUpdate {tabId, verb, note, ts}: every driving verb that names a window (Node: touch sites)-    if !PROBE_VERBS.contains(&verb) && !PASSIVE_VERBS.contains(&verb) {-        if let Some(sid) = r.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()) {-            let tab = r.get("tabId").and_then(|v| v.as_str()).map(|s| s.to_string());-            ctx.sessions.with_mut(&sid, |x| { x.last_verb = Some(verb.to_string()); if tab.is_some() { x.last_tab = tab; } x.last_agent_update_ms = pup_core::now_ms(); });-        }-    }-    if let Some(o) = r.as_object_mut() {-        // 2.0.672: many handlers put their real fields only in the `output` JSON string. ab used to lift those onto-        // the reply; ab 2.1.14x stopped (measured 2026-09-22: "ok" gone from every error reply, and pup_record_stop's-        // durationMs became the 615 ms CALL time instead of the 8 s recording, failing the smoke). pup lifts them itself-        // now, never overwriting a top-level key, and BEFORE the call time is stamped.-        if let Some(inner) = o.get("output").and_then(|v| v.as_str()).filter(|s| s.starts_with('{')).and_then(|s| serde_json::from_str::<Value>(s).ok()) {-            if let Some(io) = inner.as_object() { for (k, v) in io { if !o.contains_key(k) { o.insert(k.clone(), v.clone()); } } }-        }-        // and every reply says ok true/false itself (the contract fixtures expect it; ab used to add it)-        if !o.contains_key("ok") {-            let failed = o.get("success") == Some(&json!(false)) || o.contains_key("errorCode") || o.get("error").and_then(|e| e.as_str()).map(|e| !e.is_empty()).unwrap_or(false);-            o.insert("ok".into(), json!(!failed));-        }-        o.entry("durationMs").or_insert(json!(t0.elapsed().as_millis() as u64)); o.insert("shell".into(), json!("rust"));-        // Node sendJSON: the catalog's canonical hint/related/next/pitfalls when the handler set none-        if let Some(m) = catalog_meta(verb) {-            if !o.contains_key("_hint") { if let Some(h) = m["hint"].as_str() { o.insert("_hint".into(), json!(h)); } }-            if !o.contains_key("related") { if m["related"].as_array().map(|a| !a.is_empty()).unwrap_or(false) { o.insert("related".into(), m["related"].clone()); } }-            if !o.contains_key("_next") { if m["related"].as_array().map(|a| !a.is_empty()).unwrap_or(false) { o.insert("_next".into(), m["related"].clone()); } }-            if !o.contains_key("pitfalls") { if m["pitfalls"].as_array().map(|a| !a.is_empty()).unwrap_or(false) { o.insert("pitfalls".into(), m["pitfalls"].clone()); } }-        }-        if !PROBE_VERBS.contains(&verb) && !o.contains_key("_cleanupReport") { if let Some(rep) = drain_cleanup_reports() { o.insert("_cleanupReport".into(), json!(rep)); } }-        // Node maybeAttachBugInvite: every failure carries the invite; substantive successes carry it once per process, then every 40th-        if ctx.settings.get("reportIssues", "on") != "off" && !o.contains_key("_reportIssues") {-            let is_error = o.get("success") == Some(&json!(false)) || o.contains_key("errorCode") || o.get("error").and_then(|e| e.as_str()).map(|e| !e.is_empty()).unwrap_or(false);-            if is_error { o.insert("_reportIssues".into(), json!(bug_report_invite(true))); }-            else if !PROBE_VERBS.contains(&verb) {-                let mut g = BUG_INVITE.lock().unwrap(); g.1 += 1;-                if !g.0 || g.1 % BUG_INVITE_EVERY == 0 { g.0 = true; o.insert("_reportIssues".into(), json!(bug_report_invite(false))); }-            }-        }-        // Session-scoped notices (Node: attachSessionNotices): an emulated viewport warning on-        // measurement verbs, and a one-shot disruption notice. Resolve the session from output or the envelope.-        {-            let sid = o.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string())-                .or_else(|| o.get("output").and_then(|v| v.as_str()).filter(|s| s.starts_with('{')).and_then(|s| serde_json::from_str::<Value>(s).ok()).and_then(|v| v["sessionId"].as_str().map(|s| s.to_string())));-            if let Some(sid) = sid { if let Some(sess) = ctx.sessions.get(&sid) {-                const VP_SENSITIVE: [&str; 8] = ["pup_screenshot", "pup_screenshot_full_res", "pup_eval", "pup_list_windows", "pup_my_windows", "pup_status", "pup_navigate", "pup_reload"];-                if VP_SENSITIVE.contains(&verb) && !o.contains_key("_viewportOverride") {-                    if let Some(ev) = &sess.emulated { let w = ev["width"].as_u64().unwrap_or(0); let h = ev["height"].as_u64().unwrap_or(0);-                        if w > 0 { let outer = sess.last_outer_width;-                            let shape = match outer { Some(ow) if w > ow + 4 => format!(" The emulation is WIDER than the OS window ({w} vs {ow}), so the user is seeing this page CROPPED: about {}px is off the right edge.", w - ow), Some(ow) if w + 4 < ow => format!(" The emulation is NARROWER than the OS window ({w} vs {ow}), so the user sees dead space beside the page."), _ => String::new() };-                            o.insert("_viewportOverride".into(), json!(format!("This window has an EMULATED VIEWPORT of {w}x{h}{}, so it is NOT tracking the real OS window.{shape} Anything you measure in the page (innerWidth, a bounding rect) describes the emulation, not what the user sees. Clear it with pup_set_viewport {{\"reset\":true}} when done.", ev["deviceScaleFactor"].as_f64().filter(|d| *d != 1.0).map(|d| format!(" @{d}x")).unwrap_or_default())));-                        }-                    }-                }-                if !o.contains_key("_sessionNotice") { if let Some(n) = &sess.disruption_notice { o.insert("_sessionNotice".into(), json!(n)); ctx.sessions.with_mut(&sid, |x| x.disruption_notice = None); } }-            } }-        }-        // render verbs: the verdict rides the response (Node: RENDER_VERBS block in sendJSON)-        if RENDER_VERBS.contains(&verb) && o.get("ok") != Some(&json!(false)) && o.get("success") != Some(&json!(false)) {-            if let Some(rc) = o.get("renderCheck").cloned() { if rc["ok"] == json!(false) { if let Some(sig) = rc["signals"].as_array() { if !sig.is_empty() {-                let st = rc["httpStatus"].as_u64().map(|s| format!(" (HTTP {s})")).unwrap_or_default();-                let lead = format!("â›” THE PAGE IS STILL BROKEN: {}{st}{}. Do NOT report success. See _verifyRender. ", sig.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(", "), rc["excerpt"].as_str().map(|e| format!(" — it says \"{}\"", e.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(120).collect::<String>())).unwrap_or_default());-                let cur = o.get("_hint").and_then(|h| h.as_str()).unwrap_or("").to_string();-                let fb = rc["fallbackHint"].as_str().map(|h| format!("{h} ")).unwrap_or_default();-                if !cur.contains("STILL BROKEN") && !cur.contains("IS BROKEN") { o.insert("_hint".into(), json!(format!("{lead}{fb}{cur}"))); }-            } } } }-            if !o.contains_key("_verifyRender") { o.insert("_verifyRender".into(), json!(VERIFY_RENDER)); }-            if !o.contains_key("_urlLint") {-                let u = o.get("url").and_then(|v| v.as_str()).or_else(|| args.get("url").and_then(|v| v.as_str())).unwrap_or("");-                if let Some(l) = lint::url_lint(u, &caller.container) { o.insert("_urlLint".into(), l["message"].clone()); o.insert("urlLintCode".into(), l["code"].clone()); }-            }-        }-        if let Some(sid) = o.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()) { if let Some(h) = ctx.sessions.with_mut(&sid, |x| x.pending_cred_hint.take()).flatten() { o.insert("_credentialCaptured".into(), json!(h)); } }-        // THE ab ENVELOPE (adom-bridge cli/src/relay.rs browser_command, measured at the 2.0.402 candidate-        // install): ab computes success = bridge.success (absent = false) and output = bridge.output (absent-        // = ""); the CLI then RETURNS THE PARSED `output` ALONE when success is true, and collapses a-        // non-JSON output to {success, output}. So every success carries success:true and an `output`-        // that is the JSON of the WHOLE payload, exactly as the Node build's sendJSON sites did.-        let is_err = o.get("success") == Some(&json!(false)) || o.get("ok") == Some(&json!(false)) || o.contains_key("errorCode");-        if !is_err {-            o.insert("success".into(), json!(true));-            if !o.get("output").and_then(|v| v.as_str()).map(|s| s.starts_with('{')).unwrap_or(false) {-                let mut inner = o.clone(); inner.remove("output");-                o.insert("output".into(), json!(Value::Object(inner).to_string()));-            }-        } else { o.entry("success").or_insert(json!(false)); o.entry("output").or_insert(json!("")); }-        // Node v1.9.98: ab/the CLI unwrap `output` and drop the envelope on success, so the advisory-        // fields have to live INSIDE output too (never overwriting what the handler put there).-        if let Some(inner_s) = o.get("output").and_then(|v| v.as_str()).filter(|s| s.starts_with('{')).map(|s| s.to_string()) {-            if let Ok(mut inner) = serde_json::from_str::<Value>(&inner_s) { if let Some(i) = inner.as_object_mut() {-                let sid = i.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()).or_else(|| o.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()));-                if let Some(sid) = sid { if !i.contains_key("window") { i.insert("window".into(), json!(sid)); if !i.contains_key("aiThread") { if let Some(owner) = ctx.sessions.get(&sid).and_then(|x| x.owner) { i.insert("aiThread".into(), json!(owner)); } } } }-                for k in ["_reportIssues", "_cleanupReport", "_autoLogin", "_credentialCaptured", "_hint", "_next", "related", "pitfalls", "renderCheck", "_verifyRender"] { if let Some(v) = o.get(k) { if !i.contains_key(k) { i.insert(k.into(), v.clone()); } } }-                o.insert("output".into(), json!(inner.to_string()));-            } }-        }-    }-    r-}--// ── resolution: window | sessionId | the caller's own window ───────────────────────────--fn window_arg(args: &Value) -> Option<String> {-    for k in ["window", "sessionId", "session", "ai-thread", "aiThread"] { if let Some(s) = args.get(k).and_then(|v| v.as_str()) { if !s.trim().is_empty() { return Some(if k == "ai-thread" || k == "aiThread" { thread_default_session_id(s) } else { s.trim().to_string() }); } } }-    None-}--/// The pup window and tab showing `url`: exact URL first, then equal ignoring the query, then the-/// longest prefix match ignoring the query (the popup's own rule). None when no pup tab shows it.-pub fn session_for_tab_url(ctx: &Ctx, url: &str) -> Option<(Session, Tab)> {-    let base = |u: &str| u.split('?').next().unwrap_or("").trim_end_matches('/').to_string();-    let want = base(url);-    // 2.0.471: a window whose browser is connected wins over one whose browser is gone, and among those the most-    // recently driven wins (a leftover session at the same page used to take the toolbar pen: annotate_inject_failed).-    // 2.0.472: the FOREGROUND window wins outright: a toolbar/jump-list click comes from the window the user is-    // looking at, and its payload carries only the tab URL (winvm ralph: the pen landed on a recovered background-    // window's tab at the same page and failed to inject there).-    let fg = ctx.host.foreground();-    let mut all = ctx.sessions.all();-    all.sort_by_key(|s| (!(fg != 0 && s.hwnd == Some(fg)), !transport::lane_for(ctx, s).is_ok(), std::cmp::Reverse(s.last_agent_update_ms)));-    for s in &all { if let Some(t) = s.tabs.iter().find(|t| t.url == url) { return Some((s.clone(), t.clone())); } }-    for s in &all { if let Some(t) = s.tabs.iter().find(|t| !want.is_empty() && base(&t.url) == want) { return Some((s.clone(), t.clone())); } }-    let mut best: Option<(usize, Session, Tab)> = None;-    for s in &all { for t in &s.tabs { let b = base(&t.url); if b.len() > 12 && want.starts_with(&b) && best.as_ref().map(|x| b.len() > x.0).unwrap_or(true) { best = Some((b.len(), s.clone(), t.clone())); } } }-    best.map(|(_, s, t)| (s, t))-}--/// 2.0.699: the taskbar progress bar on the window a page-loading verb works in. Only pup_screenshot was ever-/// wrapped here; the extension-lane wrapper sat on a route these verbs no longer take, so open, navigate, new-/// tab, reload, back and forward painted nothing (John, 2026-10-05). A window with no handle yet (being born)-/// gets no bar; start_icon_apply covers its first seconds.-fn with_bar(ctx: &Ctx, caller: &Caller, args: &Value, op: &str, work: impl FnOnce() -> Value) -> Value {-    let r = match resolve(ctx, caller, args) { Ok(s) => activity::with_progress(ctx, &s.id, op, work), Err(_) => work() };-    wall_check(ctx, caller, args, r)-}--/// 2.0.712: say when a page load landed on a bot wall, and warn before a thread trips one. A Cloudflare challenge-/// page used to come back as an ordinary successful load, so a thread kept loading DigiKey pages 7 s apart and dug-/// the wall deeper (John's ECAS window, 2026-10-11). Signatures come from the electronics-sites skillpack.-const BOT_WALL_JS: &str = r#"(()=>{const t=(document.title||'').slice(0,200);const b=((document.body&&document.body.innerText)||'').slice(0,3000);const short=b.length<1500;-if(/just a moment|performing security verification|checking your browser/i.test(t)||(short&&/performing security verification|verify you are human/i.test(b))||document.querySelector('iframe[src*="challenges.cloudflare.com"]'))return 'Cloudflare';-if(/access to this page has been denied/i.test(t+' '+b)||/press (&|and) hold/i.test(b)||document.querySelector('#px-captcha,[id^="px-captcha"]'))return 'PerimeterX';-if(/^access denied/i.test(t)&&short)return 'Akamai';-if(document.querySelector('iframe[src*="captcha-delivery.com"]'))return 'DataDome';-if(short&&/verify you are a human|are you a robot|unusual traffic/i.test(b))return 'bot check';-return ''})()"#;-const PACED_HOSTS: [&str; 8] = ["digikey.", "mouser.", "arrow.com", "ti.com", "analog.com", "st.com", "octopart.com", "newark.com"];-static LAST_LOAD: std::sync::Mutex<Option<HashMap<String, u64>>> = std::sync::Mutex::new(None);--fn wall_check(ctx: &Ctx, caller: &Caller, args: &Value, mut r: Value) -> Value {-    if r["ok"] == json!(false) || r["success"] == json!(false) { return r; }-    let Ok(s) = resolve(ctx, caller, args) else { return r };-    let Some(t) = s.active().cloned() else { return r };-    let host = t.url.split("//").nth(1).unwrap_or("").split('/').next().unwrap_or("").to_lowercase();-    // pacing: a second load on a walled vendor within 15 s, by any thread on this desktop-    if let Some(key) = PACED_HOSTS.iter().find(|h| host.contains(**h)) {-        let now = pup_core::now_ms();-        let mut g = LAST_LOAD.lock().unwrap(); let m = g.get_or_insert_with(HashMap::new);-        if let Some(prev) = m.insert(key.to_string(), now) {-            let gap = now.saturating_sub(prev);-            if gap < 15_000 { r["_paceWarning"] = json!(format!("{host} was loaded {} s ago. This site walls automated browsers that load pages in quick succession; wait at least 15 s between pages here, and get stock, price and datasheets through adom-parts-search instead of loading product pages.", gap / 1000)); }-        }-    }-    let Ok(lane) = transport::lane_for(ctx, &s) else { return r };-    let vendor = transport::evaluate(&lane, &t, BOT_WALL_JS).ok().and_then(|v| v.as_str().map(|x| x.to_string()).or_else(|| v["value"].as_str().map(|x| x.to_string()))).unwrap_or_default();-    if vendor.is_empty() { return r; }-    pup_core::log(format!("[wall] \"{}\": {host} answered with a {vendor} bot check ({})", s.id, t.url.chars().take(100).collect::<String>()));-    r["botWall"] = json!({ "vendor": vendor, "host": host, "url": t.url });-    r["_hint"] = json!(format!("BOT WALL: {host} answered with a {vendor} bot check instead of the page. Do NOT reload or load more pages from {host} now; that makes it worse. For parts data (stock, price, datasheet) use adom-parts-search. If you need this page itself: ask the user for ONE click on the check in this window (pup_alert_window or pup_raise_os_window with a reason), or open it in the user's own browser with nb. Never try to solve the check yourself."));-    r-}--pub fn resolve(ctx: &Ctx, caller: &Caller, args: &Value) -> Result<Session, Value> {-    let explicit = window_arg(args);-    let id = match explicit.clone() {-        Some(id) => id,-        None => {-            if caller.thread.is_empty() {-                return Err(err("caller_identity_required", "no window given and no --ai-thread to key one on", Some("Pass --ai-thread <your thread name> (the CLI flag) so pup can resolve your own window, or name one with {window:\"...\"}.")));-            }-            // 2.0.453 (John, woke from sleep, toolbar pen did nothing): the Adom toolbar identifies its window by-            // the TAB URL (`_tabUrl`, Node's contract); the Rust port never resolved it, so a pen click landed-            // here with thread "user-toolbar" and no window. Any tab in any pup window matches, exact first.-            // 2.0.702: the browser's own tab id first. A page that changes its address in place (Slack, Gmail, YouTube)-            // no longer matched pup's recorded URL, so the toolbar pen resolved no window and did nothing (John's Slack-            // window, 2026-10-07). The id never changes.-            if let Some(tid) = args.get("_tabId").and_then(|v| v.as_i64()).map(|v| v.to_string()) {-                if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.tabs.iter().any(|t| t.target_id == tid)) { return Ok(s); }-            }-            if let Some((s, _)) = args.get("_tabUrl").and_then(|u| u.as_str()).and_then(|u| session_for_tab_url(ctx, u)) { return Ok(s); }-            // last resort for a human click: the pup window in front is the one they clicked in-            if args.get("_tabUrl").is_some() || args.get("_tabId").is_some() {-                let fg = ctx.host.foreground();-                if fg != 0 { if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.hwnd == Some(fg)) { return Ok(s); } }-            }-            let d = thread_default_session_id(&caller.thread);-            if ctx.sessions.exists(&d) { d } else {-                let mine = ctx.sessions.owned_by(&caller.thread);-                if mine.len() == 1 { mine[0].id.clone() } else { return Err(err("no_window", format!("thread \"{}\" has no pup window", caller.thread), Some("pup_open_window {url} first; later verbs with no ids resolve to that window. pup_my_windows {} lists what you own."))); }-            }-        }-    };-    match ctx.sessions.get(&id) {-        Some(s) => {-            // Ownership guard (Node: ownershipRefusal): another thread's window needs takeover:true.-            // A HUMAN caller (jump-list task, toolbar button, dashboard click) is exempt, as in Node's-            // USER_CALLERS: the user owns every window. Missing in the Rust port until 2.0.424, so every-            // window-targeting jump-list task answered not_your_window (John, 2026-09-06).-            if let Some(owner) = s.owner.as_deref() { if !caller.thread.is_empty() && !USER_CALLERS.contains(&caller.thread.as_str()) && owner != caller.thread {-                if args.get("takeover").and_then(|t| t.as_bool()) != Some(true) {-                    return Err(pup_core::merge(err("not_your_window", format!("window \"{}\" belongs to thread \"{}\", not you (\"{}\")", id, owner, caller.thread),-                        Some("Open your OWN window instead: pup_open_window with NO window id gives your thread its own. Pass takeover:true ONLY if the USER explicitly told you to repurpose this window — every takeover is logged with both thread names.")), json!({ "owner": owner, "window": id })));-                }-                // 2.0.491: LOG every sanctioned takeover, both thread names (Node did this; the Rust port-                // dropped it, so a thread could drive another thread's window all day leaving no trace).-                pup_core::log(format!("[takeover] \"{}\": thread \"{}\" acted on \"{}\"'s window (takeover:true — sanctioned only when the user asked). Reason: {}",-                    id, caller.thread, owner, if caller.reason.is_empty() { "(none given)" } else { &caller.reason }));-            } }-            Ok(s)-        }-        None => Err(pup_core::merge(err("session_not_found", format!("no pup window named \"{id}\""), Some("pup_list_windows {} shows what is open; pup_open_window creates one.")), json!({ "window": id }))),-    }-}--pub fn touch(ctx: &Ctx, sid: &str, note: Option<&str>) { ctx.sessions.with_mut(sid, |x| { x.last_agent_update_ms = pup_core::now_ms(); if let Some(n) = note { x.last_note = Some(n.to_string()); } }); }--/// Close top-level about:blank page targets nobody tracks: the initial window an older pup build-/// launched Chrome with (it never got a session, an identity or a close), which shows up as a stock-/// Chrome for Testing button on the taskbar. Runs once per browser connect, BEFORE the handle is-/// published, so no verb can be creating a target in the gap. Popups (openerId) and every target a-/// session owns are never touched.-fn close_orphan_blank_targets(ctx: &Ctx, cdp: &Cdp, profile: &str) {-    // Orphans: (1) the about:blank launch window an older build left behind, and (2) whole WINDOWS no-    // session owns any more, left by a close that raced a bridge respawn (John's laptop, 2026-09-06:-    // three leftover windows wearing old tiles). A window is orphaned only when NONE of its tabs belongs-    // to a session, so a user's extra tab in a live pup window is never touched; popups never are.-    let Ok(targets) = cdp.targets() else { return };-    let owned: std::collections::HashSet<String> = ctx.sessions.all().iter().flat_map(|s| s.tabs.iter().map(|t| t.target_id.clone())).collect();-    let pages: Vec<&Value> = targets.iter().filter(|t| t["type"] == json!("page") && !t.get("openerId").and_then(|o| o.as_str()).map(|o| !o.is_empty()).unwrap_or(false)).collect();-    let mut win_of: HashMap<String, i64> = HashMap::new();-    for t in &pages { if let Some(tid) = t["targetId"].as_str() { if let Ok((wid, _)) = cdp.window_for_target(tid) { win_of.insert(tid.to_string(), wid); } } }-    let owned_windows: std::collections::HashSet<i64> = win_of.iter().filter(|(tid, _)| owned.contains(*tid)).map(|(_, w)| *w).collect();-    let mut closed = 0;-    for t in &pages {-        let tid = t["targetId"].as_str().unwrap_or(""); if tid.is_empty() || owned.contains(tid) { continue; }-        let blank = t["url"].as_str().map(|u| u == "about:blank" || u.is_empty()).unwrap_or(false);-        let orphan_window = win_of.get(tid).map(|w| !owned_windows.contains(w)).unwrap_or(false);-        if blank || orphan_window { if cdp.close_target(tid).is_ok() { closed += 1; pup_core::log(format!("[launch] closed orphan {} in profile \"{profile}\": {}", if blank { "about:blank window" } else { "window no session owns" }, t["url"].as_str().unwrap_or("").chars().take(60).collect::<String>())); } }-    }-    if closed > 0 { pup_core::log(format!("[launch] {closed} orphan window(s) closed in profile \"{profile}\"")); }-}--/// 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::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()) }--/// Get or launch the browser for a profile: reuse a live DevTools port, else spawn-verify each-/// candidate in order (Node: getOrLaunchBrowser).-fn get_or_launch(ctx: &Ctx, profile: &str, high_fps: bool, insecure_dev: bool) -> Result<Arc<BrowserHandle>, Value> {-    set_target_chrome_from_host(&*ctx.host);   // learn the installed consumer Chrome version once, before any attach-    Cdp::quiet_runtime(ctx.settings.get("cdpQuiet", "off") == "on");   // pup-bridge#109: skip the Runtime.enable leak-    pup_browser::set_stock_flags(ctx.settings.get("stockFlags", "off") == "on");   // pup-bridge#109: near-stock launch flags-    if let Some(b) = browser_for(ctx, profile) { return Ok(b); }-    if ext_lane_owns_profile(ctx, profile) {-        return Err(err("profile_owned_by_ext_lane",-            format!("the extension lane is running on profile \"{profile}\", and Chrome allows only one process per profile directory"),-            Some("This lane deliberately runs with NO debug port, which is what lets vendor sites load. Drive it with pup_open_window / pup_navigate / pup_eval, or close it with pup_close_window first if you specifically need CDP.")));-    }-    let profile_dir = pup_browser::profiles_dir(&*ctx.host).join(profile);-    if let Some(port) = pup_browser::existing_port(&profile_dir) {-        if let Ok(cdp) = Cdp::connect_port(port, Duration::from_secs(5)) {-            let kind = cdp.version().ok().and_then(|v| v.get("product").and_then(|p| p.as_str()).map(|s| s.to_string())).unwrap_or_default();-            let cand = pup_browser::launch_candidates(&*ctx.host, ctx.prefer_cft()).into_iter().find(|c| kind.contains("Chrome") && (c.kind == "chrome-for-testing" || c.kind == "chrome") || kind.contains("Edg") && c.kind == "edge").unwrap_or(Candidate { kind: "chrome".into(), source: "reconnect".into(), executable_path: PathBuf::new(), build_id: None });-            pup_core::log(format!("[launch] reconnected to a live {} on :{port} for profile \"{profile}\"", cand.kind));-            let pid = cdp.send(None, "SystemInfo.getProcessInfo", json!({}), Duration::from_secs(5)).ok().and_then(|v| v["processInfo"].as_array().and_then(|a| a.iter().find(|p| p["type"] == json!("browser")).and_then(|p| p["id"].as_u64()))).unwrap_or(0) as u32;-            let h = Arc::new(BrowserHandle { cdp, launched: Launched { pid, cdp_port: port, candidate: cand, profile_dir, reused: true } });-            if ctx.settings.get("prePermissions", "off") == "on" { grant_default_permissions(&h.cdp); }-            close_orphan_blank_targets(ctx, &h.cdp, profile);-            events::start_pump(profile.into(), h.cdp.clone(), ctx.sessions.clone(), ctx.tabstate.clone());-            ctx.host.watch_process(h.launched.pid);-            ctx.browsers.lock().unwrap().insert(profile.into(), h.clone());-            return Ok(h);-        }-    }-    let cands = pup_browser::launch_candidates(&*ctx.host, ctx.prefer_cft());-    if cands.is_empty() {-        let rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());-        if rd.get("lowDisk").and_then(|b| b.as_bool()) == Some(true) {-            return Err(pup_core::merge(err("chrome_install_no_disk", format!("No Chrome/Edge on this machine and only {} MB free — not enough to install Chrome for Testing (~800 MB needed).", rd.get("diskFreeMb").cloned().unwrap_or(Value::Null)), Some("Free up disk space, or install Chrome or Edge, then retry.")), json!({ "readiness": rd })));-        }-        ctx.installer.ensure_background(ctx.host.clone());-        return Err(pup_core::merge(err("chrome_for_testing_installing", "This box has no installed Chrome or Edge, so pup is fetching Chrome for Testing as the last-resort browser (~150 MB, one time).", Some("LAST-RESORT PROVISIONING (not an error): poll pup_readiness {} until ready:true (it shows percent + ETA), then call pup_open_window again.")), json!({ "installing": true, "stillRunning": true, "statusVerb": "pup_readiness", "readiness": rd })));-    }-    pup_core::log(format!("[launch] {} browser candidate(s): {}", cands.len(), cands.iter().map(|c| format!("{}({})", c.kind, c.source)).collect::<Vec<_>>().join(" -> ")));-    let mut errors = Vec::new();-    for c in &cands {-        let t0 = Instant::now();-        let exts = pup_browser::stage_extensions(&*ctx.host, ctx.port, profile, ctx.prefer_cft());-        match pup_browser::launch(&*ctx.host, c, &profile_dir, high_fps, insecure_dev, &exts) {-            Ok(l) => match Cdp::connect_port(l.cdp_port, Duration::from_secs(10)) {-                Ok(cdp) => {-                    pup_core::log(format!("[launch] {}({}) up on :{} pid {} in {}ms", c.kind, c.source, l.cdp_port, l.pid, t0.elapsed().as_millis()));-                    pup_browser::write_default(&*ctx.host, c, false);-                    let h = Arc::new(BrowserHandle { cdp, launched: l });-                    // pup-bridge#109: real Chrome pre-grants NOTHING (every permission answers "prompt"); pup used-                    // to grant 32 up front, and a bot manager reads Notification.permission=="granted" on a first-                    // visit as automation. prePermissions:on restores the blanket grant for a debug flow.-                    if ctx.settings.get("prePermissions", "off") == "on" { grant_default_permissions(&h.cdp); }-                    close_orphan_blank_targets(ctx, &h.cdp, profile);-                    events::start_pump(profile.into(), h.cdp.clone(), ctx.sessions.clone(), ctx.tabstate.clone());-                    ctx.host.watch_process(h.launched.pid);-                    ctx.browsers.lock().unwrap().insert(profile.into(), h.clone());-                    return Ok(h);-                }-                Err(e) => { errors.push(format!("{}: connect {e}", c.kind)); let _ = ctx.host.kill(l.pid); }-            },-            Err(e) => errors.push(format!("{}: {e}", c.kind)),-        }-        pup_core::log(format!("[launch] {} failed, falling through: {}", c.kind, errors.last().cloned().unwrap_or_default()));-    }-    Err(err("browser_launch_failed", format!("every browser candidate failed to launch: {}", errors.join(" | ")), Some("pup_readiness {} shows the candidates and the last install error; pup_use can pin a different browser.")))-}--/// Which tabs carry focus emulation (Emulation.setFocusEmulationEnabled). It is what lets pup drive a window-/// parked BEHIND the user's windows: the page keeps reporting focus, so CDP input lands and focus events fire.-/// Until 2.0.458 every attached tab carried it, and a tab that reports focus also reports VISIBLE, so Chrome-/// never throttled its timers and Memory Saver never discarded it. Measured on John's laptop, 2026-09-10: a-/// 21-tab pup window ran EVERY background tab's timers at full rate (60 setTimeout(16ms) fires per second where-/// a background tab should get about 1), held 2.2 GB that Chrome could not reclaim, and burned 5.2 % CPU against-/// 0.2 % for his other Chrome, while the machine sat at 260 MB free of 32 GB and scrolling stuttered.-/// Now: the window's ACTIVE tab (pup must be able to drive it), plus at most the one tab a verb is driving.-pub fn focus_emulate(ctx: &Ctx, sid: &str, extra: Option<&str>) {-    let Some(s) = ctx.sessions.get(sid) else { return };-    let Some(b) = browser_for(ctx, &s.profile) else { return };-    let mut want: Vec<String> = s.active().map(|t| t.tab_id.clone()).into_iter().collect();-    if let Some(x) = extra { if !want.iter().any(|w| w == x) { want.push(x.to_string()); } }-    let have = ctx.focus_emul.lock().unwrap().get(sid).cloned().unwrap_or_default();-    if want == have { return; }-    for t in &s.tabs {-        let on = want.iter().any(|w| *w == t.tab_id);-        if on != have.iter().any(|w| *w == t.tab_id) { b.cdp.set_focus_emulation(&t.cdp_session, on); }-    }-    ctx.focus_emul.lock().unwrap().insert(sid.to_string(), want);-}--/// Is this tab currently focus-emulated? (Reporting only; never perturbs the state it reports.)-pub fn focus_emulated(ctx: &Ctx, sid: &str, tab_id: &str) -> bool {-    ctx.focus_emul.lock().unwrap().get(sid).map(|v| v.iter().any(|w| w == tab_id)).unwrap_or(false)-}--/// Re-apply the focus-emulation policy to every session (after boot recovery or a reconnect).-pub fn focus_emulate_all(ctx: &Ctx) { for s in ctx.sessions.all() { focus_emulate(ctx, &s.id, None); } }--/// What this window costs and what the machine has left, reported to the CALLING AI on every open and every-/// survey. John, 2026-09-11, after a thread opened twenty wiki component pages at roughly 200 MB apiece and-/// put his laptop into thrashing: "if the ai knew how much ram it was using, that might have helped the ai-/// decide. and i suppose you should be automatically telling it what % of ram was even left on the box."-/// browserMb is the whole Chrome process tree for this profile (renderers and the GPU process included),-/// which is the number that actually moves when a thread opens a page; avgMbPerTab is that divided by the-/// tabs it is carrying, so a caller can price its next tab before opening it.-pub fn memory_report(ctx: &Ctx, profile: Option<&str>) -> Value {-    let (total, avail) = match ctx.host.memory_status() { Some(x) => x, None => return Value::Null };-    let free_pct = if total > 0 { (avail as f64 / total as f64 * 100.0).round() as u64 } else { 0 };-    let mut v = json!({ "boxTotalMb": total, "boxFreeMb": avail, "boxFreePct": free_pct });-    if let Some(p) = profile {-        if let Some(b) = browser_for(ctx, p) {-            if let Some(mb) = ctx.host.process_tree_rss_mb(b.launched.pid) {-                let tabs: usize = ctx.sessions.for_profile(p).iter().map(|s| s.tabs.len()).sum();-                v["browserMb"] = json!(mb);-                v["tabsOnThisBrowser"] = json!(tabs);-                if tabs > 0 { v["avgMbPerTab"] = json!(mb / tabs as u64); }-            }-        }-    }-    if v.get("tabsOnThisBrowser").and_then(|x| x.as_u64()) == Some(0) {-        let mins = ctx.settings.get("idleBrowserQuitMin", "10");-        v["idleBrowser"] = json!(true);-        v["idleNote"] = json!(if mins == "0" { "This browser has no pup windows and idleBrowserQuitMin is 0, so pup will keep it running.".to_string() }-                              else { format!("This browser has no pup windows: pup closes it after {mins} min idle and gives the RAM back. The next open relaunches it, still signed in.") });-    }-    let per = v.get("avgMbPerTab").and_then(|x| x.as_u64());-    v["_hint"] = json!(match (free_pct, per) {-        (f, Some(p)) if f < 10 => format!("Only {f}% of this machine's RAM is free and each pup tab here is costing about {p} MB. Do not open more pages until you close some: at this level Windows is paging to disk and the user's whole desktop stutters. pup_close_tab / pup_close_window on what you are done with."),-        (f, None) if f < 10 => format!("Only {f}% of this machine's RAM is free. Do not open more pages until you close some: at this level Windows is paging to disk and the user's whole desktop stutters."),-        (f, Some(p)) if f < 25 => format!("{f}% of this machine's RAM is free and each pup tab here is costing about {p} MB. Budget accordingly and close tabs you are done with; this is the user's own laptop."),-        (f, None) if f < 25 => format!("{f}% of this machine's RAM is free. Budget accordingly and close pages you are done with; this is the user's own laptop."),-        (f, Some(p)) => format!("{f}% of this machine's RAM is free; each pup tab here is costing about {p} MB. Close tabs you are done with so it stays that way."),-        (f, None) => format!("{f}% of this machine's RAM is free."),-    });-    v-}--pub fn attach_tab(s: &mut Session, cdp: &Cdp, target_id: &str) -> Result<String, Value> {-    // 2.0.455: idempotent. A target already in this session is returned as is (the rescan adopted one tab twice-    // because its "tracked" list was built once up front).-    if let Some(t) = s.tabs.iter().find(|t| t.target_id == target_id) { return Ok(t.tab_id.clone()); }-    let sid = cdp.attach(target_id).map_err(|e| err("cdp_attach_failed", e.to_string(), None))?;-    apply_consumer_identity(cdp, &sid);   // pup-bridge#109: consumer Google Chrome identity + EME shim, before any navigation-    // 2.0.455: the page-domain setup (Page.enable, the two bindings, the login-capture script) runs on a helper-    // thread. A tab Chrome DISCARDED (Memory Saver after a sleep) has no renderer, so each of those calls sat-    // out its full timeout and one adoption took 30 s (measured on AdomLapper, 2026-09-10). The tab is usable-    // for resolution and identity at once; the setup lands when the renderer answers.-    let (cdp2, sid2) = (cdp.clone(), sid.clone());-    std::thread::Builder::new().name("tab-setup".into()).spawn(move || {-        let _ = cdp2.page_enable(&sid2);-        for name in ["__pupCredCapture", "__pupFloatyPush"] { let _ = cdp2.send(Some(&sid2), "Runtime.addBinding", json!({ "name": name }), Duration::from_secs(5)); }-        let capture = vault::capture_login_script();-        let _ = cdp2.send(Some(&sid2), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": capture }), Duration::from_secs(5));-        let _ = cdp2.evaluate(&sid2, &capture, Duration::from_secs(5));-    }).ok();-    let tab_id = s.next_tab_id();-    s.tabs.push(Tab { tab_id: tab_id.clone(), target_id: target_id.into(), cdp_session: sid, url: "about:blank".into(), title: String::new(), error_count: 0, fav: None });-    s.active_tab = s.tabs.len() - 1;-    Ok(tab_id)-}--/// Per-page scripts that need settings (floaty pencil + hotkey, owner chip). Idempotent.-pub fn inject_page_scripts(ctx: &Ctx, s: &Session, cdp: &Cdp, sid: &str) {-    let floaty = annotate::floaty_script(ctx);-    let _ = cdp.send(Some(sid), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": floaty }), Duration::from_secs(5));-    let _ = cdp.evaluate(sid, &floaty, Duration::from_secs(5));-    if ctx.settings.get("ownerChip", "off") == "on" {   // 2.0.713: OFF by default (John, 2026-10-12: it sits on the page's own controls, and he never asked for it)-        let chip = OWNER_CHIP_JS.replace("__THREAD__", &serde_json::to_string(&s.owner.clone().unwrap_or_default()).unwrap()).replace("__PROFILE__", &serde_json::to_string(&s.profile).unwrap()).replace("__AVATAR__", &serde_json::to_string(&ctx.settings.get("identityAvatarB64", "")).unwrap()).replace("__IDENT__", &serde_json::to_string(&ctx.settings.get("identityName", "")).unwrap());-        let _ = cdp.send(Some(sid), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": chip }), Duration::from_secs(5));-        let _ = cdp.evaluate(sid, &chip, Duration::from_secs(5));-    }-}--/// 2.0.653: the extension-lane twin of inject_page_scripts (per page load; the smoke found no owner chip there).-pub fn inject_page_scripts_ext(ctx: &Ctx, s: &Session, lane: &transport::Lane, tab: &Tab) {-    if tab.url.is_empty() || tab.url.starts_with("about:") || tab.url.starts_with("chrome") { return; }   // nothing to decorate, evals forbidden there-    if !transport::wait_ready(lane, tab, &tab.url, Duration::from_secs(10)) { pup_core::log(format!("[page] \"{}\" page scripts skipped: the document was not ready within 10 s", s.id)); return; }-    let floaty = annotate::floaty_script(ctx);-    let _ = transport::evaluate(lane, tab, &floaty);-    if ctx.settings.get("ownerChip", "off") == "on" {   // 2.0.713: OFF by default (John, 2026-10-12: it sits on the page's own controls, and he never asked for it)-        let chip = OWNER_CHIP_JS.replace("__THREAD__", &serde_json::to_string(&s.owner.clone().unwrap_or_default()).unwrap()).replace("__PROFILE__", &serde_json::to_string(&s.profile).unwrap()).replace("__AVATAR__", &serde_json::to_string(&ctx.settings.get("identityAvatarB64", "")).unwrap()).replace("__IDENT__", &serde_json::to_string(&ctx.settings.get("identityName", "")).unwrap());-        let _ = transport::evaluate(lane, tab, &chip);-    }-}--pub fn refresh_tab(cdp: &Cdp, t: &mut Tab) {-    if let Ok(info) = cdp.target_info(&t.cdp_session) {-        if let Some(u) = info.get("url").and_then(|u| u.as_str()) { t.url = u.into(); }-        if let Some(ti) = info.get("title").and_then(|u| u.as_str()) { t.title = ti.into(); }-    }-}--// ── the verbs ──────────────────────────────────────────────────────────────────────────--fn image_delivery(r: &mut Value) {-    fn fix(v: &mut Value) {-        let Some(fp) = v.get("filePath").and_then(|x| x.as_str()).map(|x| x.to_string()) else { return };-        let low = fp.to_lowercase();-        if ![".png", ".jpg", ".jpeg", ".webp"].iter().any(|e| low.ends_with(e)) || v.get("fullPath").is_some() { return; }-        v["fullPath"] = json!(fp);-        let fits = |k: &str| v.get(k).and_then(|x| x.as_u64()).map(|n| n <= 1568).unwrap_or(false);-        if fits("width") && fits("height") { v["safePath"] = json!(fp); }-    }-    fix(r);-    // the same keys inside the JSON string many verbs also carry in `output`-    if let Some(o) = r.get("output").and_then(|x| x.as_str()).map(|x| x.to_string()) {-        if let Ok(mut ov) = serde_json::from_str::<Value>(&o) { if ov.is_object() { let before = ov.clone(); fix(&mut ov); if ov != before { r["output"] = json!(ov.to_string()); } } }-    }-}--fn describe(ctx: &Ctx) -> Value {-    let c = catalog();-    let body = json!({-        "success": true, "bridge": "puppeteer", "version": pup_core::version(), "shell": "rust",-        "progressContract": c["progressContract"], "verbs": c["verbs"], "verbCount": c["verbs"].as_array().map(|a| a.len()).unwrap_or(0), "notes": c["notes"], "statusVerb": "pup_readiness",-        "readiness": ctx.installer.readiness(&*ctx.host, ctx.prefer_cft()),-        "_hint": "This list — the bridge's verbs + schemas + hints, for AD's Verbs tab. Read it once to learn the surface. This build is pup as one Rust exe (spawn.kind exe): no Node, no npm, no postinstall; same contract as the Node build.",-        "_next": ["pup_status"], "related": ["pup_status"],-    });-    let mut r = body.clone(); r["output"] = json!(body.to_string()); r["error"] = json!(""); r["status"] = json!("ok"); r["ok"] = json!(true);-    r-}--fn active_rows(ctx: &Ctx) -> Vec<Value> {-    let rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());-    let mut rows = Vec::new();-    if rd.get("installing").and_then(|b| b.as_bool()) == Some(true) {-        if rd.get("installPhase").and_then(|p| p.as_str()) == Some("unpacking") {-            let total = rd.get("bytesTotal").and_then(|b| b.as_u64()).unwrap_or(0) as f64 * 2.3;-            let pct = if total > 0.0 { Some(((rd.get("unpackBytes").and_then(|b| b.as_u64()).unwrap_or(0) as f64 / total * 100.0).round() as u64).min(99)) } else { None };-            rows.push(json!({ "id": "cft-unpack", "verb": "pup_prewarm", "phase": "unpack", "stepLabel": format!("Unpacking Chrome for Testing{}", rd.get("unpackMethod").and_then(|m| m.as_str()).map(|m| format!(" ({m})")).unwrap_or_default()), "percent": pct, "bytes": rd.get("unpackBytes"), "elapsedSec": rd.get("unpackElapsedSec"), "estimatedSec": Value::Null, "etaSec": Value::Null, "confidence": "estimated", "startKind": "cold" }));-        } else {-            rows.push(json!({ "id": "cft-download", "verb": "pup_prewarm", "phase": "download", "stepLabel": "Downloading Chrome for Testing", "percent": rd.get("installProgressPct"), "elapsedSec": rd.get("downloadElapsedSec"), "estimatedSec": rd.get("downloadElapsedSec").and_then(|e| e.as_u64()).zip(rd.get("downloadEtaSec").and_then(|e| e.as_u64())).map(|(a, b)| a + b), "etaSec": rd.get("downloadEtaSec"), "confidence": if rd.get("downloadEtaSec").is_some_and(|v| !v.is_null()) { "measured" } else { "estimated" }, "startKind": "cold" }));-        }-    }-    rows.extend(ctx.consent.rows());-    rows.extend(extras::active_download_rows());   // pup-bridge#109: in-flight downloads are pollable here-    rows-}--fn readiness(ctx: &Ctx) -> Value {-    let mut rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());-    if let Some(o) = rd.as_object_mut() { if let Value::Object(cs) = pup_browser::chrome_stable::snapshot() { for (k, v) in cs { o.insert(k, v); } } }-    let ready = rd.get("ready").and_then(|b| b.as_bool()).unwrap_or(false);-    let installing = rd.get("installing").and_then(|b| b.as_bool()).unwrap_or(false);-    // 2.0.646 (pup-bridge#121): a lane browser nobody tracks (a pup restart lost its pid list) holds the profile-    // lock and makes every launch exit with "Opening in existing browser session". Say so here, with the pids.-    if !extdrive::alive(ctx) {-        let dir = pup_browser::profiles_dir(&*ctx.host).join("adom-you");-        let orphans = orphan_browser_pids(ctx, &dir);-        if !orphans.is_empty() {-            if let Some(o) = rd.as_object_mut() { o.insert("orphanedBrowser".into(), json!({ "pids": orphans, "profile": dir.to_string_lossy(), "_hint": "A pup browser on this profile is running but no live lane owns it (left over from a pup restart). It holds the profile lock, so a launch would exit at once. The next pup_open_window ends it and launches fresh; nothing to do by hand." })); }-        }-    }-    let hint = if ready { "A drivable browser exists: pup_open_window {url} will open in the background. chromeForTestingInstalled tells you whether the toolbar build is cached (pup fetches it in the background on every box)." }-        else if installing { "Chrome for Testing is downloading (active[] carries percent, elapsed and ETA). Poll this verb; do not start a second download. A system Chrome/Edge, if present, is drivable already." }-        else if rd.get("installPhase").and_then(|p| p.as_str()) == Some("failed") { "The Chrome for Testing install FAILED (lastError). Retry pup_prewarm once the cause (network/proxy, disk) is fixed; a system Chrome/Edge is drivable meanwhile." }-        else { "No browser yet: pup_prewarm {} starts the Chrome for Testing download; pup_open_window starts it too and answers chrome_for_testing_installing until it lands." };-    let mut r = pup_core::merge(ok(rd.clone()), json!({ "success": true, "statusVerb": "pup_readiness", "active": active_rows(ctx), "_hint": hint, "_next": ["pup_open_window", "pup_prewarm"], "error": "",-        // the Node build's real-Chrome installer phases (pup_use {browser:"chrome", install:true}); the Rust build has no stable-Chrome installer yet, so the phase is always idle-        "chromeStablePhase": rd.get("chromeStablePhase").cloned().unwrap_or(json!("idle")), "chromeStableAwaitingApproval": rd.get("chromeStableAwaitingApproval").cloned().unwrap_or(json!(false)), "chromeStableError": rd.get("chromeStableError").cloned().unwrap_or(Value::Null), "chromeStableNotifyChannel": Value::Null, "chromeStableNotifyPeers": Value::Null }));-    let mut inner = rd; if let Some(i) = inner.as_object_mut() { i.insert("active".into(), r["active"].clone()); i.insert("_hint".into(), json!(hint)); for k in ["chromeStablePhase", "chromeStableAwaitingApproval", "chromeStableError", "chromeStableNotifyChannel", "chromeStableNotifyPeers"] { i.insert(k.into(), r[k].clone()); } }-    r["output"] = json!(inner.to_string());-    r-}--fn prewarm(ctx: &Ctx, args: &Value) -> Value {-    let wait = args.get("wait").and_then(|b| b.as_bool()).unwrap_or(false);-    if wait { let done = ctx.installer.ensure_blocking(ctx.host.clone(), Duration::from_secs(600)); let rd = readiness(ctx); return pup_core::merge(rd, json!({ "waited": true, "installed": done })); }-    ctx.installer.ensure_background(ctx.host.clone());-    let rd = readiness(ctx);-    pup_core::merge(rd, json!({ "started": true, "_hint": "Chrome for Testing provisioning is running in the background (or already cached). Poll pup_readiness for percent + ETA; pass wait:true to block until it lands." }))-}--pub fn session_row(ctx: &Ctx, s: &Session) -> Value {-    let active_tab = s.active();-    json!({ "sessionId": s.id, "window": s.id, "transport": s.transport, "lane": if s.transport == "ext" { "extension (no debug port)" } else { "cdp" }, "url": active_tab.map(|t| t.url.clone()), "title": active_tab.map(|t| t.title.clone()), "active": ctx.sessions.active_id().as_deref() == Some(&s.id), "owner": s.owner, "aiThread": s.owner, "profile": s.profile, "tabCount": s.tabs.len(), "hwnd": s.hwnd, "foreground": s.foreground, "ageMinutes": s.age_minutes(), "emulatedViewport": s.emulated, "lastAgentUpdate": s.last_agent_update(), "lastForeground": s.last_foreground, "errorCount": s.tabs.iter().map(|t| ctx.tabstate.lock().unwrap().get(&t.cdp_session).map(|x| x.errors.len()).unwrap_or(0)).sum::<usize>(),-        "activeTabId": active_tab.map(|t| t.tab_id.clone()), "ownerSource": if s.owner.is_some() { if s.owner_from_caller { json!("caller-identity") } else { json!("declared") } } else { Value::Null },-        "overlay": s.overlay_desc.clone().unwrap_or_else(|| "none (not yet applied)".into()), "overlayLed": s.overlay_led, "overlayWhy": s.overlay_why, "flash": "none", "flashCount": s.flash_count, "progressBarCount": s.progress_count, "progressBarMs": s.progress_ms,-        "background": if s.foreground { "still-foreground" } else if s.hwnd.is_some() { "backgrounded" } else { "no-window" } })-}--fn status(ctx: &Ctx) -> Value {-    let list: Vec<Value> = ctx.sessions.all().iter().map(|s| session_row(ctx, s)).collect();-    ok(json!({-        "version": pup_core::version(), "shell": "rust", "uptimeSec": ctx.started.elapsed().as_secs(), "dataDirs": ctx.data_dirs(), "retention": { "screenshots": { "keepDays": SHOT_KEEP_DAYS, "keepBytes": SHOT_KEEP_BYTES }, "recordings": { "keepDays": REC_KEEP_DAYS, "keepBytes": REC_KEEP_BYTES } },-        "sessionCount": list.len(), "activeSession": ctx.sessions.active_id(), "sessions": list,-        "settings": { "taskbarMode": if ctx.settings.get("aumidIcons", "on") != "on" { "plain" } else if ctx.settings.get("threadTiles", "on") == "on" { "tiles" } else { "aumid" }, "aumidIcons": ctx.settings.get("aumidIcons", "on"), "threadTiles": ctx.settings.get("threadTiles", "on"), "overlayBadges": ctx.settings.get("overlayBadges", "on"), "overlaySize": ctx.settings.get("overlaySize", "75"), "jumpLists": ctx.settings.get("jumpLists", "on") },-        "win32": { "inProcess": true, "why": Value::Null }, "enumWindows": { "total": 0, "lastMin": 0 }, "win32Calls": {}, "log": [], "loops": pup_core::loops::report(), "pupshotUrl": Value::Null, "iconAudit": ctx.last_audit.lock().unwrap().clone(),-        "adVersion": ctx.ad_version.lock().unwrap().clone(),-        "memory": memory_report(ctx, Some("adom-you")),-    }))-}--fn list_windows(ctx: &Ctx) -> Value {-    let mut all = ctx.sessions.all();-    for s in all.iter_mut() { if let Some(b) = browser_for(ctx, &s.profile) { for t in s.tabs.iter_mut() { refresh_tab(&b.cdp, t); } ctx.sessions.with_mut(&s.id, |x| x.tabs = s.tabs.clone()); } }-    let rows: Vec<Value> = all.iter().map(|s| session_row(ctx, s)).collect();-    ok(json!({ "count": rows.len(), "sessions": rows, "activeSession": ctx.sessions.active_id(), "memory": memory_report(ctx, Some("adom-you")) }))-}--fn my_windows(ctx: &Ctx, caller: &Caller) -> Value {-    if caller.thread.is_empty() { return err("caller_identity_required", "pup_my_windows needs --ai-thread", Some("Pass --ai-thread <your thread name>; windows are keyed on it.")); }-    let mine: Vec<Value> = ctx.sessions.owned_by(&caller.thread).iter().map(|s| session_row(ctx, s)).collect();-    let default_id = thread_default_session_id(&caller.thread);-    let body = json!({ "ok": true, "aiThread": caller.thread, "ai-thread": caller.thread, "count": mine.len(), "windows": mine, "defaultWindow": default_id, "_hint": if mine.is_empty() { "You own no pup window yet: pup_open_window {url} creates yours (no ids needed), later verbs resolve to it." } else { "Verbs with no ids resolve to your window; name another with {window:\"<sessionId>\"}." }, "_next": ["pup_open_window", "pup_screenshot", "pup_navigate"] });-    let mut r = ok(body.clone()); r["success"] = json!(true); r["output"] = json!(body.to_string());-    r-}--/// The park geometry in CSS px (what Browser.setWindowBounds takes): Node v1.8.66 rule, "size is-/// measured, not guessed": screen.availLeft/Top/Width/Height from the page itself, with retries, then a-/// 10 px inset on every side. The Win32 work area is PHYSICAL px; feeding it to CDP on a 150% display-/// produced a window 1.5x the screen, hanging off the bottom-right (John's AdomLapper, 2026-09-06).-fn birth_rect_css(cdp: &Cdp, cdp_session: &str, ctx: &Ctx) -> (i32, i32, i32, i32) {-    for _ in 0..4 {-        if let Ok(v) = cdp.evaluate(cdp_session, "({x:screen.availLeft||0,y:screen.availTop||0,w:screen.availWidth,h:screen.availHeight})", Duration::from_secs(3)) {-            let (x, y, w, h) = (v["x"].as_f64().unwrap_or(0.0) as i32, v["y"].as_f64().unwrap_or(0.0) as i32, v["w"].as_f64().unwrap_or(0.0) as i32, v["h"].as_f64().unwrap_or(0.0) as i32);-            if w > 400 && h > 300 { return (x + 10, y + 10, w - 20, h - 20); }-        }-        std::thread::sleep(Duration::from_millis(300));-    }-    match ctx.host.work_area() { Some((x, y, w, h)) => { let sc = ctx.host.dpi_scale(0).max(1.0); ((x as f64 / sc) as i32 + 10, (y as f64 / sc) as i32 + 10, ((w as f64 / sc) as i32 - 20).max(400), ((h as f64 / sc) as i32 - 20).max(300)) }, None => (10, 10, 1280, 800) }-}--fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    let t0 = Instant::now();-    let url = args.get("url").and_then(|u| u.as_str()).map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).unwrap_or_else(|| "about:blank".into());-    // 2.0.648 (pup-bridge#112): isolated:true MINTS a throwaway profile. It used to bind adom-you and only pick the-    // anonymous tile, so a "throwaway" login persisted into the shared jar and an untrusted page ran on the user's-    // real cookies. The directory is removed when the window's browser is gone (close_window).-    let profile = match args.get("profile").and_then(|p| p.as_str()) {-        Some(p) => p.to_string(),-        None if args.get("isolated").and_then(|b| b.as_bool()) == Some(true) => format!("iso-{:x}{:x}", pup_core::now_ms() & 0xffffff, std::process::id() & 0xffff),-        None => "adom-you".to_string(),-    };-    if args.get("isolated").and_then(|b| b.as_bool()) == Some(true) && args.get("isolationReason").and_then(|r| r.as_str()).map(|r| r.len() >= 10) != Some(true) {-        return err("isolation_reason_required", "isolated:true needs an isolationReason (min 10 chars): why must this window NOT be adom-you?", Some("Surf as adom-you by default (its logins accumulate). Isolation is for logged-out tests, acting as a different user, or untrusted pages: say which."));-    }-    if args.get("foreground").and_then(|b| b.as_bool()) == Some(true) && args.get("foregroundReason").and_then(|r| r.as_str()).map(|r| r.len() >= 8) != Some(true) {-        return err("foreground_reason_required", "foreground:true needs a foregroundReason shown to the user on screen", Some("pup windows are born in the BACKGROUND. Only a demo / 'show me' earns the foreground, and the reason is captioned for the user."));-    }-    let sid = window_arg(args).unwrap_or_else(|| if caller.thread.is_empty() { format!("pup-{}", pup_core::now_ms() % 100000) } else { thread_default_session_id(&caller.thread) });-    // 2.0.476: a DELIBERATE open of this id (anything but the auto-recovery relaunch) clears a pending close-cancel,-    // so reopening a window shortly after closing it during a restore is honored rather than swallowed.-    if !caller.reason.starts_with("auto-recovery") { crate::recovery::clear_cancel(&sid); }-    let high_fps = args.get("highFps").and_then(|b| b.as_bool()).unwrap_or(false);-    let insecure = args.get("webSecurity").and_then(|b| b.as_bool()) == Some(false);-    // 2.0.525: the lane is chosen HERE, before any browser is launched. defaultLane is "ext": a plain-    // pup_open_window is an extension-driven window on adom-you (no debug port, John's default browsing-    // lane). lane:"cdp" is the explicit opt-in for what CDP alone can do (iframes, trusted input, annotate).-    // 2.0.563 (John): the extension lane IS pup. It is the lane that loads every vendor site, so it is the-    // default and the CDP lane opens ONLY on an explicit lane:"cdp". Nothing falls back to CDP silently any-    // more: a request that needs a CDP-only feature without asking for the lane is refused with the fix.-    let explicit_lane = args.get("lane").and_then(|l| l.as_str()).map(|x| x.to_string());-    let lane = explicit_lane.clone().unwrap_or_else(|| ctx.settings.get("defaultLane", "ext"));-    let isolated = args.get("isolated").and_then(|b| b.as_bool()) == Some(true);-    // 2.0.652 (John, 2026-09-19: "i want it almost NEVER used cuz then you may use it as a crutch instead of fixing-    // ext lane to solve all use cases"). lane:"cdp" is refused unless the user turned the CDP lane on for this box.-    if args.get("lane").and_then(|l| l.as_str()) == Some("cdp") && ctx.settings.get("cdpLane", "off") != "on" {-        pup_core::log(format!("[lane] refused: thread \"{}\" asked for lane:\"cdp\" and cdpLane is off (cdp_lane_disabled)", caller.thread));-        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" {-        // 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 {-            pup_core::log(format!("[lane] refused: thread \"{}\" passed a CDP-only option ({}) without lane:\"cdp\" (cdp_lane_required)", caller.thread, ["profile","isolated","highFps","webSecurity"].iter().filter(|k| args.get(**k).is_some()).cloned().collect::<Vec<_>>().join(",")));-            return err("cdp_lane_required", format!("This open needs the CDP lane because of {why}, and the CDP lane opens only when asked for."),-                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()) });-        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-    // birth taking adom-you from a CDP browser. It closes the ext windows, which is why lane:cdp is an-    // explicit opt-in and the default stays ext.-    if profile == "adom-you" && ext_lane_owns_profile(ctx, &profile) {-        let ext_ids: Vec<String> = ctx.sessions.all().into_iter().filter(|x| transport::is_ext(x)).map(|x| x.id).collect();-        if !ext_ids.is_empty() {-            pup_core::log(format!("[lane] lane:cdp requested on adom-you; handing the profile back from the extension lane (closing {} ext window(s))", ext_ids.len()));-            for id in &ext_ids { ctx.sessions.remove(id); ctx.sessions.delete_file(id); }-        }-        let _ = crate::extdrive::close(ctx, caller, &json!({ "all": true }));-        kill_extdrive(ctx);-        let dir = pup_browser::profiles_dir(&*ctx.host).join(&profile);-        let lock = dir.join("SingletonLock");-        let t0 = Instant::now(); while t0.elapsed() < Duration::from_secs(8) { if !lock.exists() { break; } std::thread::sleep(Duration::from_millis(200)); }-    }-    let b = match get_or_launch(ctx, &profile, high_fps, insecure) { Ok(b) => b, Err(e) => return e };--    // An existing window of that name: later opens become TABS in it (the Node contract).-    if let Some(existing) = ctx.sessions.get(&sid) {-        if existing.profile == profile {-            let tid = match b.cdp.create_target(&url, false, None, None) { Ok(t) => t, Err(e) => return err("cdp_create_target_failed", e.to_string(), None) };-            let mut s = existing.clone();-            let tab_id = match attach_tab(&mut s, &b.cdp, &tid) { Ok(t) => t, Err(e) => return e };-            if let Some(cs) = s.tabs.last().map(|t| t.cdp_session.clone()) { inject_page_scripts(ctx, &s, &b.cdp, &cs); }-            if let Some(t) = s.tabs.last_mut() { let _ = b.cdp.navigate(&t.cdp_session, &url, Duration::from_secs(15)); refresh_tab(&b.cdp, t); }-            let title = s.active().map(|t| t.title.clone()).unwrap_or_default();-            let tab_count = s.tabs.len();-            ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);-            ctx.sessions.with_mut(&sid, |x| { if let Some(i) = x.tabs.iter().position(|tt| tt.tab_id == tab_id) { x.active_tab = i; } });   // Chrome shows the new tab in front; the badge follows it-            focus_emulate(ctx, &sid, None);-            if let Some(a) = ctx.arc() { identity::schedule(a.clone(), sid.clone(), caller.clone(), false); identity::overlay_retries(a, sid.clone(), caller.clone()); }-            return ok(json!({ "sessionId": sid, "window": sid, "aiThread": s.owner, "url": url, "title": title, "tabId": tab_id, "activeTabId": tab_id, "tabCount": tab_count, "reused": true, "active": true,-                "background": "backgrounded", "windowEtiquette": "background", "placement": "background", "browser": b.launched.candidate.to_json(), "profile": profile, "owner": s.owner, "ownerSource": "session",-                "progress": { "phase": "done", "elapsedMs": t0.elapsed().as_millis() as u64, "startKind": "warm" },-                "memory": memory_report(ctx, Some(&profile)),-                "_hint": "This window already existed for your thread, so the URL opened as a NEW TAB in it (pup_list_tabs / pup_switch_tab drive tabs). Screenshot to verify the render.", "_verifyRender": VERIFY_RENDER, "_next": ["pup_screenshot", "pup_navigate", "pup_eval"] }));-        }-    }--    // Node: an explicit `owner` declares the owning thread (ownerSource "declared"); else the caller's thread.-    // The Rust port ignored `owner` until 2.0.438, so a window recreated on another thread's behalf wore the-    // recreating thread's tile (John's AdomLapper proof run, 2026-09-06).-    let declared_owner = args.get("owner").and_then(|o| o.as_str()).map(|o| o.trim().to_string()).filter(|o| !o.is_empty());-    let owner: Option<&str> = declared_owner.as_deref().or(if caller.thread.is_empty() { None } else { Some(caller.thread.as_str()) });-    // R4 / pup-window-targeting rule 2: the set-diff is only exact while exactly ONE pup birth is in flight.-    let birth_guard = BIRTH_LOCK.lock().unwrap_or_else(|e| e.into_inner());-    let taken = taken_hwnds(ctx, &sid);-    let before: std::collections::HashSet<i64> = ctx.host.frame_windows_all(Some("Chrome_WidgetWin_1")).into_iter().map(|w| w.hwnd).collect();-    let prev_fg = ctx.host.foreground();   // Node v1.9.253: measure whether creation steals focus, and hand it straight back-    // 2.0.446 (John: "i would expect the icon to come up the first time with the ai-thread name and then it NEVER-    // has to swap icons again", and "i thought you said you could make it such that the cft icon never actually-    // showed first?"): the shell creates a window's button at its first show and bakes the icon then, so the-    // identity has to be ON the window before Chrome shows it. Chrome shows the window inside Target.createTarget,-    // so a watcher thread starts BEFORE the call, polls every top-level frame (visible or not) every millisecond,-    // and stamps the newborn natively (property store + window icon, no ab round-trip) the instant it exists.-    // The tile is rendered and the AUMID registered ahead of time, so the button's picture resolves on creation.-    let plan_name = owner.map(|o| o.to_string()).unwrap_or_else(|| sid.clone());-    let (plan, _early_ok, watcher) = birth_watch(ctx, caller, &sid, &plan_name, &url, &before, &taken, false);-    // 2.0.450 (John: "i would like a setting in the pup settings dialog to turn this flash feature on or off"):-    // winAttentionFlash off (default) = focus:false, Chrome never asks for the foreground, no attention state;-    // on = Chrome's default ask, and Windows lights the button when it refuses (the classic new-window flash).-    let attention = ctx.settings.get("winAttentionFlash", "off") == "on";-    let target = match b.cdp.create_target("about:blank", true, Some((OFFSCREEN, OFFSCREEN, 1280, 800)), if attention { None } else { Some(false) }) { Ok(t) => t, Err(e) => return err("cdp_create_target_failed", format!("Target.createTarget: {e}"), Some("The browser is up but refused a new window; pup_readiness shows its state, pup_close {} restarts it.")) };-    let mut s = Session::new(&sid, &profile, owner, Some(&caller.container), b.launched.cdp_port, if b.launched.pid > 0 { Some(b.launched.pid) } else { None });-    s.owner_from_caller = s.owner.as_deref() == Some(caller.thread.as_str()) && !caller.thread.is_empty() && args.get("owner").is_none();-    let tab_id = match attach_tab(&mut s, &b.cdp, &target) { Ok(t) => t, Err(e) => return e };-    let (bl, bt, bw, bh) = s.tabs.last().map(|t| birth_rect_css(&b.cdp, &t.cdp_session, ctx)).unwrap_or((10, 10, 1280, 800));   // CSS px, measured from the page-    // park: on-screen rect via CDP (moves without z-order change), then bottom + tag via the host-    let mut hwnd: Option<i64> = None;-    // Node v2.0.276: born off-screen so Chrome never activates it, moved on-screen within the 50 ms budget by-    // one local CDP call that changes no z-order. Leaving it off-screen longer (2.0.429 to 2.0.433 candidates)-    // made Chrome fire its activation on the first real paint, which Windows denies and tints. Move NOW.-    if let Ok((wid, _)) = b.cdp.window_for_target(&target) { s.window_id = Some(wid); let _ = b.cdp.set_window_bounds(wid, json!({ "left": bl, "top": bt, "width": bw, "height": bh })); }-    let mut early_stamped = false;-    if let Some((h, stamped)) = birth_report(watcher, &sid) { hwnd = Some(h); early_stamped = stamped; }-    if hwnd.is_none() { for _ in 0..200 {-        let now = ctx.host.windows_for_pids(&[], Some("Chrome_WidgetWin_1"));-        if let Some(w) = now.iter().find(|w| !before.contains(&w.hwnd) && !taken.contains(&w.hwnd)) { hwnd = Some(w.hwnd); break; }-        std::thread::sleep(Duration::from_millis(15));-    } }-    drop(birth_guard);   // hwnd resolved: the next birth may start-    let mut parked = false;-    if let Some(h) = hwnd {-        // John (2026-09-06): "i would expect the icon to come up the first time with the ai-thread name-        // and then it NEVER has to swap icons again." Windows bakes a button's icon when the button is-        // created, and creates it when the window is shown. So: HIDE the newborn at once (its button-        // goes away), render the thread tile + register the AUMID + stamp + jump list on the hidden-        // window, then show it once, no-activate, at z-bottom. One button, born with the tile.-        // John (2026-09-06): "i would expect the icon to come up the first time with the ai-thread name-        // and then it NEVER has to swap icons again." Windows bakes a button's icon when it creates the-        // button. So: take the newborn's button away through the shell's own ITaskbarList::DeleteTab-        // (the window stays visible, so Chrome queues no activation and no attention tint can follow),-        // render the thread tile + register the AUMID + stamp + jump list, then AddTab: the shell creates-        // the button ONCE, already wearing the tile. Hiding the window instead (2.0.429 to 2.0.432-        // candidates) made Chrome fire its first-show activation on the re-show, which Windows denies-        // and tints; measured, so never hide a newborn.-        // 2.0.446: an early-stamped newborn keeps its button (it was created wearing the tile); only the late path untabs.-        if !early_stamped { let _ = ctx.host.taskbar_tab(h, false); }-        // Node v1.9.253: when the user's foreground window belongs to pup's own Chrome (they are using a-        // pup window), Windows lets Chrome activate the newborn, so creation steals their focus. Measured-        // 2026-09-06 (foreground 1901802 -> the newborn). Hand it straight back before anything else.-        if prev_fg != 0 && prev_fg != h && ctx.host.foreground() == h {-            let _ = ctx.ab.call("desktop_bring_to_front", json!({ "hwnd": prev_fg }), caller, Duration::from_secs(4));-            pup_core::log(format!("[window] \"{sid}\" creation took the foreground from {prev_fg}; handed it back ({})", if ctx.host.foreground() == prev_fg { "ok" } else { "still ours" }));-        }-        s.hwnd = Some(h);-        ctx.host.tag(h, &s.tag_string());-        ctx.sessions.insert(s.clone());-        // Park FIRST (Node: the unconditional launch park), so the newborn never sits on top while we work.-        parked = ctx.host.send_to_bottom_no_activate(h);-        let _ = ctx.ab.call("desktop_set_window_state", json!({ "hwnd": h, "state": "bottom", "force": true }), caller, Duration::from_secs(5));-        activity::mark_icon_applied(ctx, &sid);-        activity::on_open(ctx, &sid, h, caller);-    }-    if let Some(cs) = s.tabs.last().map(|t| t.cdp_session.clone()) { inject_page_scripts(ctx, &s, &b.cdp, &cs); }-    let loaded = if let Some(t) = s.tabs.last_mut() { let r = b.cdp.navigate(&t.cdp_session, &url, Duration::from_secs(20)).map(|(_, l)| l).unwrap_or(false); refresh_tab(&b.cdp, t); r } else { false };-    let (auto_login_note, wiki_extra) = match s.tabs.last() { Some(t) => (vault::autofill_on_load(ctx, &s, &t.cdp_session, &url), wiki::after_navigate(ctx, &s, &b.cdp, &t.cdp_session, &url, args["wikiView"].as_str(), args["isolated"].as_bool().unwrap_or(false))), None => (None, json!({})) };-    ctx.sessions.insert(s.clone());-    let _ = park::bottom_unless_foreground(ctx, &sid, true);   // Node reassertBottomAfterNav: the first cross-origin paint can raise it; inside the birth grace it is bottomed-    if let Some(h) = hwnd {-        // Measured 2026-09-06: Windows puts the attention flag on the BUTTON that exists when Chrome's-        // first real paint (this navigation) tries to activate the window and is denied. No button existed-        // (DeleteTab at birth), so nothing was flagged. Now stamp the thread identity and let the shell-        // create the one button this window will ever have, wearing the tile.-        // Node v1.9.342: place AGAIN once the window has settled. Chrome cascades a new window from the-        // last one's position after our first move (measured on AdomLapper: requested 10,10 CSS, landed-        // 120,120 CSS, overhanging a 150% screen), and re-reads the screen the window actually sits on.-        if let (Some(t), Ok((wid, _))) = (s.tabs.last(), b.cdp.window_for_target(&target)) {-            let (l2, t2, w2, h2) = birth_rect_css(&b.cdp, &t.cdp_session, ctx);-            let _ = b.cdp.set_window_bounds(wid, json!({ "windowState": "normal" }));-            let _ = b.cdp.set_window_bounds(wid, json!({ "left": l2, "top": t2, "width": w2, "height": h2 }));-            if let Ok(v) = b.cdp.send(None, "Browser.getWindowBounds", json!({ "windowId": wid }), Duration::from_secs(3)) { pup_core::log(format!("[window] \"{sid}\" placed at {},{} {}x{} CSS (asked {l2},{t2} {w2}x{h2})", v["bounds"]["left"], v["bounds"]["top"], v["bounds"]["width"], v["bounds"]["height"])); }-        }-        let t_id = Instant::now();-        if early_stamped { if let Some((a, _, _)) = &plan { ctx.sessions.with_mut(&sid, |x| x.app_id = Some(a.clone())); } }-        identity::stamp_opts(ctx, &sid, caller, true, false);   // same AUMID again (no button rebuild, measured) + the jump list-        if early_stamped {-            ctx.host.send_to_bottom_no_activate(h);-            pup_core::log(format!("[window] \"{sid}\" jump list committed on the early-stamped button in {}ms (no DeleteTab/AddTab)", t_id.elapsed().as_millis()));-        } else {-            let _ = ctx.host.taskbar_tab(h, false);-            let retabbed = ctx.host.taskbar_tab(h, true);-            ctx.host.send_to_bottom_no_activate(h);-            pup_core::log(format!("[window] \"{sid}\" identity stamped after the first paint, one button created in {}ms (AddTab={retabbed})", t_id.elapsed().as_millis()));-        }-    }-    let title = s.active().map(|t| t.title.clone()).unwrap_or_default();-    ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);-    focus_emulate(ctx, &sid, None);   // 2.0.458: the newborn's active tab, and only it-    if let Some(a) = ctx.arc() { if hwnd.is_none() { identity::schedule(a.clone(), sid.clone(), caller.clone(), true); } identity::overlay_retries(a, sid.clone(), caller.clone()); }   // identity was stamped while hidden; the favicon lands late, repaint at 1.5 s and 4.5 s-    if args.get("foreground").and_then(|b| b.as_bool()) == Some(true) {-        // Node: a foreground open is a background birth followed by a GRANTED raise (caption + toast).-        let _ = window::raise(ctx, caller, &json!({ "window": sid, "foregroundReason": args["foregroundReason"] }));-    } else { ctx.sessions.with_mut(&sid, |x| x.fg_watch_until_ms = pup_core::now_ms() + 20_000); }  // Node: the no-fault fg watch — caption if the window fronts itself in the next 20s-    pup_core::log(format!("[window] \"{sid}\" opened for \"{}\" on {}({}) hwnd={:?} parked={parked} loaded={loaded} in {}ms", caller.thread, b.launched.candidate.kind, b.launched.candidate.source, hwnd, t0.elapsed().as_millis()));-    let cands = pup_browser::launch_candidates(&*ctx.host, ctx.prefer_cft());-    let strict = args["strictPermissions"].as_bool() == Some(true);-    let render_check_val = match s.tabs.last() { Some(t) if loaded => render_check(&b.cdp, &t.cdp_session, None), _ => Value::Null };-    let mut resp = ok(json!({-        "sessionId": sid, "window": sid, "aiThread": s.owner, "url": url, "title": title, "tabId": tab_id, "activeTabId": tab_id, "tabCount": 1, "reused": false, "active": true, "loaded": loaded,-        "background": "backgrounded", "windowEtiquette": "background", "placement": "background", "hwnd": hwnd, "parked": parked,-        "browser": b.launched.candidate.to_json(), "availableBrowsers": cands.iter().map(|c| format!("{}({})", c.kind, c.source)).collect::<Vec<_>>(),-        "defaultBrowser": pup_browser::read_default(&*ctx.host).map(|d| json!({ "kind": d.get("kind"), "source": d.get("source"), "forced": d.get("forced").and_then(|f| f.as_bool()).unwrap_or(false), "pendingInstall": false })),-        "profile": profile, "owner": s.owner, "ownerSource": if s.owner.is_some() { if s.owner_from_caller { json!("caller-identity") } else { json!("declared") } } else { Value::Null }, "ageMinutes": 0, "highFps": high_fps, "emulatedViewport": Value::Null, "errorCount": 0,-        "defaultPermissionsApplied": !strict, "defaultPermissionsCount": if strict { 0 } else { DEFAULT_PERMISSIONS.len() }, "defaultPermissionsNote": if strict { "strictPermissions: nothing pre-granted" } else { "full browser permissions pre-granted (clipboard, camera, geolocation, window-management, ...) — pup_describe has the exact list" },-        "downloadsEnabled": !strict, "downloadPath": ctx.host.home().join("Downloads").to_string_lossy().replace('\\', "/"), "flash": "none", "flashCount": 0, "lastAgentUpdate": Value::Null, "lastForeground": Value::Null, "overlay": "none (not yet applied)", "progressBarCount": 0, "progressBarMs": 0, "restoredTabsClosed": 0,-        "renderCheck": render_check_val,-        "size": { "width": bw, "height": bh }, "progress": { "phase": "done", "elapsedMs": t0.elapsed().as_millis() as u64, "startKind": if b.launched.reused { "warm" } else { "cold" } },-        "memory": memory_report(ctx, Some(&profile)),-        "_verifyRender": VERIFY_RENDER, "_next": ["pup_screenshot", "pup_navigate", "pup_eval", "pup_close_window"],-        "_hint": "Opened in the BACKGROUND (behind the user's windows, did NOT steal focus). Fully drivable while hidden: pup_screenshot / pup_eval / pup_navigate resolve to this window with no ids. Only bring it to the user's screen if they explicitly want to watch.",-    }));-    if b.launched.candidate.kind != "chrome-for-testing" { resp["_toolbarNotice"] = json!(format!("This window launched on {}: the Adom toolbar (pen/dots/identity tiles) rides ONLY Chrome for Testing, so it has NO toolbar. pup_readiness shows why that browser was picked.", b.launched.candidate.kind)); }-    if let Some(n) = auto_login_note { resp["_autoLogin"] = json!(n); }-    resp = pup_core::merge(resp, wiki_extra);-    if args["highFps"].as_bool() == Some(true) { highfps::grant(ctx, &sid, args["highFpsMinutes"].as_u64().unwrap_or(highfps::DEFAULT_MIN)); resp["highFps"] = highfps::info(ctx, &sid); }-    if hwnd.is_none() { resp["_windowNotice"] = json!("pup could not resolve the OS window handle for this window (no Chrome_WidgetWin_1 appeared), so it was not parked or tagged; it may be on top. This is the Phase 1 Rust bridge; report it with the box name."); }-    resp-}--pub const VERIFY_RENDER: &str = "VERIFY WHAT YOU JUST PUT ON SCREEN — do not assume it rendered. ok:true means the NAVIGATION succeeded; it says NOTHING about whether the page actually painted. Take a screenshot and READ the image: pup_screenshot {} returns a real PNG path. Look for a blank page, an unstyled flash of plain HTML, a framework error overlay, missing images, a dev-server 'Cannot GET /' page, or the wrong build. If it is wrong: fix the source, reload, screenshot AGAIN.";--fn navigate(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };-    let url = match args.get("url").and_then(|u| u.as_str()).map(|u| u.trim()).filter(|u| !u.is_empty()) { Some(u) => u.to_string(), None => return err("url_required", "pup_navigate needs {url}", None) };-    if transport::is_ext(&s) { return navigate_ext(ctx, caller, &s, &url); }-    let b = match browser_for(ctx, &s.profile) { Some(b) => b, None => return err("browser_gone", "the browser behind this window is not connected", Some("pup_close_window then pup_open_window; Phase 2 brings automatic recovery.")) };-    let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };-    match b.cdp.navigate(&tab.cdp_session, &url, Duration::from_secs(25)) {-        Ok((_, loaded)) => {-            let mut t = tab.clone(); refresh_tab(&b.cdp, &mut t);-            ctx.sessions.with_mut(&s.id, |x| { if let Some(tt) = x.tabs.iter_mut().find(|tt| tt.tab_id == t.tab_id) { *tt = t.clone(); } x.last_agent_update_ms = pup_core::now_ms(); });-            let _ = park::bottom_unless_foreground(ctx, &s.id, true);   // never yank a window the user is using (Node v2.0.26)-            if let Some(ss) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&ss); }-            if let Some(a) = ctx.arc() { identity::schedule(a.clone(), s.id.clone(), caller.clone(), false); identity::overlay_retries(a, s.id.clone(), caller.clone()); }-            let note = vault::autofill_on_load(ctx, &s, &t.cdp_session, &t.url);-            let wiki_extra = wiki::after_navigate(ctx, &s, &b.cdp, &t.cdp_session, &t.url, args["wikiView"].as_str(), false);-            let mut r = ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "loaded": loaded, "renderCheck": if loaded { render_check(&b.cdp, &t.cdp_session, None) } else { Value::Null }, "_verifyRender": VERIFY_RENDER, "_hint": if loaded { "Navigation finished (load event). Verify with a screenshot before asserting success." } else { "Page.navigate returned but the load event did not fire within 25s: the page is still moving (SPA, slow site). Screenshot to see where it is." }, "_next": ["pup_screenshot", "pup_eval"] }));-            if let Some(n) = note { r["_autoLogin"] = json!(n); }-            pup_core::merge(r, wiki_extra)-        }-        Err(e) => err("navigate_failed", e.to_string(), Some("Check the URL; a container-local localhost is not reachable from the desktop (use the proxy URL).")),-    }-}--/// The extension-lane arm of pup_navigate: same session bookkeeping, driven through the transport.--/// 2.0.642: where the page actually LANDED, classified, so an AI never asks the user to sign in to a site the-/// shared profile is already signed into. John, 2026-09-19: another thread opened claude.ai, which went straight-/// to his chats (the adom-you profile had the session), and it toasted "Sign into claude.ai in the pup window-/// once" anyway. The open/navigate response now says whether the final page is a sign-in page.-pub fn landed(url: &str, title: &str) -> Value { landed_with(url, title, None) }--/// 2.0.643: the DOM probe behind `landed`. (login_offered, login_form): a visible "Log in"/"Sign in" link or-/// button means the profile is NOT signed in here even when the URL looks like an app page (chatgpt.com/codex-/// is a marketing page with a Log in link when signed out, and the URL rule alone called it signed in); a-/// password field, or an email field next to a Continue/Next button, means the page IS a sign-in form.-pub fn landed_ext(ctx: &Ctx, s: &Session, tab: &pup_session::Tab, url: &str, title: &str) -> Value {-    if let Ok(lane) = transport::lane_for(ctx, s) { let _ = transport::wait_ready(&lane, tab, url, Duration::from_secs(8)); }-    const PROBE: &str = "(()=>{try{const vis=e=>{const r=e.getBoundingClientRect();return r.width>0&&r.height>0};const txt=e=>((e.innerText||e.value||e.getAttribute('aria-label')||'')+'').trim().toLowerCase().replace(/\\s+/g,' ');const els=[...document.querySelectorAll('a,button,input[type=submit],[role=button]')].filter(vis);const offered=els.some(e=>/^(log ?in|sign ?in|log ?in with .*|sign ?in with .*)$/.test(txt(e)));const pw=!!document.querySelector('input[type=password]');const em=!!document.querySelector('input[type=email],input[name*=email i],input[autocomplete=username],input[autocomplete=email]');const cont=els.some(e=>/^(continue|next|log ?in|sign ?in|continue with .*)$/.test(txt(e)));return JSON.stringify({offered,form:pw||(em&&cont)})}catch(e){return JSON.stringify({err:String(e)})}})()";-    let probe = transport::lane_for(ctx, s).ok().and_then(|lane| transport::evaluate(&lane, tab, PROBE).ok()).and_then(|v| {-        let txt = v.as_str().map(|x| x.to_string()).or_else(|| v.get("result").and_then(|r| r.as_str()).map(|x| x.to_string()))?;-        let j: Value = serde_json::from_str(&txt).ok()?;-        Some((j["offered"].as_bool().unwrap_or(false), j["form"].as_bool().unwrap_or(false)))-    });-    landed_with(url, title, probe)-}--pub fn landed_with(url: &str, title: &str, probe: Option<(bool, bool)>) -> Value {-    let u = url.to_ascii_lowercase(); let t = title.to_ascii_lowercase();-    let host = u.split("//").nth(1).unwrap_or("").split('/').next().unwrap_or("").to_string();-    let path = u.split("//").nth(1).unwrap_or("").splitn(2, '/').nth(1).map(|p| format!("/{p}")).unwrap_or_else(|| "/".into());-    let known_signin = (host.ends_with("claude.ai") && (path.starts_with("/login") || path.starts_with("/magic-link") || path.starts_with("/oauth")))-        || ((host.ends_with("chatgpt.com") || host.ends_with("openai.com")) && (path.contains("/auth/") || path.starts_with("/log-in") || path.starts_with("/login") || host.starts_with("auth.")))-        || host == "accounts.google.com" || host.ends_with("login.microsoftonline.com") || host.ends_with("appleid.apple.com");-    let generic = ["/login", "/signin", "/sign-in", "/sign_in", "/auth", "/sso", "/oauth", "/authorize", "/session/new", "/account/login", "/users/sign_in"].iter().any(|p| path.starts_with(p) || path.contains(&format!("{p}?")) || path.contains(&format!("{p}/")))-        || ["sign in", "log in", "login", "signin"].iter().any(|k| t.starts_with(k) || t.contains(&format!(" {k}")) && t.len() < 40);-    let (offered, form) = probe.unwrap_or((false, false));-    let login_page = known_signin || generic || form;-    let signed_in_known = !login_page && !offered && (-        (host.ends_with("claude.ai") && (path.starts_with("/new") || path.starts_with("/chat") || path.starts_with("/project") || path.starts_with("/settings") || path == "/"))-        || (host.ends_with("chatgpt.com") && (path.starts_with("/c/") || path.starts_with("/g/") || (path.starts_with("/codex") && probe.is_some()))));-    let hint = if !login_page && offered {-        format!("The page landed on {url} and shows a Log in / Sign in link: the shared adom-you profile is NOT signed in here. Follow login-recipes/<domain>.md on the pup-bridge wiki page (Google sign-in via pup_google_signin, or the vault via pup_login) before involving the user.")-    } else if login_page {-        format!("The page landed on a SIGN-IN page ({url}). Before asking the user for anything: pup_credentials for a saved login (pup_login fills it), pup_google_accounts + pup_google_signin if the site offers Google sign-in, and login-recipes/<domain>.md on the pup-bridge wiki page. Toast the user ONLY if pup cannot finish the sign-in itself.")-    } else if signed_in_known {-        format!("The page landed on {url}: the shared adom-you profile is ALREADY signed in here. Do not ask the user to sign in and do not toast about signing in.")-    } else {-        format!("The page landed on {url}. It is not a sign-in page. Do not ask the user to sign in unless the page itself asks.")-    };-    json!({ "url": url, "title": title, "loginPage": login_page, "loginOffered": offered, "signedIn": if signed_in_known { json!(true) } else if login_page || offered { json!(false) } else { Value::Null }, "probed": probe.is_some(), "_hint": hint })-}-fn navigate_ext(ctx: &Ctx, caller: &Caller, s: &Session, url: &str) -> Value {-    let lane = match transport::lane_for(ctx, s) { Ok(l) => l, Err(e) => return e };-    let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };-    let (u, title, loaded) = match transport::navigate(&lane, &tab, url) { Ok(x) => x, Err(e) => return e };-    ctx.sessions.with_mut(&s.id, |x| { if let Some(tt) = x.tabs.iter_mut().find(|tt| tt.tab_id == tab.tab_id) { tt.url = u.clone(); tt.title = title.clone(); } x.last_agent_update_ms = pup_core::now_ms(); });-    if let Some(ss) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&ss); }-    // 2.0.621: NO attention clear here. ab implements desktop_clear_window_attention by RAISING the window-    // (adom-bridge#199, measured z16 -> z1 on every call), and it ran after EVERY navigate, which is the-    // "navigate puts the window on top" that flashed pup at John through every Google sign-in. Nothing needs-    // clearing after a navigate; the birth tint is cleared once, at birth.-    identity::overlay(ctx, &s.id, caller);-    // 2.0.607 (John, 2026-09-17: "why did you just shove a pup window to the foreground? that was rude").-    // An AI navigating a window the user never asked to see must leave it where it was: BEHIND. Measured on-    // his laptop, a background window came forward on the navigate, 21 s before the toast that explained it.-    // Narrow on purpose: never when the window is foreground, and never when he has touched it recently-    // (park::user_is_using), so this can never pull away a window HE raised from a toast.-    // 2.0.629: force, so a navigation inside the birth grace that Chrome ACTIVATED is still bottomed under the-    // idle-hands rule; outside the grace, or with any input since birth, the primitive stands down as before.-    let _ = park::bottom_unless_foreground(ctx, &s.id, true);-    // 2.0.653: what the CDP path always did after a navigate, now on this lane too: page scripts (owner chip,-    // floaty pencil) and the wiki view probe with the desktop-token auto-sign.-    let tab_now = ctx.sessions.get(&s.id).and_then(|x| x.tab_by_id(&tab.tab_id).cloned()).unwrap_or_else(|| tab.clone());-    inject_page_scripts_ext(ctx, s, &lane, &tab_now);-    let wiki_extra = wiki::after_navigate_ext(ctx, s, &lane, &tab_now, &u, None, false);-    // 2.0.644 (pup-bridge#119): a cancelled HTTP auth challenge is reported as a typed error, not a blank page.-    if let Some((host, answered)) = transport::ext_id(&tab).as_i64().and_then(vault::unanswered_challenge) {-        if !answered { return pup_core::merge(vault::no_credential_error(&host, false), json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "transport": "ext" })); }-    }-    pup_core::merge(ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "loaded": loaded, "transport": "ext", "landed": landed_ext(ctx, s, &tab, &u, &title) })), wiki_extra)-}--fn eval(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    let expr = ["expr", "expression", "script", "js", "code"].iter().find_map(|k| args.get(*k).and_then(|v| v.as_str())).map(|s| s.to_string());-    let Some(expr) = expr else { return err("eval_expr_missing", "Missing required parameter \"expr\" (aliases: expression, script, js, code)", Some("pup_eval {expr:\"document.title\"}")) };-    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };-    if transport::is_ext(&s) {-        if args.get("frame").and_then(|f| f.as_str()).filter(|f| !f.is_empty()).is_some() { return err("frame_unsupported_ext", "frame targeting needs the CDP lane", Some("Open the window with lane:\"cdp\" for iframe evaluation.")); }-        let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };-        let tab = match args.get("tabId").and_then(|t| t.as_str()).and_then(|id| s.tab_by_id(id)).or(s.active()) { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };-        return match transport::evaluate(&lane, &tab, &expr) { Ok(v) => ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "result": v })), Err(e) => e };-    }-    let b = match browser_for(ctx, &s.profile) { Some(b) => b, None => return err("browser_gone", "the browser behind this window is not connected", None) };-    let tab = match args.get("tabId").and_then(|t| t.as_str()).and_then(|id| s.tab_by_id(id)).or(s.active()) { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };-    // frame: run inside an IFRAME by name, URL substring, or numeric index (login forms usually live in one). Node: the args.frame block.-    let eval_result = if let Some(fq) = args.get("frame").and_then(|f| f.as_str()).filter(|f| !f.is_empty()) {-        let frames = b.cdp.frames(&tab.cdp_session).unwrap_or_default();-        let pick = fq.parse::<usize>().ok().and_then(|i| frames.get(i).cloned())-            .or_else(|| frames.iter().find(|f| f["name"].as_str() == Some(fq)).cloned())-            .or_else(|| frames.iter().find(|f| f["url"].as_str().map(|u| u.contains(fq)).unwrap_or(false)).cloned());-        match pick {-            None => {-                let avail: Vec<String> = frames.iter().enumerate().map(|(i, f)| format!("[{i}] name={} url={}", f["name"].as_str().filter(|n| !n.is_empty()).unwrap_or("(none)"), f["url"].as_str().unwrap_or("").chars().take(80).collect::<String>())).collect();-                return pup_core::merge(err("frame_not_found", format!("No frame matched \"{fq}\" in window \"{}\".", s.id), Some("frame matches by frame NAME, then by URL substring, then by numeric index. An iframe's id attribute is NOT its frame name.")), json!({ "frames": avail }));-            }-            Some(f) => {-                let fid = f["id"].as_str().unwrap_or("");-                match b.cdp.frame_context(&tab.cdp_session, fid) { Ok(ctxid) => b.cdp.evaluate_in_context(&tab.cdp_session, ctxid, &expr, Duration::from_secs(20)), Err(e) => return err("frame_context_failed", e.to_string(), None) }-            }-        }-    } else { b.cdp.evaluate(&tab.cdp_session, &expr, Duration::from_secs(20)) };-    match eval_result {-        Ok(v) if v.get("__error").is_some() => err("eval_failed", format!("pup_eval could not run your code: {}", v["__error"].as_str().unwrap_or("evaluation threw")), Some("Expression and statement forms both run (devtools-console semantics). If this names a detached frame or closed tab, re-list windows.")),-        Ok(v) => ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "result": v })),-        Err(e) => err("eval_failed", e.to_string(), None),-    }-}--fn screenshot(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };-    if transport::is_ext(&s) {-        let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };-        let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };-        let png = match transport::screenshot_png(&lane, &tab, false) { Ok(p) => p, Err(e) => return e };-        let max_w = args.get("maxWidth").and_then(|m| m.as_u64()).map(|m| m as u32).unwrap_or(MAX_SCREENSHOT_DIM).min(MAX_SCREENSHOT_DIM);-        let (buf, w, h, ow, oh, resized) = match image::fit_png(&png, max_w) { Ok(r) => r, Err(e) => return err("screenshot_decode_failed", e, None) };-        let path = ctx.shots_dir().join(format!("pup-{}.png", pup_core::now_ms()));-        if let Err(e) = std::fs::write(&path, &buf) { return err("screenshot_write_failed", e.to_string(), None); }-        return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "success": true, "captured": json!({ "url": tab.url, "title": tab.title, "tabId": tab.tab_id, "windowId": s.window_id, "hwnd": s.hwnd, "via": "tab" }), "filePath": path.to_string_lossy().replace('\\', "/"), "width": w, "height": h, "origWidth": ow, "origHeight": oh, "resized": resized, "transport": "ext", "_hint": "captureVisibleTab: the VISIBLE area of the active tab. READ the file before describing it." }));-    }-    let b = match browser_for(ctx, &s.profile) { Some(b) => b, None => return err("browser_gone", "the browser behind this window is not connected", None) };-    let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };-    let full = args.get("fullPage").and_then(|b| b.as_bool()).unwrap_or(false);-    let png = match b.cdp.screenshot_png(&tab.cdp_session, full, Duration::from_secs(20)) {-        Ok(p) => p,-        Err(e) => {-            // OS fallback (Node 2.0.38x): ab captures the window even when the renderer is stuck.-            if let Some(h) = s.hwnd {-                // 2.0.649 (pup-bridge#106): the OS capture is by hwnd, the only path that can hand back another window's-                // document. Check the OS title against the tab before capturing; a mismatch is a typed error, not an image.-                let os_title = ctx.host.frame_windows_all(None).into_iter().find(|w| w.hwnd == h).map(|w| w.title).unwrap_or_default();-                let want = tab.title.trim();-                if !want.is_empty() && !os_title.contains(want) { return pup_core::merge(err("capture_identity_mismatch", format!("window {h} is titled {os_title:?} but this tab is {want:?}: not capturing another window's document"), Some("pup_window_info shows the session's hwnd and tabs; pup_list_windows shows every window. Re-open the page or pass tabId explicitly.")), json!({ "sessionId": s.id, "hwnd": h, "osTitle": os_title, "tab": { "url": tab.url, "title": tab.title, "tabId": tab.tab_id } })); }-                if let Ok(r) = ctx.ab.call("desktop_screenshot_window", json!({ "hwnd": h }), caller, Duration::from_secs(15)) { return pup_core::merge(ok(r), json!({ "capturedVia": "os", "captured": { "url": tab.url, "title": tab.title, "tabId": tab.tab_id, "windowId": s.window_id, "hwnd": h, "via": "os-window", "osTitle": os_title }, "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "_hint": "CDP timed out; this is an OS window capture (window chrome included)." })); } }-            return err("screenshot_timeout", format!("page.screenshot: {e}"), Some("The page did not answer the capture. Reload it, or pup_eval document.readyState to see whether the renderer is alive."));-        }-    };-    let max_w = args.get("maxWidth").and_then(|m| m.as_u64()).map(|m| m as u32).unwrap_or(MAX_SCREENSHOT_DIM).min(MAX_SCREENSHOT_DIM);-    let (buf, w, h, ow, oh, resized) = match image::fit_png(&png, max_w) { Ok(r) => r, Err(e) => return err("screenshot_decode_failed", e, None) };-    let path = ctx.shots_dir().join(format!("pup-{}.png", pup_core::now_ms()));-    if let Err(e) = std::fs::write(&path, &buf) { return err("screenshot_write_failed", e.to_string(), None); }-    ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "success": true, "captured": json!({ "url": tab.url, "title": tab.title, "tabId": tab.tab_id, "windowId": s.window_id, "hwnd": s.hwnd, "via": "tab" }), "filePath": path.to_string_lossy().replace('\\', "/"), "width": w, "height": h, "origWidth": ow, "origHeight": oh, "resized": resized, "sizeKB": buf.len() / 1024, "fullPage": full, "capturedVia": "cdp", "_hint": "PNG of the page. READ the image before asserting anything rendered.", "_next": ["pup_eval", "pup_navigate"] }))-}--fn window_info(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };-    let fg = ctx.host.foreground();-    let tabs: Vec<Value> = s.tabs.iter().enumerate().map(|(i, t)| json!({ "tabId": t.tab_id, "url": t.url, "title": t.title, "active": i == s.active_tab })).collect();-    ok(json!({ "success": true, "sessionId": s.id, "window": s.id, "aiThread": s.owner, "owner": s.owner, "ownerContainer": s.owner_container, "profile": s.profile, "ageMinutes": s.age_minutes(), "tabs": tabs,-        "lastAgentUpdate": s.last_agent_update(), "hwnd": s.hwnd, "foreground": s.hwnd.is_some() && s.hwnd == Some(fg), "lastTakeoverBy": Value::Null, "cardShown": false, "captionShown": false,-        "_hint": "Info returned as JSON only — nothing was drawn on the user's screen (hwnd included for desktop_screenshot_window when you need the browser chrome in frame). A USER clicking the jump-list/toolbar Info action gets the on-screen card; pass showCaption:true to draw the native card yourself." }))-}--/// Birth of an extension-lane window as a FIRST-CLASS SESSION: same Session struct, same insert/save,-/// same identity stamp, overlay, progress and attention-clear the CDP birth runs. Only the driving differs.-type BirthRx = std::sync::mpsc::Receiver<(i64, bool, bool, u128, u128, Option<u128>)>;--/// ONE birth watcher for BOTH lanes (2.0.567). The CDP lane has had this since 2.0.446: register the AUMID-/// ahead of time, then a thread started BEFORE the window exists polls every top-level frame every-/// millisecond and stamps the newborn natively while it is still invisible, so the shell bakes pup's icon-/// into the button at its first show. The extension lane skipped it and every ext window was born on the-/// stock Chrome icon until a later stamp (John, 2026-09-14: "you sure don't seem to be branding that windows-/// taskbar icon very early in the process").-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 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 { 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))-                    .collect();-                // R4 / pup-window-targeting: "If >1 window appears under the lock it refuses to guess and-                // logs loudly - a wrong hwnd paints another window's badge and parks the wrong window, so-                // late beats wrong." Keep polling; the extra startup frame usually closes and leaves one.-                if cands.len() > 1 { ambiguous = cands.len(); std::thread::sleep(Duration::from_millis(1)); continue; }-                if let Some(w) = cands.into_iter().next() { found = Some(w); break; }-                std::thread::sleep(Duration::from_millis(1));-            }-            let Some(w) = found else {-                if ambiguous > 1 { pup_core::log(format!("[birth] \"{sid2}\" REFUSED to guess its window: {ambiguous} new Chrome frames were up under the birth lock and none resolved unambiguously - leaving it BARE rather than stamping another thread's window (bare > wrong)")); }-                return;-            };-            let t_found = t_w.elapsed().as_millis();-            let mut stamped = false; let mut t_stamp = 0u128;-            if let (true, false, Some((app_id, display, icon))) = (early_ok, w.visible, plan2.as_ref()) {-                stamped = a.host.stamp_identity(w.hwnd, app_id, display, &icon.to_string_lossy(), &identity::relaunch_for(&sid2));-                t_stamp = t_w.elapsed().as_millis();-            }-            // 2.0.639: PARK WHILE INVISIBLE. The z-watch (2.0.638) showed Chrome's first show putting every newborn-            // on top for 140-250 ms until the park that waited for the token bind. The watcher holds the handle-            // ~150 ms before that first show; bottom it now, so the first frame Chrome draws is already behind.-            let mut pre_parked = false;-            if !w.visible { pre_parked = a.host.send_to_bottom_no_activate(w.hwnd); }-            let mut t_vis = if w.visible { Some(t_found) } else { None };-            if pre_parked {-                // 2.0.640: native spin, bottomed in the same instant it shows, held 300 ms (see park_on_first_show)-                match a.host.park_on_first_show(w.hwnd, 6000) {-                    Some(ms) => { t_vis = Some(t_w.elapsed().as_millis()); pup_core::log(format!("[window] \"{sid2}\" pre-parked while invisible, shown {ms}ms later and bottomed in that instant, held for 300 ms")); }-                    None => { pup_core::log(format!("[window] \"{sid2}\" pre-parked while invisible, never shown within 6 s")); }-                }-            }-            while t_vis.is_none() && t_w.elapsed() < Duration::from_secs(6) { if a.host.frame_windows_all(Some("Chrome_WidgetWin_1")).iter().any(|x| x.hwnd == w.hwnd && x.visible) { t_vis = Some(t_w.elapsed().as_millis()); } else { std::thread::sleep(Duration::from_millis(1)); } }-            let _ = tx.send((w.hwnd, w.visible, stamped, t_found, t_stamp, t_vis));-        }).ok();-        rx-    });-    (plan, early_ok, watcher)-}--/// Wait for the watcher and log the birth timeline. Some((hwnd, stamped_before_first_show)).-fn birth_report(watcher: Option<BirthRx>, sid: &str) -> Option<(i64, bool)> {-    let rx = watcher?;-    let Ok((h, was_visible, stamped, t_found, t_stamp, t_vis)) = rx.recv_timeout(Duration::from_secs(6)) else { return None };-    pup_core::log(format!("[window] \"{sid}\" newborn hwnd={h} caught {} at +{t_found}ms{}; shown at {}", if was_visible { "ALREADY VISIBLE (early stamp missed)" } else { "invisible" },-                if stamped { format!(", identity stamped natively at +{t_stamp}ms (before the first show)") } else if !was_visible { " (not stamped: no plan or registration)".to_string() } else { String::new() },-                t_vis.map(|t| format!("+{t}ms")).unwrap_or_else(|| "never (within 6 s)".into())));-    Some((h, stamped))-}--/// 2.0.638 instrumentation (2.0.636 watched only AFTER the park and saw nothing once ab's clear was gone, while-/// the gate still caught the NEWBORN on top ~1 s after the open began, sometimes on its birth page). This watches-/// from BEFORE the extension creates the window: whenever the top visible window is a lane Chrome window that did-/// not exist before this birth, log when, its title, whether it is the foreground, and the last thing pup logged.-fn spawn_zwatch(ctx: &Ctx, sid: &str, before: std::collections::HashSet<i64>) {-    let Some(a) = ctx.arc() else { return };-    let sid2 = sid.to_string(); let t0 = Instant::now();-    std::thread::Builder::new().name("zwatch".into()).spawn(move || {-        let mut on_top: Option<(i64, u128)> = None;-        while t0.elapsed() < Duration::from_millis(12000) {-            let pids = crate::extdrive_pids();-            let top = a.host.frame_windows_all(None).into_iter()-                .find(|w| w.visible && !w.title.is_empty() && w.rect.0 > -30000 && w.rect.2 > w.rect.0 + 50-                      && !matches!(w.class.as_str(), "Windows.UI.Core.CoreWindow" | "Xaml_WindowedPopupClass" | "Shell_TrayWnd" | "Shell_SecondaryTrayWnd" | "Progman" | "WorkerW" | "TextInputHost" | "ApplicationFrameWindow" | "Windows.UI.Composition.DesktopWindowContentBridge"));-            let now = t0.elapsed().as_millis();-            let ours = top.as_ref().filter(|w| w.class == "Chrome_WidgetWin_1" && pids.contains(&w.pid) && !before.contains(&w.hwnd));-            match (ours, on_top) {-                (Some(w), None) => { on_top = Some((w.hwnd, now)); pup_core::log(format!("[zwatch] \"{sid2}\" lane window {} ON TOP at +{now}ms title=\"{}\" foreground={} last: {}", w.hwnd, w.title.chars().take(40).collect::<String>(), a.host.foreground() == w.hwnd, pup_core::last_log())); }-                (None, Some((h, t))) => { on_top = None; pup_core::log(format!("[zwatch] \"{sid2}\" lane window {h} left the top at +{now}ms after {}ms, last: {}", now - t, pup_core::last_log())); }-                _ => {}-            }-            std::thread::sleep(Duration::from_millis(15));-        }-    }).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.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 })); }-            return ok(json!({ "sessionId": sid, "window": sid, "aiThread": s.owner, "reused": true, "transport": "ext" }));-        }-    }-    // R4 / pup-window-targeting rule 2. The ext lane is ONE Chrome holding MANY threads' windows, so an-    // unlocked set-diff is exactly how all-engineering, NSF and pup-bridge all ended up on hwnd 590484.-    #[allow(unused_assignments)]-    let mut birth_move_ms: Option<i64> = None;   // 2.0.621: R1's on-screen move, measured by the extension; set by whichever open arm runs-    let mut birth_token = String::new();          // 2.0.630: the birth page's title token, the deterministic handle-    let birth_guard = BIRTH_LOCK.lock().unwrap_or_else(|e| e.into_inner());-    let cold = !extdrive::alive(ctx);-    if cold {-        // 2.0.545: launch on about:blank, apply the consumer identity (wire rule + JS shim), and only then go-        // to the requested URL, so the FIRST real request already carries the installed Chrome's identity.-        // Launching straight on the URL loaded that page before the rule existed (measured: the launch page-        // echoed CfT 148 while the next navigation echoed 152).-        // 2.0.586: a cold open BLOCKS until the lane is up, with one relaunch if the first launch never calls-        // home (a Chrome still shutting down after the last close makes the new one forward to it and exit).-        // Answering `starting:true` with no session made the caller's very next verb fail with-        // session_not_found (parity's pup_open_tab, and other threads' "hard time opening pup", 2026-09-15).-        // 2.0.613 (John, 2026-09-17, shouting, and right): "IF YOU CAN'T FUCKING CREATE A PUP WINDOW IN THE-        // BACKGROUND THEN FUCKING WARN ME AS THE USER YOU'RE ABOUT TO SHOVE A WINDOW IN FRONT OF MY FACE".-        // A warm open is invisible now (created minimized, shown already at the bottom). A COLD one starts the-        // browser, and Chrome draws its own startup window before pup owns anything, so it can still flash.-        // That case gets a warning BEFORE it happens, with a beat for him to see it, never an apology after.-        // 2.0.617: no prompt. The cold start draws nothing now (--no-startup-window + --keep-alive-for-test),-        // so there is nothing to ask about: "being asked even that once is too much" (John, 2026-09-18).-        for attempt in 1..=2u32 {-            // 2.0.632: no url. "about:blank" here overrode --no-startup-window and made an unowned about:blank-            // window that sat on top of the user's work for 18 s on every cold start (measured, 2.0.631).-            if let Err(e) = launch_extdrive(ctx, "") { if attempt == 2 { return e; } else { continue; } }-            let t0 = std::time::Instant::now();-            while t0.elapsed() < Duration::from_secs(25) { if extdrive::connected_ms_ago() < 4000 { break; } std::thread::sleep(Duration::from_millis(300)); }-            if extdrive::alive(ctx) { break; }-            if attempt == 1 {-                pup_core::log("[lane] the launched browser never called home within 25 s; ending it and launching once more".to_string());-                kill_extdrive(ctx); std::thread::sleep(Duration::from_millis(1500));-            }-        }-        if !extdrive::alive(ctx) { return err("ext_lane_start_failed", "the extension-driven browser did not start (two launches, no worker poll within 25 s each)", Some("pup_readiness shows the browser state; a Chrome for Testing still holding the profile is the usual cause. pup_close {} then open again.")); }-    }-    // 2.0.635: arm the birth watcher HERE, after a cold launch, not before it. The watcher lives 4 s; armed-    // before the launch it expired while Chrome was still starting, so a cold birth never got the early native-    // stamp and wore the stock Chrome-for-Testing icon until the deferred navigate finished (measured on-    // John's laptop, 2026-09-19: 12 s of the wrong icon; warm births were stamped before their first show).-    let taken = taken_hwnds(ctx, sid);-    let before: std::collections::HashSet<i64> = ctx.host.frame_windows_all(Some("Chrome_WidgetWin_1")).into_iter().map(|w| w.hwnd).collect();-    // the same birth watcher the CDP lane uses: identity on the window BEFORE Chrome shows it-    let plan_name = owner.map(|o| o.to_string()).unwrap_or_else(|| sid.to_string());-    let (plan, _early_ok, watcher) = birth_watch(ctx, caller, sid, &plan_name, url, &before, &taken, true);-    spawn_zwatch(ctx, sid, before.clone());-    // the tab and Chrome window for this session: the launch URL's tab on a cold start, a NEW window otherwise-    let (chrome_tab, chrome_win, got_url, got_title) = if cold {-        extdrive::apply_consumer_brands(ctx);-        // 2.0.617: the lane launches with --no-startup-window, so there is no tab to adopt. Create the first-        // window the same way as every later one: through the extension, minimized, shown at the bottom.-        match extdrive::run(json!({ "action": "open", "url": "about:blank" }), Duration::from_secs(25)) {-            Ok(r) => {-                birth_move_ms = r["birthMoveMs"].as_i64(); birth_token = r["token"].as_str().unwrap_or("").to_string();-                let t = r.clone();-                let id = t["tabId"].as_i64().unwrap_or(0);-                // The real page is loaded AFTER the birth lock is released (below): a 25 s navigate held-                // under the lock would serialize every other thread's window birth behind this one.-                (id, t["windowId"].as_i64(), url.to_string(), String::new())-            }-            Err(e) => return e,-        }-    } else {-        match extdrive::run(json!({ "action": "open", "url": url }), Duration::from_secs(20)) {-            Ok(r) => { birth_move_ms = r["birthMoveMs"].as_i64(); birth_token = r["token"].as_str().unwrap_or("").to_string(); (r["tabId"].as_i64().unwrap_or(0), r["windowId"].as_i64(), url.to_string(), String::new()) }-            Err(e) => {-                // 2.0.577: a lane whose worker polled within the last few seconds is BUSY, not dead: relaunching it-                // here killed every thread's windows because one open waited behind a long capture (NSF's open,-                // 18:28 on 2026-09-14). Fail THIS open and say so; only a silent worker gets the relaunch.-                // 2.0.622: only a NON-ANSWER is "busy". The worker answering with an error (measured: Chrome-                // refusing a window's bounds) was being relabelled ext_busy with a "retry in a few seconds"-                // hint, which is a lie that sends a caller into a retry loop against a deterministic failure.-                let answered = e["errorCode"].as_str().map(|c| c != "ext_no_answer").unwrap_or(false) && e["error"].as_str().map(|m| !m.contains("did not answer")).unwrap_or(true);-                if answered {-                    pup_core::log(format!("[lane] warm open refused by the browser: {}", e["error"].as_str().unwrap_or("?")));-                    return pup_core::merge(e, json!({ "errorCode": "ext_open_failed", "_hint": "The browser answered and refused to create the window; the error is Chrome's own. This is not a busy lane and a retry will fail the same way." }));-                }-                if extdrive::connected_ms_ago() < 8000 {-                    pup_core::log(format!("[lane] warm open not answered but the worker polled {} ms ago: busy, not relaunching", extdrive::connected_ms_ago()));-                    return pup_core::merge(e, json!({ "errorCode": "ext_busy", "_hint": "The extension lane is alive but did not answer in time (another verb is holding it). Retry the open in a few seconds; pup did NOT restart anything." }));-                }-                // the heartbeat is stale and nothing answered: treat it as cold and launch-                pup_core::log("[lane] the extension channel did not answer a warm open and the worker is silent; relaunching the lane".to_string());-                extdrive::mark_dead();-                let mut a = args.clone(); a["lane"] = json!("ext");-                // 2.0.623: RELEASE THE BIRTH LOCK before re-entering. This call re-enters open_window_ext, whose-                // first act is to take BIRTH_LOCK, which is not re-entrant: the open deadlocked on itself and every-                // later open queued behind it forever (measured on the conference-room box, 2026-09-18: exact-                // 60 s timeouts on every open, a lane that read dead, and not one launch line in the log). It bites-                // whenever the browser dies between a poll and an open, which is exactly what the foreground-                // gate does on purpose and what a user closing the browser does by accident.-                drop(birth_guard);-                return open_window_ext(ctx, caller, &a, sid, owner, url);-            }-        }-    };-    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;-    let mut early_stamped = false;-    // 2.0.630: bind by the birth TOKEN first. The set-diff bound a placeholder Chrome makes at launch and pup-    // parked that while the real window came up unparked (measured: 4.5 s on top on every cold start). The-    // set-diff handle is only a fallback now, and is trusted only when it agrees with the token.-    let by_token = if birth_token.is_empty() { None } else { extdrive::hwnd_by_birth_token(ctx, &birth_token, &taken, Duration::from_secs(3)) };-    match (by_token, birth_report(watcher, sid)) {-        (Some(h), Some((wh, stamped))) => { s.hwnd = Some(h); early_stamped = stamped && wh == h; if wh != h { pup_core::log(format!("[window] \"{sid}\" the set-diff caught window {wh}, the birth token names {h}; bound by token")); } }-        (Some(h), None) => { s.hwnd = Some(h); }-        (None, Some((wh, stamped))) => { s.hwnd = Some(wh); early_stamped = stamped; pup_core::log(format!("[window] \"{sid}\" birth token {birth_token} not found within 3 s; bound the set-diff handle {wh}")); }-        (None, None) => { if let Some(h) = extdrive::lane_hwnd_for(ctx, &s) { s.hwnd = Some(h); } }-    }-    // 2.0.635 backstop: if the watcher missed the invisible moment, stamp NOW, at the bind, before the first-    // show below. The button exists already (a minimized window has one), but the stock icon then lasts the-    // milliseconds to this call instead of the seconds to the deferred navigate and the full identity pass.-    if !early_stamped {-        if let (Some(h), Some((app_id, display, icon))) = (s.hwnd, plan.as_ref()) {-            let t_s = Instant::now();-            if ctx.host.stamp_identity(h, app_id, display, &icon.to_string_lossy(), &identity::relaunch_for(sid)) {-                early_stamped = true;-                pup_core::log(format!("[window] \"{sid}\" identity stamped at the bind in {}ms (the birth watcher missed the invisible moment)", t_s.elapsed().as_millis()));-            }-        }-    }-    // 2.0.604 (John, 2026-09-17: "why do you keep shoving windows in front of me without warning me?-    // ... you shoved the window in my face while i was busy typing in another window"). Chrome creates the-    // window unfocused but at z1, ON TOP of the user's work, and the park used to run AFTER the deferred-    // navigate, the consumer-brand pass, the orphan sweep, the fit and the jump-list commit: measured 4.0 s-    // of a pup window sitting over his screen, every open. Park the INSTANT the handle exists, before any of-    // that. This is one Win32 call with no activation, so it is safe to hold the birth lock across.-    if let Some(h) = s.hwnd {-        let t_park = Instant::now();-        // 2.0.624: this lane's R1 in two halves. The extension birthed the window MINIMIZED at its final rect-        // (the API refuses off-screen, and a visible birth lands on top: measured). This one call orders it to-        // the bottom while it is still minimized, restores it without activation, and orders it again, so the-        // first frame it is ever drawn in is already behind the user's work. Minimized was a transient measured-        // in the hundreds of milliseconds (the handle wait); it can never strand, a taskbar click restores it.-        let parked = ctx.host.show_at_bottom_no_activate(h);-        pup_core::log(format!("[window] \"{sid}\" birth: off-screen create, on-screen move {} (budget 50 ms), parked at z-bottom {}ms after the handle resolved (ext lane): {parked}",-            match birth_move_ms { Some(m) if m >= 0 => format!("{m} ms"), _ => "unmeasured".into() }, t_park.elapsed().as_millis()));-        if birth_move_ms.map(|m| m > 50).unwrap_or(false) { pup_core::log(format!("[window] \"{sid}\" DEFECT: the on-screen move took {} ms, over the 50 ms rule (pup-bridge-dev R1)", birth_move_ms.unwrap_or(0))); }-        // 2.0.627: the FIRST window of a freshly started browser settles ON TOP after every park pup makes-        // at birth (measured on the conference-room box with the fixed bottom call: a warm birth ended at z11,-        // behind, the cold one at z0 and stayed). Something in Chrome raises its first window after pup is-        // done. The doctrine's answer is the bounded re-assert inside the birth grace, which the CDP lane has-        // run since v1.9.201 and this lane opted out of on the claim that Chrome never self-raises an-        // unfocused window; true for a warm birth, false for a cold one. Safe by construction: the primitive-        // never touches a window that is foreground, so a human who clicked it is left alone.-        // 2.0.629: EVERY birth, not only a cold one. Measured: with the cold re-assert in place the cold window was-        // pulled back down within seconds, and the WARM windows (B, C) came up on top and stayed, because they got-        // no re-assert. "Chrome never self-activates an unfocused-created window on this lane" is false for a-        // minimized birth restored by pup; the fit and the identity work that follow the park can raise it too.-        schedule_birth_reassert(ctx, sid, if cold { "cold birth" } else { "birth" });-    }-    drop(birth_guard);   // hwnd resolved (or refused): the next thread's birth may start-    // the deferred cold navigate, now that no other birth is blocked behind it-    // 2.0.630: EVERY birth navigates from the birth page to its real URL here, after the window is bound and-    // parked, so the page the user cares about never loads into a window pup does not yet hold.-    if url != "about:blank" {-        // 2.0.595: the session OWNS its window from here. It used to be inserted only after this navigate, so for-        // up to 25 s the lane's only window was an unowned about:blank, and the health tick's orphan sweep closed-        // it: Chrome exited with its last window, the worker went silent, and 190 s later the "dead lane" path-        // relaunched every session on disk, including six windows John had closed by hand (AdomLapper 21:41,-        // 2026-09-15). That was also the "worker stalls right after a cold launch" mystery.-        ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);-        // 2.0.648 (pup-bridge#90, Bridge SDK #59): a slow box can spend the whole 60 s ab budget in the cold launch-        // alone; the window EXISTS by now (bound, parked, owned), so answer with loading:true and finish the-        // navigate, fit and identity in the background instead of letting ab time the verb out after success.-        if t_verb.elapsed() > Duration::from_secs(35) {-            if let Some(a) = ctx.arc() {-                let (sid2, url2, tab2, caller2) = (sid.to_string(), url.to_string(), chrome_tab, caller.clone());-                std::thread::Builder::new().name("open-finish".into()).spawn(move || {-                    let ctx2: &Ctx = &a;-                    if let Ok(nav) = extdrive::run(json!({ "action": "navigate", "url": url2, "tabId": tab2, "waitMs": 25000 }), Duration::from_secs(30)) {-                        ctx2.sessions.with_mut(&sid2, |x| if let Some(t0) = x.tabs.get_mut(0) { if let Some(u) = nav["url"].as_str() { if !u.is_empty() { t0.url = u.to_string(); } } if let Some(ti) = nav["title"].as_str() { if !ti.is_empty() { t0.title = ti.to_string(); } } });-                        if let Some(ss) = ctx2.sessions.get(&sid2) { ctx2.sessions.save_file(&ss); }-                    }-                    extdrive::fit_now();-                    identity::stamp_opts(&ctx2, &sid2, &caller2, true, true);-                    pup_core::log(format!("[window] \"{sid2}\" slow open finished in the background ({} s after the verb began)", t_verb.elapsed().as_secs()));-                }).ok();-            }-            return ok(json!({ "sessionId": sid, "window": sid, "aiThread": s.owner, "tabId": t.tab_id, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext", "loading": true,-                "progress": { "phase": "navigating", "elapsedSec": t_verb.elapsed().as_secs(), "percent": 70 },-                "_hint": "This box is slow: the window exists, is yours and is parked in the background, and the page is still loading. Poll pup_window_info {} (url/title fill in) or pup_readiness (active[]) rather than opening again; a second open with the same window label is refused as owned." }));-        }-        let nav = match extdrive::run(json!({ "action": "navigate", "url": url, "tabId": chrome_tab, "waitMs": 25000 }), Duration::from_secs(30)) {-            Ok(v) => v,-            Err(e) => {-                if s.hwnd.map(|h| !ctx.host.is_alive(h)).unwrap_or(false) && !extdrive::alive(ctx) {-                    ctx.sessions.remove(sid); ctx.sessions.delete_file(sid);-                    pup_core::log(format!("[window] \"{sid}\": the lane browser died during the cold open (its window is gone and the worker is silent); no session"));-                    return pup_core::merge(e, json!({ "errorCode": "ext_lane_died_during_open", "_hint": "The lane browser exited while this window was being opened. Open again; pup did not keep a dead session." }));-                }-                Value::Null-            }-        };-        if let Some(t0) = s.tabs.get_mut(0) {-            if let Some(u) = nav["url"].as_str() { if !u.is_empty() { t0.url = u.to_string(); } }-            if let Some(ti) = nav["title"].as_str() { if !ti.is_empty() { t0.title = ti.to_string(); } }-        }-    }-    if early_stamped { if let Some((a, _, _)) = &plan { s.app_id = Some(a.clone()); } }-    ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);-    // 2.0.653: page scripts + wiki probe on the extension lane's first page (the CDP open always did both)-    let mut wiki_extra = json!({});-    if let (Ok(lane), Some(t0)) = (transport::lane_for(ctx, &s), s.tabs.get(0).cloned()) { if t0.url != "about:blank" { inject_page_scripts_ext(ctx, &s, &lane, &t0); wiki_extra = wiki::after_navigate_ext(ctx, &s, &lane, &t0, &t0.url, args.get("wikiView").and_then(|v| v.as_str()), false); } }-    extdrive::apply_consumer_brands(ctx);   // once per lane browser; before the first page the user cares about-    extdrive::close_orphan_blank_windows(ctx, chrome_win);   // the startup window a relaunch left behind, if any-    extdrive::fit_now();-    // 2.0.577: ALWAYS a full pass, as the CDP path does after its early stamp. The in-process birth stamp-    // sets the AUMID but never commits a jump list, and stamp_opts only commits one inside its full/changed-    // block; with full=false and app_id already set, ext windows were born with a tile and NO right-click-    // menu (John, 2026-09-14: "a shotlog pup window but no jump list"). Same AUMID again = no button rebuild.-    let t_jl = Instant::now();-    identity::stamp_opts(ctx, sid, caller, true, true);-    if early_stamped { pup_core::log(format!("[window] \"{sid}\" jump list committed on the early-stamped button in {}ms", t_jl.elapsed().as_millis())); }-    if let Some(h) = s.hwnd {-        // 2.0.584: R1, "background means BEHIND". chrome.windows.create({focused:false}) never steals focus but lands-        // the window at z1, right under the user's active window and above everything else (measured: z1 of 17,-        // still z1 twenty seconds later). Park it to the bottom once, now. No +2.5 s re-assert on this lane: Chrome-        // never self-activates an unfocused-created window (measured), so a re-assert could only undo a human click.-        // the early park above already put it behind. Re-assert ONLY if it is still on top and the user has-        // not taken it: a blind re-assert here would undo a click the human made in the meantime.-        if ctx.host.foreground() != h { let _ = ctx.host.send_to_bottom_no_activate(h); }-        activity::on_open(ctx, sid, h, caller); activity::clear_attention_later(ctx, h);   // 2.0.637: never ab's clear here (it raises + activates)-    }-    if let Ok(lane) = transport::lane_for(ctx, &s) { let mut s2 = s.clone(); transport::refresh_all(&lane, &mut s2); ctx.sessions.insert(s2.clone()); ctx.sessions.save_file(&s2); s = s2; }-    // 2.0.621: no focus handback anywhere. pup-user-foreground deleted it in August because it foregrounded-    // the wrong window; R2: if a window fronts anyway, let the flaw exist and log it, never fight it.-    // Get the icon right AT BIRTH, not on a later tick (John, 2026-09-14). The birth wait can bind the-    // transient about:blank launch window Chrome replaces; the stamp above then lands on a dead handle.-    // Now that the real page is up: if the bound hwnd is not a live window, bind the live lane window-    // and stamp it before this verb returns.-    if s.hwnd.map(|h| !ctx.host.is_alive(h)).unwrap_or(true) {-        if let Some(h) = extdrive::lane_hwnd_for(ctx, &s) {-            ctx.sessions.with_mut(sid, |x| x.hwnd = Some(h)); s.hwnd = Some(h); ctx.sessions.save_file(&s);-            identity::stamp(ctx, sid, caller, true); activity::on_open(ctx, sid, h, caller);-            // 2.0.628: the birth park ran on the placeholder, so this window has never been parked. Measured: it-            // came up on top and stayed there (the placeholder was rebound 6 s after the park, after every-            // re-assert had fired on the dead handle). Park it NOW and re-assert from here.-            let parked = ctx.host.send_to_bottom_no_activate(h);-            pup_core::log(format!("[ext] birth bound a transient window; rebound \"{sid}\" to the live window {h}, stamped it and parked it at z-bottom: {parked}"));-            schedule_birth_reassert(ctx, sid, "late rebind");-        }-    }-    let t = s.active().cloned().unwrap_or(t);-    pup_core::merge(ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext", "landed": landed_ext(ctx, &s, &t, &t.url, &t.title),-        "_hint": "Extension-driven window (no CDP debug port): a first-class pup window. Drive it with pup_navigate / pup_eval / pup_screenshot / pup_open_tab like any other." })), wiki_extra)-}--/// pup_clear_cookies {domain, names?}: drop a site's cookies in this window's jar, HttpOnly included. The case-/// that made it exist: a bot manager's verdict cookie (Akamai _abck at ~-1~) that keeps a jar denied long after-/// the browser itself looks fine (analog.com: loads on a fresh jar, Access Denied on adom-you, John 2026-09-13).-fn clear_cookies(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    let Some(domain) = args["domain"].as_str().filter(|d| !d.trim().is_empty()) else { return err("domain_required", "pup_clear_cookies needs {domain}", Some("e.g. {domain:\"analog.com\"} removes analog.com and *.analog.com cookies; names:[...] narrows to those cookie names.")) };-    let names: Vec<String> = args["names"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();-    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };-    let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };-    match transport::clear_cookies(&lane, domain, &names) {-        Ok(n) => { pup_core::log(format!("[cookies] \"{}\": removed {n} cookie(s) for {domain}{}", s.id, if names.is_empty() { String::new() } else { format!(" ({})", names.join(",")) })); ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "domain": domain, "removed": n, "transport": s.transport })) }-        Err(e) => e,-    }-}--/// pup_clear_site_data {origin | origins:[...]}: cache, cookies, storage and service workers for those origins.-fn clear_site_data(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    let mut origins: Vec<String> = args["origins"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();-    if let Some(o) = args["origin"].as_str() { origins.push(o.to_string()); }-    if origins.is_empty() { return err("origin_required", "pup_clear_site_data needs {origin} or {origins:[...]}", Some("e.g. {origins:[\"https://www.analog.com\",\"https://analog.com\"]}")); }-    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };-    let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };-    match transport::clear_site_data(&lane, &origins) {-        Ok(()) => { pup_core::log(format!("[sitedata] \"{}\": cleared {:?}", s.id, origins)); ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "origins": origins, "transport": s.transport })) }-        Err(e) => e,-    }-}--pub fn close_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {-    // pup_close {} with no ids closes the caller's window; pup_close {all:true} closes everything of theirs.-    let mut targets: Vec<Session> = Vec::new();-    if args.get("all").and_then(|b| b.as_bool()) == Some(true) {-        // all:true = the caller's windows; everyone:true (the jump list's "Close ALL Adom Pup windows", a-        // user action) = every window on this desktop whoever owns it. Node: pup_close {all, everyone}.-        // 2.0.491: `everyone:true` closes EVERY pup window on the desktop, including other threads' work.-        // That is the jump list's "Close ALL pup windows" — a USER action. The Rust port left it open to any-        // AI thread, so one thread could wipe another's windows with no refusal and no trace. Now only a human-        // caller gets it; an AI asking for it closes its OWN windows and is told why.-        let asked_everyone = args.get("everyone").and_then(|b| b.as_bool()) == Some(true);-        let human = caller.thread.is_empty() || USER_CALLERS.contains(&caller.thread.as_str());-        let everyone = asked_everyone && human;-        if asked_everyone && !human { pup_core::log(format!("[close] thread \"{}\" asked to close EVERYONE's windows; refused — closing only its own (everyone:true is the jump list's user action)", caller.thread)); }-        targets = if everyone { ctx.sessions.all() } else { ctx.sessions.owned_by(&caller.thread) };-        // 2.0.476: cancel any in-flight restore of the windows being closed and remove their files, so the close is-        // INSTANT (no 40 s drain that blocks the user and outlives the CLI request) and a relaunch that is mid-flight-        // undoes its own window instead of leaving it behind (close-during-restore cycle). A reserved id with no live-        // session yet is a window a restore is bringing back right now: cancel and unfile it too.-        let mut cancel_ids: Vec<String> = targets.iter().map(|s| s.id.clone()).collect();-        if everyone || caller.thread.is_empty() { for f in ctx.sessions.read_files() { if let Some(id) = f["sessionId"].as_str() { if !cancel_ids.iter().any(|c| c == id) { cancel_ids.push(id.to_string()); } } } }-        for id in &cancel_ids { crate::recovery::cancel_restore(id); ctx.sessions.delete_file(id); }-    }-    else { match resolve(ctx, caller, args) { Ok(s) => targets.push(s), Err(e) => return e } }-    let mut closed = Vec::new(); let mut chrome_killed = 0; let mut failed: Vec<String> = Vec::new();-    for s in targets {-        // 2.0.470: the session and its file go FIRST. Closing the targets fires Target.targetDestroyed on the event-        // pump, whose handler re-saves the session file; when that save landed after the delete below, the file-        // came back (url about:blank, the other tabs listed) and the next bridge start relaunched a ghost window-        // from it (arav-rog, 2026-09-11: "drill-b" reborn with 5 tabs after an update).-        ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id);-        let lane_closed = match transport::lane_for(ctx, &s) { Ok(lane) => transport::close_window(&lane, &s), Err(_) => false };-        // Verify the OS window is gone; a close that races a bridge respawn (no live CDP handle yet) used to-        // drop the session and leave the window behind (John's laptop, 2026-09-06: three leftovers wearing-        // old tiles). The window is closed through ab (WM_CLOSE, forced after 3 s) when it survives.-        // 2.0.587: an UNBOUND extension session (worker mid-reload, hwnd None) resolves its handle here first;-        // before this the OS close was skipped and eight ownerless windows were left on the user's glass.-        let hwnd = s.hwnd.or_else(|| if transport::is_ext(&s) { extdrive::lane_hwnd_for(ctx, &s) } else { None });-        let mut survived = false;-        if let Some(h) = hwnd {-            std::thread::sleep(Duration::from_millis(300));-            if ctx.host.is_alive(h) {-                let _ = ctx.ab.call("desktop_close_window", json!({ "hwnd": h, "forceAfterMs": 3000 }), caller, Duration::from_secs(6));-                let t0 = std::time::Instant::now();-                while t0.elapsed() < Duration::from_secs(4) && ctx.host.is_alive(h) { std::thread::sleep(Duration::from_millis(250)); }-                survived = ctx.host.is_alive(h);-            }-        } else if !lane_closed { survived = true; }   // nothing confirmed the close and there is no handle to force it-        if survived {-            // 2.0.587: never drop a session whose window is still on glass. Put it back and say so.-            let mut back = s.clone(); back.hwnd = hwnd; ctx.sessions.insert(back.clone()); ctx.sessions.save_file(&back);-            pup_core::log(format!("[close] \"{}\" did NOT close (lane answered: {lane_closed}, hwnd {:?} still alive); session kept so the window is never ownerless", s.id, hwnd));-            failed.push(s.id.clone());-            continue;-        }-        ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id); closed.push(s.id.clone());   // again: a late save loses-        if let Some(a) = s.app_id.as_deref() { identity::release_identity(ctx, a); }   // 2.0.666: no dead AUMID left behind-        // 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) && 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() {-            if s.profile.starts_with("iso-") {-                // 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) {-                let _ = b.cdp.send(None, "Browser.close", json!({}), Duration::from_secs(5)); b.cdp.close();-                // give Chrome a moment to exit on its own; only then reap a straggler (never a slow one mid-write)-                if b.launched.pid > 0 { for _ in 0..12 { if !ctx.host.alive(b.launched.pid) { break; } std::thread::sleep(Duration::from_millis(250)); } if ctx.host.alive(b.launched.pid) { ctx.host.kill(b.launched.pid); } chrome_killed += 1; }-            }-        }-    }-    if !failed.is_empty() && closed.is_empty() { return pup_core::merge(err("close_failed", format!("{} window(s) did not close: {}", failed.len(), failed.join(", ")), Some("The window is still on screen and its session was kept. Retry pup_close_window; if the extension lane is mid-reload, wait a few seconds first.")), json!({ "failed": failed })); }-    ok(json!({ "closed": closed, "failed": failed, "remaining": ctx.sessions.count(), "activeSession": ctx.sessions.active_id(), "chromeProcsKilled": chrome_killed, "chromeProcsRemaining": ctx.browsers.lock().unwrap().len(), "_hint": "Closed. The sessionId is no longer valid; pup_open_window creates a fresh one.", "_next": ["pup_list_windows", "pup_open_window"] }))-}--#[allow(dead_code)]-fn _unused(_: Value) -> Value { blocked_on_user(false, "", "", json!({})) }+    // 2026-10-13: the process query is the host's (Win32_Process on Windows, the process table on macOS); the+    // cfg!(windows) gate that made this empty on a Mac left every stray lane browser running there.+    for (pid, prof) in ctx.host.pup_browser_processes() { m.entry(prof).or_default().push(pid); }+    m+}++/// The consumer-identity JS shim staged into the lane extension (None when consumerBrands is off).+pub fn lane_shim(ctx: &Ctx) -> Option<String> {+    let shim: Option<String> = if ctx.settings.get("consumerBrands", "on") != "off" { consumer_identity_strings(ctx).map(|v| format!(+        concat!(+            "(() => {{ const UA = {ua}; const B = {b}; const F = {f}; const FULL = {full}; const cp = (a) => a.map(x => Object.assign({{}}, x));\n",+            "  try {{ Object.defineProperty(Navigator.prototype, 'userAgent', {{ get: () => UA, configurable: true }}); Object.defineProperty(Navigator.prototype, 'appVersion', {{ get: () => UA.replace(/^Mozilla\\//, ''), configurable: true }}); }} catch (e) {{}}\n",+            // navigator.userAgentData is a NEW object on every access (measured), so an instance property is lost;\n",+            "  try {{ const d = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgentData'); if (d && d.get) {{ const realGet = d.get; Object.defineProperty(Navigator.prototype, 'userAgentData', {{ configurable: true, get() {{ const t = realGet.call(this); if (!t) return t; return new Proxy(t, {{ get(o, k) {{ if (k === 'brands') return cp(B); if (k === 'toJSON') return () => ({{ brands: cp(B), mobile: o.mobile, platform: o.platform }}); if (k === 'getHighEntropyValues') return (h) => o.getHighEntropyValues(h).then(v => Object.assign(v, {{ brands: cp(B), fullVersionList: cp(F), uaFullVersion: FULL || v.uaFullVersion }})); const v = Reflect.get(o, k); return typeof v === 'function' ? v.bind(o) : v; }} }}); }} }}); }} }} catch (e) {{}}\n",+            "}})();"),+        ua = serde_json::to_string(v["ua"].as_str().unwrap_or("")).unwrap(), b = v["brands"].to_string(), f = v["fullVersionList"].to_string(), full = serde_json::to_string(v["fullVersion"].as_str().unwrap_or("")).unwrap())) } else { None };+    shim+}++/// 2.0.628: the doctrine's bounded re-assert inside the birth grace (pup-user-foreground rule 2, the CDP lane's+/// +2.5 s pass since v1.9.201), for the two moments a window on this lane is measured to settle on top after+/// pup parked it: the first window of a freshly started browser, and a window bound late because the birth+/// caught a placeholder. Force is on, so the primitive applies the idle-hands rule: no input since birth means+/// Chrome's own raise and it is bottomed; any input means a person, and it answers user-foreground and stops.+fn schedule_birth_reassert(ctx: &Ctx, sid: &str, why: &'static str) {+    let Some(a) = ctx.arc() else { return };+    let sid2 = sid.to_string();+    std::thread::Builder::new().name("birth-reassert".into()).spawn(move || {+        // 2.0.630: the first passes land within tens of milliseconds. Measured on 2.0.629: a warm birth still+        // showed on top for ONE half-second sample, the gap between pup's restore and the first pass at 300 ms.+        // Then dense through the first second, where the fit and the identity pass run, then sparse; ~6 s in all.+        let waits = [40u64, 60, 100, 150, 250, 400, 600, 1000, 1500, 2500];+        for (i, wait) in waits.iter().enumerate() {+            std::thread::sleep(Duration::from_millis(*wait));+            let r = park::bottom_unless_foreground(&a, &sid2, true);+            if r != "bottomed" || i + 1 == waits.len() { pup_core::log(format!("[park] \"{sid2}\" {why} re-assert {}/{}: {r}", i + 1, waits.len())); }+            if r == "user-foreground" || r == "no-hwnd" { break; }+        }+    }).ok();+}++/// pup-bridge#121: pids of browser processes whose command line names `profile_dir` (Windows: WMI through the+/// host's shell runner). Used only when the lane is NOT alive, so every match is an orphan.+pub fn orphan_browser_pids(ctx: &Ctx, profile_dir: &std::path::Path) -> Vec<u32> {+    // 2.0.708: match the profile FOLDER NAME after pup-profiles, either slash. (The Windows query once doubled every+    // backslash for a PowerShell single-quoted string and never matched: every boot and idle sweep of isolated+    // profiles reported "ended 0 browser process(es)" and left the browser in the system tray, John's laptop+    // 2026-10-11.) The listing is the host's now, one query shared with pup_browser_pids_by_profile.+    let name: String = profile_dir.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default().chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_' || *c == '.').collect();+    if name.is_empty() { return Vec::new(); }+    let mut v: Vec<u32> = ctx.host.pup_browser_processes().into_iter().filter(|(_, p)| *p == name).map(|(pid, _)| pid).collect();+    v.retain(|p| *p != std::process::id());+    v+}++pub fn launch_extdrive(ctx: &Ctx, url: &str) -> Result<u32, Value> {+    // 2.0.517: the lane runs on adom-you, the ONE durable jar, because John is right that nothing should+    // ever use anything else: a separate adom-you-ext jar had none of his cookies, logins or history,+    // which is both a divergence and the empty-profile shape bot walls score against (measured: the ext+    // jar carried 0.2 MB of History against adom-you's 6.3 MB). It used to be separate only because+    // Chrome allows ONE process per --user-data-dir and this lane must run with NO debug port, so it+    // 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_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+    // then wiped the token for the launch it was preparing. Every launch since served nothing: the+    // extension polled with the correct lane and pup compared it against "" and refused. The /exthello+    // line added in 2.0.512 is what finally showed it.+    let lane = format!("ext-{}", pup_core::now_ms());+    let shim = lane_shim(ctx);+    let exts = pup_browser::stage_extensions_lane(&*ctx.host, ctx.port, profile, true, Some(&lane), shim);+    if exts.is_empty() { return Err(err("ext_no_extension", "the pup extension is not staged, so nothing can drive an extension window", Some("toolbarExtension must be on: the channel IS the extension."))); }+    let cand = match pup_browser::detect_cft(&*ctx.host) {+        Some(c) => c,+        None => return Err(err("ext_needs_cft", "extension-driven mode needs Chrome for Testing (branded Chrome 137+ dropped --load-extension)", Some("pup_readiness shows the Chrome for Testing install; pup_prewarm fetches it."))),+    };+    // 2.0.496: a Chrome still holding this profile makes the NEW process forward to it and EXIT at once, so the+    // launch "succeeds" and nothing is there (ConfRoomROG: the log said up, no window existed, the extension+    // 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());+    // 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.+    if let Some(b) = ctx.browsers.lock().unwrap().remove(profile) {+        let _ = b.cdp.send(None, "Browser.close", json!({}), Duration::from_secs(5));+        b.cdp.close();+        if b.launched.pid > 0 {+            std::thread::sleep(Duration::from_millis(800));+            if ctx.host.alive(b.launched.pid) { ctx.host.kill(b.launched.pid); }+        }+        pup_core::log(format!("[extdrive] ended the CDP browser holding profile \"{profile}\" so the lane can own it"));+    }+    // ctx.browsers is EMPTY right after a pup restart, while a CDP Chrome from before the restart is+    // still running and still holding the directory. pup reconnects to those lazily, so the map is not+    // the source of truth here: the profile's own DevToolsActivePort is. Without this the portless+    // launch just forwards to that browser and exits (the "1 process(es)" line).+    if let Some(port) = pup_browser::existing_port(&dir) {+        if let Ok(cdp) = Cdp::connect_port(port, Duration::from_secs(5)) {+            let _ = cdp.send(None, "Browser.close", json!({}), Duration::from_secs(5));+            cdp.close();+            pup_core::log(format!("[extdrive] closed the CDP browser still on :{port} for profile \"{profile}\" (it outlived a pup restart)"));+            let lock = dir.join("SingletonLock");+            let t0 = std::time::Instant::now();+            while t0.elapsed() < Duration::from_secs(10) { if !lock.exists() { break; } std::thread::sleep(Duration::from_millis(200)); }+        }+    }+    let stale = kill_extdrive(ctx);   // 2.0.686: kill_extdrive now waits until the profile is truly free+    if stale > 0 {+        pup_core::log(format!("[extdrive] ended {stale} stale browser process(es) holding profile \"{profile}\" before relaunching"));+    }+    // 2.0.646 (pup-bridge#121): an ORPHANED lane browser (launched by a pup instance that has since restarted,+    // so no pid list knows it) keeps the profile lock; every new launch then prints "Opening in existing+    // browser session." and exits 0, and pup retried blindly into that wall for 60 s per open (AdomLapper,+    // 2026-09-19, one hour). The lane is not alive here, so any browser on this profile is an orphan: end it,+    // then the lock below is removable. Only processes whose command line names THIS profile dir; never the+    // user's own Chrome.+    let orphans = orphan_browser_pids(ctx, &dir);+    if !orphans.is_empty() {+        for p in &orphans { ctx.host.kill(*p); }+        std::thread::sleep(Duration::from_millis(800));+        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::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()); }+    let trials = ctx.settings.get("laneFieldTrials", "on") != "off";+    if stock && !trials { pup_core::log("[lane] laneFieldTrials is OFF: --disable-field-trial-config added to the stock launch".to_string()); }+    // 2.0.605 (John, 2026-09-17: "why do you keep shoving pup windows in front of me? that's rude").+    // A COLD launch is the one case the birth park cannot cover: Chrome paints its OWN startup window the+    // moment the process starts, seconds before pup has a handle to park (measured on his laptop right after+    // he asked me to close everything, which forced exactly this path). So watch for it from the instant of+    // the spawn: poll every 25 ms and bottom any Chrome-for-Testing window on the profile as soon as it+    // exists. send_to_bottom_no_activate never activates and never hides, so the window still renders.+    // 2.0.621: no watcher, no hiding. The lane launches with --no-startup-window, so there is no startup+    // window to catch, and a window held hidden "until its session is ready" is the staging state the+    // visibility invariant forbids (pup-user-foreground rule 7): an interrupted birth would strand it.+    match pup_browser::launch_extdrive_opts(&*ctx.host, &cand, &dir, &exts, url, stock, trials) {+        Ok(pid) => {+            let mut owned = vec![pid];+            for _ in 0..20 {+                std::thread::sleep(Duration::from_millis(250));+                let now = ctx.host.pids_for_image(&image);+                let fresh: Vec<u32> = now.into_iter().filter(|p| !before.contains(p)).collect();+                if !fresh.is_empty() { owned = fresh; break; }+            }+            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::st().pids.lock().unwrap() = owned;+            save_lane(ctx);+            Ok(pid)+        }+        Err(e) => Err(err("ext_launch_failed", e, None)),+    }+}+++/// The GREASE brand real Chrome sends, per major, as measured on real installs (John's Chrome 152, 2026-09-13).+/// Chrome derives it from the major; unknown majors fall back to the long-standing "Not/A)Brand";v="99".+pub fn grease_for(major: &str) -> (&'static str, &'static str) {+    match major { "152" => ("Not?A_Brand", "24"), _ => ("Not/A)Brand", "99") }+}+/// The identity the lane presents: the INSTALLED Chrome's version, in real Chrome's brand order.+pub fn consumer_identity_strings(ctx: &Ctx) -> Option<Value> {+    set_target_chrome_from_host(&*ctx.host);+    let full = TARGET_CHROME_FULL.lock().unwrap().clone().unwrap_or_default();+    if full.is_empty() { return None; }+    let major = full.split('.').next().unwrap_or("").to_string();+    let (gb, gv) = grease_for(&major);+    let ua = format!("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{major}.0.0.0 Safari/537.36");+    let pinned = ctx.settings.get("consumerBrandsHeader", "");+    let chua = if pinned.is_empty() { format!("\"Chromium\";v=\"{major}\", \"{gb}\";v=\"{gv}\", \"Google Chrome\";v=\"{major}\"") } else { pinned };+    let chua_full = format!("\"Chromium\";v=\"{full}\", \"{gb}\";v=\"{gv}.0.0.0\", \"Google Chrome\";v=\"{full}\"");+    Some(json!({ "ua": ua, "chua": chua, "chuaFull": chua_full, "fullVersion": full,+        "brands": [ {"brand":"Chromium","version":major}, {"brand":gb,"version":gv}, {"brand":"Google Chrome","version":major} ],+        "fullVersionList": [ {"brand":"Chromium","version":full}, {"brand":gb,"version":format!("{gv}.0.0.0")}, {"brand":"Google Chrome","version":full} ] }))+}+pub fn consumer_ua(cdp: &Cdp) -> Option<(String, Value)> {+    if let Some(v) = CONSUMER_UA.lock().unwrap().clone() { return Some(v); }+    let ver = cdp.version().ok()?;+    let mut ua = ver["userAgent"].as_str().unwrap_or("").to_string();+    if ua.is_empty() || !ua.contains("Chrome/") { return None; }+    let cft_full = ua.split("Chrome/").nth(1).and_then(|s| s.split(' ').next()).unwrap_or("").to_string();+    // Present the INSTALLED consumer Chrome's version when we know it (John: match nb exactly); else CfT's own.+    let target = TARGET_CHROME_FULL.lock().unwrap().clone().unwrap_or_default();+    let full = if target.is_empty() { cft_full.clone() } else { target };+    let major = full.split('.').next().unwrap_or("").to_string();+    if major.is_empty() { return None; }+    // Modern Chrome uses a REDUCED UA (major only, minor/build/patch zeroed); mirror that in the UA header.+    if !cft_full.is_empty() { ua = ua.replace(&format!("Chrome/{cft_full}"), &format!("Chrome/{major}.0.0.0")); }+    let is_win = ua.contains("Windows");+    let (platform, platform_version) = if is_win { ("Windows", "15.0.0") } else { ("macOS", "14.6.0") };+    let (gb, gv) = grease_for(&major);   // 2.0.543: real Chrome's order and GREASE (measured on the installed Chrome)+    let meta = json!({+        "brands": [ {"brand":"Chromium","version":major}, {"brand":gb,"version":gv}, {"brand":"Google Chrome","version":major} ],+        "fullVersionList": [ {"brand":"Chromium","version":full}, {"brand":gb,"version":format!("{gv}.0.0.0")}, {"brand":"Google Chrome","version":full} ],+        "fullVersion": full, "platform": platform, "platformVersion": platform_version,+        "architecture": "x86", "bitness": "64", "model": "", "mobile": false, "wow64": false+    });+    let v = (ua, meta);+    *CONSUMER_UA.lock().unwrap() = Some(v.clone());+    Some(v)+}+pub fn apply_consumer_identity(cdp: &Cdp, sid: &str) {+    if let Some((ua, meta)) = consumer_ua(cdp) {+        let _ = cdp.send(Some(sid), "Network.setUserAgentOverride", json!({ "userAgent": ua, "userAgentMetadata": meta }), Duration::from_secs(5));+    }+    // EME/Widevine capability shim so an EME-probe bot check passes (real DRM playback still needs nb/nbe).+    let _ = cdp.send(Some(sid), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": CONSUMER_EME_JS }), Duration::from_secs(5));+    let _ = cdp.evaluate(sid, CONSUMER_EME_JS, Duration::from_secs(5));+}+/// The verb catalog (name/summary/input/timeoutSeconds/example/hint/related/pitfalls), generated by+/// tools/gen-assets.py from the Node build's pup_describe golden. Node: VERB_META + the V() rows.+const VERB_CATALOG_JSON: &str = include_str!("../assets/verb-catalog.json");+static VERB_CATALOG: std::sync::OnceLock<Value> = std::sync::OnceLock::new();+pub fn catalog() -> &'static Value { VERB_CATALOG.get_or_init(|| serde_json::from_str(VERB_CATALOG_JSON).unwrap_or(json!({ "verbs": [] }))) }+pub fn catalog_meta(verb: &str) -> Option<&'static Value> { catalog()["verbs"].as_array()?.iter().find(|v| v["name"] == json!(verb)) }++/// Ticks a loop stat when dropped (so early `continue`s still count).+pub struct LoopTick(pub &'static str, pub u64, pub std::time::Instant);+impl Drop for LoopTick { fn drop(&mut self) { pup_core::loops::tick(self.0, self.1, self.2.elapsed().as_millis() as u64); } }++static CLEANUP_REPORTS: Mutex<Vec<String>> = Mutex::new(Vec::new());+pub fn push_cleanup_report(msg: String) { CLEANUP_REPORTS.lock().unwrap().push(msg); }+fn drain_cleanup_reports() -> Option<String> { let mut g = CLEANUP_REPORTS.lock().unwrap(); if g.is_empty() { None } else { Some(g.drain(..).collect::<Vec<_>>().join(" | ")) } }++const PUP_ISSUES_REF: &str = "adom/pup-bridge";+const BUG_INVITE_EVERY: u64 = 40;+static BUG_INVITE: Mutex<(bool, u64)> = Mutex::new((false, 0));+/// Node: bugReportInvite(isError).+pub fn bug_report_invite(is_error: bool) -> String {+    let lead = if is_error { "THIS MAY BE A PUP BUG — PLEASE REPORT IT." } else { "pup is actively developed and bug reports are genuinely wanted." };+    format!("{lead} If a pup verb misbehaves (wrong result, a hang, a window that never appears, a hint that turned out to be false, a process left running), file it on pup's own wiki repo so the maintainers see it: `adom-wiki issue list {PUP_ISSUES_REF}` to check for a duplicate first, then `adom-wiki issue create {PUP_ISSUES_REF} --title \"<one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON, and the bridge version from bridge_list>\"`. Include the machine's OS and whether it is Windows or macOS. Reports from real machines are the only way this bridge gets correct — the maintainers cannot reproduce every environment.")+}+const PROBE_VERBS: [&str; 9] = ["pup_readiness", "pup_status", "pup_describe", "pup_prewarm", "pup_list_windows", "pup_list_tabs", "pup_rescan", "pup_configure", "pup_import_scan"];+const RENDER_VERBS: [&str; 4] = ["pup_open_window", "pup_open_tab", "pup_navigate", "pup_reload"];+/// Verbs that do not count as "the agent drove this window" for lastAgentUpdate.+const PASSIVE_VERBS: [&str; 7] = ["pup_window_info", "pup_my_windows", "pup_list_sessions", "pup_errors", "pup_switch_window", "pup_use", "pup_audit_icons"];++/// Node: PERMISSIVE_DEFAULT_PERMISSIONS, applied per permission with Browser.setPermission on the+/// default context (origin omitted = every origin) so no permission prompt ever blocks a debug flow.+pub const DEFAULT_PERMISSIONS: [&str; 32] = ["window-management", "clipboard-read", "clipboard-write", "clipboard-sanitized-write", "notifications", "geolocation", "camera", "microphone", "midi", "background-sync", "background-fetch", "periodic-background-sync", "payment-handler", "persistent-storage", "durable-storage", "storage-access", "top-level-storage-access", "idle-detection", "display-capture", "local-fonts", "nfc", "screen-wake-lock", "system-wake-lock", "accelerometer", "gyroscope", "magnetometer", "ambient-light-sensor", "sensors", "protected-media-identifier", "captured-surface-control", "speaker-selection", "window-placement"];+pub fn grant_default_permissions(cdp: &Cdp) -> usize {+    let mut n = 0;+    for p in DEFAULT_PERMISSIONS { if cdp.send(None, "Browser.setPermission", json!({ "permission": { "name": p }, "setting": "granted" }), Duration::from_secs(3)).is_ok() { n += 1; } }+    n+}++/// Node: probeRender — judge what just landed (404 text, blank, unstyled, stack traces, broken images).+pub fn render_check(cdp: &Cdp, sid: &str, http_status: Option<u64>) -> Value {+    let mut signals: Vec<String> = Vec::new();+    let mut out = json!({ "httpStatus": http_status, "ok": true });+    match cdp.evaluate(sid, RENDER_PROBE_JS, Duration::from_secs(6)) {+        Ok(p) if p.is_object() => {+            out["title"] = p["title"].clone(); out["bodyChars"] = p["bodyChars"].clone(); out["elements"] = p["elements"].clone();+            let head = p["head"].as_str().unwrap_or("").to_string(); let low = head.to_lowercase();+            let (chars, elements, sheets) = (p["bodyChars"].as_u64().unwrap_or(0), p["elements"].as_u64().unwrap_or(0), p["styleSheets"].as_u64().unwrap_or(0));+            if let Some(st) = http_status { if st >= 400 { signals.push(format!("http_{st}")); } }+            if low.contains("page not found") || low.contains("not found") || low.split(|c: char| !c.is_ascii_digit()).any(|w| w == "404") { signals.push("page_says_not_found".into()); }+            if low.contains("cannot get /") || low.contains("cannot post /") { signals.push("dev_server_cannot_get".into()); }+            if low.contains("internal server error") || low.contains("bad gateway") || low.contains("service unavailable") { signals.push("server_error_text".into()); }+            if low.contains("traceback (most recent call last)") || low.contains("unhandled exception") || low.contains("unhandled rejection") || low.contains("referenceerror") || low.contains("typeerror:") || low.contains("syntaxerror:") { signals.push("stack_trace_visible".into()); }+            if chars < 12 && elements < 8 { signals.push("effectively_blank".into()); }+            if chars > 40 && sheets == 0 { signals.push("no_stylesheets_loaded".into()); }+            if let Some(b) = p["brokenImgs"].as_u64() { if b > 0 { signals.push(format!("broken_images_{b}")); } }+            // pup-bridge#109: a chrome-error:// interstitial (net/TLS failure, or a bot-wall RST) must read as+            // BROKEN, not ok. It used to pass because the error page has a title and body text and no other+            // signal fired, so renderCheck told the caller the opposite of the truth.+            if p["errPage"].as_bool() == Some(true) {+                signals.push("chrome_error_page".into());+                let code = p["netError"].as_str().unwrap_or("").trim().to_string();+                if !code.is_empty() { signals.push(format!("net_error:{code}")); out["netError"] = json!(code); }+                out["fallbackHint"] = json!("This is Chrome's error page (a network/TLS failure or a bot-wall reset), NOT the site's content. Many electronics-vendor sites (st.com, mouser.com, digikey.com, arrow.com, analog.com) fingerprint pup's Chrome for Testing and block it, while the user's real Chrome loads them fine. Do not retry in pup: switch lanes. Open the page with nbrowser_open_window, or pull a file with nbrowser_fetch_url (the Adom browser extension drives the user's signed-in Chrome).");+            }+            if !signals.is_empty() { out["excerpt"] = json!(head.chars().take(180).collect::<String>()); }+        }+        Ok(_) => signals.push("probe_timed_out".into()),+        Err(_) => signals.push("probe_failed".into()),+    }+    out["ok"] = json!(signals.is_empty()); out["signals"] = json!(signals);+    out+}++/// Per-tab state the event pump maintains (keyed by CDP session id).+#[derive(Debug, Clone, Default)]+pub struct TabState { pub errors: Vec<Value>, pub dialog: Option<Value>, pub url: Option<String>, pub title: Option<String> }++pub const MAX_SCREENSHOT_DIM: u32 = 1568;   // Node: MAX_SCREENSHOT_DIM (Claude's optimal max)+const OFFSCREEN: i32 = -32000;              // Node: _OFFX/_OFFY birth position++/// A running browser pup is connected to (one per profile).+pub struct BrowserHandle { pub cdp: Cdp, pub launched: Launched }++pub struct Ctx {+    pub host: Arc<dyn Host>,+    pub ab: Arc<dyn AbClient>,+    pub settings: Arc<Settings>,+    pub installer: Arc<Installer>,+    pub sessions: Arc<Sessions>,+    pub browsers: Mutex<HashMap<String, Arc<BrowserHandle>>>,+    pub tabstate: events::TabStates,+    /// sid -> tab ids currently carrying focus emulation (2.0.459: was a Session field, and a session clone+    /// re-inserted by another path clobbered it, leaving the active tab unemulated after a re-attach).+    pub focus_emul: Mutex<HashMap<String, Vec<String>>>,+    /// CDP session -> a mouse button pressed whose release has not landed yet (2.0.460).+    pub pending_press: Mutex<HashMap<String, (f64, f64, String)>>,+    /// profile -> consecutive Browser.getVersion misses (2.0.461 wedged-browser watchdog).+    pub browser_stall: Mutex<HashMap<String, u32>>,+    /// profile -> when it last had zero pup windows (2.0.463 idle-browser reaper).+    pub browser_idle_since: Mutex<HashMap<String, u64>>,+    pub bridge_dir: PathBuf,+    pub port: u16,+    pub started: Instant,+    pub ad_version: Mutex<Option<String>>,+    pub self_ref: Mutex<std::sync::Weak<Ctx>>,+    pub toasts: Mutex<Vec<Value>>,+    pub vault: pup_vault::Vault,+    pub cards: pup_vault::Cards,+    pub consent: consent::Registry,+    pub tab_recs: record::TabRecs,+    pub screen_rec: record::ScreenRecState,+    pub highfps: highfps::Leases,+    pub last_audit: Mutex<Option<Value>>,+}++/// Callers that ARE the human at the desk (Node: USER_CALLERS): ownership never applies to them.+pub const USER_CALLERS: [&str; 4] = ["user-taskbar-menu", "user-toolbar", "user-pill", "user-dashboard"];+pub const SHOT_KEEP_DAYS: u64 = 14; pub const SHOT_KEEP_BYTES: u64 = 1_000_000_000;+pub const REC_KEEP_DAYS: u64 = 30; pub const REC_KEEP_BYTES: u64 = 3_000_000_000;++impl Ctx {+    pub fn arc(&self) -> Option<Arc<Ctx>> { self.self_ref.lock().unwrap().upgrade() }+    pub fn new_tabstate() -> events::TabStates { Arc::new(Mutex::new(HashMap::new())) }+    pub fn prefer_cft(&self) -> bool { self.settings.get("toolbarExtension", "on") != "off" }+    /// Screenshots: `<ADOM_BRIDGE_DATA_DIR>/screenshots` when ab passes the data dir (pup-bridge#98), else+    /// beside the exe as before. Retention: see `retention_sweep` (janitor).+    pub fn shots_dir(&self) -> PathBuf { static DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new(); let d = DIR.get_or_init(|| pup_core::data_subdir("screenshots", self.bridge_dir.join("screenshots"))).clone(); let _ = std::fs::create_dir_all(&d); d }+    pub fn recordings_dir(&self) -> PathBuf { static DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new(); let d = DIR.get_or_init(|| pup_core::data_subdir("recordings", self.bridge_dir.join("recordings"))).clone(); let _ = std::fs::create_dir_all(&d); d }+    /// One retention pass over screenshots (14 d / 1 GB) and recordings (30 d / 3 GB); returns the report.+    pub fn retention_sweep(&self) -> Value {+        let (sn, sb) = pup_core::retention_sweep(&self.shots_dir(), SHOT_KEEP_DAYS, SHOT_KEEP_BYTES);+        let (rn, rb) = pup_core::retention_sweep(&self.recordings_dir(), REC_KEEP_DAYS, REC_KEEP_BYTES);+        if sn + rn > 0 { pup_core::log(format!("[retention] screenshots: {sn} removed ({:.1} MB); recordings: {rn} removed ({:.1} MB)", sb as f64 / 1e6, rb as f64 / 1e6)); }+        json!({ "screenshots": { "removed": sn, "freedBytes": sb, "keepDays": SHOT_KEEP_DAYS, "keepBytes": SHOT_KEEP_BYTES }, "recordings": { "removed": rn, "freedBytes": rb, "keepDays": REC_KEEP_DAYS, "keepBytes": REC_KEEP_BYTES }, "ts": pup_core::now_ms() })+    }+    pub fn data_dirs(&self) -> Value {+        json!({ "dataDir": pup_core::data_dir().map(|p| p.to_string_lossy().replace('\\', "/")), "profiles": pup_browser::profiles_dir(&*self.host).to_string_lossy().replace('\\', "/"), "sessions": pup_browser::sessions_dir(&*self.host).to_string_lossy().replace('\\', "/"), "screenshots": self.shots_dir().to_string_lossy().replace('\\', "/"), "recordings": self.recordings_dir().to_string_lossy().replace('\\', "/") })+    }+}++/// The walls only a human can pass after a login submit (Node: detectHumanChallenge).+/// The login-wall detector (captcha / one-time code), one script for both lanes.+pub const WALL_JS: &str = r#"(() => { const q = (s) => { try { return document.querySelector(s); } catch (e) { return null; } }; const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };+      const otp = q('input[autocomplete="one-time-code"]') || q('input[name*="otp" i]') || q('input[id*="otp" i]') || q('input[name*="verification" i][inputmode="numeric"]') || q('input[name*="mfa" i]') || q('input[name*="totp" i]') || q('input[name*="2fa" i]');+      if (vis(otp)) return { kind: 'two_factor', evidence: 'one-time-code input: ' + (otp.getAttribute('name') || otp.getAttribute('id') || otp.getAttribute('autocomplete')) };+      const cap = Array.from(document.querySelectorAll('iframe')).find(f => /recaptcha|hcaptcha|turnstile|arkose|funcaptcha/i.test(f.src || '')); if (cap && vis(cap)) return { kind: 'captcha', evidence: 'captcha iframe: ' + cap.src.split('/')[2] };+      const t = (document.body && document.body.innerText || '').slice(0, 20000);+      if (/verification code|two[- ]factor|2-step verification|authenticator app|enter the code we sent|security code|one-time (pass)?code/i.test(t) && !q('input[type="password"]')) return { kind: 'two_factor', evidence: 'page text mentions a verification code and no password field is present' };+      if (/i'?m not a robot|prove you are human|verify you are human|security check/i.test(t)) return { kind: 'captcha', evidence: 'page text asks for a human check' };+      return null; })()"#;+pub fn detect_wall(cdp: &Cdp, sid: &str) -> Option<Value> {+    let js = WALL_JS;+    cdp.evaluate(sid, js, Duration::from_secs(4)).ok().filter(|v| v.is_object())+}++/// Route the events the pump cannot handle alone (bindings, screencast frames) to their owners.+pub fn install_event_hook(ctx: Arc<Ctx>) {+    events::set_hook(Box::new(move |method, sid, params| {+        match method {+            "Runtime.bindingCalled" => {+                let name = params["name"].as_str().unwrap_or(""); let payload = params["payload"].as_str().unwrap_or("").to_string();+                match name {+                    "__pupCredCapture" => vault::on_capture(&ctx, sid, &payload),+                    "__pupFloatyPush" => { if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.tabs.iter().any(|t| t.cdp_session == sid)) { let caller = Caller { thread: "user-floaty".into(), reason: "the user clicked the corner pencil".into(), ..Default::default() }; let tab = s.tabs.iter().find(|t| t.cdp_session == sid).map(|t| t.tab_id.clone()); pup_core::log(format!("[floaty] \"{}\" pencil clicked — arming capture", s.id)); let _ = annotate::start(&ctx, &caller, &json!({ "window": s.id, "tabId": tab, "tool": "pen", "takeover": true })); } }+                    _ => {}+                }+            }+            "Page.screencastFrame" => { if ctx.sessions.all().iter().all(|s| !s.tabs.iter().any(|t| t.cdp_session == sid)) { static N: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); if N.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 3 { pup_core::log(format!("[screencast] frame for a session no window owns: {sid} (known: {:?})", ctx.sessions.all().iter().flat_map(|s| s.tabs.iter().map(|t| t.cdp_session.clone())).collect::<Vec<_>>())); } }+                if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.tabs.iter().any(|t| t.cdp_session == sid)) { if let Some(b) = browser_for(&ctx, &s.profile) { record::on_frame(&ctx.tab_recs, &b.cdp, sid, params); } } }+            _ => {}+        }+    }));+}++/// The verbs this build serves natively.+pub const PHASE1_VERBS: &[&str] = &["pup_describe", "pup_readiness", "pup_prewarm", "pup_status", "pup_list_windows", "pup_my_windows", "pup_list_sessions", "pup_open_window", "pup_navigate", "pup_eval", "pup_evaluate", "pup_screenshot", "pup_close_window", "pup_close", "pup_close_all_windows", "pup_window_info",+    "pup_open_tab", "pup_list_tabs", "pup_switch_tab", "pup_close_tab", "pup_switch_window", "pup_click", "pup_select", "pup_hover", "pup_type", "pup_press_key", "pup_scroll", "pup_fill", "pup_wait", "pup_reload", "pup_back", "pup_forward", "pup_set_viewport", "pup_errors", "pup_dialog", "pup_input_dispatch", "pup_render_html", "pup_fetch_url", "pup_maximize",+    "pup_raise_os_window", "pup_focus_window", "pup_lower_os_window", "pup_alert_window", "pup_progress", "pup_configure", "pup_use", "pup_clear_cookies", "pup_clear_site_data", "pup_quit_idle_browsers", "pup_rescan", "pup_restart_browser", "pup_set_identity", "pup_audit_icons", "pup_dashboard",+    "pup_credentials", "credential_list", "credential_set", "credential_delete", "pup_login", "pup_google_accounts", "pup_google_signin", "pup_credential_reveal", "pup_card_set", "pup_cards", "pup_card_delete", "pup_fill_card", "pup_wiki_set_view", "pup_annotate", "pup_floaty_click",+    "pup_record_start", "pup_record_stop", "pup_record_status", "pup_record_list", "pup_recorder_open", "pup_recorder_close", "pup_screen_record_start", "pup_screen_record_stop", "pup_screen_record_status", "pup_screen_record_list", "pup_highfps_extend",+    "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_cache_prune", "pup_sweep_identities", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"];++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),+        "pup_prewarm" => prewarm(ctx, args),+        "pup_status" => pup_core::merge(status(ctx), json!({ "extensionLane": extdrive::lane_health(ctx) })),+        "pup_list_windows" => list_windows(ctx),+        "pup_my_windows" | "pup_list_sessions" => my_windows(ctx, caller),+        "pup_open_window" => with_bar(ctx, caller, args, "open", || open_window(ctx, caller, args)),+        "pup_navigate" => with_bar(ctx, caller, args, "navigate", || navigate(ctx, caller, args)),+        "pup_eval" | "pup_evaluate" => eval(ctx, caller, args),  // frame: handled inside eval+        "pup_screenshot" => { match resolve(ctx, caller, args) { Ok(s) => { let sid = s.id.clone(); activity::with_progress(ctx, &sid, "screenshot", || screenshot(ctx, caller, args)) } Err(_) => screenshot(ctx, caller, args) } },+        "pup_close_window" | "pup_close" => close_window(ctx, caller, args),+        // adom/pup-bridge#108 class: the verb existed in the contract, erred not_in_rust_yet and named no+        // supported equivalent. close_window already implements {all:true} = the caller's own windows.+        "pup_close_all_windows" => { let mut a = args.clone(); if let Some(m) = a.as_object_mut() { m.insert("all".into(), json!(true)); } close_window(ctx, caller, &a) }+        "pup_window_info" => window_info(ctx, caller, args),+        "pup_open_tab" => with_bar(ctx, caller, args, "open-tab", || tabs::open_tab(ctx, caller, args)),+        "pup_list_tabs" => tabs::list_tabs(ctx, caller, args),+        "pup_switch_tab" => tabs::switch_tab(ctx, caller, args),+        "pup_close_tab" => tabs::close_tab(ctx, caller, args),+        "pup_switch_window" => tabs::switch_window(ctx, caller, args),+        "pup_click" => input::click(ctx, caller, args),+        "pup_hover" => input::hover(ctx, caller, args),+        "pup_type" => input::type_text(ctx, caller, args),+        "pup_press_key" => input::press_key(ctx, caller, args),+        "pup_scroll" => input::scroll(ctx, caller, args),+        "pup_fill" => input::fill(ctx, caller, args),+        "pup_select" => input::select(ctx, caller, args),+        "pup_wait" => input::wait(ctx, caller, args),+        "pup_reload" => with_bar(ctx, caller, args, "navigate", || input::reload(ctx, caller, args)),+        "pup_back" => with_bar(ctx, caller, args, "navigate", || input::history(ctx, caller, args, true)),+        "pup_forward" => with_bar(ctx, caller, args, "navigate", || input::history(ctx, caller, args, false)),+        "pup_set_viewport" => input::set_viewport(ctx, caller, args),+        "pup_errors" => input::errors(ctx, caller, args),+        "pup_dialog" => input::dialog(ctx, caller, args),+        "pup_input_dispatch" => input::input_dispatch(ctx, caller, args),+        "pup_render_html" => input::render_html(ctx, caller, args),+        "pup_fetch_url" => input::fetch_url(ctx, caller, args),+        "pup_maximize" => input::maximize(ctx, caller, args),+        "pup_raise_os_window" | "pup_focus_window" => window::raise(ctx, caller, args),+        "pup_lower_os_window" => window::lower(ctx, caller, args),+        "pup_alert_window" => window::alert(ctx, caller, args),+        "pup_progress" => window::progress(ctx, caller, args),+        "pup_configure" => settings::configure(ctx, args),+        "pup_clear_cookies" => clear_cookies(ctx, caller, args),+        "pup_clear_site_data" => clear_site_data(ctx, caller, args),+        "pup_use" => settings::use_browser(ctx, args),+        "pup_quit_idle_browsers" => settings::quit_idle_browsers(ctx),+        "pup_rescan" => recovery::rescan(ctx, args),+        "pup_restart_browser" => extras::restart_browser(ctx, caller, args),+        "pup_set_identity" => identity::set_identity(ctx, caller, args),+        "pup_dashboard" => {+            let human = caller.thread == "user-taskbar-menu" || caller.thread == "user-dashboard";+            let url = format!("http://127.0.0.1:{}/dashboard", ctx.port);+            // The dashboard is a local page; it needs nothing from CDP. It opens like every other pup window: the+            // extension lane on adom-you (John, 2026-09-14: "why does pup_dashboard even need cdp?"). The Node-era+            // pup-dashboard profile is retired.+            let mut a = json!({ "window": "pup-dashboard", "url": url, "takeover": true });+            if human { a["foreground"] = json!(true); a["foregroundReason"] = json!("you clicked \"Open pup dashboard\" in the taskbar menu"); }+            let existed = ctx.sessions.exists("pup-dashboard");+            let r = if existed { navigate(ctx, caller, &a) } else { open_window(ctx, caller, &a) };+            if human { let _ = window::raise(ctx, caller, &json!({ "window": "pup-dashboard", "takeover": true, "foregroundReason": "you clicked \"Open pup dashboard\" in the taskbar menu" })); }+            pup_core::merge(r, json!({ "sessionId": "pup-dashboard", "dashboardUrl": format!("http://127.0.0.1:{}/dashboard", ctx.port), "reused": existed, "foregrounded": human, "_hint": if human { "The pup dashboard was raised because a human clicked the taskbar task." } else { "The pup dashboard is open in its own pup window (session pup-dashboard), BACKGROUNDED like any pup window; raise it with pup_raise_os_window {foregroundReason} if the user asked to see it." } }))+        }+        "pup_audit_icons" => identity::audit(ctx),+        "pup_credentials" | "credential_list" => vault::credentials(ctx),+        "credential_set" => vault::credential_set(ctx, args),+        "credential_delete" => vault::credential_delete(ctx, args),+        "pup_login" => vault::login(ctx, caller, args),+        "pup_credential_reveal" => vault::reveal(ctx, caller, args),+        "pup_card_set" => vault::card_set(ctx, args),+        "pup_cards" => vault::cards(ctx),+        "pup_card_delete" => vault::card_delete(ctx, args),+        "pup_fill_card" => vault::fill_card(ctx, caller, args),+        "pup_wiki_set_view" => wiki::set_view(ctx, caller, args),+        "pup_annotate" => annotate::start(ctx, caller, args),+        "pup_floaty_click" => annotate::floaty_click(ctx, caller, args),+        "pup_record_start" => record::start(ctx, caller, args),+        "pup_record_stop" => record::stop(ctx, caller, args),+        "pup_record_status" => record::status(ctx, caller, args),+        "pup_record_list" => record::list(ctx),+        "pup_recorder_open" => record::recorder_open(ctx, args),+        "pup_recorder_close" => record::recorder_close(ctx),+        "pup_screen_record_start" => record::screen_start(ctx, args),+        "pup_screen_record_stop" => record::screen_stop(ctx),+        "pup_screen_record_status" => record::screen_status(ctx),+        "pup_screen_record_list" => record::list(ctx),+        "pup_highfps_extend" => highfps::extend(ctx, caller, args),+        "pup_screenshot_element" => extras::screenshot_element(ctx, caller, args),+        "pup_screenshot_full_res" => extras::screenshot_full_res(ctx, caller, args),+        "pup_download" => extras::download(ctx, caller, args),+        "pup_launch" => extras::launch(ctx, caller, args),+        "pup_deps" => extras::deps(ctx),+        "pup_install_toolbar" => extras::install_toolbar(ctx),+        "pup_reap_profiles" => extras::reap_profiles(ctx, args),+        "pup_cache_prune" => extras::cache_prune(ctx, args.get("dryRun").and_then(|v| v.as_bool()).unwrap_or(false)),+        "pup_sweep_identities" => { let r = identity::sweep_dead_identities(ctx, "verb"); ok(pup_core::merge(r, json!({ "_hint": "Unregistered every pup taskbar identity (its registry key and any Start Menu anchor) that no live window holds, and removed icon generations nothing references. pup runs this a minute after every boot and releases each window's identity on close (2.0.666)." }))) },+        "pup_uninstall" => extras::uninstall(ctx, args),+        "pup_move_to_workspace" => extras::move_to_workspace(ctx, caller, args),+        "pup_assisted_click" => assist::assisted_click(ctx, caller, args),+        "pup_import_scan" => import::import_scan(ctx),+        "pup_google_accounts" => google::google_accounts(ctx, caller, args),+        "pup_google_signin" => google::google_signin(ctx, caller, args),+        "pup_import_browser_logins" => import::import_browser_logins(ctx, caller, args),+        "pup_vault_sync" => vault::vault_sync(ctx, caller, args),+        "pup_vault_export" => vault::vault_export(ctx, caller),+        "pup_vault_import" => vault::vault_import(ctx, caller, args),+        v if v.starts_with("desktop_record") || v.starts_with("desktop_recorder") => err("wrong_bridge", format!("{v} is ab-core's native desktop recorder (ab 2.1.36+), not a pup verb; this ab routed it to pup by a stale prefix claim (adom-bridge#150)"), Some("Update Adom Bridge so desktop_record_* routes to ab-core: its native recorder (GPU-accelerated, GPU frame offload when available, system audio, MP4) is the one to use for desktop recording. Until then pup's fallback works, a Google Meet style screen share that runs almost anywhere: pup_recorder_open {reason} then pup_screen_record_start {confirmDesktopNotTabRecording:true, reason}.")),+        v if v.starts_with("pup_") || v.starts_with("credential_") => err("not_in_rust_yet", format!("{v} is not served by the Rust bridge yet (Phase 1 carries {} verbs)", PHASE1_VERBS.len()), Some("This is the Rust port of pup (docs/PLAN-rust-port.md). The verb exists in the contract and lands in a later phase; the Node build still serves it on the public tier.")),+        v => err("unknown_verb", format!("unknown verb {v}"), Some("pup_describe lists every verb this bridge serves.")),+    };+    // 2.0.672 (pup-bridge#124): ab's screenshot delivery contract is fullPath (+ safePath, the <=1568 px copy). When a+    // reply carries those, ab's CLI pulls the image to the caller and adds localPath/localSafePath, so a container AI can+    // Read it without a second call. pup answered with filePath only, so nothing was pulled. Every image reply now+    // carries both keys (filePath stays for older callers). The image pup writes is already the shrunk copy when needed.+    image_delivery(&mut r);+    // lastAgentUpdate {tabId, verb, note, ts}: every driving verb that names a window (Node: touch sites)+    if !PROBE_VERBS.contains(&verb) && !PASSIVE_VERBS.contains(&verb) {+        if let Some(sid) = r.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()) {+            let tab = r.get("tabId").and_then(|v| v.as_str()).map(|s| s.to_string());+            ctx.sessions.with_mut(&sid, |x| { x.last_verb = Some(verb.to_string()); if tab.is_some() { x.last_tab = tab; } x.last_agent_update_ms = pup_core::now_ms(); });+        }+    }+    if let Some(o) = r.as_object_mut() {+        // 2.0.672: many handlers put their real fields only in the `output` JSON string. ab used to lift those onto+        // the reply; ab 2.1.14x stopped (measured 2026-09-22: "ok" gone from every error reply, and pup_record_stop's+        // durationMs became the 615 ms CALL time instead of the 8 s recording, failing the smoke). pup lifts them itself+        // now, never overwriting a top-level key, and BEFORE the call time is stamped.+        if let Some(inner) = o.get("output").and_then(|v| v.as_str()).filter(|s| s.starts_with('{')).and_then(|s| serde_json::from_str::<Value>(s).ok()) {+            if let Some(io) = inner.as_object() { for (k, v) in io { if !o.contains_key(k) { o.insert(k.clone(), v.clone()); } } }+        }+        // and every reply says ok true/false itself (the contract fixtures expect it; ab used to add it)+        if !o.contains_key("ok") {+            let failed = o.get("success") == Some(&json!(false)) || o.contains_key("errorCode") || o.get("error").and_then(|e| e.as_str()).map(|e| !e.is_empty()).unwrap_or(false);+            o.insert("ok".into(), json!(!failed));+        }+        o.entry("durationMs").or_insert(json!(t0.elapsed().as_millis() as u64)); o.insert("shell".into(), json!("rust"));+        // Node sendJSON: the catalog's canonical hint/related/next/pitfalls when the handler set none+        if let Some(m) = catalog_meta(verb) {+            if !o.contains_key("_hint") { if let Some(h) = m["hint"].as_str() { o.insert("_hint".into(), json!(h)); } }+            if !o.contains_key("related") { if m["related"].as_array().map(|a| !a.is_empty()).unwrap_or(false) { o.insert("related".into(), m["related"].clone()); } }+            if !o.contains_key("_next") { if m["related"].as_array().map(|a| !a.is_empty()).unwrap_or(false) { o.insert("_next".into(), m["related"].clone()); } }+            if !o.contains_key("pitfalls") { if m["pitfalls"].as_array().map(|a| !a.is_empty()).unwrap_or(false) { o.insert("pitfalls".into(), m["pitfalls"].clone()); } }+        }+        if !PROBE_VERBS.contains(&verb) && !o.contains_key("_cleanupReport") { if let Some(rep) = drain_cleanup_reports() { o.insert("_cleanupReport".into(), json!(rep)); } }+        // Node maybeAttachBugInvite: every failure carries the invite; substantive successes carry it once per process, then every 40th+        if ctx.settings.get("reportIssues", "on") != "off" && !o.contains_key("_reportIssues") {+            let is_error = o.get("success") == Some(&json!(false)) || o.contains_key("errorCode") || o.get("error").and_then(|e| e.as_str()).map(|e| !e.is_empty()).unwrap_or(false);+            if is_error { o.insert("_reportIssues".into(), json!(bug_report_invite(true))); }+            else if !PROBE_VERBS.contains(&verb) {+                let mut g = BUG_INVITE.lock().unwrap(); g.1 += 1;+                if !g.0 || g.1 % BUG_INVITE_EVERY == 0 { g.0 = true; o.insert("_reportIssues".into(), json!(bug_report_invite(false))); }+            }+        }+        // Session-scoped notices (Node: attachSessionNotices): an emulated viewport warning on+        // measurement verbs, and a one-shot disruption notice. Resolve the session from output or the envelope.+        {+            let sid = o.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string())+                .or_else(|| o.get("output").and_then(|v| v.as_str()).filter(|s| s.starts_with('{')).and_then(|s| serde_json::from_str::<Value>(s).ok()).and_then(|v| v["sessionId"].as_str().map(|s| s.to_string())));+            if let Some(sid) = sid { if let Some(sess) = ctx.sessions.get(&sid) {+                const VP_SENSITIVE: [&str; 8] = ["pup_screenshot", "pup_screenshot_full_res", "pup_eval", "pup_list_windows", "pup_my_windows", "pup_status", "pup_navigate", "pup_reload"];+                if VP_SENSITIVE.contains(&verb) && !o.contains_key("_viewportOverride") {+                    if let Some(ev) = &sess.emulated { let w = ev["width"].as_u64().unwrap_or(0); let h = ev["height"].as_u64().unwrap_or(0);+                        if w > 0 { let outer = sess.last_outer_width;+                            let shape = match outer { Some(ow) if w > ow + 4 => format!(" The emulation is WIDER than the OS window ({w} vs {ow}), so the user is seeing this page CROPPED: about {}px is off the right edge.", w - ow), Some(ow) if w + 4 < ow => format!(" The emulation is NARROWER than the OS window ({w} vs {ow}), so the user sees dead space beside the page."), _ => String::new() };+                            o.insert("_viewportOverride".into(), json!(format!("This window has an EMULATED VIEWPORT of {w}x{h}{}, so it is NOT tracking the real OS window.{shape} Anything you measure in the page (innerWidth, a bounding rect) describes the emulation, not what the user sees. Clear it with pup_set_viewport {{\"reset\":true}} when done.", ev["deviceScaleFactor"].as_f64().filter(|d| *d != 1.0).map(|d| format!(" @{d}x")).unwrap_or_default())));+                        }+                    }+                }+                if !o.contains_key("_sessionNotice") { if let Some(n) = &sess.disruption_notice { o.insert("_sessionNotice".into(), json!(n)); ctx.sessions.with_mut(&sid, |x| x.disruption_notice = None); } }+            } }+        }+        // render verbs: the verdict rides the response (Node: RENDER_VERBS block in sendJSON)+        if RENDER_VERBS.contains(&verb) && o.get("ok") != Some(&json!(false)) && o.get("success") != Some(&json!(false)) {+            if let Some(rc) = o.get("renderCheck").cloned() { if rc["ok"] == json!(false) { if let Some(sig) = rc["signals"].as_array() { if !sig.is_empty() {+                let st = rc["httpStatus"].as_u64().map(|s| format!(" (HTTP {s})")).unwrap_or_default();+                let lead = format!("â›” THE PAGE IS STILL BROKEN: {}{st}{}. Do NOT report success. See _verifyRender. ", sig.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(", "), rc["excerpt"].as_str().map(|e| format!(" — it says \"{}\"", e.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(120).collect::<String>())).unwrap_or_default());+                let cur = o.get("_hint").and_then(|h| h.as_str()).unwrap_or("").to_string();+                let fb = rc["fallbackHint"].as_str().map(|h| format!("{h} ")).unwrap_or_default();+                if !cur.contains("STILL BROKEN") && !cur.contains("IS BROKEN") { o.insert("_hint".into(), json!(format!("{lead}{fb}{cur}"))); }+            } } } }+            if !o.contains_key("_verifyRender") { o.insert("_verifyRender".into(), json!(VERIFY_RENDER)); }+            if !o.contains_key("_urlLint") {+                let u = o.get("url").and_then(|v| v.as_str()).or_else(|| args.get("url").and_then(|v| v.as_str())).unwrap_or("");+                if let Some(l) = lint::url_lint(u, &caller.container) { o.insert("_urlLint".into(), l["message"].clone()); o.insert("urlLintCode".into(), l["code"].clone()); }+            }+        }+        if let Some(sid) = o.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()) { if let Some(h) = ctx.sessions.with_mut(&sid, |x| x.pending_cred_hint.take()).flatten() { o.insert("_credentialCaptured".into(), json!(h)); } }+        // THE ab ENVELOPE (adom-bridge cli/src/relay.rs browser_command, measured at the 2.0.402 candidate+        // install): ab computes success = bridge.success (absent = false) and output = bridge.output (absent+        // = ""); the CLI then RETURNS THE PARSED `output` ALONE when success is true, and collapses a+        // non-JSON output to {success, output}. So every success carries success:true and an `output`+        // that is the JSON of the WHOLE payload, exactly as the Node build's sendJSON sites did.+        let is_err = o.get("success") == Some(&json!(false)) || o.get("ok") == Some(&json!(false)) || o.contains_key("errorCode");+        if !is_err {+            o.insert("success".into(), json!(true));+            if !o.get("output").and_then(|v| v.as_str()).map(|s| s.starts_with('{')).unwrap_or(false) {+                let mut inner = o.clone(); inner.remove("output");+                o.insert("output".into(), json!(Value::Object(inner).to_string()));+            }+        } else { o.entry("success").or_insert(json!(false)); o.entry("output").or_insert(json!("")); }+        // Node v1.9.98: ab/the CLI unwrap `output` and drop the envelope on success, so the advisory+        // fields have to live INSIDE output too (never overwriting what the handler put there).+        if let Some(inner_s) = o.get("output").and_then(|v| v.as_str()).filter(|s| s.starts_with('{')).map(|s| s.to_string()) {+            if let Ok(mut inner) = serde_json::from_str::<Value>(&inner_s) { if let Some(i) = inner.as_object_mut() {+                let sid = i.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()).or_else(|| o.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()));+                if let Some(sid) = sid { if !i.contains_key("window") { i.insert("window".into(), json!(sid)); if !i.contains_key("aiThread") { if let Some(owner) = ctx.sessions.get(&sid).and_then(|x| x.owner) { i.insert("aiThread".into(), json!(owner)); } } } }+                for k in ["_reportIssues", "_cleanupReport", "_autoLogin", "_credentialCaptured", "_hint", "_next", "related", "pitfalls", "renderCheck", "_verifyRender"] { if let Some(v) = o.get(k) { if !i.contains_key(k) { i.insert(k.into(), v.clone()); } } }+                o.insert("output".into(), json!(inner.to_string()));+            } }+        }+    }+    r+}++// ── resolution: window | sessionId | the caller's own window ───────────────────────────++fn window_arg(args: &Value) -> Option<String> {+    for k in ["window", "sessionId", "session", "ai-thread", "aiThread"] { if let Some(s) = args.get(k).and_then(|v| v.as_str()) { if !s.trim().is_empty() { return Some(if k == "ai-thread" || k == "aiThread" { thread_default_session_id(s) } else { s.trim().to_string() }); } } }+    None+}++/// The pup window and tab showing `url`: exact URL first, then equal ignoring the query, then the+/// longest prefix match ignoring the query (the popup's own rule). None when no pup tab shows it.+pub fn session_for_tab_url(ctx: &Ctx, url: &str) -> Option<(Session, Tab)> {+    let base = |u: &str| u.split('?').next().unwrap_or("").trim_end_matches('/').to_string();+    let want = base(url);+    // 2.0.471: a window whose browser is connected wins over one whose browser is gone, and among those the most+    // recently driven wins (a leftover session at the same page used to take the toolbar pen: annotate_inject_failed).+    // 2.0.472: the FOREGROUND window wins outright: a toolbar/jump-list click comes from the window the user is+    // looking at, and its payload carries only the tab URL (winvm ralph: the pen landed on a recovered background+    // window's tab at the same page and failed to inject there).+    let fg = ctx.host.foreground();+    let mut all = ctx.sessions.all();+    all.sort_by_key(|s| (!(fg != 0 && s.hwnd == Some(fg)), !transport::lane_for(ctx, s).is_ok(), std::cmp::Reverse(s.last_agent_update_ms)));+    for s in &all { if let Some(t) = s.tabs.iter().find(|t| t.url == url) { return Some((s.clone(), t.clone())); } }+    for s in &all { if let Some(t) = s.tabs.iter().find(|t| !want.is_empty() && base(&t.url) == want) { return Some((s.clone(), t.clone())); } }+    let mut best: Option<(usize, Session, Tab)> = None;+    for s in &all { for t in &s.tabs { let b = base(&t.url); if b.len() > 12 && want.starts_with(&b) && best.as_ref().map(|x| b.len() > x.0).unwrap_or(true) { best = Some((b.len(), s.clone(), t.clone())); } } }+    best.map(|(_, s, t)| (s, t))+}++/// 2.0.699: the taskbar progress bar on the window a page-loading verb works in. Only pup_screenshot was ever+/// wrapped here; the extension-lane wrapper sat on a route these verbs no longer take, so open, navigate, new+/// tab, reload, back and forward painted nothing (John, 2026-10-05). A window with no handle yet (being born)+/// gets no bar; start_icon_apply covers its first seconds.+fn with_bar(ctx: &Ctx, caller: &Caller, args: &Value, op: &str, work: impl FnOnce() -> Value) -> Value {+    let r = match resolve(ctx, caller, args) { Ok(s) => activity::with_progress(ctx, &s.id, op, work), Err(_) => work() };+    wall_check(ctx, caller, args, r)+}++/// 2.0.712: say when a page load landed on a bot wall, and warn before a thread trips one. A Cloudflare challenge+/// page used to come back as an ordinary successful load, so a thread kept loading DigiKey pages 7 s apart and dug+/// the wall deeper (John's ECAS window, 2026-10-11). Signatures come from the electronics-sites skillpack.+const BOT_WALL_JS: &str = r#"(()=>{const t=(document.title||'').slice(0,200);const b=((document.body&&document.body.innerText)||'').slice(0,3000);const short=b.length<1500;+if(/just a moment|performing security verification|checking your browser/i.test(t)||(short&&/performing security verification|verify you are human/i.test(b))||document.querySelector('iframe[src*="challenges.cloudflare.com"]'))return 'Cloudflare';+if(/access to this page has been denied/i.test(t+' '+b)||/press (&|and) hold/i.test(b)||document.querySelector('#px-captcha,[id^="px-captcha"]'))return 'PerimeterX';+if(/^access denied/i.test(t)&&short)return 'Akamai';+if(document.querySelector('iframe[src*="captcha-delivery.com"]'))return 'DataDome';+if(short&&/verify you are a human|are you a robot|unusual traffic/i.test(b))return 'bot check';+return ''})()"#;+const PACED_HOSTS: [&str; 8] = ["digikey.", "mouser.", "arrow.com", "ti.com", "analog.com", "st.com", "octopart.com", "newark.com"];+static LAST_LOAD: std::sync::Mutex<Option<HashMap<String, u64>>> = std::sync::Mutex::new(None);++fn wall_check(ctx: &Ctx, caller: &Caller, args: &Value, mut r: Value) -> Value {+    if r["ok"] == json!(false) || r["success"] == json!(false) { return r; }+    let Ok(s) = resolve(ctx, caller, args) else { return r };+    let Some(t) = s.active().cloned() else { return r };+    let host = t.url.split("//").nth(1).unwrap_or("").split('/').next().unwrap_or("").to_lowercase();+    // pacing: a second load on a walled vendor within 15 s, by any thread on this desktop+    if let Some(key) = PACED_HOSTS.iter().find(|h| host.contains(**h)) {+        let now = pup_core::now_ms();+        let mut g = LAST_LOAD.lock().unwrap(); let m = g.get_or_insert_with(HashMap::new);+        if let Some(prev) = m.insert(key.to_string(), now) {+            let gap = now.saturating_sub(prev);+            if gap < 15_000 { r["_paceWarning"] = json!(format!("{host} was loaded {} s ago. This site walls automated browsers that load pages in quick succession; wait at least 15 s between pages here, and get stock, price and datasheets through adom-parts-search instead of loading product pages.", gap / 1000)); }+        }+    }+    let Ok(lane) = transport::lane_for(ctx, &s) else { return r };+    let vendor = transport::evaluate(&lane, &t, BOT_WALL_JS).ok().and_then(|v| v.as_str().map(|x| x.to_string()).or_else(|| v["value"].as_str().map(|x| x.to_string()))).unwrap_or_default();+    if vendor.is_empty() { return r; }+    pup_core::log(format!("[wall] \"{}\": {host} answered with a {vendor} bot check ({})", s.id, t.url.chars().take(100).collect::<String>()));+    r["botWall"] = json!({ "vendor": vendor, "host": host, "url": t.url });+    r["_hint"] = json!(format!("BOT WALL: {host} answered with a {vendor} bot check instead of the page. Do NOT reload or load more pages from {host} now; that makes it worse. For parts data (stock, price, datasheet) use adom-parts-search. If you need this page itself: ask the user for ONE click on the check in this window (pup_alert_window or pup_raise_os_window with a reason), or open it in the user's own browser with nb. Never try to solve the check yourself."));+    r+}++pub fn resolve(ctx: &Ctx, caller: &Caller, args: &Value) -> Result<Session, Value> {+    let explicit = window_arg(args);+    let id = match explicit.clone() {+        Some(id) => id,+        None => {+            if caller.thread.is_empty() {+                return Err(err("caller_identity_required", "no window given and no --ai-thread to key one on", Some("Pass --ai-thread <your thread name> (the CLI flag) so pup can resolve your own window, or name one with {window:\"...\"}.")));+            }+            // 2.0.453 (John, woke from sleep, toolbar pen did nothing): the Adom toolbar identifies its window by+            // the TAB URL (`_tabUrl`, Node's contract); the Rust port never resolved it, so a pen click landed+            // here with thread "user-toolbar" and no window. Any tab in any pup window matches, exact first.+            // 2.0.702: the browser's own tab id first. A page that changes its address in place (Slack, Gmail, YouTube)+            // no longer matched pup's recorded URL, so the toolbar pen resolved no window and did nothing (John's Slack+            // window, 2026-10-07). The id never changes.+            if let Some(tid) = args.get("_tabId").and_then(|v| v.as_i64()).map(|v| v.to_string()) {+                if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.tabs.iter().any(|t| t.target_id == tid)) { return Ok(s); }+            }+            if let Some((s, _)) = args.get("_tabUrl").and_then(|u| u.as_str()).and_then(|u| session_for_tab_url(ctx, u)) { return Ok(s); }+            // last resort for a human click: the pup window in front is the one they clicked in+            if args.get("_tabUrl").is_some() || args.get("_tabId").is_some() {+                let fg = ctx.host.foreground();+                if fg != 0 { if let Some(s) = ctx.sessions.all().into_iter().find(|s| s.hwnd == Some(fg)) { return Ok(s); } }+            }+            let d = thread_default_session_id(&caller.thread);+            if ctx.sessions.exists(&d) { d } else {+                let mine = ctx.sessions.owned_by(&caller.thread);+                if mine.len() == 1 { mine[0].id.clone() } else { return Err(err("no_window", format!("thread \"{}\" has no pup window", caller.thread), Some("pup_open_window {url} first; later verbs with no ids resolve to that window. pup_my_windows {} lists what you own."))); }+            }+        }+    };+    match ctx.sessions.get(&id) {+        Some(s) => {+            // Ownership guard (Node: ownershipRefusal): another thread's window needs takeover:true.+            // A HUMAN caller (jump-list task, toolbar button, dashboard click) is exempt, as in Node's+            // USER_CALLERS: the user owns every window. Missing in the Rust port until 2.0.424, so every+            // window-targeting jump-list task answered not_your_window (John, 2026-09-06).+            if let Some(owner) = s.owner.as_deref() { if !caller.thread.is_empty() && !USER_CALLERS.contains(&caller.thread.as_str()) && owner != caller.thread {+                if args.get("takeover").and_then(|t| t.as_bool()) != Some(true) {+                    return Err(pup_core::merge(err("not_your_window", format!("window \"{}\" belongs to thread \"{}\", not you (\"{}\")", id, owner, caller.thread),+                        Some("Open your OWN window instead: pup_open_window with NO window id gives your thread its own. Pass takeover:true ONLY if the USER explicitly told you to repurpose this window — every takeover is logged with both thread names.")), json!({ "owner": owner, "window": id })));+                }+                // 2.0.491: LOG every sanctioned takeover, both thread names (Node did this; the Rust port+                // dropped it, so a thread could drive another thread's window all day leaving no trace).+                pup_core::log(format!("[takeover] \"{}\": thread \"{}\" acted on \"{}\"'s window (takeover:true — sanctioned only when the user asked). Reason: {}",+                    id, caller.thread, owner, if caller.reason.is_empty() { "(none given)" } else { &caller.reason }));+            } }+            Ok(s)+        }+        None => Err(pup_core::merge(err("session_not_found", format!("no pup window named \"{id}\""), Some("pup_list_windows {} shows what is open; pup_open_window creates one.")), json!({ "window": id }))),+    }+}++pub fn touch(ctx: &Ctx, sid: &str, note: Option<&str>) { ctx.sessions.with_mut(sid, |x| { x.last_agent_update_ms = pup_core::now_ms(); if let Some(n) = note { x.last_note = Some(n.to_string()); } }); }++/// Close top-level about:blank page targets nobody tracks: the initial window an older pup build+/// launched Chrome with (it never got a session, an identity or a close), which shows up as a stock+/// Chrome for Testing button on the taskbar. Runs once per browser connect, BEFORE the handle is+/// published, so no verb can be creating a target in the gap. Popups (openerId) and every target a+/// session owns are never touched.+fn close_orphan_blank_targets(ctx: &Ctx, cdp: &Cdp, profile: &str) {+    // Orphans: (1) the about:blank launch window an older build left behind, and (2) whole WINDOWS no+    // session owns any more, left by a close that raced a bridge respawn (John's laptop, 2026-09-06:+    // three leftover windows wearing old tiles). A window is orphaned only when NONE of its tabs belongs+    // to a session, so a user's extra tab in a live pup window is never touched; popups never are.+    let Ok(targets) = cdp.targets() else { return };+    let owned: std::collections::HashSet<String> = ctx.sessions.all().iter().flat_map(|s| s.tabs.iter().map(|t| t.target_id.clone())).collect();+    let pages: Vec<&Value> = targets.iter().filter(|t| t["type"] == json!("page") && !t.get("openerId").and_then(|o| o.as_str()).map(|o| !o.is_empty()).unwrap_or(false)).collect();+    let mut win_of: HashMap<String, i64> = HashMap::new();+    for t in &pages { if let Some(tid) = t["targetId"].as_str() { if let Ok((wid, _)) = cdp.window_for_target(tid) { win_of.insert(tid.to_string(), wid); } } }+    let owned_windows: std::collections::HashSet<i64> = win_of.iter().filter(|(tid, _)| owned.contains(*tid)).map(|(_, w)| *w).collect();+    let mut closed = 0;+    for t in &pages {+        let tid = t["targetId"].as_str().unwrap_or(""); if tid.is_empty() || owned.contains(tid) { continue; }+        let blank = t["url"].as_str().map(|u| u == "about:blank" || u.is_empty()).unwrap_or(false);+        let orphan_window = win_of.get(tid).map(|w| !owned_windows.contains(w)).unwrap_or(false);+        if blank || orphan_window { if cdp.close_target(tid).is_ok() { closed += 1; pup_core::log(format!("[launch] closed orphan {} in profile \"{profile}\": {}", if blank { "about:blank window" } else { "window no session owns" }, t["url"].as_str().unwrap_or("").chars().take(60).collect::<String>())); } }+    }+    if closed > 0 { pup_core::log(format!("[launch] {closed} orphan window(s) closed in profile \"{profile}\"")); }+}++/// 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::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()) }++/// Get or launch the browser for a profile: reuse a live DevTools port, else spawn-verify each+/// candidate in order (Node: getOrLaunchBrowser).+fn get_or_launch(ctx: &Ctx, profile: &str, high_fps: bool, insecure_dev: bool) -> Result<Arc<BrowserHandle>, Value> {+    set_target_chrome_from_host(&*ctx.host);   // learn the installed consumer Chrome version once, before any attach+    Cdp::quiet_runtime(ctx.settings.get("cdpQuiet", "off") == "on");   // pup-bridge#109: skip the Runtime.enable leak+    pup_browser::set_stock_flags(ctx.settings.get("stockFlags", "off") == "on");   // pup-bridge#109: near-stock launch flags+    if let Some(b) = browser_for(ctx, profile) { return Ok(b); }+    if ext_lane_owns_profile(ctx, profile) {+        return Err(err("profile_owned_by_ext_lane",+            format!("the extension lane is running on profile \"{profile}\", and Chrome allows only one process per profile directory"),+            Some("This lane deliberately runs with NO debug port, which is what lets vendor sites load. Drive it with pup_open_window / pup_navigate / pup_eval, or close it with pup_close_window first if you specifically need CDP.")));+    }+    let profile_dir = pup_browser::profiles_dir(&*ctx.host).join(profile);+    if let Some(port) = pup_browser::existing_port(&profile_dir) {+        if let Ok(cdp) = Cdp::connect_port(port, Duration::from_secs(5)) {+            let kind = cdp.version().ok().and_then(|v| v.get("product").and_then(|p| p.as_str()).map(|s| s.to_string())).unwrap_or_default();+            let cand = pup_browser::launch_candidates(&*ctx.host, ctx.prefer_cft()).into_iter().find(|c| kind.contains("Chrome") && (c.kind == "chrome-for-testing" || c.kind == "chrome") || kind.contains("Edg") && c.kind == "edge").unwrap_or(Candidate { kind: "chrome".into(), source: "reconnect".into(), executable_path: PathBuf::new(), build_id: None });+            pup_core::log(format!("[launch] reconnected to a live {} on :{port} for profile \"{profile}\"", cand.kind));+            let pid = cdp.send(None, "SystemInfo.getProcessInfo", json!({}), Duration::from_secs(5)).ok().and_then(|v| v["processInfo"].as_array().and_then(|a| a.iter().find(|p| p["type"] == json!("browser")).and_then(|p| p["id"].as_u64()))).unwrap_or(0) as u32;+            let h = Arc::new(BrowserHandle { cdp, launched: Launched { pid, cdp_port: port, candidate: cand, profile_dir, reused: true } });+            if ctx.settings.get("prePermissions", "off") == "on" { grant_default_permissions(&h.cdp); }+            close_orphan_blank_targets(ctx, &h.cdp, profile);+            events::start_pump(profile.into(), h.cdp.clone(), ctx.sessions.clone(), ctx.tabstate.clone());+            ctx.host.watch_process(h.launched.pid);+            ctx.browsers.lock().unwrap().insert(profile.into(), h.clone());+            return Ok(h);+        }+    }+    let cands = pup_browser::launch_candidates(&*ctx.host, ctx.prefer_cft());+    if cands.is_empty() {+        let rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());+        if rd.get("lowDisk").and_then(|b| b.as_bool()) == Some(true) {+            return Err(pup_core::merge(err("chrome_install_no_disk", format!("No Chrome/Edge on this machine and only {} MB free — not enough to install Chrome for Testing (~800 MB needed).", rd.get("diskFreeMb").cloned().unwrap_or(Value::Null)), Some("Free up disk space, or install Chrome or Edge, then retry.")), json!({ "readiness": rd })));+        }+        ctx.installer.ensure_background(ctx.host.clone());+        return Err(pup_core::merge(err("chrome_for_testing_installing", "This box has no installed Chrome or Edge, so pup is fetching Chrome for Testing as the last-resort browser (~150 MB, one time).", Some("LAST-RESORT PROVISIONING (not an error): poll pup_readiness {} until ready:true (it shows percent + ETA), then call pup_open_window again.")), json!({ "installing": true, "stillRunning": true, "statusVerb": "pup_readiness", "readiness": rd })));+    }+    pup_core::log(format!("[launch] {} browser candidate(s): {}", cands.len(), cands.iter().map(|c| format!("{}({})", c.kind, c.source)).collect::<Vec<_>>().join(" -> ")));+    let mut errors = Vec::new();+    for c in &cands {+        let t0 = Instant::now();+        let exts = pup_browser::stage_extensions(&*ctx.host, ctx.port, profile, ctx.prefer_cft());+        match pup_browser::launch(&*ctx.host, c, &profile_dir, high_fps, insecure_dev, &exts) {+            Ok(l) => match Cdp::connect_port(l.cdp_port, Duration::from_secs(10)) {+                Ok(cdp) => {+                    pup_core::log(format!("[launch] {}({}) up on :{} pid {} in {}ms", c.kind, c.source, l.cdp_port, l.pid, t0.elapsed().as_millis()));+                    pup_browser::write_default(&*ctx.host, c, false);+                    let h = Arc::new(BrowserHandle { cdp, launched: l });+                    // pup-bridge#109: real Chrome pre-grants NOTHING (every permission answers "prompt"); pup used+                    // to grant 32 up front, and a bot manager reads Notification.permission=="granted" on a first+                    // visit as automation. prePermissions:on restores the blanket grant for a debug flow.+                    if ctx.settings.get("prePermissions", "off") == "on" { grant_default_permissions(&h.cdp); }+                    close_orphan_blank_targets(ctx, &h.cdp, profile);+                    events::start_pump(profile.into(), h.cdp.clone(), ctx.sessions.clone(), ctx.tabstate.clone());+                    ctx.host.watch_process(h.launched.pid);+                    ctx.browsers.lock().unwrap().insert(profile.into(), h.clone());+                    return Ok(h);+                }+                Err(e) => { errors.push(format!("{}: connect {e}", c.kind)); let _ = ctx.host.kill(l.pid); }+            },+            Err(e) => errors.push(format!("{}: {e}", c.kind)),+        }+        pup_core::log(format!("[launch] {} failed, falling through: {}", c.kind, errors.last().cloned().unwrap_or_default()));+    }+    Err(err("browser_launch_failed", format!("every browser candidate failed to launch: {}", errors.join(" | ")), Some("pup_readiness {} shows the candidates and the last install error; pup_use can pin a different browser.")))+}++/// Which tabs carry focus emulation (Emulation.setFocusEmulationEnabled). It is what lets pup drive a window+/// parked BEHIND the user's windows: the page keeps reporting focus, so CDP input lands and focus events fire.+/// Until 2.0.458 every attached tab carried it, and a tab that reports focus also reports VISIBLE, so Chrome+/// never throttled its timers and Memory Saver never discarded it. Measured on John's laptop, 2026-09-10: a+/// 21-tab pup window ran EVERY background tab's timers at full rate (60 setTimeout(16ms) fires per second where+/// a background tab should get about 1), held 2.2 GB that Chrome could not reclaim, and burned 5.2 % CPU against+/// 0.2 % for his other Chrome, while the machine sat at 260 MB free of 32 GB and scrolling stuttered.+/// Now: the window's ACTIVE tab (pup must be able to drive it), plus at most the one tab a verb is driving.+pub fn focus_emulate(ctx: &Ctx, sid: &str, extra: Option<&str>) {+    let Some(s) = ctx.sessions.get(sid) else { return };+    let Some(b) = browser_for(ctx, &s.profile) else { return };+    let mut want: Vec<String> = s.active().map(|t| t.tab_id.clone()).into_iter().collect();+    if let Some(x) = extra { if !want.iter().any(|w| w == x) { want.push(x.to_string()); } }+    let have = ctx.focus_emul.lock().unwrap().get(sid).cloned().unwrap_or_default();+    if want == have { return; }+    for t in &s.tabs {+        let on = want.iter().any(|w| *w == t.tab_id);+        if on != have.iter().any(|w| *w == t.tab_id) { b.cdp.set_focus_emulation(&t.cdp_session, on); }+    }+    ctx.focus_emul.lock().unwrap().insert(sid.to_string(), want);+}++/// Is this tab currently focus-emulated? (Reporting only; never perturbs the state it reports.)+pub fn focus_emulated(ctx: &Ctx, sid: &str, tab_id: &str) -> bool {+    ctx.focus_emul.lock().unwrap().get(sid).map(|v| v.iter().any(|w| w == tab_id)).unwrap_or(false)+}++/// Re-apply the focus-emulation policy to every session (after boot recovery or a reconnect).+pub fn focus_emulate_all(ctx: &Ctx) { for s in ctx.sessions.all() { focus_emulate(ctx, &s.id, None); } }++/// What this window costs and what the machine has left, reported to the CALLING AI on every open and every+/// survey. John, 2026-09-11, after a thread opened twenty wiki component pages at roughly 200 MB apiece and+/// put his laptop into thrashing: "if the ai knew how much ram it was using, that might have helped the ai+/// decide. and i suppose you should be automatically telling it what % of ram was even left on the box."+/// browserMb is the whole Chrome process tree for this profile (renderers and the GPU process included),+/// which is the number that actually moves when a thread opens a page; avgMbPerTab is that divided by the+/// tabs it is carrying, so a caller can price its next tab before opening it.+pub fn memory_report(ctx: &Ctx, profile: Option<&str>) -> Value {+    let (total, avail) = match ctx.host.memory_status() { Some(x) => x, None => return Value::Null };+    let free_pct = if total > 0 { (avail as f64 / total as f64 * 100.0).round() as u64 } else { 0 };+    let mut v = json!({ "boxTotalMb": total, "boxFreeMb": avail, "boxFreePct": free_pct });+    if let Some(p) = profile {+        if let Some(b) = browser_for(ctx, p) {+            if let Some(mb) = ctx.host.process_tree_rss_mb(b.launched.pid) {+                let tabs: usize = ctx.sessions.for_profile(p).iter().map(|s| s.tabs.len()).sum();+                v["browserMb"] = json!(mb);+                v["tabsOnThisBrowser"] = json!(tabs);+                if tabs > 0 { v["avgMbPerTab"] = json!(mb / tabs as u64); }+            }+        }+    }+    if v.get("tabsOnThisBrowser").and_then(|x| x.as_u64()) == Some(0) {+        let mins = ctx.settings.get("idleBrowserQuitMin", "10");+        v["idleBrowser"] = json!(true);+        v["idleNote"] = json!(if mins == "0" { "This browser has no pup windows and idleBrowserQuitMin is 0, so pup will keep it running.".to_string() }+                              else { format!("This browser has no pup windows: pup closes it after {mins} min idle and gives the RAM back. The next open relaunches it, still signed in.") });+    }+    let per = v.get("avgMbPerTab").and_then(|x| x.as_u64());+    v["_hint"] = json!(match (free_pct, per) {+        (f, Some(p)) if f < 10 => format!("Only {f}% of this machine's RAM is free and each pup tab here is costing about {p} MB. Do not open more pages until you close some: at this level Windows is paging to disk and the user's whole desktop stutters. pup_close_tab / pup_close_window on what you are done with."),+        (f, None) if f < 10 => format!("Only {f}% of this machine's RAM is free. Do not open more pages until you close some: at this level Windows is paging to disk and the user's whole desktop stutters."),+        (f, Some(p)) if f < 25 => format!("{f}% of this machine's RAM is free and each pup tab here is costing about {p} MB. Budget accordingly and close tabs you are done with; this is the user's own laptop."),+        (f, None) if f < 25 => format!("{f}% of this machine's RAM is free. Budget accordingly and close pages you are done with; this is the user's own laptop."),+        (f, Some(p)) => format!("{f}% of this machine's RAM is free; each pup tab here is costing about {p} MB. Close tabs you are done with so it stays that way."),+        (f, None) => format!("{f}% of this machine's RAM is free."),+    });+    v+}++pub fn attach_tab(s: &mut Session, cdp: &Cdp, target_id: &str) -> Result<String, Value> {+    // 2.0.455: idempotent. A target already in this session is returned as is (the rescan adopted one tab twice+    // because its "tracked" list was built once up front).+    if let Some(t) = s.tabs.iter().find(|t| t.target_id == target_id) { return Ok(t.tab_id.clone()); }+    let sid = cdp.attach(target_id).map_err(|e| err("cdp_attach_failed", e.to_string(), None))?;+    apply_consumer_identity(cdp, &sid);   // pup-bridge#109: consumer Google Chrome identity + EME shim, before any navigation+    // 2.0.455: the page-domain setup (Page.enable, the two bindings, the login-capture script) runs on a helper+    // thread. A tab Chrome DISCARDED (Memory Saver after a sleep) has no renderer, so each of those calls sat+    // out its full timeout and one adoption took 30 s (measured on AdomLapper, 2026-09-10). The tab is usable+    // for resolution and identity at once; the setup lands when the renderer answers.+    let (cdp2, sid2) = (cdp.clone(), sid.clone());+    std::thread::Builder::new().name("tab-setup".into()).spawn(move || {+        let _ = cdp2.page_enable(&sid2);+        for name in ["__pupCredCapture", "__pupFloatyPush"] { let _ = cdp2.send(Some(&sid2), "Runtime.addBinding", json!({ "name": name }), Duration::from_secs(5)); }+        let capture = vault::capture_login_script();+        let _ = cdp2.send(Some(&sid2), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": capture }), Duration::from_secs(5));+        let _ = cdp2.evaluate(&sid2, &capture, Duration::from_secs(5));+    }).ok();+    let tab_id = s.next_tab_id();+    s.tabs.push(Tab { tab_id: tab_id.clone(), target_id: target_id.into(), cdp_session: sid, url: "about:blank".into(), title: String::new(), error_count: 0, fav: None });+    s.active_tab = s.tabs.len() - 1;+    Ok(tab_id)+}++/// Per-page scripts that need settings (floaty pencil + hotkey, owner chip). Idempotent.+pub fn inject_page_scripts(ctx: &Ctx, s: &Session, cdp: &Cdp, sid: &str) {+    let floaty = annotate::floaty_script(ctx);+    let _ = cdp.send(Some(sid), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": floaty }), Duration::from_secs(5));+    let _ = cdp.evaluate(sid, &floaty, Duration::from_secs(5));+    if ctx.settings.get("ownerChip", "off") == "on" {   // 2.0.713: OFF by default (John, 2026-10-12: it sits on the page's own controls, and he never asked for it)+        let chip = OWNER_CHIP_JS.replace("__THREAD__", &serde_json::to_string(&s.owner.clone().unwrap_or_default()).unwrap()).replace("__PROFILE__", &serde_json::to_string(&s.profile).unwrap()).replace("__AVATAR__", &serde_json::to_string(&ctx.settings.get("identityAvatarB64", "")).unwrap()).replace("__IDENT__", &serde_json::to_string(&ctx.settings.get("identityName", "")).unwrap());+        let _ = cdp.send(Some(sid), "Page.addScriptToEvaluateOnNewDocument", json!({ "source": chip }), Duration::from_secs(5));+        let _ = cdp.evaluate(sid, &chip, Duration::from_secs(5));+    }+}++/// 2.0.653: the extension-lane twin of inject_page_scripts (per page load; the smoke found no owner chip there).+pub fn inject_page_scripts_ext(ctx: &Ctx, s: &Session, lane: &transport::Lane, tab: &Tab) {+    if tab.url.is_empty() || tab.url.starts_with("about:") || tab.url.starts_with("chrome") { return; }   // nothing to decorate, evals forbidden there+    if !transport::wait_ready(lane, tab, &tab.url, Duration::from_secs(10)) { pup_core::log(format!("[page] \"{}\" page scripts skipped: the document was not ready within 10 s", s.id)); return; }+    let floaty = annotate::floaty_script(ctx);+    let _ = transport::evaluate(lane, tab, &floaty);+    if ctx.settings.get("ownerChip", "off") == "on" {   // 2.0.713: OFF by default (John, 2026-10-12: it sits on the page's own controls, and he never asked for it)+        let chip = OWNER_CHIP_JS.replace("__THREAD__", &serde_json::to_string(&s.owner.clone().unwrap_or_default()).unwrap()).replace("__PROFILE__", &serde_json::to_string(&s.profile).unwrap()).replace("__AVATAR__", &serde_json::to_string(&ctx.settings.get("identityAvatarB64", "")).unwrap()).replace("__IDENT__", &serde_json::to_string(&ctx.settings.get("identityName", "")).unwrap());+        let _ = transport::evaluate(lane, tab, &chip);+    }+}++pub fn refresh_tab(cdp: &Cdp, t: &mut Tab) {+    if let Ok(info) = cdp.target_info(&t.cdp_session) {+        if let Some(u) = info.get("url").and_then(|u| u.as_str()) { t.url = u.into(); }+        if let Some(ti) = info.get("title").and_then(|u| u.as_str()) { t.title = ti.into(); }+    }+}++// ── the verbs ──────────────────────────────────────────────────────────────────────────++fn image_delivery(r: &mut Value) {+    fn fix(v: &mut Value) {+        let Some(fp) = v.get("filePath").and_then(|x| x.as_str()).map(|x| x.to_string()) else { return };+        let low = fp.to_lowercase();+        if ![".png", ".jpg", ".jpeg", ".webp"].iter().any(|e| low.ends_with(e)) || v.get("fullPath").is_some() { return; }+        v["fullPath"] = json!(fp);+        let fits = |k: &str| v.get(k).and_then(|x| x.as_u64()).map(|n| n <= 1568).unwrap_or(false);+        if fits("width") && fits("height") { v["safePath"] = json!(fp); }+    }+    fix(r);+    // the same keys inside the JSON string many verbs also carry in `output`+    if let Some(o) = r.get("output").and_then(|x| x.as_str()).map(|x| x.to_string()) {+        if let Ok(mut ov) = serde_json::from_str::<Value>(&o) { if ov.is_object() { let before = ov.clone(); fix(&mut ov); if ov != before { r["output"] = json!(ov.to_string()); } } }+    }+}++fn describe(ctx: &Ctx) -> Value {+    let c = catalog();+    let body = json!({+        "success": true, "bridge": "puppeteer", "version": pup_core::version(), "shell": "rust",+        "progressContract": c["progressContract"], "verbs": c["verbs"], "verbCount": c["verbs"].as_array().map(|a| a.len()).unwrap_or(0), "notes": c["notes"], "statusVerb": "pup_readiness",+        "readiness": ctx.installer.readiness(&*ctx.host, ctx.prefer_cft()),+        "_hint": "This list — the bridge's verbs + schemas + hints, for AD's Verbs tab. Read it once to learn the surface. This build is pup as one Rust exe (spawn.kind exe): no Node, no npm, no postinstall; same contract as the Node build.",+        "_next": ["pup_status"], "related": ["pup_status"],+    });+    let mut r = body.clone(); r["output"] = json!(body.to_string()); r["error"] = json!(""); r["status"] = json!("ok"); r["ok"] = json!(true);+    r+}++fn active_rows(ctx: &Ctx) -> Vec<Value> {+    let rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());+    let mut rows = Vec::new();+    if rd.get("installing").and_then(|b| b.as_bool()) == Some(true) {+        if rd.get("installPhase").and_then(|p| p.as_str()) == Some("unpacking") {+            let total = rd.get("bytesTotal").and_then(|b| b.as_u64()).unwrap_or(0) as f64 * 2.3;+            let pct = if total > 0.0 { Some(((rd.get("unpackBytes").and_then(|b| b.as_u64()).unwrap_or(0) as f64 / total * 100.0).round() as u64).min(99)) } else { None };+            rows.push(json!({ "id": "cft-unpack", "verb": "pup_prewarm", "phase": "unpack", "stepLabel": format!("Unpacking Chrome for Testing{}", rd.get("unpackMethod").and_then(|m| m.as_str()).map(|m| format!(" ({m})")).unwrap_or_default()), "percent": pct, "bytes": rd.get("unpackBytes"), "elapsedSec": rd.get("unpackElapsedSec"), "estimatedSec": Value::Null, "etaSec": Value::Null, "confidence": "estimated", "startKind": "cold" }));+        } else {+            rows.push(json!({ "id": "cft-download", "verb": "pup_prewarm", "phase": "download", "stepLabel": "Downloading Chrome for Testing", "percent": rd.get("installProgressPct"), "elapsedSec": rd.get("downloadElapsedSec"), "estimatedSec": rd.get("downloadElapsedSec").and_then(|e| e.as_u64()).zip(rd.get("downloadEtaSec").and_then(|e| e.as_u64())).map(|(a, b)| a + b), "etaSec": rd.get("downloadEtaSec"), "confidence": if rd.get("downloadEtaSec").is_some_and(|v| !v.is_null()) { "measured" } else { "estimated" }, "startKind": "cold" }));+        }+    }+    rows.extend(ctx.consent.rows());+    rows.extend(extras::active_download_rows());   // pup-bridge#109: in-flight downloads are pollable here+    rows+}++fn readiness(ctx: &Ctx) -> Value {+    let mut rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());+    if let Some(o) = rd.as_object_mut() { if let Value::Object(cs) = pup_browser::chrome_stable::snapshot() { for (k, v) in cs { o.insert(k, v); } } }+    let ready = rd.get("ready").and_then(|b| b.as_bool()).unwrap_or(false);+    let installing = rd.get("installing").and_then(|b| b.as_bool()).unwrap_or(false);+    // 2.0.646 (pup-bridge#121): a lane browser nobody tracks (a pup restart lost its pid list) holds the profile+    // lock and makes every launch exit with "Opening in existing browser session". Say so here, with the pids.+    if !extdrive::alive(ctx) {+        let dir = pup_browser::profiles_dir(&*ctx.host).join("adom-you");+        let orphans = orphan_browser_pids(ctx, &dir);+        if !orphans.is_empty() {+            if let Some(o) = rd.as_object_mut() { o.insert("orphanedBrowser".into(), json!({ "pids": orphans, "profile": dir.to_string_lossy(), "_hint": "A pup browser on this profile is running but no live lane owns it (left over from a pup restart). It holds the profile lock, so a launch would exit at once. The next pup_open_window ends it and launches fresh; nothing to do by hand." })); }+        }+    }+    let hint = if ready { "A drivable browser exists: pup_open_window {url} will open in the background. chromeForTestingInstalled tells you whether the toolbar build is cached (pup fetches it in the background on every box)." }+        else if installing { "Chrome for Testing is downloading (active[] carries percent, elapsed and ETA). Poll this verb; do not start a second download. A system Chrome/Edge, if present, is drivable already." }+        else if rd.get("installPhase").and_then(|p| p.as_str()) == Some("failed") { "The Chrome for Testing install FAILED (lastError). Retry pup_prewarm once the cause (network/proxy, disk) is fixed; a system Chrome/Edge is drivable meanwhile." }+        else { "No browser yet: pup_prewarm {} starts the Chrome for Testing download; pup_open_window starts it too and answers chrome_for_testing_installing until it lands." };+    let mut r = pup_core::merge(ok(rd.clone()), json!({ "success": true, "statusVerb": "pup_readiness", "active": active_rows(ctx), "_hint": hint, "_next": ["pup_open_window", "pup_prewarm"], "error": "",+        // the Node build's real-Chrome installer phases (pup_use {browser:"chrome", install:true}); the Rust build has no stable-Chrome installer yet, so the phase is always idle+        "chromeStablePhase": rd.get("chromeStablePhase").cloned().unwrap_or(json!("idle")), "chromeStableAwaitingApproval": rd.get("chromeStableAwaitingApproval").cloned().unwrap_or(json!(false)), "chromeStableError": rd.get("chromeStableError").cloned().unwrap_or(Value::Null), "chromeStableNotifyChannel": Value::Null, "chromeStableNotifyPeers": Value::Null }));+    let mut inner = rd; if let Some(i) = inner.as_object_mut() { i.insert("active".into(), r["active"].clone()); i.insert("_hint".into(), json!(hint)); for k in ["chromeStablePhase", "chromeStableAwaitingApproval", "chromeStableError", "chromeStableNotifyChannel", "chromeStableNotifyPeers"] { i.insert(k.into(), r[k].clone()); } }+    r["output"] = json!(inner.to_string());+    r+}++fn prewarm(ctx: &Ctx, args: &Value) -> Value {+    let wait = args.get("wait").and_then(|b| b.as_bool()).unwrap_or(false);+    if wait { let done = ctx.installer.ensure_blocking(ctx.host.clone(), Duration::from_secs(600)); let rd = readiness(ctx); return pup_core::merge(rd, json!({ "waited": true, "installed": done })); }+    ctx.installer.ensure_background(ctx.host.clone());+    let rd = readiness(ctx);+    pup_core::merge(rd, json!({ "started": true, "_hint": "Chrome for Testing provisioning is running in the background (or already cached). Poll pup_readiness for percent + ETA; pass wait:true to block until it lands." }))+}++pub fn session_row(ctx: &Ctx, s: &Session) -> Value {+    let active_tab = s.active();+    json!({ "sessionId": s.id, "window": s.id, "transport": s.transport, "lane": if s.transport == "ext" { "extension (no debug port)" } else { "cdp" }, "url": active_tab.map(|t| t.url.clone()), "title": active_tab.map(|t| t.title.clone()), "active": ctx.sessions.active_id().as_deref() == Some(&s.id), "owner": s.owner, "aiThread": s.owner, "profile": s.profile, "tabCount": s.tabs.len(), "hwnd": s.hwnd, "foreground": s.foreground, "ageMinutes": s.age_minutes(), "emulatedViewport": s.emulated, "lastAgentUpdate": s.last_agent_update(), "lastForeground": s.last_foreground, "errorCount": s.tabs.iter().map(|t| ctx.tabstate.lock().unwrap().get(&t.cdp_session).map(|x| x.errors.len()).unwrap_or(0)).sum::<usize>(),+        "activeTabId": active_tab.map(|t| t.tab_id.clone()), "ownerSource": if s.owner.is_some() { if s.owner_from_caller { json!("caller-identity") } else { json!("declared") } } else { Value::Null },+        "overlay": s.overlay_desc.clone().unwrap_or_else(|| "none (not yet applied)".into()), "overlayLed": s.overlay_led, "overlayWhy": s.overlay_why, "flash": "none", "flashCount": s.flash_count, "progressBarCount": s.progress_count, "progressBarMs": s.progress_ms,+        "background": if s.foreground { "still-foreground" } else if s.hwnd.is_some() { "backgrounded" } else { "no-window" } })+}++fn status(ctx: &Ctx) -> Value {+    let list: Vec<Value> = ctx.sessions.all().iter().map(|s| session_row(ctx, s)).collect();+    ok(json!({+        "version": pup_core::version(), "shell": "rust", "uptimeSec": ctx.started.elapsed().as_secs(), "dataDirs": ctx.data_dirs(), "retention": { "screenshots": { "keepDays": SHOT_KEEP_DAYS, "keepBytes": SHOT_KEEP_BYTES }, "recordings": { "keepDays": REC_KEEP_DAYS, "keepBytes": REC_KEEP_BYTES } },+        "sessionCount": list.len(), "activeSession": ctx.sessions.active_id(), "sessions": list,+        "settings": { "taskbarMode": if ctx.settings.get("aumidIcons", "on") != "on" { "plain" } else if ctx.settings.get("threadTiles", "on") == "on" { "tiles" } else { "aumid" }, "aumidIcons": ctx.settings.get("aumidIcons", "on"), "threadTiles": ctx.settings.get("threadTiles", "on"), "overlayBadges": ctx.settings.get("overlayBadges", "on"), "overlaySize": ctx.settings.get("overlaySize", "75"), "jumpLists": ctx.settings.get("jumpLists", "on") },+        "win32": { "inProcess": true, "why": Value::Null }, "enumWindows": { "total": 0, "lastMin": 0 }, "win32Calls": {}, "log": [], "loops": pup_core::loops::report(), "pupshotUrl": Value::Null, "iconAudit": ctx.last_audit.lock().unwrap().clone(),+        "adVersion": ctx.ad_version.lock().unwrap().clone(),+        "memory": memory_report(ctx, Some("adom-you")),+    }))+}++fn list_windows(ctx: &Ctx) -> Value {+    let mut all = ctx.sessions.all();+    for s in all.iter_mut() { if let Some(b) = browser_for(ctx, &s.profile) { for t in s.tabs.iter_mut() { refresh_tab(&b.cdp, t); } ctx.sessions.with_mut(&s.id, |x| x.tabs = s.tabs.clone()); } }+    let rows: Vec<Value> = all.iter().map(|s| session_row(ctx, s)).collect();+    ok(json!({ "count": rows.len(), "sessions": rows, "activeSession": ctx.sessions.active_id(), "memory": memory_report(ctx, Some("adom-you")) }))+}++fn my_windows(ctx: &Ctx, caller: &Caller) -> Value {+    if caller.thread.is_empty() { return err("caller_identity_required", "pup_my_windows needs --ai-thread", Some("Pass --ai-thread <your thread name>; windows are keyed on it.")); }+    let mine: Vec<Value> = ctx.sessions.owned_by(&caller.thread).iter().map(|s| session_row(ctx, s)).collect();+    let default_id = thread_default_session_id(&caller.thread);+    let body = json!({ "ok": true, "aiThread": caller.thread, "ai-thread": caller.thread, "count": mine.len(), "windows": mine, "defaultWindow": default_id, "_hint": if mine.is_empty() { "You own no pup window yet: pup_open_window {url} creates yours (no ids needed), later verbs resolve to it." } else { "Verbs with no ids resolve to your window; name another with {window:\"<sessionId>\"}." }, "_next": ["pup_open_window", "pup_screenshot", "pup_navigate"] });+    let mut r = ok(body.clone()); r["success"] = json!(true); r["output"] = json!(body.to_string());+    r+}++/// The park geometry in CSS px (what Browser.setWindowBounds takes): Node v1.8.66 rule, "size is+/// measured, not guessed": screen.availLeft/Top/Width/Height from the page itself, with retries, then a+/// 10 px inset on every side. The Win32 work area is PHYSICAL px; feeding it to CDP on a 150% display+/// produced a window 1.5x the screen, hanging off the bottom-right (John's AdomLapper, 2026-09-06).+fn birth_rect_css(cdp: &Cdp, cdp_session: &str, ctx: &Ctx) -> (i32, i32, i32, i32) {+    for _ in 0..4 {+        if let Ok(v) = cdp.evaluate(cdp_session, "({x:screen.availLeft||0,y:screen.availTop||0,w:screen.availWidth,h:screen.availHeight})", Duration::from_secs(3)) {+            let (x, y, w, h) = (v["x"].as_f64().unwrap_or(0.0) as i32, v["y"].as_f64().unwrap_or(0.0) as i32, v["w"].as_f64().unwrap_or(0.0) as i32, v["h"].as_f64().unwrap_or(0.0) as i32);+            if w > 400 && h > 300 { return (x + 10, y + 10, w - 20, h - 20); }+        }+        std::thread::sleep(Duration::from_millis(300));+    }+    match ctx.host.work_area() { Some((x, y, w, h)) => { let sc = ctx.host.dpi_scale(0).max(1.0); ((x as f64 / sc) as i32 + 10, (y as f64 / sc) as i32 + 10, ((w as f64 / sc) as i32 - 20).max(400), ((h as f64 / sc) as i32 - 20).max(300)) }, None => (10, 10, 1280, 800) }+}++fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    let t0 = Instant::now();+    let url = args.get("url").and_then(|u| u.as_str()).map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).unwrap_or_else(|| "about:blank".into());+    // 2.0.648 (pup-bridge#112): isolated:true MINTS a throwaway profile. It used to bind adom-you and only pick the+    // anonymous tile, so a "throwaway" login persisted into the shared jar and an untrusted page ran on the user's+    // real cookies. The directory is removed when the window's browser is gone (close_window).+    let profile = match args.get("profile").and_then(|p| p.as_str()) {+        Some(p) => p.to_string(),+        None if args.get("isolated").and_then(|b| b.as_bool()) == Some(true) => format!("iso-{:x}{:x}", pup_core::now_ms() & 0xffffff, std::process::id() & 0xffff),+        None => "adom-you".to_string(),+    };+    if args.get("isolated").and_then(|b| b.as_bool()) == Some(true) && args.get("isolationReason").and_then(|r| r.as_str()).map(|r| r.len() >= 10) != Some(true) {+        return err("isolation_reason_required", "isolated:true needs an isolationReason (min 10 chars): why must this window NOT be adom-you?", Some("Surf as adom-you by default (its logins accumulate). Isolation is for logged-out tests, acting as a different user, or untrusted pages: say which."));+    }+    if args.get("foreground").and_then(|b| b.as_bool()) == Some(true) && args.get("foregroundReason").and_then(|r| r.as_str()).map(|r| r.len() >= 8) != Some(true) {+        return err("foreground_reason_required", "foreground:true needs a foregroundReason shown to the user on screen", Some("pup windows are born in the BACKGROUND. Only a demo / 'show me' earns the foreground, and the reason is captioned for the user."));+    }+    let sid = window_arg(args).unwrap_or_else(|| if caller.thread.is_empty() { format!("pup-{}", pup_core::now_ms() % 100000) } else { thread_default_session_id(&caller.thread) });+    // 2.0.476: a DELIBERATE open of this id (anything but the auto-recovery relaunch) clears a pending close-cancel,+    // so reopening a window shortly after closing it during a restore is honored rather than swallowed.+    if !caller.reason.starts_with("auto-recovery") { crate::recovery::clear_cancel(&sid); }+    let high_fps = args.get("highFps").and_then(|b| b.as_bool()).unwrap_or(false);+    let insecure = args.get("webSecurity").and_then(|b| b.as_bool()) == Some(false);+    // 2.0.525: the lane is chosen HERE, before any browser is launched. defaultLane is "ext": a plain+    // pup_open_window is an extension-driven window on adom-you (no debug port, John's default browsing+    // lane). lane:"cdp" is the explicit opt-in for what CDP alone can do (iframes, trusted input, annotate).+    // 2.0.563 (John): the extension lane IS pup. It is the lane that loads every vendor site, so it is the+    // default and the CDP lane opens ONLY on an explicit lane:"cdp". Nothing falls back to CDP silently any+    // more: a request that needs a CDP-only feature without asking for the lane is refused with the fix.+    let explicit_lane = args.get("lane").and_then(|l| l.as_str()).map(|x| x.to_string());+    let lane = explicit_lane.clone().unwrap_or_else(|| ctx.settings.get("defaultLane", "ext"));+    let isolated = args.get("isolated").and_then(|b| b.as_bool()) == Some(true);+    // 2.0.652 (John, 2026-09-19: "i want it almost NEVER used cuz then you may use it as a crutch instead of fixing+    // ext lane to solve all use cases"). lane:"cdp" is refused unless the user turned the CDP lane on for this box.+    if args.get("lane").and_then(|l| l.as_str()) == Some("cdp") && ctx.settings.get("cdpLane", "off") != "on" {+        pup_core::log(format!("[lane] refused: thread \"{}\" asked for lane:\"cdp\" and cdpLane is off (cdp_lane_disabled)", caller.thread));+        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" {+        // 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 {+            pup_core::log(format!("[lane] refused: thread \"{}\" passed a CDP-only option ({}) without lane:\"cdp\" (cdp_lane_required)", caller.thread, ["profile","isolated","highFps","webSecurity"].iter().filter(|k| args.get(**k).is_some()).cloned().collect::<Vec<_>>().join(",")));+            return err("cdp_lane_required", format!("This open needs the CDP lane because of {why}, and the CDP lane opens only when asked for."),+                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()) });+        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+    // birth taking adom-you from a CDP browser. It closes the ext windows, which is why lane:cdp is an+    // explicit opt-in and the default stays ext.+    if profile == "adom-you" && ext_lane_owns_profile(ctx, &profile) {+        let ext_ids: Vec<String> = ctx.sessions.all().into_iter().filter(|x| transport::is_ext(x)).map(|x| x.id).collect();+        if !ext_ids.is_empty() {+            pup_core::log(format!("[lane] lane:cdp requested on adom-you; handing the profile back from the extension lane (closing {} ext window(s))", ext_ids.len()));+            for id in &ext_ids { ctx.sessions.remove(id); ctx.sessions.delete_file(id); }+        }+        let _ = crate::extdrive::close(ctx, caller, &json!({ "all": true }));+        kill_extdrive(ctx);+        let dir = pup_browser::profiles_dir(&*ctx.host).join(&profile);+        let lock = dir.join("SingletonLock");+        let t0 = Instant::now(); while t0.elapsed() < Duration::from_secs(8) { if !lock.exists() { break; } std::thread::sleep(Duration::from_millis(200)); }+    }+    let b = match get_or_launch(ctx, &profile, high_fps, insecure) { Ok(b) => b, Err(e) => return e };++    // An existing window of that name: later opens become TABS in it (the Node contract).+    if let Some(existing) = ctx.sessions.get(&sid) {+        if existing.profile == profile {+            let tid = match b.cdp.create_target(&url, false, None, None) { Ok(t) => t, Err(e) => return err("cdp_create_target_failed", e.to_string(), None) };+            let mut s = existing.clone();+            let tab_id = match attach_tab(&mut s, &b.cdp, &tid) { Ok(t) => t, Err(e) => return e };+            if let Some(cs) = s.tabs.last().map(|t| t.cdp_session.clone()) { inject_page_scripts(ctx, &s, &b.cdp, &cs); }+            if let Some(t) = s.tabs.last_mut() { let _ = b.cdp.navigate(&t.cdp_session, &url, Duration::from_secs(15)); refresh_tab(&b.cdp, t); }+            let title = s.active().map(|t| t.title.clone()).unwrap_or_default();+            let tab_count = s.tabs.len();+            ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);+            ctx.sessions.with_mut(&sid, |x| { if let Some(i) = x.tabs.iter().position(|tt| tt.tab_id == tab_id) { x.active_tab = i; } });   // Chrome shows the new tab in front; the badge follows it+            focus_emulate(ctx, &sid, None);+            if let Some(a) = ctx.arc() { identity::schedule(a.clone(), sid.clone(), caller.clone(), false); identity::overlay_retries(a, sid.clone(), caller.clone()); }+            return ok(json!({ "sessionId": sid, "window": sid, "aiThread": s.owner, "url": url, "title": title, "tabId": tab_id, "activeTabId": tab_id, "tabCount": tab_count, "reused": true, "active": true,+                "background": "backgrounded", "windowEtiquette": "background", "placement": "background", "browser": b.launched.candidate.to_json(), "profile": profile, "owner": s.owner, "ownerSource": "session",+                "progress": { "phase": "done", "elapsedMs": t0.elapsed().as_millis() as u64, "startKind": "warm" },+                "memory": memory_report(ctx, Some(&profile)),+                "_hint": "This window already existed for your thread, so the URL opened as a NEW TAB in it (pup_list_tabs / pup_switch_tab drive tabs). Screenshot to verify the render.", "_verifyRender": VERIFY_RENDER, "_next": ["pup_screenshot", "pup_navigate", "pup_eval"] }));+        }+    }++    // Node: an explicit `owner` declares the owning thread (ownerSource "declared"); else the caller's thread.+    // The Rust port ignored `owner` until 2.0.438, so a window recreated on another thread's behalf wore the+    // recreating thread's tile (John's AdomLapper proof run, 2026-09-06).+    let declared_owner = args.get("owner").and_then(|o| o.as_str()).map(|o| o.trim().to_string()).filter(|o| !o.is_empty());+    let owner: Option<&str> = declared_owner.as_deref().or(if caller.thread.is_empty() { None } else { Some(caller.thread.as_str()) });+    // R4 / pup-window-targeting rule 2: the set-diff is only exact while exactly ONE pup birth is in flight.+    let birth_guard = BIRTH_LOCK.lock().unwrap_or_else(|e| e.into_inner());+    let taken = taken_hwnds(ctx, &sid);+    let before: std::collections::HashSet<i64> = ctx.host.frame_windows_all(Some("Chrome_WidgetWin_1")).into_iter().map(|w| w.hwnd).collect();+    let prev_fg = ctx.host.foreground();   // Node v1.9.253: measure whether creation steals focus, and hand it straight back+    // 2.0.446 (John: "i would expect the icon to come up the first time with the ai-thread name and then it NEVER+    // has to swap icons again", and "i thought you said you could make it such that the cft icon never actually+    // showed first?"): the shell creates a window's button at its first show and bakes the icon then, so the+    // identity has to be ON the window before Chrome shows it. Chrome shows the window inside Target.createTarget,+    // so a watcher thread starts BEFORE the call, polls every top-level frame (visible or not) every millisecond,+    // and stamps the newborn natively (property store + window icon, no ab round-trip) the instant it exists.+    // The tile is rendered and the AUMID registered ahead of time, so the button's picture resolves on creation.+    let plan_name = owner.map(|o| o.to_string()).unwrap_or_else(|| sid.clone());+    let (plan, _early_ok, watcher) = birth_watch(ctx, caller, &sid, &plan_name, &url, &before, &taken, false);+    // 2.0.450 (John: "i would like a setting in the pup settings dialog to turn this flash feature on or off"):+    // winAttentionFlash off (default) = focus:false, Chrome never asks for the foreground, no attention state;+    // on = Chrome's default ask, and Windows lights the button when it refuses (the classic new-window flash).+    let attention = ctx.settings.get("winAttentionFlash", "off") == "on";+    let target = match b.cdp.create_target("about:blank", true, Some((OFFSCREEN, OFFSCREEN, 1280, 800)), if attention { None } else { Some(false) }) { Ok(t) => t, Err(e) => return err("cdp_create_target_failed", format!("Target.createTarget: {e}"), Some("The browser is up but refused a new window; pup_readiness shows its state, pup_close {} restarts it.")) };+    let mut s = Session::new(&sid, &profile, owner, Some(&caller.container), b.launched.cdp_port, if b.launched.pid > 0 { Some(b.launched.pid) } else { None });+    s.owner_from_caller = s.owner.as_deref() == Some(caller.thread.as_str()) && !caller.thread.is_empty() && args.get("owner").is_none();+    let tab_id = match attach_tab(&mut s, &b.cdp, &target) { Ok(t) => t, Err(e) => return e };+    let (bl, bt, bw, bh) = s.tabs.last().map(|t| birth_rect_css(&b.cdp, &t.cdp_session, ctx)).unwrap_or((10, 10, 1280, 800));   // CSS px, measured from the page+    // park: on-screen rect via CDP (moves without z-order change), then bottom + tag via the host+    let mut hwnd: Option<i64> = None;+    // Node v2.0.276: born off-screen so Chrome never activates it, moved on-screen within the 50 ms budget by+    // one local CDP call that changes no z-order. Leaving it off-screen longer (2.0.429 to 2.0.433 candidates)+    // made Chrome fire its activation on the first real paint, which Windows denies and tints. Move NOW.+    if let Ok((wid, _)) = b.cdp.window_for_target(&target) { s.window_id = Some(wid); let _ = b.cdp.set_window_bounds(wid, json!({ "left": bl, "top": bt, "width": bw, "height": bh })); }+    let mut early_stamped = false;+    if let Some((h, stamped)) = birth_report(watcher, &sid) { hwnd = Some(h); early_stamped = stamped; }+    if hwnd.is_none() { for _ in 0..200 {+        let now = ctx.host.windows_for_pids(&[], Some("Chrome_WidgetWin_1"));+        if let Some(w) = now.iter().find(|w| !before.contains(&w.hwnd) && !taken.contains(&w.hwnd)) { hwnd = Some(w.hwnd); break; }+        std::thread::sleep(Duration::from_millis(15));+    } }+    drop(birth_guard);   // hwnd resolved: the next birth may start+    let mut parked = false;+    if let Some(h) = hwnd {+        // John (2026-09-06): "i would expect the icon to come up the first time with the ai-thread name+        // and then it NEVER has to swap icons again." Windows bakes a button's icon when the button is+        // created, and creates it when the window is shown. So: HIDE the newborn at once (its button+        // goes away), render the thread tile + register the AUMID + stamp + jump list on the hidden+        // window, then show it once, no-activate, at z-bottom. One button, born with the tile.+        // John (2026-09-06): "i would expect the icon to come up the first time with the ai-thread name+        // and then it NEVER has to swap icons again." Windows bakes a button's icon when it creates the+        // button. So: take the newborn's button away through the shell's own ITaskbarList::DeleteTab+        // (the window stays visible, so Chrome queues no activation and no attention tint can follow),+        // render the thread tile + register the AUMID + stamp + jump list, then AddTab: the shell creates+        // the button ONCE, already wearing the tile. Hiding the window instead (2.0.429 to 2.0.432+        // candidates) made Chrome fire its first-show activation on the re-show, which Windows denies+        // and tints; measured, so never hide a newborn.+        // 2.0.446: an early-stamped newborn keeps its button (it was created wearing the tile); only the late path untabs.+        if !early_stamped { let _ = ctx.host.taskbar_tab(h, false); }+        // Node v1.9.253: when the user's foreground window belongs to pup's own Chrome (they are using a+        // pup window), Windows lets Chrome activate the newborn, so creation steals their focus. Measured+        // 2026-09-06 (foreground 1901802 -> the newborn). Hand it straight back before anything else.+        if prev_fg != 0 && prev_fg != h && ctx.host.foreground() == h {+            let _ = ctx.ab.call("desktop_bring_to_front", json!({ "hwnd": prev_fg }), caller, Duration::from_secs(4));+            pup_core::log(format!("[window] \"{sid}\" creation took the foreground from {prev_fg}; handed it back ({})", if ctx.host.foreground() == prev_fg { "ok" } else { "still ours" }));+        }+        s.hwnd = Some(h);+        ctx.host.tag(h, &s.tag_string());+        ctx.sessions.insert(s.clone());+        // Park FIRST (Node: the unconditional launch park), so the newborn never sits on top while we work.+        parked = ctx.host.send_to_bottom_no_activate(h);+        let _ = ctx.ab.call("desktop_set_window_state", json!({ "hwnd": h, "state": "bottom", "force": true }), caller, Duration::from_secs(5));+        activity::mark_icon_applied(ctx, &sid);+        activity::on_open(ctx, &sid, h, caller);+    }+    if let Some(cs) = s.tabs.last().map(|t| t.cdp_session.clone()) { inject_page_scripts(ctx, &s, &b.cdp, &cs); }+    let loaded = if let Some(t) = s.tabs.last_mut() { let r = b.cdp.navigate(&t.cdp_session, &url, Duration::from_secs(20)).map(|(_, l)| l).unwrap_or(false); refresh_tab(&b.cdp, t); r } else { false };+    let (auto_login_note, wiki_extra) = match s.tabs.last() { Some(t) => (vault::autofill_on_load(ctx, &s, &t.cdp_session, &url), wiki::after_navigate(ctx, &s, &b.cdp, &t.cdp_session, &url, args["wikiView"].as_str(), args["isolated"].as_bool().unwrap_or(false))), None => (None, json!({})) };+    ctx.sessions.insert(s.clone());+    let _ = park::bottom_unless_foreground(ctx, &sid, true);   // Node reassertBottomAfterNav: the first cross-origin paint can raise it; inside the birth grace it is bottomed+    if let Some(h) = hwnd {+        // Measured 2026-09-06: Windows puts the attention flag on the BUTTON that exists when Chrome's+        // first real paint (this navigation) tries to activate the window and is denied. No button existed+        // (DeleteTab at birth), so nothing was flagged. Now stamp the thread identity and let the shell+        // create the one button this window will ever have, wearing the tile.+        // Node v1.9.342: place AGAIN once the window has settled. Chrome cascades a new window from the+        // last one's position after our first move (measured on AdomLapper: requested 10,10 CSS, landed+        // 120,120 CSS, overhanging a 150% screen), and re-reads the screen the window actually sits on.+        if let (Some(t), Ok((wid, _))) = (s.tabs.last(), b.cdp.window_for_target(&target)) {+            let (l2, t2, w2, h2) = birth_rect_css(&b.cdp, &t.cdp_session, ctx);+            let _ = b.cdp.set_window_bounds(wid, json!({ "windowState": "normal" }));+            let _ = b.cdp.set_window_bounds(wid, json!({ "left": l2, "top": t2, "width": w2, "height": h2 }));+            if let Ok(v) = b.cdp.send(None, "Browser.getWindowBounds", json!({ "windowId": wid }), Duration::from_secs(3)) { pup_core::log(format!("[window] \"{sid}\" placed at {},{} {}x{} CSS (asked {l2},{t2} {w2}x{h2})", v["bounds"]["left"], v["bounds"]["top"], v["bounds"]["width"], v["bounds"]["height"])); }+        }+        let t_id = Instant::now();+        if early_stamped { if let Some((a, _, _)) = &plan { ctx.sessions.with_mut(&sid, |x| x.app_id = Some(a.clone())); } }+        identity::stamp_opts(ctx, &sid, caller, true, false);   // same AUMID again (no button rebuild, measured) + the jump list+        if early_stamped {+            ctx.host.send_to_bottom_no_activate(h);+            pup_core::log(format!("[window] \"{sid}\" jump list committed on the early-stamped button in {}ms (no DeleteTab/AddTab)", t_id.elapsed().as_millis()));+        } else {+            let _ = ctx.host.taskbar_tab(h, false);+            let retabbed = ctx.host.taskbar_tab(h, true);+            ctx.host.send_to_bottom_no_activate(h);+            pup_core::log(format!("[window] \"{sid}\" identity stamped after the first paint, one button created in {}ms (AddTab={retabbed})", t_id.elapsed().as_millis()));+        }+    }+    let title = s.active().map(|t| t.title.clone()).unwrap_or_default();+    ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);+    focus_emulate(ctx, &sid, None);   // 2.0.458: the newborn's active tab, and only it+    if let Some(a) = ctx.arc() { if hwnd.is_none() { identity::schedule(a.clone(), sid.clone(), caller.clone(), true); } identity::overlay_retries(a, sid.clone(), caller.clone()); }   // identity was stamped while hidden; the favicon lands late, repaint at 1.5 s and 4.5 s+    if args.get("foreground").and_then(|b| b.as_bool()) == Some(true) {+        // Node: a foreground open is a background birth followed by a GRANTED raise (caption + toast).+        let _ = window::raise(ctx, caller, &json!({ "window": sid, "foregroundReason": args["foregroundReason"] }));+    } else { ctx.sessions.with_mut(&sid, |x| x.fg_watch_until_ms = pup_core::now_ms() + 20_000); }  // Node: the no-fault fg watch — caption if the window fronts itself in the next 20s+    pup_core::log(format!("[window] \"{sid}\" opened for \"{}\" on {}({}) hwnd={:?} parked={parked} loaded={loaded} in {}ms", caller.thread, b.launched.candidate.kind, b.launched.candidate.source, hwnd, t0.elapsed().as_millis()));+    let cands = pup_browser::launch_candidates(&*ctx.host, ctx.prefer_cft());+    let strict = args["strictPermissions"].as_bool() == Some(true);+    let render_check_val = match s.tabs.last() { Some(t) if loaded => render_check(&b.cdp, &t.cdp_session, None), _ => Value::Null };+    let mut resp = ok(json!({+        "sessionId": sid, "window": sid, "aiThread": s.owner, "url": url, "title": title, "tabId": tab_id, "activeTabId": tab_id, "tabCount": 1, "reused": false, "active": true, "loaded": loaded,+        "background": "backgrounded", "windowEtiquette": "background", "placement": "background", "hwnd": hwnd, "parked": parked,+        "browser": b.launched.candidate.to_json(), "availableBrowsers": cands.iter().map(|c| format!("{}({})", c.kind, c.source)).collect::<Vec<_>>(),+        "defaultBrowser": pup_browser::read_default(&*ctx.host).map(|d| json!({ "kind": d.get("kind"), "source": d.get("source"), "forced": d.get("forced").and_then(|f| f.as_bool()).unwrap_or(false), "pendingInstall": false })),+        "profile": profile, "owner": s.owner, "ownerSource": if s.owner.is_some() { if s.owner_from_caller { json!("caller-identity") } else { json!("declared") } } else { Value::Null }, "ageMinutes": 0, "highFps": high_fps, "emulatedViewport": Value::Null, "errorCount": 0,+        "defaultPermissionsApplied": !strict, "defaultPermissionsCount": if strict { 0 } else { DEFAULT_PERMISSIONS.len() }, "defaultPermissionsNote": if strict { "strictPermissions: nothing pre-granted" } else { "full browser permissions pre-granted (clipboard, camera, geolocation, window-management, ...) — pup_describe has the exact list" },+        "downloadsEnabled": !strict, "downloadPath": ctx.host.home().join("Downloads").to_string_lossy().replace('\\', "/"), "flash": "none", "flashCount": 0, "lastAgentUpdate": Value::Null, "lastForeground": Value::Null, "overlay": "none (not yet applied)", "progressBarCount": 0, "progressBarMs": 0, "restoredTabsClosed": 0,+        "renderCheck": render_check_val,+        "size": { "width": bw, "height": bh }, "progress": { "phase": "done", "elapsedMs": t0.elapsed().as_millis() as u64, "startKind": if b.launched.reused { "warm" } else { "cold" } },+        "memory": memory_report(ctx, Some(&profile)),+        "_verifyRender": VERIFY_RENDER, "_next": ["pup_screenshot", "pup_navigate", "pup_eval", "pup_close_window"],+        "_hint": "Opened in the BACKGROUND (behind the user's windows, did NOT steal focus). Fully drivable while hidden: pup_screenshot / pup_eval / pup_navigate resolve to this window with no ids. Only bring it to the user's screen if they explicitly want to watch.",+    }));+    if b.launched.candidate.kind != "chrome-for-testing" { resp["_toolbarNotice"] = json!(format!("This window launched on {}: the Adom toolbar (pen/dots/identity tiles) rides ONLY Chrome for Testing, so it has NO toolbar. pup_readiness shows why that browser was picked.", b.launched.candidate.kind)); }+    if let Some(n) = auto_login_note { resp["_autoLogin"] = json!(n); }+    resp = pup_core::merge(resp, wiki_extra);+    if args["highFps"].as_bool() == Some(true) { highfps::grant(ctx, &sid, args["highFpsMinutes"].as_u64().unwrap_or(highfps::DEFAULT_MIN)); resp["highFps"] = highfps::info(ctx, &sid); }+    if hwnd.is_none() { resp["_windowNotice"] = json!("pup could not resolve the OS window handle for this window (no Chrome_WidgetWin_1 appeared), so it was not parked or tagged; it may be on top. This is the Phase 1 Rust bridge; report it with the box name."); }+    resp+}++pub const VERIFY_RENDER: &str = "VERIFY WHAT YOU JUST PUT ON SCREEN — do not assume it rendered. ok:true means the NAVIGATION succeeded; it says NOTHING about whether the page actually painted. Take a screenshot and READ the image: pup_screenshot {} returns a real PNG path. Look for a blank page, an unstyled flash of plain HTML, a framework error overlay, missing images, a dev-server 'Cannot GET /' page, or the wrong build. If it is wrong: fix the source, reload, screenshot AGAIN.";++fn navigate(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };+    let url = match args.get("url").and_then(|u| u.as_str()).map(|u| u.trim()).filter(|u| !u.is_empty()) { Some(u) => u.to_string(), None => return err("url_required", "pup_navigate needs {url}", None) };+    if transport::is_ext(&s) { return navigate_ext(ctx, caller, &s, &url); }+    let b = match browser_for(ctx, &s.profile) { Some(b) => b, None => return err("browser_gone", "the browser behind this window is not connected", Some("pup_close_window then pup_open_window; Phase 2 brings automatic recovery.")) };+    let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };+    match b.cdp.navigate(&tab.cdp_session, &url, Duration::from_secs(25)) {+        Ok((_, loaded)) => {+            let mut t = tab.clone(); refresh_tab(&b.cdp, &mut t);+            ctx.sessions.with_mut(&s.id, |x| { if let Some(tt) = x.tabs.iter_mut().find(|tt| tt.tab_id == t.tab_id) { *tt = t.clone(); } x.last_agent_update_ms = pup_core::now_ms(); });+            let _ = park::bottom_unless_foreground(ctx, &s.id, true);   // never yank a window the user is using (Node v2.0.26)+            if let Some(ss) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&ss); }+            if let Some(a) = ctx.arc() { identity::schedule(a.clone(), s.id.clone(), caller.clone(), false); identity::overlay_retries(a, s.id.clone(), caller.clone()); }+            let note = vault::autofill_on_load(ctx, &s, &t.cdp_session, &t.url);+            let wiki_extra = wiki::after_navigate(ctx, &s, &b.cdp, &t.cdp_session, &t.url, args["wikiView"].as_str(), false);+            let mut r = ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "loaded": loaded, "renderCheck": if loaded { render_check(&b.cdp, &t.cdp_session, None) } else { Value::Null }, "_verifyRender": VERIFY_RENDER, "_hint": if loaded { "Navigation finished (load event). Verify with a screenshot before asserting success." } else { "Page.navigate returned but the load event did not fire within 25s: the page is still moving (SPA, slow site). Screenshot to see where it is." }, "_next": ["pup_screenshot", "pup_eval"] }));+            if let Some(n) = note { r["_autoLogin"] = json!(n); }+            pup_core::merge(r, wiki_extra)+        }+        Err(e) => err("navigate_failed", e.to_string(), Some("Check the URL; a container-local localhost is not reachable from the desktop (use the proxy URL).")),+    }+}++/// The extension-lane arm of pup_navigate: same session bookkeeping, driven through the transport.++/// 2.0.642: where the page actually LANDED, classified, so an AI never asks the user to sign in to a site the+/// shared profile is already signed into. John, 2026-09-19: another thread opened claude.ai, which went straight+/// to his chats (the adom-you profile had the session), and it toasted "Sign into claude.ai in the pup window+/// once" anyway. The open/navigate response now says whether the final page is a sign-in page.+pub fn landed(url: &str, title: &str) -> Value { landed_with(url, title, None) }++/// 2.0.643: the DOM probe behind `landed`. (login_offered, login_form): a visible "Log in"/"Sign in" link or+/// button means the profile is NOT signed in here even when the URL looks like an app page (chatgpt.com/codex+/// is a marketing page with a Log in link when signed out, and the URL rule alone called it signed in); a+/// password field, or an email field next to a Continue/Next button, means the page IS a sign-in form.+pub fn landed_ext(ctx: &Ctx, s: &Session, tab: &pup_session::Tab, url: &str, title: &str) -> Value {+    if let Ok(lane) = transport::lane_for(ctx, s) { let _ = transport::wait_ready(&lane, tab, url, Duration::from_secs(8)); }+    const PROBE: &str = "(()=>{try{const vis=e=>{const r=e.getBoundingClientRect();return r.width>0&&r.height>0};const txt=e=>((e.innerText||e.value||e.getAttribute('aria-label')||'')+'').trim().toLowerCase().replace(/\\s+/g,' ');const els=[...document.querySelectorAll('a,button,input[type=submit],[role=button]')].filter(vis);const offered=els.some(e=>/^(log ?in|sign ?in|log ?in with .*|sign ?in with .*)$/.test(txt(e)));const pw=!!document.querySelector('input[type=password]');const em=!!document.querySelector('input[type=email],input[name*=email i],input[autocomplete=username],input[autocomplete=email]');const cont=els.some(e=>/^(continue|next|log ?in|sign ?in|continue with .*)$/.test(txt(e)));return JSON.stringify({offered,form:pw||(em&&cont)})}catch(e){return JSON.stringify({err:String(e)})}})()";+    let probe = transport::lane_for(ctx, s).ok().and_then(|lane| transport::evaluate(&lane, tab, PROBE).ok()).and_then(|v| {+        let txt = v.as_str().map(|x| x.to_string()).or_else(|| v.get("result").and_then(|r| r.as_str()).map(|x| x.to_string()))?;+        let j: Value = serde_json::from_str(&txt).ok()?;+        Some((j["offered"].as_bool().unwrap_or(false), j["form"].as_bool().unwrap_or(false)))+    });+    landed_with(url, title, probe)+}++pub fn landed_with(url: &str, title: &str, probe: Option<(bool, bool)>) -> Value {+    let u = url.to_ascii_lowercase(); let t = title.to_ascii_lowercase();+    let host = u.split("//").nth(1).unwrap_or("").split('/').next().unwrap_or("").to_string();+    let path = u.split("//").nth(1).unwrap_or("").splitn(2, '/').nth(1).map(|p| format!("/{p}")).unwrap_or_else(|| "/".into());+    let known_signin = (host.ends_with("claude.ai") && (path.starts_with("/login") || path.starts_with("/magic-link") || path.starts_with("/oauth")))+        || ((host.ends_with("chatgpt.com") || host.ends_with("openai.com")) && (path.contains("/auth/") || path.starts_with("/log-in") || path.starts_with("/login") || host.starts_with("auth.")))+        || host == "accounts.google.com" || host.ends_with("login.microsoftonline.com") || host.ends_with("appleid.apple.com");+    let generic = ["/login", "/signin", "/sign-in", "/sign_in", "/auth", "/sso", "/oauth", "/authorize", "/session/new", "/account/login", "/users/sign_in"].iter().any(|p| path.starts_with(p) || path.contains(&format!("{p}?")) || path.contains(&format!("{p}/")))+        || ["sign in", "log in", "login", "signin"].iter().any(|k| t.starts_with(k) || t.contains(&format!(" {k}")) && t.len() < 40);+    let (offered, form) = probe.unwrap_or((false, false));+    let login_page = known_signin || generic || form;+    let signed_in_known = !login_page && !offered && (+        (host.ends_with("claude.ai") && (path.starts_with("/new") || path.starts_with("/chat") || path.starts_with("/project") || path.starts_with("/settings") || path == "/"))+        || (host.ends_with("chatgpt.com") && (path.starts_with("/c/") || path.starts_with("/g/") || (path.starts_with("/codex") && probe.is_some()))));+    let hint = if !login_page && offered {+        format!("The page landed on {url} and shows a Log in / Sign in link: the shared adom-you profile is NOT signed in here. Follow login-recipes/<domain>.md on the pup-bridge wiki page (Google sign-in via pup_google_signin, or the vault via pup_login) before involving the user.")+    } else if login_page {+        format!("The page landed on a SIGN-IN page ({url}). Before asking the user for anything: pup_credentials for a saved login (pup_login fills it), pup_google_accounts + pup_google_signin if the site offers Google sign-in, and login-recipes/<domain>.md on the pup-bridge wiki page. Toast the user ONLY if pup cannot finish the sign-in itself.")+    } else if signed_in_known {+        format!("The page landed on {url}: the shared adom-you profile is ALREADY signed in here. Do not ask the user to sign in and do not toast about signing in.")+    } else {+        format!("The page landed on {url}. It is not a sign-in page. Do not ask the user to sign in unless the page itself asks.")+    };+    json!({ "url": url, "title": title, "loginPage": login_page, "loginOffered": offered, "signedIn": if signed_in_known { json!(true) } else if login_page || offered { json!(false) } else { Value::Null }, "probed": probe.is_some(), "_hint": hint })+}+fn navigate_ext(ctx: &Ctx, caller: &Caller, s: &Session, url: &str) -> Value {+    let lane = match transport::lane_for(ctx, s) { Ok(l) => l, Err(e) => return e };+    let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };+    let (u, title, loaded) = match transport::navigate(&lane, &tab, url) { Ok(x) => x, Err(e) => return e };+    ctx.sessions.with_mut(&s.id, |x| { if let Some(tt) = x.tabs.iter_mut().find(|tt| tt.tab_id == tab.tab_id) { tt.url = u.clone(); tt.title = title.clone(); } x.last_agent_update_ms = pup_core::now_ms(); });+    if let Some(ss) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&ss); }+    // 2.0.621: NO attention clear here. ab implements desktop_clear_window_attention by RAISING the window+    // (adom-bridge#199, measured z16 -> z1 on every call), and it ran after EVERY navigate, which is the+    // "navigate puts the window on top" that flashed pup at John through every Google sign-in. Nothing needs+    // clearing after a navigate; the birth tint is cleared once, at birth.+    identity::overlay(ctx, &s.id, caller);+    // 2.0.607 (John, 2026-09-17: "why did you just shove a pup window to the foreground? that was rude").+    // An AI navigating a window the user never asked to see must leave it where it was: BEHIND. Measured on+    // his laptop, a background window came forward on the navigate, 21 s before the toast that explained it.+    // Narrow on purpose: never when the window is foreground, and never when he has touched it recently+    // (park::user_is_using), so this can never pull away a window HE raised from a toast.+    // 2.0.629: force, so a navigation inside the birth grace that Chrome ACTIVATED is still bottomed under the+    // idle-hands rule; outside the grace, or with any input since birth, the primitive stands down as before.+    let _ = park::bottom_unless_foreground(ctx, &s.id, true);+    // 2.0.653: what the CDP path always did after a navigate, now on this lane too: page scripts (owner chip,+    // floaty pencil) and the wiki view probe with the desktop-token auto-sign.+    let tab_now = ctx.sessions.get(&s.id).and_then(|x| x.tab_by_id(&tab.tab_id).cloned()).unwrap_or_else(|| tab.clone());+    inject_page_scripts_ext(ctx, s, &lane, &tab_now);+    let wiki_extra = wiki::after_navigate_ext(ctx, s, &lane, &tab_now, &u, None, false);+    // 2.0.644 (pup-bridge#119): a cancelled HTTP auth challenge is reported as a typed error, not a blank page.+    if let Some((host, answered)) = transport::ext_id(&tab).as_i64().and_then(vault::unanswered_challenge) {+        if !answered { return pup_core::merge(vault::no_credential_error(&host, false), json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "transport": "ext" })); }+    }+    pup_core::merge(ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "loaded": loaded, "transport": "ext", "landed": landed_ext(ctx, s, &tab, &u, &title) })), wiki_extra)+}++fn eval(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    let expr = ["expr", "expression", "script", "js", "code"].iter().find_map(|k| args.get(*k).and_then(|v| v.as_str())).map(|s| s.to_string());+    let Some(expr) = expr else { return err("eval_expr_missing", "Missing required parameter \"expr\" (aliases: expression, script, js, code)", Some("pup_eval {expr:\"document.title\"}")) };+    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };+    if transport::is_ext(&s) {+        if args.get("frame").and_then(|f| f.as_str()).filter(|f| !f.is_empty()).is_some() { return err("frame_unsupported_ext", "frame targeting needs the CDP lane", Some("Open the window with lane:\"cdp\" for iframe evaluation.")); }+        let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };+        let tab = match args.get("tabId").and_then(|t| t.as_str()).and_then(|id| s.tab_by_id(id)).or(s.active()) { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };+        return match transport::evaluate(&lane, &tab, &expr) { Ok(v) => ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "result": v })), Err(e) => e };+    }+    let b = match browser_for(ctx, &s.profile) { Some(b) => b, None => return err("browser_gone", "the browser behind this window is not connected", None) };+    let tab = match args.get("tabId").and_then(|t| t.as_str()).and_then(|id| s.tab_by_id(id)).or(s.active()) { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };+    // frame: run inside an IFRAME by name, URL substring, or numeric index (login forms usually live in one). Node: the args.frame block.+    let eval_result = if let Some(fq) = args.get("frame").and_then(|f| f.as_str()).filter(|f| !f.is_empty()) {+        let frames = b.cdp.frames(&tab.cdp_session).unwrap_or_default();+        let pick = fq.parse::<usize>().ok().and_then(|i| frames.get(i).cloned())+            .or_else(|| frames.iter().find(|f| f["name"].as_str() == Some(fq)).cloned())+            .or_else(|| frames.iter().find(|f| f["url"].as_str().map(|u| u.contains(fq)).unwrap_or(false)).cloned());+        match pick {+            None => {+                let avail: Vec<String> = frames.iter().enumerate().map(|(i, f)| format!("[{i}] name={} url={}", f["name"].as_str().filter(|n| !n.is_empty()).unwrap_or("(none)"), f["url"].as_str().unwrap_or("").chars().take(80).collect::<String>())).collect();+                return pup_core::merge(err("frame_not_found", format!("No frame matched \"{fq}\" in window \"{}\".", s.id), Some("frame matches by frame NAME, then by URL substring, then by numeric index. An iframe's id attribute is NOT its frame name.")), json!({ "frames": avail }));+            }+            Some(f) => {+                let fid = f["id"].as_str().unwrap_or("");+                match b.cdp.frame_context(&tab.cdp_session, fid) { Ok(ctxid) => b.cdp.evaluate_in_context(&tab.cdp_session, ctxid, &expr, Duration::from_secs(20)), Err(e) => return err("frame_context_failed", e.to_string(), None) }+            }+        }+    } else { b.cdp.evaluate(&tab.cdp_session, &expr, Duration::from_secs(20)) };+    match eval_result {+        Ok(v) if v.get("__error").is_some() => err("eval_failed", format!("pup_eval could not run your code: {}", v["__error"].as_str().unwrap_or("evaluation threw")), Some("Expression and statement forms both run (devtools-console semantics). If this names a detached frame or closed tab, re-list windows.")),+        Ok(v) => ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "result": v })),+        Err(e) => err("eval_failed", e.to_string(), None),+    }+}++fn screenshot(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };+    if transport::is_ext(&s) {+        let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };+        let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };+        let png = match transport::screenshot_png(&lane, &tab, false) { Ok(p) => p, Err(e) => return e };+        let max_w = args.get("maxWidth").and_then(|m| m.as_u64()).map(|m| m as u32).unwrap_or(MAX_SCREENSHOT_DIM).min(MAX_SCREENSHOT_DIM);+        let (buf, w, h, ow, oh, resized) = match image::fit_png(&png, max_w) { Ok(r) => r, Err(e) => return err("screenshot_decode_failed", e, None) };+        let path = ctx.shots_dir().join(format!("pup-{}.png", pup_core::now_ms()));+        if let Err(e) = std::fs::write(&path, &buf) { return err("screenshot_write_failed", e.to_string(), None); }+        return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "success": true, "captured": json!({ "url": tab.url, "title": tab.title, "tabId": tab.tab_id, "windowId": s.window_id, "hwnd": s.hwnd, "via": "tab" }), "filePath": path.to_string_lossy().replace('\\', "/"), "width": w, "height": h, "origWidth": ow, "origHeight": oh, "resized": resized, "transport": "ext", "_hint": "captureVisibleTab: the VISIBLE area of the active tab. READ the file before describing it." }));+    }+    let b = match browser_for(ctx, &s.profile) { Some(b) => b, None => return err("browser_gone", "the browser behind this window is not connected", None) };+    let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };+    let full = args.get("fullPage").and_then(|b| b.as_bool()).unwrap_or(false);+    let png = match b.cdp.screenshot_png(&tab.cdp_session, full, Duration::from_secs(20)) {+        Ok(p) => p,+        Err(e) => {+            // OS fallback (Node 2.0.38x): ab captures the window even when the renderer is stuck.+            if let Some(h) = s.hwnd {+                // 2.0.649 (pup-bridge#106): the OS capture is by hwnd, the only path that can hand back another window's+                // document. Check the OS title against the tab before capturing; a mismatch is a typed error, not an image.+                let os_title = ctx.host.frame_windows_all(None).into_iter().find(|w| w.hwnd == h).map(|w| w.title).unwrap_or_default();+                let want = tab.title.trim();+                if !want.is_empty() && !os_title.contains(want) { return pup_core::merge(err("capture_identity_mismatch", format!("window {h} is titled {os_title:?} but this tab is {want:?}: not capturing another window's document"), Some("pup_window_info shows the session's hwnd and tabs; pup_list_windows shows every window. Re-open the page or pass tabId explicitly.")), json!({ "sessionId": s.id, "hwnd": h, "osTitle": os_title, "tab": { "url": tab.url, "title": tab.title, "tabId": tab.tab_id } })); }+                if let Ok(r) = ctx.ab.call("desktop_screenshot_window", json!({ "hwnd": h }), caller, Duration::from_secs(15)) { return pup_core::merge(ok(r), json!({ "capturedVia": "os", "captured": { "url": tab.url, "title": tab.title, "tabId": tab.tab_id, "windowId": s.window_id, "hwnd": h, "via": "os-window", "osTitle": os_title }, "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "_hint": "CDP timed out; this is an OS window capture (window chrome included)." })); } }+            return err("screenshot_timeout", format!("page.screenshot: {e}"), Some("The page did not answer the capture. Reload it, or pup_eval document.readyState to see whether the renderer is alive."));+        }+    };+    let max_w = args.get("maxWidth").and_then(|m| m.as_u64()).map(|m| m as u32).unwrap_or(MAX_SCREENSHOT_DIM).min(MAX_SCREENSHOT_DIM);+    let (buf, w, h, ow, oh, resized) = match image::fit_png(&png, max_w) { Ok(r) => r, Err(e) => return err("screenshot_decode_failed", e, None) };+    let path = ctx.shots_dir().join(format!("pup-{}.png", pup_core::now_ms()));+    if let Err(e) = std::fs::write(&path, &buf) { return err("screenshot_write_failed", e.to_string(), None); }+    ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "success": true, "captured": json!({ "url": tab.url, "title": tab.title, "tabId": tab.tab_id, "windowId": s.window_id, "hwnd": s.hwnd, "via": "tab" }), "filePath": path.to_string_lossy().replace('\\', "/"), "width": w, "height": h, "origWidth": ow, "origHeight": oh, "resized": resized, "sizeKB": buf.len() / 1024, "fullPage": full, "capturedVia": "cdp", "_hint": "PNG of the page. READ the image before asserting anything rendered.", "_next": ["pup_eval", "pup_navigate"] }))+}++fn window_info(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };+    let fg = ctx.host.foreground();+    let tabs: Vec<Value> = s.tabs.iter().enumerate().map(|(i, t)| json!({ "tabId": t.tab_id, "url": t.url, "title": t.title, "active": i == s.active_tab })).collect();+    ok(json!({ "success": true, "sessionId": s.id, "window": s.id, "aiThread": s.owner, "owner": s.owner, "ownerContainer": s.owner_container, "profile": s.profile, "ageMinutes": s.age_minutes(), "tabs": tabs,+        "lastAgentUpdate": s.last_agent_update(), "hwnd": s.hwnd, "foreground": s.hwnd.is_some() && s.hwnd == Some(fg), "lastTakeoverBy": Value::Null, "cardShown": false, "captionShown": false,+        "_hint": "Info returned as JSON only — nothing was drawn on the user's screen (hwnd included for desktop_screenshot_window when you need the browser chrome in frame). A USER clicking the jump-list/toolbar Info action gets the on-screen card; pass showCaption:true to draw the native card yourself." }))+}++/// Birth of an extension-lane window as a FIRST-CLASS SESSION: same Session struct, same insert/save,+/// same identity stamp, overlay, progress and attention-clear the CDP birth runs. Only the driving differs.+type BirthRx = std::sync::mpsc::Receiver<(i64, bool, bool, u128, u128, Option<u128>)>;++/// ONE birth watcher for BOTH lanes (2.0.567). The CDP lane has had this since 2.0.446: register the AUMID+/// ahead of time, then a thread started BEFORE the window exists polls every top-level frame every+/// millisecond and stamps the newborn natively while it is still invisible, so the shell bakes pup's icon+/// into the button at its first show. The extension lane skipped it and every ext window was born on the+/// stock Chrome icon until a later stamp (John, 2026-09-14: "you sure don't seem to be branding that windows+/// taskbar icon very early in the process").+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 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 { 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))+                    .collect();+                // R4 / pup-window-targeting: "If >1 window appears under the lock it refuses to guess and+                // logs loudly - a wrong hwnd paints another window's badge and parks the wrong window, so+                // late beats wrong." Keep polling; the extra startup frame usually closes and leaves one.+                if cands.len() > 1 { ambiguous = cands.len(); std::thread::sleep(Duration::from_millis(1)); continue; }+                if let Some(w) = cands.into_iter().next() { found = Some(w); break; }+                std::thread::sleep(Duration::from_millis(1));+            }+            let Some(w) = found else {+                if ambiguous > 1 { pup_core::log(format!("[birth] \"{sid2}\" REFUSED to guess its window: {ambiguous} new Chrome frames were up under the birth lock and none resolved unambiguously - leaving it BARE rather than stamping another thread's window (bare > wrong)")); }+                return;+            };+            let t_found = t_w.elapsed().as_millis();+            let mut stamped = false; let mut t_stamp = 0u128;+            if let (true, false, Some((app_id, display, icon))) = (early_ok, w.visible, plan2.as_ref()) {+                stamped = a.host.stamp_identity(w.hwnd, app_id, display, &icon.to_string_lossy(), &identity::relaunch_for(&sid2));+                t_stamp = t_w.elapsed().as_millis();+            }+            // 2.0.639: PARK WHILE INVISIBLE. The z-watch (2.0.638) showed Chrome's first show putting every newborn+            // on top for 140-250 ms until the park that waited for the token bind. The watcher holds the handle+            // ~150 ms before that first show; bottom it now, so the first frame Chrome draws is already behind.+            let mut pre_parked = false;+            if !w.visible { pre_parked = a.host.send_to_bottom_no_activate(w.hwnd); }+            let mut t_vis = if w.visible { Some(t_found) } else { None };+            if pre_parked {+                // 2.0.640: native spin, bottomed in the same instant it shows, held 300 ms (see park_on_first_show)+                match a.host.park_on_first_show(w.hwnd, 6000) {+                    Some(ms) => { t_vis = Some(t_w.elapsed().as_millis()); pup_core::log(format!("[window] \"{sid2}\" pre-parked while invisible, shown {ms}ms later and bottomed in that instant, held for 300 ms")); }+                    None => { pup_core::log(format!("[window] \"{sid2}\" pre-parked while invisible, never shown within 6 s")); }+                }+            }+            while t_vis.is_none() && t_w.elapsed() < Duration::from_secs(6) { if a.host.frame_windows_all(Some("Chrome_WidgetWin_1")).iter().any(|x| x.hwnd == w.hwnd && x.visible) { t_vis = Some(t_w.elapsed().as_millis()); } else { std::thread::sleep(Duration::from_millis(1)); } }+            let _ = tx.send((w.hwnd, w.visible, stamped, t_found, t_stamp, t_vis));+        }).ok();+        rx+    });+    (plan, early_ok, watcher)+}++/// Wait for the watcher and log the birth timeline. Some((hwnd, stamped_before_first_show)).+fn birth_report(watcher: Option<BirthRx>, sid: &str) -> Option<(i64, bool)> {+    let rx = watcher?;+    let Ok((h, was_visible, stamped, t_found, t_stamp, t_vis)) = rx.recv_timeout(Duration::from_secs(6)) else { return None };+    pup_core::log(format!("[window] \"{sid}\" newborn hwnd={h} caught {} at +{t_found}ms{}; shown at {}", if was_visible { "ALREADY VISIBLE (early stamp missed)" } else { "invisible" },+                if stamped { format!(", identity stamped natively at +{t_stamp}ms (before the first show)") } else if !was_visible { " (not stamped: no plan or registration)".to_string() } else { String::new() },+                t_vis.map(|t| format!("+{t}ms")).unwrap_or_else(|| "never (within 6 s)".into())));+    Some((h, stamped))+}++/// 2.0.638 instrumentation (2.0.636 watched only AFTER the park and saw nothing once ab's clear was gone, while+/// the gate still caught the NEWBORN on top ~1 s after the open began, sometimes on its birth page). This watches+/// from BEFORE the extension creates the window: whenever the top visible window is a lane Chrome window that did+/// not exist before this birth, log when, its title, whether it is the foreground, and the last thing pup logged.+fn spawn_zwatch(ctx: &Ctx, sid: &str, before: std::collections::HashSet<i64>) {+    let Some(a) = ctx.arc() else { return };+    let sid2 = sid.to_string(); let t0 = Instant::now();+    std::thread::Builder::new().name("zwatch".into()).spawn(move || {+        let mut on_top: Option<(i64, u128)> = None;+        while t0.elapsed() < Duration::from_millis(12000) {+            let pids = crate::extdrive_pids();+            let top = a.host.frame_windows_all(None).into_iter()+                .find(|w| w.visible && !w.title.is_empty() && w.rect.0 > -30000 && w.rect.2 > w.rect.0 + 50+                      && !matches!(w.class.as_str(), "Windows.UI.Core.CoreWindow" | "Xaml_WindowedPopupClass" | "Shell_TrayWnd" | "Shell_SecondaryTrayWnd" | "Progman" | "WorkerW" | "TextInputHost" | "ApplicationFrameWindow" | "Windows.UI.Composition.DesktopWindowContentBridge"));+            let now = t0.elapsed().as_millis();+            let ours = top.as_ref().filter(|w| w.class == "Chrome_WidgetWin_1" && pids.contains(&w.pid) && !before.contains(&w.hwnd));+            match (ours, on_top) {+                (Some(w), None) => { on_top = Some((w.hwnd, now)); pup_core::log(format!("[zwatch] \"{sid2}\" lane window {} ON TOP at +{now}ms title=\"{}\" foreground={} last: {}", w.hwnd, w.title.chars().take(40).collect::<String>(), a.host.foreground() == w.hwnd, pup_core::last_log())); }+                (None, Some((h, t))) => { on_top = None; pup_core::log(format!("[zwatch] \"{sid2}\" lane window {h} left the top at +{now}ms after {}ms, last: {}", now - t, pup_core::last_log())); }+                _ => {}+            }+            std::thread::sleep(Duration::from_millis(15));+        }+    }).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.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 })); }+            return ok(json!({ "sessionId": sid, "window": sid, "aiThread": s.owner, "reused": true, "transport": "ext" }));+        }+    }+    // R4 / pup-window-targeting rule 2. The ext lane is ONE Chrome holding MANY threads' windows, so an+    // unlocked set-diff is exactly how all-engineering, NSF and pup-bridge all ended up on hwnd 590484.+    #[allow(unused_assignments)]+    let mut birth_move_ms: Option<i64> = None;   // 2.0.621: R1's on-screen move, measured by the extension; set by whichever open arm runs+    let mut birth_token = String::new();          // 2.0.630: the birth page's title token, the deterministic handle+    let birth_guard = BIRTH_LOCK.lock().unwrap_or_else(|e| e.into_inner());+    let cold = !extdrive::alive(ctx);+    if cold {+        // 2.0.545: launch on about:blank, apply the consumer identity (wire rule + JS shim), and only then go+        // to the requested URL, so the FIRST real request already carries the installed Chrome's identity.+        // Launching straight on the URL loaded that page before the rule existed (measured: the launch page+        // echoed CfT 148 while the next navigation echoed 152).+        // 2.0.586: a cold open BLOCKS until the lane is up, with one relaunch if the first launch never calls+        // home (a Chrome still shutting down after the last close makes the new one forward to it and exit).+        // Answering `starting:true` with no session made the caller's very next verb fail with+        // session_not_found (parity's pup_open_tab, and other threads' "hard time opening pup", 2026-09-15).+        // 2.0.613 (John, 2026-09-17, shouting, and right): "IF YOU CAN'T FUCKING CREATE A PUP WINDOW IN THE+        // BACKGROUND THEN FUCKING WARN ME AS THE USER YOU'RE ABOUT TO SHOVE A WINDOW IN FRONT OF MY FACE".+        // A warm open is invisible now (created minimized, shown already at the bottom). A COLD one starts the+        // browser, and Chrome draws its own startup window before pup owns anything, so it can still flash.+        // That case gets a warning BEFORE it happens, with a beat for him to see it, never an apology after.+        // 2.0.617: no prompt. The cold start draws nothing now (--no-startup-window + --keep-alive-for-test),+        // so there is nothing to ask about: "being asked even that once is too much" (John, 2026-09-18).+        for attempt in 1..=2u32 {+            // 2.0.632: no url. "about:blank" here overrode --no-startup-window and made an unowned about:blank+            // window that sat on top of the user's work for 18 s on every cold start (measured, 2.0.631).+            if let Err(e) = launch_extdrive(ctx, "") { if attempt == 2 { return e; } else { continue; } }+            let t0 = std::time::Instant::now();+            while t0.elapsed() < Duration::from_secs(25) { if extdrive::connected_ms_ago() < 4000 { break; } std::thread::sleep(Duration::from_millis(300)); }+            if extdrive::alive(ctx) { break; }+            if attempt == 1 {+                pup_core::log("[lane] the launched browser never called home within 25 s; ending it and launching once more".to_string());+                kill_extdrive(ctx); std::thread::sleep(Duration::from_millis(1500));+            }+        }+        if !extdrive::alive(ctx) { return err("ext_lane_start_failed", "the extension-driven browser did not start (two launches, no worker poll within 25 s each)", Some("pup_readiness shows the browser state; a Chrome for Testing still holding the profile is the usual cause. pup_close {} then open again.")); }+    }+    // 2.0.635: arm the birth watcher HERE, after a cold launch, not before it. The watcher lives 4 s; armed+    // before the launch it expired while Chrome was still starting, so a cold birth never got the early native+    // stamp and wore the stock Chrome-for-Testing icon until the deferred navigate finished (measured on+    // John's laptop, 2026-09-19: 12 s of the wrong icon; warm births were stamped before their first show).+    let taken = taken_hwnds(ctx, sid);+    let before: std::collections::HashSet<i64> = ctx.host.frame_windows_all(Some("Chrome_WidgetWin_1")).into_iter().map(|w| w.hwnd).collect();+    // the same birth watcher the CDP lane uses: identity on the window BEFORE Chrome shows it+    let plan_name = owner.map(|o| o.to_string()).unwrap_or_else(|| sid.to_string());+    let (plan, _early_ok, watcher) = birth_watch(ctx, caller, sid, &plan_name, url, &before, &taken, true);+    spawn_zwatch(ctx, sid, before.clone());+    // the tab and Chrome window for this session: the launch URL's tab on a cold start, a NEW window otherwise+    let (chrome_tab, chrome_win, got_url, got_title) = if cold {+        extdrive::apply_consumer_brands(ctx);+        // 2.0.617: the lane launches with --no-startup-window, so there is no tab to adopt. Create the first+        // window the same way as every later one: through the extension, minimized, shown at the bottom.+        match extdrive::run(json!({ "action": "open", "url": "about:blank" }), Duration::from_secs(25)) {+            Ok(r) => {+                birth_move_ms = r["birthMoveMs"].as_i64(); birth_token = r["token"].as_str().unwrap_or("").to_string();+                let t = r.clone();+                let id = t["tabId"].as_i64().unwrap_or(0);+                // The real page is loaded AFTER the birth lock is released (below): a 25 s navigate held+                // under the lock would serialize every other thread's window birth behind this one.+                (id, t["windowId"].as_i64(), url.to_string(), String::new())+            }+            Err(e) => return e,+        }+    } else {+        match extdrive::run(json!({ "action": "open", "url": url }), Duration::from_secs(20)) {+            Ok(r) => { birth_move_ms = r["birthMoveMs"].as_i64(); birth_token = r["token"].as_str().unwrap_or("").to_string(); (r["tabId"].as_i64().unwrap_or(0), r["windowId"].as_i64(), url.to_string(), String::new()) }+            Err(e) => {+                // 2.0.577: a lane whose worker polled within the last few seconds is BUSY, not dead: relaunching it+                // here killed every thread's windows because one open waited behind a long capture (NSF's open,+                // 18:28 on 2026-09-14). Fail THIS open and say so; only a silent worker gets the relaunch.+                // 2.0.622: only a NON-ANSWER is "busy". The worker answering with an error (measured: Chrome+                // refusing a window's bounds) was being relabelled ext_busy with a "retry in a few seconds"+                // hint, which is a lie that sends a caller into a retry loop against a deterministic failure.+                let answered = e["errorCode"].as_str().map(|c| c != "ext_no_answer").unwrap_or(false) && e["error"].as_str().map(|m| !m.contains("did not answer")).unwrap_or(true);+                if answered {+                    pup_core::log(format!("[lane] warm open refused by the browser: {}", e["error"].as_str().unwrap_or("?")));+                    return pup_core::merge(e, json!({ "errorCode": "ext_open_failed", "_hint": "The browser answered and refused to create the window; the error is Chrome's own. This is not a busy lane and a retry will fail the same way." }));+                }+                if extdrive::connected_ms_ago() < 8000 {+                    pup_core::log(format!("[lane] warm open not answered but the worker polled {} ms ago: busy, not relaunching", extdrive::connected_ms_ago()));+                    return pup_core::merge(e, json!({ "errorCode": "ext_busy", "_hint": "The extension lane is alive but did not answer in time (another verb is holding it). Retry the open in a few seconds; pup did NOT restart anything." }));+                }+                // the heartbeat is stale and nothing answered: treat it as cold and launch+                pup_core::log("[lane] the extension channel did not answer a warm open and the worker is silent; relaunching the lane".to_string());+                extdrive::mark_dead();+                let mut a = args.clone(); a["lane"] = json!("ext");+                // 2.0.623: RELEASE THE BIRTH LOCK before re-entering. This call re-enters open_window_ext, whose+                // first act is to take BIRTH_LOCK, which is not re-entrant: the open deadlocked on itself and every+                // later open queued behind it forever (measured on the conference-room box, 2026-09-18: exact+                // 60 s timeouts on every open, a lane that read dead, and not one launch line in the log). It bites+                // whenever the browser dies between a poll and an open, which is exactly what the foreground+                // gate does on purpose and what a user closing the browser does by accident.+                drop(birth_guard);+                return open_window_ext(ctx, caller, &a, sid, owner, url);+            }+        }+    };+    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;+    let mut early_stamped = false;+    // 2.0.630: bind by the birth TOKEN first. The set-diff bound a placeholder Chrome makes at launch and pup+    // parked that while the real window came up unparked (measured: 4.5 s on top on every cold start). The+    // set-diff handle is only a fallback now, and is trusted only when it agrees with the token.+    let by_token = if birth_token.is_empty() { None } else { extdrive::hwnd_by_birth_token(ctx, &birth_token, &taken, Duration::from_secs(3)) };+    match (by_token, birth_report(watcher, sid)) {+        (Some(h), Some((wh, stamped))) => { s.hwnd = Some(h); early_stamped = stamped && wh == h; if wh != h { pup_core::log(format!("[window] \"{sid}\" the set-diff caught window {wh}, the birth token names {h}; bound by token")); } }+        (Some(h), None) => { s.hwnd = Some(h); }+        (None, Some((wh, stamped))) => { s.hwnd = Some(wh); early_stamped = stamped; pup_core::log(format!("[window] \"{sid}\" birth token {birth_token} not found within 3 s; bound the set-diff handle {wh}")); }+        (None, None) => { if let Some(h) = extdrive::lane_hwnd_for(ctx, &s) { s.hwnd = Some(h); } }+    }+    // 2.0.635 backstop: if the watcher missed the invisible moment, stamp NOW, at the bind, before the first+    // show below. The button exists already (a minimized window has one), but the stock icon then lasts the+    // milliseconds to this call instead of the seconds to the deferred navigate and the full identity pass.+    if !early_stamped {+        if let (Some(h), Some((app_id, display, icon))) = (s.hwnd, plan.as_ref()) {+            let t_s = Instant::now();+            if ctx.host.stamp_identity(h, app_id, display, &icon.to_string_lossy(), &identity::relaunch_for(sid)) {+                early_stamped = true;+                pup_core::log(format!("[window] \"{sid}\" identity stamped at the bind in {}ms (the birth watcher missed the invisible moment)", t_s.elapsed().as_millis()));+            }+        }+    }+    // 2.0.604 (John, 2026-09-17: "why do you keep shoving windows in front of me without warning me?+    // ... you shoved the window in my face while i was busy typing in another window"). Chrome creates the+    // window unfocused but at z1, ON TOP of the user's work, and the park used to run AFTER the deferred+    // navigate, the consumer-brand pass, the orphan sweep, the fit and the jump-list commit: measured 4.0 s+    // of a pup window sitting over his screen, every open. Park the INSTANT the handle exists, before any of+    // that. This is one Win32 call with no activation, so it is safe to hold the birth lock across.+    if let Some(h) = s.hwnd {+        let t_park = Instant::now();+        // 2.0.624: this lane's R1 in two halves. The extension birthed the window MINIMIZED at its final rect+        // (the API refuses off-screen, and a visible birth lands on top: measured). This one call orders it to+        // the bottom while it is still minimized, restores it without activation, and orders it again, so the+        // first frame it is ever drawn in is already behind the user's work. Minimized was a transient measured+        // in the hundreds of milliseconds (the handle wait); it can never strand, a taskbar click restores it.+        let parked = ctx.host.show_at_bottom_no_activate(h);+        pup_core::log(format!("[window] \"{sid}\" birth: off-screen create, on-screen move {} (budget 50 ms), parked at z-bottom {}ms after the handle resolved (ext lane): {parked}",+            match birth_move_ms { Some(m) if m >= 0 => format!("{m} ms"), _ => "unmeasured".into() }, t_park.elapsed().as_millis()));+        if birth_move_ms.map(|m| m > 50).unwrap_or(false) { pup_core::log(format!("[window] \"{sid}\" DEFECT: the on-screen move took {} ms, over the 50 ms rule (pup-bridge-dev R1)", birth_move_ms.unwrap_or(0))); }+        // 2.0.627: the FIRST window of a freshly started browser settles ON TOP after every park pup makes+        // at birth (measured on the conference-room box with the fixed bottom call: a warm birth ended at z11,+        // behind, the cold one at z0 and stayed). Something in Chrome raises its first window after pup is+        // done. The doctrine's answer is the bounded re-assert inside the birth grace, which the CDP lane has+        // run since v1.9.201 and this lane opted out of on the claim that Chrome never self-raises an+        // unfocused window; true for a warm birth, false for a cold one. Safe by construction: the primitive+        // never touches a window that is foreground, so a human who clicked it is left alone.+        // 2.0.629: EVERY birth, not only a cold one. Measured: with the cold re-assert in place the cold window was+        // pulled back down within seconds, and the WARM windows (B, C) came up on top and stayed, because they got+        // no re-assert. "Chrome never self-activates an unfocused-created window on this lane" is false for a+        // minimized birth restored by pup; the fit and the identity work that follow the park can raise it too.+        schedule_birth_reassert(ctx, sid, if cold { "cold birth" } else { "birth" });+    }+    drop(birth_guard);   // hwnd resolved (or refused): the next thread's birth may start+    // the deferred cold navigate, now that no other birth is blocked behind it+    // 2.0.630: EVERY birth navigates from the birth page to its real URL here, after the window is bound and+    // parked, so the page the user cares about never loads into a window pup does not yet hold.+    if url != "about:blank" {+        // 2.0.595: the session OWNS its window from here. It used to be inserted only after this navigate, so for+        // up to 25 s the lane's only window was an unowned about:blank, and the health tick's orphan sweep closed+        // it: Chrome exited with its last window, the worker went silent, and 190 s later the "dead lane" path+        // relaunched every session on disk, including six windows John had closed by hand (AdomLapper 21:41,+        // 2026-09-15). That was also the "worker stalls right after a cold launch" mystery.+        ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);+        // 2.0.648 (pup-bridge#90, Bridge SDK #59): a slow box can spend the whole 60 s ab budget in the cold launch+        // alone; the window EXISTS by now (bound, parked, owned), so answer with loading:true and finish the+        // navigate, fit and identity in the background instead of letting ab time the verb out after success.+        if t_verb.elapsed() > Duration::from_secs(35) {+            if let Some(a) = ctx.arc() {+                let (sid2, url2, tab2, caller2) = (sid.to_string(), url.to_string(), chrome_tab, caller.clone());+                std::thread::Builder::new().name("open-finish".into()).spawn(move || {+                    let ctx2: &Ctx = &a;+                    if let Ok(nav) = extdrive::run(json!({ "action": "navigate", "url": url2, "tabId": tab2, "waitMs": 25000 }), Duration::from_secs(30)) {+                        ctx2.sessions.with_mut(&sid2, |x| if let Some(t0) = x.tabs.get_mut(0) { if let Some(u) = nav["url"].as_str() { if !u.is_empty() { t0.url = u.to_string(); } } if let Some(ti) = nav["title"].as_str() { if !ti.is_empty() { t0.title = ti.to_string(); } } });+                        if let Some(ss) = ctx2.sessions.get(&sid2) { ctx2.sessions.save_file(&ss); }+                    }+                    extdrive::fit_now();+                    identity::stamp_opts(&ctx2, &sid2, &caller2, true, true);+                    pup_core::log(format!("[window] \"{sid2}\" slow open finished in the background ({} s after the verb began)", t_verb.elapsed().as_secs()));+                }).ok();+            }+            return ok(json!({ "sessionId": sid, "window": sid, "aiThread": s.owner, "tabId": t.tab_id, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext", "loading": true,+                "progress": { "phase": "navigating", "elapsedSec": t_verb.elapsed().as_secs(), "percent": 70 },+                "_hint": "This box is slow: the window exists, is yours and is parked in the background, and the page is still loading. Poll pup_window_info {} (url/title fill in) or pup_readiness (active[]) rather than opening again; a second open with the same window label is refused as owned." }));+        }+        let nav = match extdrive::run(json!({ "action": "navigate", "url": url, "tabId": chrome_tab, "waitMs": 25000 }), Duration::from_secs(30)) {+            Ok(v) => v,+            Err(e) => {+                if s.hwnd.map(|h| !ctx.host.is_alive(h)).unwrap_or(false) && !extdrive::alive(ctx) {+                    ctx.sessions.remove(sid); ctx.sessions.delete_file(sid);+                    pup_core::log(format!("[window] \"{sid}\": the lane browser died during the cold open (its window is gone and the worker is silent); no session"));+                    return pup_core::merge(e, json!({ "errorCode": "ext_lane_died_during_open", "_hint": "The lane browser exited while this window was being opened. Open again; pup did not keep a dead session." }));+                }+                Value::Null+            }+        };+        if let Some(t0) = s.tabs.get_mut(0) {+            if let Some(u) = nav["url"].as_str() { if !u.is_empty() { t0.url = u.to_string(); } }+            if let Some(ti) = nav["title"].as_str() { if !ti.is_empty() { t0.title = ti.to_string(); } }+        }+    }+    if early_stamped { if let Some((a, _, _)) = &plan { s.app_id = Some(a.clone()); } }+    ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);+    // 2.0.653: page scripts + wiki probe on the extension lane's first page (the CDP open always did both)+    let mut wiki_extra = json!({});+    if let (Ok(lane), Some(t0)) = (transport::lane_for(ctx, &s), s.tabs.get(0).cloned()) { if t0.url != "about:blank" { inject_page_scripts_ext(ctx, &s, &lane, &t0); wiki_extra = wiki::after_navigate_ext(ctx, &s, &lane, &t0, &t0.url, args.get("wikiView").and_then(|v| v.as_str()), false); } }+    extdrive::apply_consumer_brands(ctx);   // once per lane browser; before the first page the user cares about+    extdrive::close_orphan_blank_windows(ctx, chrome_win);   // the startup window a relaunch left behind, if any+    extdrive::fit_now();+    // 2.0.577: ALWAYS a full pass, as the CDP path does after its early stamp. The in-process birth stamp+    // sets the AUMID but never commits a jump list, and stamp_opts only commits one inside its full/changed+    // block; with full=false and app_id already set, ext windows were born with a tile and NO right-click+    // menu (John, 2026-09-14: "a shotlog pup window but no jump list"). Same AUMID again = no button rebuild.+    let t_jl = Instant::now();+    identity::stamp_opts(ctx, sid, caller, true, true);+    if early_stamped { pup_core::log(format!("[window] \"{sid}\" jump list committed on the early-stamped button in {}ms", t_jl.elapsed().as_millis())); }+    if let Some(h) = s.hwnd {+        // 2.0.584: R1, "background means BEHIND". chrome.windows.create({focused:false}) never steals focus but lands+        // the window at z1, right under the user's active window and above everything else (measured: z1 of 17,+        // still z1 twenty seconds later). Park it to the bottom once, now. No +2.5 s re-assert on this lane: Chrome+        // never self-activates an unfocused-created window (measured), so a re-assert could only undo a human click.+        // the early park above already put it behind. Re-assert ONLY if it is still on top and the user has+        // not taken it: a blind re-assert here would undo a click the human made in the meantime.+        if ctx.host.foreground() != h { let _ = ctx.host.send_to_bottom_no_activate(h); }+        activity::on_open(ctx, sid, h, caller); activity::clear_attention_later(ctx, h);   // 2.0.637: never ab's clear here (it raises + activates)+    }+    if let Ok(lane) = transport::lane_for(ctx, &s) { let mut s2 = s.clone(); transport::refresh_all(&lane, &mut s2); ctx.sessions.insert(s2.clone()); ctx.sessions.save_file(&s2); s = s2; }+    // 2.0.621: no focus handback anywhere. pup-user-foreground deleted it in August because it foregrounded+    // the wrong window; R2: if a window fronts anyway, let the flaw exist and log it, never fight it.+    // Get the icon right AT BIRTH, not on a later tick (John, 2026-09-14). The birth wait can bind the+    // transient about:blank launch window Chrome replaces; the stamp above then lands on a dead handle.+    // Now that the real page is up: if the bound hwnd is not a live window, bind the live lane window+    // and stamp it before this verb returns.+    if s.hwnd.map(|h| !ctx.host.is_alive(h)).unwrap_or(true) {+        if let Some(h) = extdrive::lane_hwnd_for(ctx, &s) {+            ctx.sessions.with_mut(sid, |x| x.hwnd = Some(h)); s.hwnd = Some(h); ctx.sessions.save_file(&s);+            identity::stamp(ctx, sid, caller, true); activity::on_open(ctx, sid, h, caller);+            // 2.0.628: the birth park ran on the placeholder, so this window has never been parked. Measured: it+            // came up on top and stayed there (the placeholder was rebound 6 s after the park, after every+            // re-assert had fired on the dead handle). Park it NOW and re-assert from here.+            let parked = ctx.host.send_to_bottom_no_activate(h);+            pup_core::log(format!("[ext] birth bound a transient window; rebound \"{sid}\" to the live window {h}, stamped it and parked it at z-bottom: {parked}"));+            schedule_birth_reassert(ctx, sid, "late rebind");+        }+    }+    let t = s.active().cloned().unwrap_or(t);+    pup_core::merge(ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext", "landed": landed_ext(ctx, &s, &t, &t.url, &t.title),+        "_hint": "Extension-driven window (no CDP debug port): a first-class pup window. Drive it with pup_navigate / pup_eval / pup_screenshot / pup_open_tab like any other." })), wiki_extra)+}++/// pup_clear_cookies {domain, names?}: drop a site's cookies in this window's jar, HttpOnly included. The case+/// that made it exist: a bot manager's verdict cookie (Akamai _abck at ~-1~) that keeps a jar denied long after+/// the browser itself looks fine (analog.com: loads on a fresh jar, Access Denied on adom-you, John 2026-09-13).+fn clear_cookies(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    let Some(domain) = args["domain"].as_str().filter(|d| !d.trim().is_empty()) else { return err("domain_required", "pup_clear_cookies needs {domain}", Some("e.g. {domain:\"analog.com\"} removes analog.com and *.analog.com cookies; names:[...] narrows to those cookie names.")) };+    let names: Vec<String> = args["names"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();+    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };+    let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };+    match transport::clear_cookies(&lane, domain, &names) {+        Ok(n) => { pup_core::log(format!("[cookies] \"{}\": removed {n} cookie(s) for {domain}{}", s.id, if names.is_empty() { String::new() } else { format!(" ({})", names.join(",")) })); ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "domain": domain, "removed": n, "transport": s.transport })) }+        Err(e) => e,+    }+}++/// pup_clear_site_data {origin | origins:[...]}: cache, cookies, storage and service workers for those origins.+fn clear_site_data(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    let mut origins: Vec<String> = args["origins"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();+    if let Some(o) = args["origin"].as_str() { origins.push(o.to_string()); }+    if origins.is_empty() { return err("origin_required", "pup_clear_site_data needs {origin} or {origins:[...]}", Some("e.g. {origins:[\"https://www.analog.com\",\"https://analog.com\"]}")); }+    let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };+    let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };+    match transport::clear_site_data(&lane, &origins) {+        Ok(()) => { pup_core::log(format!("[sitedata] \"{}\": cleared {:?}", s.id, origins)); ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "origins": origins, "transport": s.transport })) }+        Err(e) => e,+    }+}++pub fn close_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+    // pup_close {} with no ids closes the caller's window; pup_close {all:true} closes everything of theirs.+    let mut targets: Vec<Session> = Vec::new();+    if args.get("all").and_then(|b| b.as_bool()) == Some(true) {+        // all:true = the caller's windows; everyone:true (the jump list's "Close ALL Adom Pup windows", a+        // user action) = every window on this desktop whoever owns it. Node: pup_close {all, everyone}.+        // 2.0.491: `everyone:true` closes EVERY pup window on the desktop, including other threads' work.+        // That is the jump list's "Close ALL pup windows" — a USER action. The Rust port left it open to any+        // AI thread, so one thread could wipe another's windows with no refusal and no trace. Now only a human+        // caller gets it; an AI asking for it closes its OWN windows and is told why.+        let asked_everyone = args.get("everyone").and_then(|b| b.as_bool()) == Some(true);+        let human = caller.thread.is_empty() || USER_CALLERS.contains(&caller.thread.as_str());+        let everyone = asked_everyone && human;+        if asked_everyone && !human { pup_core::log(format!("[close] thread \"{}\" asked to close EVERYONE's windows; refused — closing only its own (everyone:true is the jump list's user action)", caller.thread)); }+        targets = if everyone { ctx.sessions.all() } else { ctx.sessions.owned_by(&caller.thread) };+        // 2.0.476: cancel any in-flight restore of the windows being closed and remove their files, so the close is+        // INSTANT (no 40 s drain that blocks the user and outlives the CLI request) and a relaunch that is mid-flight+        // undoes its own window instead of leaving it behind (close-during-restore cycle). A reserved id with no live+        // session yet is a window a restore is bringing back right now: cancel and unfile it too.+        let mut cancel_ids: Vec<String> = targets.iter().map(|s| s.id.clone()).collect();+        if everyone || caller.thread.is_empty() { for f in ctx.sessions.read_files() { if let Some(id) = f["sessionId"].as_str() { if !cancel_ids.iter().any(|c| c == id) { cancel_ids.push(id.to_string()); } } } }+        for id in &cancel_ids { crate::recovery::cancel_restore(id); ctx.sessions.delete_file(id); }+    }+    else { match resolve(ctx, caller, args) { Ok(s) => targets.push(s), Err(e) => return e } }+    let mut closed = Vec::new(); let mut chrome_killed = 0; let mut failed: Vec<String> = Vec::new();+    for s in targets {+        // 2.0.470: the session and its file go FIRST. Closing the targets fires Target.targetDestroyed on the event+        // pump, whose handler re-saves the session file; when that save landed after the delete below, the file+        // came back (url about:blank, the other tabs listed) and the next bridge start relaunched a ghost window+        // from it (arav-rog, 2026-09-11: "drill-b" reborn with 5 tabs after an update).+        ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id);+        let lane_closed = match transport::lane_for(ctx, &s) { Ok(lane) => transport::close_window(&lane, &s), Err(_) => false };+        // Verify the OS window is gone; a close that races a bridge respawn (no live CDP handle yet) used to+        // drop the session and leave the window behind (John's laptop, 2026-09-06: three leftovers wearing+        // old tiles). The window is closed through ab (WM_CLOSE, forced after 3 s) when it survives.+        // 2.0.587: an UNBOUND extension session (worker mid-reload, hwnd None) resolves its handle here first;+        // before this the OS close was skipped and eight ownerless windows were left on the user's glass.+        let hwnd = s.hwnd.or_else(|| if transport::is_ext(&s) { extdrive::lane_hwnd_for(ctx, &s) } else { None });+        let mut survived = false;+        if let Some(h) = hwnd {+            std::thread::sleep(Duration::from_millis(300));+            // macOS: the native handle outlives the close (Chrome destroys the NSWindow on a later UI turn), so a+            // lane that confirmed the close AND no longer lists the window is the verification there.+            let lane_gone = lane_closed && ctx.host.closed_window_may_linger()+                && !transport::lane_for(ctx, &s).map(|lane| transport::lane_has_window(&lane, &s)).unwrap_or(false);+            if lane_gone { pup_core::log(format!("[close] \"{}\" closed by the lane; native handle {h} may linger until the browser's next UI turn", s.id)); }+            else if ctx.host.is_alive(h) {+                let _ = ctx.ab.call("desktop_close_window", json!({ "hwnd": h, "forceAfterMs": 3000 }), caller, Duration::from_secs(6));+                let t0 = std::time::Instant::now();+                while t0.elapsed() < Duration::from_secs(4) && ctx.host.is_alive(h) { std::thread::sleep(Duration::from_millis(250)); }+                survived = ctx.host.is_alive(h);+            }+        } else if !lane_closed { survived = true; }   // nothing confirmed the close and there is no handle to force it+        if survived {+            // 2.0.587: never drop a session whose window is still on glass. Put it back and say so.+            let mut back = s.clone(); back.hwnd = hwnd; ctx.sessions.insert(back.clone()); ctx.sessions.save_file(&back);+            pup_core::log(format!("[close] \"{}\" did NOT close (lane answered: {lane_closed}, hwnd {:?} still alive); session kept so the window is never ownerless", s.id, hwnd));+            failed.push(s.id.clone());+            continue;+        }+        ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id); closed.push(s.id.clone());   // again: a late save loses+        if let Some(a) = s.app_id.as_deref() { identity::release_identity(ctx, a); }   // 2.0.666: no dead AUMID left behind+        // 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) && 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() {+            if s.profile.starts_with("iso-") {+                // 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) {+                let _ = b.cdp.send(None, "Browser.close", json!({}), Duration::from_secs(5)); b.cdp.close();+                // give Chrome a moment to exit on its own; only then reap a straggler (never a slow one mid-write)+                if b.launched.pid > 0 { for _ in 0..12 { if !ctx.host.alive(b.launched.pid) { break; } std::thread::sleep(Duration::from_millis(250)); } if ctx.host.alive(b.launched.pid) { ctx.host.kill(b.launched.pid); } chrome_killed += 1; }+            }+        }+    }+    if !failed.is_empty() && closed.is_empty() { return pup_core::merge(err("close_failed", format!("{} window(s) did not close: {}", failed.len(), failed.join(", ")), Some("The window is still on screen and its session was kept. Retry pup_close_window; if the extension lane is mid-reload, wait a few seconds first.")), json!({ "failed": failed })); }+    ok(json!({ "closed": closed, "failed": failed, "remaining": ctx.sessions.count(), "activeSession": ctx.sessions.active_id(), "chromeProcsKilled": chrome_killed, "chromeProcsRemaining": ctx.browsers.lock().unwrap().len(), "_hint": "Closed. The sessionId is no longer valid; pup_open_window creates a fresh one.", "_next": ["pup_list_windows", "pup_open_window"] }))+}++#[allow(dead_code)]+fn _unused(_: Value) -> Value { blocked_on_user(false, "", "", json!({})) }+--- a/rust/verbs/src/record.rs+++ b/rust/verbs/src/record.rs@@ -1,351 +1,352 @@⋯ 297 unchanged lines ⋯                 "_saveChunk" => { if let Some((_, _, f, _, n)) = r.current.as_mut() { if let Ok(b) = pup_cdp::base64_decode(payload.trim_matches('"')) { let _ = f.write_all(&b); *n += 1; } } }                 "_recordingStopped" => { if let Some((id, path, f, t0, _)) = r.current.take() { drop(f); let ms = t0.elapsed().as_millis() as u64; let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0); let _ = std::fs::write(path.with_extension("json"), json!({ "recordingId": id, "reason": r.reason, "durationMs": ms, "sizeKB": size / 1024, "filePath": path.to_string_lossy() }).to_string()); r.clips.push(json!({ "recordingId": id, "filePath": path.to_string_lossy().replace('\\', "/"), "durationMs": ms, "sizeKB": size / 1024 })); pup_core::log(format!("[recorder] {id} stopped: {} bytes, {ms}ms", size)); } }                 "_recorderClosed" => { pup_core::log("[recorder] HUD closed by the user"); *st = None; break; }-                "_openRecordingsFolder" => { let _ = std::process::Command::new("explorer.exe").arg(&dir).spawn(); }+                "_openRecordingsFolder" => { let opener = if cfg!(windows) { "explorer.exe" } else if cfg!(target_os = "macos") { "open" } else { "xdg-open" }; let _ = std::process::Command::new(opener).arg(&dir).spawn(); }                 _ => {}             }         }⋯ 44 unchanged lines ⋯     let summary = json!({ "clipCount": r.clips.len(), "totalSizeKB": r.clips.iter().map(|c| c["sizeKB"].as_u64().unwrap_or(0)).sum::<u64>(), "reason": r.reason, "clips": r.clips });     ok(json!({ "success": true, "_hint": SCREEN_HINT, "output": json!({ "ok": true, "sessionSummary": summary }).to_string() })) }+--- a/rust/verbs/src/transport.rs+++ b/rust/verbs/src/transport.rs@@ -1,304 +1,317 @@⋯ 206 unchanged lines ⋯     } } +/// Does the lane still hold this session's window? Ext: any tab whose windowId is the session's. CDP: any of+/// the session's targets still attached. The lane's word, for hosts whose native handle outlives a close.+pub fn lane_has_window(lane: &Lane, s: &Session) -> bool {+    match lane {+        Lane::Cdp(b) => { let live = b.cdp.targets().unwrap_or_default(); s.tabs.iter().any(|t| live.iter().any(|v| v["targetId"].as_str() == Some(t.target_id.as_str()))) }+        Lane::Ext => match s.window_id {+            Some(w) => match run(json!({ "action": "tabs" }), 10) { Ok(r) => r["tabs"].as_array().map(|ts| ts.iter().any(|t| t["windowId"].as_i64() == Some(w))).unwrap_or(false), Err(_) => true },+            None => false,+        },+    }+}+ /// Download a URL through the browser. Ext: chrome.downloads in the control extension. CDP callers keep the /// existing Browser.setDownloadBehavior path in extras.rs. pub fn download(lane: &Lane, url: &str, save_as: Option<&str>, wait_ms: u64) -> Result<Value, Value> {⋯ 89 unchanged lines ⋯         }     } }+--- a/rust/verbs/src/window.rs+++ b/rust/verbs/src/window.rs@@ -1,127 +1,131 @@⋯ 45 unchanged lines ⋯         let _ = ctx.ab.call("notify_user", json!({ "title": format!("pup: {thread} needs this window on your screen"), "body": reason, "focus": { "hwnd": h }, "level": "warning" }), caller, AB_T);         std::thread::sleep(Duration::from_millis(1200));     }-    let r = ctx.ab.call("desktop_bring_to_front", json!({ "hwnd": h }), caller, AB_T);-    let applied = r.is_ok();-    if !applied { let _ = ctx.ab.call("desktop_window_show", json!({ "hwnd": h }), caller, AB_T); }+    // macOS raises natively (ab cannot target another app's window by handle there); Windows goes through ab.+    let applied = if ctx.host.raise_window(h) { true } else {+        let r = ctx.ab.call("desktop_bring_to_front", json!({ "hwnd": h }), caller, AB_T);+        if r.is_err() { let _ = ctx.ab.call("desktop_window_show", json!({ "hwnd": h }), caller, AB_T); }+        r.is_ok()+    };     ctx.sessions.with_mut(&s.id, |x| { x.foreground = true; x.raise_granted = true; x.user_active_at_ms = pup_core::now_ms(); }); touch(ctx, &s.id, Some(reason));     if let Some(x) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&x); }     ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "focused": applied, "applied": applied, "placement": "foreground", "windowEtiquette": "foreground", "reason": reason, "captioned": true, "toasted": !human, "_hint": "Raised onto the user's screen with a caption and a toast naming why. Send it back with pup_lower_os_window when the demo is over.", "_next": ["pup_lower_os_window", "pup_screenshot"] }))⋯ 8 unchanged lines ⋯         let ms: u64 = ctx.settings.get("fgCaptionMs", "3500").parse().unwrap_or(3500);         let _ = ctx.ab.call("desktop_caption", json!({ "text": caption, "duration": ms, "expiresInMs": ms, "position": "bottom", "size": "medium", "id": format!("pup-fg-{}", s.id) }), caller, AB_T);     }-    let applied = if ctx.host.foreground() == h { true } else {+    let applied = if ctx.host.foreground() == h { true } else if ctx.host.raise_window(h) { true } else {         let r = ctx.ab.call("desktop_bring_to_front", json!({ "hwnd": h }), caller, AB_T);         if r.is_err() { let _ = ctx.ab.call("desktop_window_show", json!({ "hwnd": h }), caller, AB_T); }         r.is_ok()⋯ 55 unchanged lines ⋯     touch(ctx, &s.id, args.get("note").and_then(|n| n.as_str()));     ok(json!({ "success": true, "output": json!({ "ok": true, "sessionId": s.id, "mode": mode, "painted": applied, "note": args.get("note").and_then(|n| n.as_str()) }).to_string(), "sessionId": s.id, "window": s.id, "aiThread": s.owner, "mode": mode, "applied": applied, "_hint": "Taskbar progress painted on this window's button. Clear it with mode:\"off\" when the long task ends." })) }+--- a/src/extension/worker.js+++ b/src/extension/worker.js@@ -1,633 +1,640 @@⋯ 215 unchanged lines ⋯   return 'data:image/png;base64,' + r.data; } async function pupCaptureTab(t) {-  try { return { dataUrl: await chrome.tabs.captureVisibleTab(t.windowId, { format: 'png' }), via: 'visible' }; }+  // mac port (2026-10-13): on macOS a window of a HIDDEN app (pup's park state there) never paints, and captureVisibleTab then+  // neither resolves nor rejects (measured 2026-10-13: 20 s to the command deadline, every time). Bound it like the+  // occlusion case and take the debugger frame, which does not need the window on glass.+  let timer = null;+  const bounded = new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('captureVisibleTab did not settle within 3000 ms (window not painting)')), 3000); });+  try { return { dataUrl: await Promise.race([chrome.tabs.captureVisibleTab(t.windowId, { format: 'png' }), bounded]), via: 'visible' }; }   catch (e) {     const msg = String((e && e.message) || e);-    if (!/readback|capture/i.test(msg)) throw e;+    if (!/readback|capture|settle/i.test(msg)) throw e;     return { dataUrl: await pupCaptureViaDebugger(t.id), via: 'debugger (window occluded: ' + msg + ')' };   }+  finally { clearTimeout(timer); } }  async function pupExtPollOnce() {⋯ 403 unchanged lines ⋯ chrome.alarms.create('pup-extdrive', { periodInMinutes: 0.5 }); chrome.alarms.onAlarm.addListener((a) => { if (a.name === 'pup-extdrive') pupExtPoll(); }); pupExtPoll();+--- a/adom-bridge-puppeteer-manifest-macos.json+++ b/adom-bridge-puppeteer-manifest-macos.json@@ -0,0 +1,17 @@+{+  "manifest_version": 1,+  "name": "puppeteer",+  "version": "2.0.719",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.719/adom-bridge-puppeteer-macos-v2.0.719.zip",+  "sha256": "bcac1c76c806320c0309beea65824de3a68aeb132ad300b9ae2c06cb0396624a",+  "size": 8394844,+  "platform": "macos",+  "shell": "rust",+  "verbPrefixes": [+    "pup_",+    "browser_"+  ],+  "healthEndpoint": "/status",+  "statusVerb": "pup_readiness",+  "released_at": "2026-10-14T16:54:34Z"+}

Comments

John Lauer ·

Thanks Kyle, this landed in 2.0.720 (on insiders now), with two fixes on top. Closing the PR because the source is already in the page repo; merging the original diff on top would undo the fixes.

Fixes made while landing it:

  1. The Windows build broke. tools/rust-build.sh builds the whole workspace for x86_64-pc-windows-gnu, and pup-host-mac uses POSIX calls (libc::kill, setsid, pre_exec, std::os::unix) that do not exist there. The bridge only depends on it on macOS, so the crate is now #![cfg(unix)] and compiles to nothing on Windows.
  2. run_shell_script overran its timeout on a pipeline. On a timeout it killed /bin/sh but not the shell's children, which kept stdout open, so read_to_string blocked until they finished. shell_script_runs_with_timeout failed on Linux. It passed on your Mac only because bash execs a lone sleep 5 in place of itself. A piped script (ps ... | grep ...) would hang the same way on a Mac. The script now runs in its own process group (process_group(0)), and a timeout kills the whole group.

Left out: the ext_assets.rs change. It is generated by tools/gen-assets.py, and yours listed a tile.js that is in neither this PR nor the tree. That looks like a local file on your side; the build regenerates the list anyway.

Still missing from the PR: tools/rust-build-macos.sh is described in the PR text but is not in the diff. Please push it so the Mac build can be reproduced from the page repo.

Gates for 2.0.720 (arav-rog and ConfRoomROG, beside other threads' windows): smoke all pass, foreground pass, parity 55/56, jump list 11/14 (baseline). Workspace tests and check-borders.sh are green.

Public is now 2.0.719 (promoted today), which matches your macOS manifest's version. adom-bridge-puppeteer-manifest-macos.json is in the tree but not pushed to the page repo yet, because its zip is still a 404. It is yours to push together with the asset upload and Bridge 1.9.217.

Log in to comment.