-
Notifications
You must be signed in to change notification settings - Fork 241
fix: browser session persistence across workers and restarts #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c59ca80
1caa405
9128ee2
5e47b3c
db9636e
ace5ddb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,9 +161,14 @@ pub struct BrowserState { | |
| element_refs: HashMap<String, ElementRef>, | ||
| /// Counter for generating element refs. | ||
| next_ref: usize, | ||
| /// Per-launch temp directory for Chrome's user data. Cleaned up on drop to | ||
| /// prevent stale singleton locks from blocking subsequent launches. | ||
| /// Chrome's user data directory. For ephemeral sessions this is a random | ||
| /// temp dir cleaned up on drop. For persistent sessions this is a stable | ||
| /// path under the instance dir that survives agent restarts. | ||
| user_data_dir: Option<PathBuf>, | ||
| /// When true, `user_data_dir` is a stable path that should NOT be deleted | ||
| /// on drop — it holds cookies, localStorage, and login sessions that must | ||
| /// survive across agent restarts. | ||
| persistent_profile: bool, | ||
| } | ||
|
|
||
| impl BrowserState { | ||
|
|
@@ -176,6 +181,7 @@ impl BrowserState { | |
| element_refs: HashMap::new(), | ||
| next_ref: 0, | ||
| user_data_dir: None, | ||
| persistent_profile: false, | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -184,6 +190,13 @@ impl Drop for BrowserState { | |
| fn drop(&mut self) { | ||
| // Browser and handler task are dropped automatically — | ||
| // chromiumoxide's Child has kill_on_drop(true). | ||
|
|
||
| // Persistent profiles store cookies, localStorage, and login sessions | ||
| // that must survive across agent restarts — never delete them. | ||
| if self.persistent_profile { | ||
| return; | ||
| } | ||
|
|
||
| if let Some(dir) = self.user_data_dir.take() { | ||
| // Offload sync fs cleanup to a blocking thread so we don't stall | ||
| // the tokio worker that's dropping this state. | ||
|
|
@@ -199,7 +212,12 @@ impl Drop for BrowserState { | |
| }); | ||
| } else { | ||
| // Dropped outside a tokio runtime (unlikely) — clean up inline. | ||
| let _ = std::fs::remove_dir_all(&dir); | ||
| if let Err(error) = std::fs::remove_dir_all(&dir) { | ||
| eprintln!( | ||
| "failed to clean up browser user data dir {}: {error}", | ||
| dir.display() | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -212,6 +230,7 @@ impl std::fmt::Debug for BrowserState { | |
| .field("pages", &self.pages.len()) | ||
| .field("active_target", &self.active_target) | ||
| .field("element_refs", &self.element_refs.len()) | ||
| .field("persistent_profile", &self.persistent_profile) | ||
| .finish() | ||
| } | ||
| } | ||
|
|
@@ -554,10 +573,16 @@ impl BrowserTool { | |
| // 4. Auto-download via BrowserFetcher (cached in chrome_cache_dir) | ||
| let executable = resolve_chrome_executable(&self.config).await?; | ||
|
|
||
| // Use a unique temp dir per launch to avoid singleton lock collisions | ||
| // when multiple workers launch browsers or a previous session crashed. | ||
| let user_data_dir = | ||
| std::env::temp_dir().join(format!("spacebot-browser-{}", uuid::Uuid::new_v4())); | ||
| // Persistent sessions use a stable profile dir under chrome_cache_dir so | ||
| // cookies, localStorage, and login sessions survive across agent restarts. | ||
| // Ephemeral sessions use a random temp dir to avoid singleton lock collisions. | ||
| let (user_data_dir, persistent_profile) = if self.config.persist_session { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Persistent sessions now share a stable user-data-dir; if two workers call |
||
| (self.config.chrome_cache_dir.join("profile"), true) | ||
| } else { | ||
|
Comment on lines
+576
to
+581
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scope the persistent profile path to the agent/site, not the whole instance. Line 575 hard-codes 🤖 Prompt for AI Agents |
||
| let dir = | ||
| std::env::temp_dir().join(format!("spacebot-browser-{}", uuid::Uuid::new_v4())); | ||
| (dir, false) | ||
| }; | ||
|
|
||
| let mut builder = ChromeConfig::builder() | ||
| .no_sandbox() | ||
|
|
@@ -594,12 +619,21 @@ impl BrowserTool { | |
| // the browser we just created and return success. | ||
| drop(browser); | ||
| handler_task.abort(); | ||
| // Clean up the temp user data dir asynchronously so we don't block | ||
| // while holding the shared mutex. | ||
| let dir = user_data_dir; | ||
| tokio::spawn(async move { | ||
| let _ = tokio::fs::remove_dir_all(&dir).await; | ||
| }); | ||
| // Clean up the temp user data dir — but only for ephemeral sessions. | ||
| // Persistent profiles use a shared stable path that the winner is | ||
| // actively using. | ||
| if !persistent_profile { | ||
| let dir = user_data_dir; | ||
| tokio::spawn(async move { | ||
| if let Err(error) = tokio::fs::remove_dir_all(&dir).await { | ||
| tracing::debug!( | ||
| path = %dir.display(), | ||
| %error, | ||
| "failed to clean up browser user data dir (concurrent launch race)" | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| if self.config.persist_session { | ||
| return self.reconnect_existing_tabs(&mut state).await; | ||
|
|
@@ -610,6 +644,7 @@ impl BrowserTool { | |
| state.browser = Some(browser); | ||
| state._handler_task = Some(handler_task); | ||
| state.user_data_dir = Some(user_data_dir); | ||
| state.persistent_profile = persistent_profile; | ||
|
|
||
| tracing::info!("browser launched"); | ||
| Ok(BrowserOutput::success("Browser launched successfully")) | ||
|
|
@@ -1196,16 +1231,17 @@ impl BrowserTool { | |
| ClosePolicy::CloseBrowser => { | ||
| // Take everything out of state under the lock, then do the | ||
| // actual teardown outside it. | ||
| let (browser, handler_task, user_data_dir) = { | ||
| let (browser, handler_task, user_data_dir, persistent_profile) = { | ||
| let mut state = self.state.lock().await; | ||
| let browser = state.browser.take(); | ||
| let handler_task = state._handler_task.take(); | ||
| let user_data_dir = state.user_data_dir.take(); | ||
| let persistent_profile = state.persistent_profile; | ||
| state.pages.clear(); | ||
| state.active_target = None; | ||
| state.element_refs.clear(); | ||
| state.next_ref = 0; | ||
| (browser, handler_task, user_data_dir) | ||
| (browser, handler_task, user_data_dir, persistent_profile) | ||
| }; | ||
|
|
||
| if let Some(task) = handler_task { | ||
|
|
@@ -1220,8 +1256,10 @@ impl BrowserTool { | |
| return Err(BrowserError::new(message)); | ||
| } | ||
|
|
||
| // Clean up the per-launch user data dir to free disk space. | ||
| if let Some(dir) = user_data_dir { | ||
| // Clean up the user data dir — but only for ephemeral sessions. | ||
| // Persistent profiles hold cookies and login sessions that must | ||
| // survive browser restarts. | ||
| if !persistent_profile && let Some(dir) = user_data_dir { | ||
| tokio::spawn(async move { | ||
| if let Err(error) = tokio::fs::remove_dir_all(&dir).await { | ||
| tracing::debug!( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive this instruction from the effective close policy, not
browser_persist_session.persist_session=trueonly changes the default. If config explicitly setsclose_policy = "close_browser"orclose_policy = "close_tabs", this text is now wrong and will tell the worker to preserve tabs/session when it will not. Please render against the resolvedClosePolicyand add aclose_tabsbranch as well.🤖 Prompt for AI Agents