Compare commits

...
Author SHA1 Message Date
leeguooooo 4bcfe74514 chore(release): 0.27.0-fork.25 — relay liveness fix (Browser.getVersion local) stops reconnect-storm drift
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
2026-06-10 01:38:19 +09:00
leeguooooo bb41c24c08 fix(connect): relay answers Browser.getVersion locally (stops reconnect storm)
ROOT CAUSE of per-session command drift on the extension path: the daemon's
liveness check (`is_connection_alive` → `Browser.getVersion`) is a BROWSER-level
command. The relay only answered Target.* locally and forwarded the rest, so
Browser.getVersion went to the extension, which can only do per-tab
chrome.debugger → it errored → CdpClient saw TransportError → connection deemed
DEAD → the daemon closed + reconnected + re-ran discover_and_attach_targets on
EVERY command. Each re-discover rebuilds pages from the relay's minimal
targetInfo and resets active_page_index=0, so eval/get-title/screenshot drifted
to the first tab (about:blank / a foreign focused tab).

Reproduced locally (throwaway Chrome + Extensions.loadUnpacked + fork.24 nm-host):
trace showed discover_and_attach_targets running on every command (pages
before=0) and [ev] active_idx reset to 0.

Fix: relay answers Browser.getVersion locally with a stub version (like
getTargets), so the liveness probe succeeds → connection stays alive → no
reconnect/re-discover → the session's active tab is preserved. Pairs with
fork.24's add_background_page. relay.rs: 10 unit tests.
2026-06-10 01:38:18 +09:00
leeguooooo 75bd1d21a7 chore(release): 0.27.0-fork.24 — passive tab discovery no longer hijacks active tab (per-session control)
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
2026-06-10 00:42:16 +09:00
leeguooooo 06c75af46a fix(connect): passively-discovered tabs no longer steal the active tab
After connect+grouping worked, follow-up eval/get-title/screenshot drifted to a
foreign tab: on a shared browser, Target.targetCreated events for tabs the user
or OTHER agent sessions open stream in and are drained on every command. The
drain path routed them through add_page(), which sets active_page_index to the
new page — so the session's active tab silently jumped to a foreign tab and its
commands landed there.

Add BrowserManager::add_background_page() (push without touching active, dedup by
target_id) and use it in the event-drain path. Explicit opens (tab new, the
add-and-switch paths) keep using add_page() and still focus the new tab.

Closes the last gap in concurrent multi-agent: each session now drives its OWN
tab regardless of other sessions'/the user's tab activity.
2026-06-10 00:42:16 +09:00
leeguooooo 312bb0d65b chore(release): 0.27.0-fork.23 — tolerate minimal targetInfo from relay (extension connect getTargets)
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
2026-06-10 00:07:59 +09:00
leeguooooo cff003c333 fix(connect): tolerate minimal targetInfo (relay re-announce omits title/url)
After the connect fix, extension connect reached the relay but Target.getTargets
failed: 'missing field title'. The ab-connect relay builds targets from the
extension's synthesized Target.attachedToTarget; the re-announce path
(reannounceAttachedTabs) emits a minimal targetInfo {targetId,type,attached}
with no title/url, so strict deserialize of TargetInfo blew up the whole
getTargets response.

Make TargetInfo.title/url #[serde(default)] (empty) — tolerant of minimal CDP
targetInfo from the relay (and the occasional real-CDP omission). Titles
re-populate from Target.targetInfoChanged / page events after attach.
2026-06-10 00:07:58 +09:00
7 changed files with 59 additions and 4 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser-stealth"
version = "0.27.0-fork.22"
version = "0.27.0-fork.25"
dependencies = [
"aes-gcm",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.27.0-fork.22"
version = "0.27.0-fork.25"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
+4 -1
View File
@@ -672,7 +672,10 @@ impl DaemonState {
}
let tab_id = mgr.assign_tab_id();
mgr.add_page(super::browser::PageInfo {
// Passively discovered (event-driven) — must NOT steal the
// active tab, or a foreign/user/other-session tab opening
// hijacks this session's eval/screenshot target.
mgr.add_background_page(super::browser::PageInfo {
tab_id,
label: None,
target_id: te.target_info.target_id.clone(),
+15
View File
@@ -1532,6 +1532,21 @@ impl BrowserManager {
self.active_page_index = index;
}
/// Add a passively-discovered page WITHOUT changing the active tab.
///
/// On a shared browser (ab-connect), `Target.targetCreated` events stream in
/// for tabs the user or OTHER agent sessions open. Those are drained on every
/// command; routing them through `add_page` made the active tab silently jump
/// to a foreign tab, so the session's own `eval`/`get title`/`screenshot`
/// landed on the wrong page. Passively-tracked pages must not steal focus —
/// only explicit opens (`tab new`, switch) set the active tab.
pub fn add_background_page(&mut self, page: PageInfo) {
if self.pages.iter().any(|p| p.target_id == page.target_id) {
return;
}
self.pages.push(page);
}
pub fn update_page_target_info(&mut self, target: &TargetInfo) -> bool {
update_page_target_info_in_pages(&mut self.pages, target)
}
+6
View File
@@ -106,7 +106,13 @@ pub struct TargetInfo {
pub target_id: String,
#[serde(rename = "type")]
pub target_type: String,
// Tolerate minimal targetInfo: the ab-connect relay's synthesized
// Target.attachedToTarget (re-announce path) omits title/url, and real CDP
// occasionally omits them too. Default to empty rather than fail the whole
// Target.getTargets deserialize.
#[serde(default)]
pub title: String,
#[serde(default)]
pub url: String,
pub attached: Option<bool>,
pub browser_context_id: Option<String>,
+31
View File
@@ -107,6 +107,22 @@ impl RelayState {
let session_id = raw.get("sessionId").and_then(|s| s.as_str());
match method {
// Browser-level command the daemon uses as its liveness probe
// (`is_connection_alive` → `Browser.getVersion`). The extension only
// speaks per-tab `chrome.debugger`, so forwarding it errors → the
// daemon would deem the connection dead and reconnect+re-discover on
// EVERY command, resetting the active tab (eval/screenshot drift).
// Answer it locally so the relay connection reads as alive.
"Browser.getVersion" => ClientRoute::Local(json!({
"id": id,
"result": {
"protocolVersion": "1.3",
"product": "Chrome/ab-connect-relay",
"revision": "",
"userAgent": "",
"jsVersion": ""
}
})),
// Discovery is best-effort and event-driven in real CDP; abs only
// reads the getTargets result, so an empty ack is enough here.
"Target.setDiscoverTargets" | "Target.setAutoAttach" => {
@@ -302,6 +318,21 @@ mod tests {
}
}
#[test]
fn browser_get_version_is_answered_locally() {
// Liveness probe must NOT be forwarded (the extension can't do
// browser-level commands) — else the daemon reconnects on every command.
let mut s = RelayState::new();
let route = s.route_client_command(1, &json!({ "id": 7, "method": "Browser.getVersion" }));
match route {
ClientRoute::Local(v) => {
assert_eq!(v["id"], 7);
assert!(v["result"]["protocolVersion"].is_string());
}
_ => panic!("Browser.getVersion must be answered locally"),
}
}
#[test]
fn attach_to_target_returns_known_session() {
let mut s = RelayState::new();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-browser-stealth",
"version": "0.27.0-fork.22",
"version": "0.27.0-fork.25",
"description": "Browser automation CLI for AI agents \u2014 stealth fork with anti-detection",
"type": "module",
"packageManager": "pnpm@11.1.3",