app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
v2.0.693 (insiders): Annotate starts fast: pup's extension now holds a long poll that pup answers the moment it has a command, instead of checking every 1.2 s. Every command on the extension lane is picked up at once; Annotate's two steps took 1.5 to 2.2 s before you could draw (John, 2026-10-04). Measured: Annotate start 121 ms (was 0.6 to 2.3 s), pup_eval 121 ms (was 0.7 to 2.3 s).
5 files changed
+45−18
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.692"+version = "2.0.693" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.692"+version = "2.0.693" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.692"+version = "2.0.693" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.692"+version = "2.0.693" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.692"+version = "2.0.693" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.692"+version = "2.0.693" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.692"+version = "2.0.693" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.692"+version = "2.0.693" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.692"+version = "2.0.693" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.692"+version = "2.0.693" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.692"+version = "2.0.693" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.692"+version = "2.0.693" dependencies = [ "base64", "png 0.17.16",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session", # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.692"+version = "2.0.693" edition = "2021" license = "MIT"
rust/bridge/src/main.rs+2−1@@ -143,7 +143,8 @@ fn main() { let q = url.split('?').nth(1).unwrap_or(""); let lane = q.split('&').find_map(|kv| kv.strip_prefix("lane=")).unwrap_or("").to_string(); let removed: Vec<i64> = q.split('&').find_map(|kv| kv.strip_prefix("removed=")).unwrap_or("").split(',').filter_map(|x| x.parse().ok()).collect();- let _ = req.respond(json_response(200, &pup_verbs::extdrive::take_command_for(&lane, &removed)));+ let wait: u64 = q.split('&').find_map(|kv| kv.strip_prefix("wait=")).and_then(|x| x.parse().ok()).unwrap_or(0);+ let _ = req.respond(json_response(200, &pup_verbs::extdrive::take_command_for(&lane, &removed, wait))); } // 2.0.602: the extension lane's login-capture rail. The CDP lane got captures through a // DevTools binding; the default lane had none, so logins typed in normal pup windows were lost.
rust/verbs/src/extdrive.rs+18−1@@ -83,13 +83,29 @@ static SEQ: Mutex<u64> = Mutex::new(0); pub fn take_removed() -> Vec<i64> { std::mem::take(&mut *st().removed.lock().unwrap()) } /// GET /extcmd — the extension asks for work. Returns `{}` when there is nothing to do.-pub fn take_command_for(lane: &str, removed: &[i64]) -> Value {+pub fn take_command_for(lane: &str, removed: &[i64], wait_ms: u64) -> Value { // 2.0.502: only a browser carrying a lane token pup issued may take work. 2.0.659: the token names WHICH // lane (default adom-you, or a per-profile lane), so several browsers can be driven at once. let Some(l) = lane_for_token(lane) else { return json!({}) }; if !removed.is_empty() { l.removed.lock().unwrap().extend_from_slice(removed); }+ let cmd = take_command_on(&l);+ if cmd["id"].is_u64() || wait_ms == 0 { return cmd; }+ // 2.0.693: a LONG poll. The worker used to ask every 1.2 s, so every command waited up to 1.2 s just to+ // be picked up, and a two-step verb like Annotate (inject the overlay, then start it) took 1.5 to 2.2 s+ // before the user could draw (John, 2026-10-04). Now the request waits here and run() wakes it the moment+ // a command is queued. Capped well under the MV3 worker's 30 s idle limit and alive()'s 20 s heartbeat.+ let deadline = Instant::now() + Duration::from_millis(wait_ms.min(10_000));+ let mut g = QUEUED.0.lock().unwrap();+ while l.pending.lock().unwrap().is_empty() {+ let now = Instant::now();+ if now >= deadline { break; }+ g = QUEUED.1.wait_timeout(g, deadline - now).unwrap().0;+ }+ drop(g); take_command_on(&l) }+/// Rung by run() whenever a command is queued, so a waiting /extcmd long poll returns at once.+static QUEUED: (Mutex<()>, std::sync::Condvar) = (Mutex::new(()), std::sync::Condvar::new()); pub fn take_command() -> Value { take_command_on(&st()) } fn take_command_on(l: &LaneState) -> Value {@@ -310,6 +326,7 @@ fn run_inner(cmd: Value, wait: Duration) -> Result<Value, Value> { let mut c = cmd; c["id"] = json!(id); st().pending.lock().unwrap().push_back(c);+ { let _g = QUEUED.0.lock().unwrap(); QUEUED.1.notify_all(); } let t0 = Instant::now(); while t0.elapsed() < wait { let hit = st().results.lock().unwrap().as_mut().and_then(|m| m.remove(&id));
src/extension/worker.js+12−3@@ -256,11 +256,14 @@ async function pupExtPollOnce() { const removed = pupRemovedWindows.splice(0, pupRemovedWindows.length); const rq = removed.length ? `&removed=${removed.join(',')}` : ''; let r;- try { r = await fetch(`http://127.0.0.1:${found.port}/extcmd?lane=${encodeURIComponent(_pkgLane)}${rq}`, { signal: AbortSignal.timeout(2500) }); }+ // 2.0.693: a long poll. pup holds this request until it has a command (up to 8 s) and answers the moment+ // one is queued, so a command no longer waits for the next 1.2 s tick (Annotate took 1.5 to 2.2 s).+ pupExtLastAsk = Date.now();+ try { r = await fetch(`http://127.0.0.1:${found.port}/extcmd?lane=${encodeURIComponent(_pkgLane)}${rq}&wait=8000`, { signal: AbortSignal.timeout(12000) }); } catch (e) { pupRemovedWindows.unshift(...removed); throw e; } cmd = await r.json(); } catch (e) { return false; }- if (!cmd || !cmd.id) return false;+ if (!cmd || !cmd.id) return 'empty'; let out = { id: cmd.id, ok: false, error: `unsupported action ${cmd.action}` }; // every command races a deadline: a hung Chrome API answers as an error and names itself, instead of@@ -558,15 +561,21 @@ async function pupExtPollOnce() { // bridge saw total silence and declared the lane dead. Three rules: every command has a deadline, the busy // guard cannot stick (a drain older than 60 s is abandoned), and the hello has its own alarm below. let pupExtBusySince = 0;+let pupExtLastAsk = 0;+// 2.0.693: the drain keeps a long poll open back to back, so pup can hand over a command the moment it has+// one. The 1.2 s interval below only restarts the loop if it ever stops. An empty answer that came back+// fast means an older pup that does not hold the request: pause like the old tick so this cannot spin. async function pupExtPoll() { if (pupExtBusy && Date.now() - pupExtBusySince < 60000) return; if (pupExtBusy) { console.warn('[pup] a drain has been stuck for 60 s; starting a fresh one'); } pupExtBusy = true; pupExtBusySince = Date.now(); try {- for (let k = 0; k < 12; k++) {+ for (;;) {+ pupExtBusySince = Date.now(); const step = await Promise.race([pupExtPollOnce(), new Promise((res) => setTimeout(() => res('deadline'), 50000))]); if (step === 'deadline') { console.warn('[pup] a poll step did not settle within 50 s; abandoning it'); break; } if (!step) break;+ if (step === 'empty' && Date.now() - pupExtLastAsk < 1000) break; } } finally { pupExtBusy = false; }