master
Colby Knox v2.0.740 (insiders): New verb pup_upload_file (pup-bridge#145): put a file from this PC on a page's file input with no… 63fa650 5d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235
//! pup-verbs: the verb handlers. Each is a function of (ctx, caller, args) -> Value and knows
//! nothing about HTTP. `dispatch` routes every pup_* / credential_* name and applies the response
//! envelope the Node build's sendJSON applied (catalog hint/related/pitfalls, the bug-report invite,
//! render verdicts, the `output` mirror), so callers see the same shapes as docs/contract/*.json.
//! Response shapes follow docs/contract/*.json (recorded from the Node build) field for field.

#![recursion_limit = "512"]

use pup_browser::{Candidate, Installer, Launched};
use pup_cdp::Cdp;
use pup_core::{blocked_on_user, err, ok, AbClient, Caller, Settings};
use pup_host::Host;
use pup_session::{thread_default_session_id, Session, Sessions, Tab};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

pub mod image;
pub mod events;
pub mod activity;
pub mod park;
pub mod tabs;
pub mod input;
pub mod keys;
pub mod recovery;
pub mod window;
pub mod settings;
pub mod identity;
pub mod consent;
pub mod vault;
pub mod wiki;
pub mod annotate;
pub mod record;
pub mod highfps;
pub mod extras;
pub mod upload;
pub mod lint;
pub mod assist;
pub mod import;
pub mod google;

pub fn assets_recorder_html() -> Option<&'static [u8]> { Some(include_bytes!("../../../src/recorder.html")) }
pub const OWNER_CHIP_JS: &str = include_str!("../assets/owner-chip.js");
pub mod extdrive;
pub mod transport;
pub const RENDER_PROBE_JS: &str = include_str!("../assets/render-probe.js");
pub const CONSUMER_EME_JS: &str = include_str!("../assets/consumer-eme.js");
/// pup-bridge#109: present as consumer Google Chrome, the browser the user actually runs. Chrome for Testing's
/// UA string already looks like Chrome, but its client-hint BRANDS report only [Not/A)Brand, Chromium] with no
/// "Google Chrome" — and Cloudflare/Akamai wall exactly that missing brand (analog.com, arrow.com). This adds
/// the Google Chrome brand to the UA header AND navigator.userAgentData together (a mismatch is itself a bot
/// tell), via Network.setUserAgentOverride. Not spoofing a different device: it is the same Windows/Mac Chrome
/// the user runs, correctly declaring itself. Ported from the Node build (computeConsumerUA), dropped in the
/// Rust port. Cached once per process off the running browser's real version.
static TARGET_CHROME_FULL: Mutex<Option<String>> = Mutex::new(None);
/// Read the installed consumer Chrome's version once (registry App Paths -> exe ProductVersion) so pup can
/// present EXACTLY the browser the user runs (John: "match nb's useragent, i am a human surfing with CfT").
/// Windows only for now; a box with no installed Chrome (CfT only) keeps CfT's own version.
pub fn set_target_chrome_from_host(host: &dyn pup_host::Host) {
    if TARGET_CHROME_FULL.lock().unwrap().is_some() { return; }
    match host.installed_chrome_version() {
        Some(v) => { pup_core::log(format!("[identity] installed consumer Chrome is {v}; pup will present that version")); *TARGET_CHROME_FULL.lock().unwrap() = Some(v); }
        None => { *TARGET_CHROME_FULL.lock().unwrap() = Some(String::new()); }   // checked, none — do not re-run
    }
}
static CONSUMER_UA: Mutex<Option<(String, Value)>> = Mutex::new(None);
/// pup-bridge#109: the extension-driven browser (no CDP debug port). Tracked by pid only — there is
/// deliberately no CDP handle for it, so none of the session machinery applies.
/// 2.0.493: the pids that appeared when we launched. Chrome's launcher process exits at once and hands off
/// to a NEW pid, so the pid `spawn_detached` returned is dead within a second while the browser runs — which
/// made pup_close_window report success and kill nothing (John had to watch me close the windows by hand).
/// ONE pup birth at a time. `pup-window-targeting` rule 2 / `pup-bridge-dev` R4: the hwnd is a set-diff
/// taken across window creation, and "the lock guarantees exactly one pup birth at a time". The Rust port
/// dropped it, so two threads opening at once each diffed against their own `before` snapshot, both saw the
/// SAME new frame, and both claimed it: three sessions on one hwnd, each stamping its tile over the last
/// (John, 2026-09-14: "you fucking stick them in nsf's pup windows taskbar icon and vice versa"). Held
/// across [snapshot -> create window -> resolve hwnd] ONLY; navigation happens after it is released.
pub(crate) static BIRTH_LOCK: Mutex<()> = Mutex::new(());
/// Every hwnd another session already owns. A handle is NEVER handed to a second session: that is what
/// collapses distinct threads onto one taskbar button. (readopt_ext already did this; birth and the late
/// rebind did not.)
pub(crate) fn taken_hwnds(ctx: &Ctx, except: &str) -> std::collections::HashSet<i64> {
    ctx.sessions.all().iter().filter(|s| s.id != except).filter_map(|s| s.hwnd).collect()
}
// EXTDRIVE_PIDS lives in extdrive::LaneState.pids since 2.0.659 (per profile)
/// The token only the extension-driven browser carries (see stage_extensions_lane).
// EXTDRIVE_LANE lives in extdrive::LaneState.token since 2.0.659 (per profile)
pub fn extdrive_pids() -> Vec<u32> { extdrive::st().pids.lock().unwrap().clone() }
pub(crate) fn set_extdrive_pids(p: Vec<u32>) { *extdrive::st().pids.lock().unwrap() = p; }
/// Write lane.json (token + pids) so the next pup can re-attach (restore_extdrive).
pub(crate) fn persist_lane(ctx: &Ctx) {
    let p = lane_file(ctx);
    if let Some(d) = p.parent() { let _ = std::fs::create_dir_all(d); }
    let _ = std::fs::write(&p, json!({ "lane": extdrive::st().token.lock().unwrap().clone(), "pids": extdrive::st().pids.lock().unwrap().clone() }).to_string());
}
pub fn extdrive_browser_alive(ctx: &Ctx) -> bool { extdrive::st().pids.lock().unwrap().iter().any(|p| ctx.host.alive(*p)) }
/// 2.0.686 (John, AdomLapper, 2026-10-03: a "Something went wrong when opening your profile" dialog). After pup ends a
/// browser, wait until NO process still runs that profile folder before anything starts on it again. The old waits
/// looked for "SingletonLock", which Chrome only creates on Linux/macOS, so on Windows they returned at once and the
/// next browser started 1 s after a kill while the old one's helper processes still held the profile's files.
/// Stragglers are force-ended after 8 s; gives up (and says so) after `max`.
pub fn wait_profile_free(ctx: &Ctx, profile: &str, max: Duration) -> bool {
    let dir = pup_browser::profiles_dir(&*ctx.host).join(profile);
    let t0 = std::time::Instant::now(); let mut forced = false;
    loop {
        let left = orphan_browser_pids(ctx, &dir);
        if left.is_empty() {
            if t0.elapsed() > Duration::from_millis(300) { pup_core::log(format!("[extdrive] profile \"{profile}\" is free after {} ms", t0.elapsed().as_millis())); }
            return true;
        }
        if !forced && t0.elapsed() > Duration::from_secs(8) {
            for p in &left { ctx.host.kill(*p); }
            forced = true;
            pup_core::log(format!("[extdrive] profile \"{profile}\": {} process(es) still held it 8 s after the browser ended; force-ended them", left.len()));
        }
        if t0.elapsed() > max { pup_core::log(format!("[extdrive] profile \"{profile}\" still held by {} process(es) after {} s", left.len(), max.as_secs())); return false; }
        std::thread::sleep(Duration::from_millis(400));
    }
}

pub fn kill_extdrive(ctx: &Ctx) -> u32 {
    // 2.0.723 (John's laptop, 2026-10-15: every pup window vanished). The lane is chosen ONCE. Closing an isolated
    // window waited 20 s for its profile to free up, the health sweep dropped that lane meanwhile, and the clears
    // below then resolved st() to the adom-you lane: its token and pids were wiped, and the next open ended the
    // user's live adom-you browser as an "orphan" (18 processes).
    let lane = extdrive::st();
    let lane_path = lane_file(ctx);
    let pids: Vec<u32> = lane.pids.lock().unwrap().clone();
    let mut n = 0;
    for p in pids { if ctx.host.alive(p) { ctx.host.kill(p); n += 1; } }
    if n > 0 { let _ = wait_profile_free(ctx, &lane.profile, Duration::from_secs(20)); }
    lane.pids.lock().unwrap().clear();
    lane.token.lock().unwrap().clear();
    *lane.last_poll.lock().unwrap() = 0;
    lane.worker.lock().unwrap().clear();   // 2.0.647: the next hello is the truth, not the last lane's
    let _ = std::fs::remove_file(lane_path);
    if n > 0 { pup_core::log(format!("[extdrive] closed {n} browser process(es)")); }
    n
}

/// Where the lane's identity is parked so it survives a pup restart. 2.0.510: EXTDRIVE_LANE and
/// EXTDRIVE_PIDS were in-memory only, so every `bridge_install` (which restarts pup) orphaned a running
/// extension browser FOREVER: take_command_for rejects every poll while the token is empty, so the live
/// extension could never re-attach, `alive()` stayed false, the next pup_open_window launched a SECOND
/// Chrome that forwarded to the running one and exited at once ("1 process(es)" in the log), and
/// pup_close_window killed nothing because it knew no pids. Measured on AdomLapper 2026-09-13: pup said
/// browserRunning:false while two of its own Chrome for Testing windows sat on John's screen.
fn lane_file(ctx: &Ctx) -> std::path::PathBuf { let p = extdrive::cur_profile(); ctx.host.state_dir().join(if p == "adom-you" { "extlane".to_string() } else { format!("extlane-{p}") }).join("lane.json") }

fn save_lane(ctx: &Ctx) {
    let lane = extdrive::st().token.lock().unwrap().clone();
    let pids = extdrive::st().pids.lock().unwrap().clone();
    let p = lane_file(ctx);
    if let Some(d) = p.parent() { let _ = std::fs::create_dir_all(d); }
    let _ = std::fs::write(&p, json!({ "lane": lane, "pids": pids }).to_string());
}

/// Re-attach to an extension browser that outlived this pup process. Called at boot.
pub fn restore_extdrive(ctx: &Ctx) {
    // 2.0.670: EVERY profile's lane comes back, not only adom-you's, and a lane file is trusted only when its recorded
    // processes really run that profile's folder. Before, only extlane/lane.json was read; the other lane browsers
    // said hello with tokens pup no longer knew, and the first one was adopted as adom-you (AdomLapper, 2026-09-21).
    struct Done; impl Drop for Done { fn drop(&mut self) { extdrive::RESTORED.store(true, std::sync::atomic::Ordering::SeqCst); } }
    let _done = Done;
    let by_profile = pup_browser_pids_by_profile(ctx);
    let Ok(rd) = std::fs::read_dir(ctx.host.state_dir()) else { return };
    let mut seen: Vec<String> = Vec::new();
    for e in rd.flatten() {
        let name = e.file_name().to_string_lossy().to_string();
        let profile = if name == "extlane" { "adom-you".to_string() } else if let Some(p) = name.strip_prefix("extlane-") { p.to_string() } else { continue };
        let p = e.path().join("lane.json");
        let Ok(txt) = std::fs::read_to_string(&p) else { continue };
        let Ok(v) = serde_json::from_str::<Value>(&txt) else { continue };
        let lane = v["lane"].as_str().unwrap_or("").to_string();
        let pids: Vec<u32> = v["pids"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|n| n as u32)).collect()).unwrap_or_default();
        let truth = by_profile.get(&profile).cloned().unwrap_or_default();
        let alive: Vec<u32> = pids.iter().copied().filter(|p| truth.contains(p)).collect();
        if lane.is_empty() || alive.is_empty() || seen.contains(&lane) {
            let wrong = pids.iter().any(|p| ctx.host.alive(*p));
            if wrong && alive.is_empty() { pup_core::log(format!("[extdrive] the {profile} lane file pointed at processes that are not running the {profile} profile; dropped it (that browser keeps its own lane)")); }
            let _ = std::fs::remove_file(&p);
            continue;
        }
        seen.push(lane.clone());
        let l = extdrive::ensure_lane(&profile);
        *l.token.lock().unwrap() = lane.clone();
        *l.pids.lock().unwrap() = alive.clone();
        pup_core::log(format!("[extdrive] re-attached to the {profile} extension browser that outlived this pup ({} process(es), lane {lane})", alive.len()));
    }
}

/// 2.0.670: every pup browser process on the box, keyed by the profile folder its command line names
/// (`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();
    // 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());
    if profile == "adom-you" { *crate::recovery::LANE_BUILD.lock().unwrap() = cand.build_id.clone(); }   // 2.0.734: which build the main lane runs
    // Hand the profile over from the CDP lane. Without this the portless launch FORWARDS to the running
    // CDP browser and exits at once, which silently gives you a window that still carries a debug port -
    // the exact thing this lane exists to avoid.
    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)); }
        }
    }
    // 2.0.723: a browser on this profile that polled the lane in the last 10 s is ALIVE and connected, whatever the
    // pid list says (it can be emptied by a lost lane record). Adopt it; never end it as stale or orphaned.
    if extdrive::connected_ms_ago() < 10_000 {
        let live = orphan_browser_pids(ctx, &dir);
        if !live.is_empty() {
            set_extdrive_pids(live.clone()); persist_lane(ctx);
            pup_core::log(format!("[lane] profile \"{profile}\": its browser is connected ({} process(es)) but pup had no pids for it; adopted, not relaunched", live.len()));
            return Ok(live[0]);
        }
    }
    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;

/// The host, reachable from code that has no Ctx (the transport's capture paths). Set once by the binary.
pub static CAPTURE_HOST: std::sync::OnceLock<Arc<dyn Host>> = std::sync::OnceLock::new();

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_devtools", "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_upload_file", "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_devtools" => input::devtools(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_upload_file" => upload::upload_file(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", "_loginTip", "_importTip", "_popupHint", "loginPage", "savedLogins", "_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()) { extdrive::keep_url(&mut t.url, u); }
        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" {
        // 2.0.726 (John, 2026-10-19: AIs answered "CDP is switched off on this box, so pup can't open the page here at
        // all" and went to another machine). Never refuse an open over the lane: open it on the extension lane, which
        // has the DevTools protocol through pup's extension, and say so.
        pup_core::log(format!("[lane] thread \"{}\" asked for lane:\"cdp\"; cdpLane is off, so the window opens on the extension lane instead", caller.thread));
        let mut a2 = args.clone();
        if let Some(o) = a2.as_object_mut() { o.remove("lane"); }
        let r = open_window(ctx, caller, &a2);
        return pup_core::merge(r, json!({ "lane": "ext", "laneRequested": "cdp", "_laneNote": "You asked for lane:\"cdp\". The CDP lane is off on this box, so pup opened this window on its extension lane, which is the one every site loads on. Drive it as usual: pup_click, pup_type, pup_eval, pup_screenshot (element and full-page too), pup_input_dispatch for trusted input. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off." }));
    }
    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("Drop that option and open normally: the extension lane is pup's default and every site loads on it. Without highFps a window covered by other windows may render slowly until it is visible; screenshots still work. Instead of webSecurity:false, fetch cross-origin data with pup_fetch_url {url}. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off."));
        }
        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 = pup_core::merge(wiki::after_navigate_ext(ctx, s, &lane, &tab_now, &u, None, false), vault::login_check_ext(ctx, &lane, &tab_now, &u));
    // 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", "pup_eval {frame} is not wired on the extension lane yet", Some("Reach the iframe with pup_devtools: {method:\"Page.getFrameTree\"} lists the frames, {method:\"Page.createIsolatedWorld\", params:{frameId}} gives an executionContextId, then {method:\"Runtime.evaluate\", params:{expression, contextId, returnByValue:true}}. A same-origin iframe is also reachable from pup_eval through document.querySelector('iframe').contentDocument. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off.")); }
        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 {
    // 2.0.726 (John, 2026-10-19: "this interstitial window then explains the ai thread that opened this window, what
    // it's reason was, and then even a description of what pup is"). The birth page shows who opened the window, why,
    // and where it is going, for as long as a slow page keeps it there.
    let birth_reason = [caller.reason.as_str(), args["reason"].as_str().unwrap_or(""), args["foregroundReason"].as_str().unwrap_or("")].iter().find(|r| !r.trim().is_empty()).copied().unwrap_or("").to_string();
    let birth_thread = owner.unwrap_or(caller.thread.as_str()).to_string();
    let birth_info = json!({ "thread": birth_thread, "reason": birth_reason, "target": url, "window": sid });
    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", "birth": birth_info.clone() }), 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, "birth": birth_info.clone() }), 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() { extdrive::keep_url(&mut t0.url, u); } 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." }));
        }
        // 2.0.724 (John, 2026-10-17: a window on his taskbar titled PUPBIRTH-...): a failed navigate here was
        // swallowed, the open answered success, and the window stayed on the birth page for good. Retry once; the
        // health tick's rescue_birth_tabs is the net for anything that still sticks.
        let nav_try = || extdrive::run(json!({ "action": "navigate", "url": url, "tabId": chrome_tab, "waitMs": 25000 }), Duration::from_secs(30));
        let nav = match nav_try().or_else(|e| { pup_core::log(format!("[window] \"{sid}\" first navigate off the birth page failed ({}); retrying once", e["error"].as_str().unwrap_or("?"))); std::thread::sleep(Duration::from_millis(1500)); nav_try() }) {
            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 extdrive::is_birth_url(u) { pup_core::log(format!("[window] \"{sid}\" the navigate to {} answered but the tab is still on the birth page; the health check will send it on", t0.url)); }
                extdrive::keep_url(&mut t0.url, u);
            }
            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); wiki_extra = pup_core::merge(wiki_extra, vault::login_check_ext(ctx, &lane, &t0, &t0.url)); } }
    wiki_extra = pup_core::merge(wiki_extra, vault::import_tip_once(ctx, owner.unwrap_or(caller.thread.as_str())));
    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!({})) }