feat(daemon): preserve URL across version-mismatch restart

Before: after `npm i -g` upgrade, the next agent-browser command would
detect daemon version mismatch, kill the old daemon, spawn a fresh one,
and connect to a brand-new about:blank tab. The user's previous
navigation state was silently lost — `get url` returned about:blank
even though the user's Chrome was still on the same page.

Now: before killing the old daemon, the CLI synchronously asks it for
its current URL via the existing socket. If non-empty and not
about:blank, it's persisted to a `.restore-url` sidecar in the socket
dir. After the new daemon spawns and auto-connects, it reads the
sidecar (read-and-delete), navigates the fresh tab to the saved URL,
and prints `⚠ Restored previous URL: <url>`.

Manual `agent-browser close` does NOT write the sidecar, so a clean
shutdown won't trigger surprise navigation. The sidecar is consumed on
read regardless of whether navigation succeeded, so a stale entry
can't haunt later auto-launches.
This commit is contained in:
leeguooooo
2026-05-09 01:41:07 +09:00
parent 6c556e519d
commit 2b1a3c308a
2 changed files with 86 additions and 1 deletions
+45 -1
View File
@@ -9,7 +9,7 @@ use std::sync::Arc;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use tokio::sync::{broadcast, oneshot, RwLock};
use crate::connection::get_socket_dir;
use crate::connection::{get_restore_url_path, get_socket_dir};
use super::auth;
use super::browser::{should_track_target, BrowserManager, WaitUntil};
@@ -1552,6 +1552,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
try_auto_restore_state(state).await;
try_load_storage_state(state, &storage_state_path).await;
apply_stealth_to_browser(state).await;
try_restore_navigation(state).await;
return Ok(());
}
@@ -1573,6 +1574,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
try_auto_restore_state(state).await;
try_load_storage_state(state, &storage_state_path).await;
apply_stealth_to_browser(state).await;
try_restore_navigation(state).await;
return Ok(());
}
Err(_e) => {
@@ -1660,6 +1662,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
try_load_storage_state(state, &storage_state_path).await;
// Apply stealth anti-detection patches after browser is ready
apply_stealth_to_browser(state).await;
try_restore_navigation(state).await;
Ok(())
}
@@ -1770,6 +1773,47 @@ async fn apply_stealth_to_browser(state: &DaemonState) {
}
}
/// If the previous daemon left a `.restore-url` sidecar (because it was killed
/// by a version-mismatch restart), navigate the freshly-connected browser to
/// that URL so `agent-browser get url` after `npm i -g` upgrade still reports
/// the page the user was on. Read-and-delete: the file is removed regardless
/// of whether navigation succeeds, so a stale sidecar can't haunt later
/// auto-launches.
async fn try_restore_navigation(state: &mut DaemonState) {
let path = get_restore_url_path(&state.session_id);
let url = match fs::read_to_string(&path) {
Ok(s) => s.trim().to_string(),
Err(_) => return,
};
let _ = fs::remove_file(&path);
if url.is_empty() {
return;
}
let Some(mgr) = state.browser.as_mut() else {
return;
};
state.ref_map.clear();
state.iframe_sessions.clear();
state.active_frame_id = None;
match mgr.navigate(&url, super::browser::WaitUntil::Load).await {
Ok(_) => {
eprintln!(
"{} Restored previous URL: {}",
crate::color::warning_indicator(),
url
);
}
Err(e) => {
eprintln!(
"{} Could not restore previous URL ({}): {}",
crate::color::warning_indicator(),
url,
e
);
}
}
}
fn launch_options_from_env() -> LaunchOptions {
let headed = env::var("AGENT_BROWSER_HEADED")
.map(|v| v == "1" || v == "true")