Skip to content

feat: persistent browser sessions with configurable close policy#309

Merged
jamiepine merged 5 commits intomainfrom
persistent-browser
Mar 4, 2026
Merged

feat: persistent browser sessions with configurable close policy#309
jamiepine merged 5 commits intomainfrom
persistent-browser

Conversation

@jamiepine
Copy link
Member

Summary

  • Add persist_session and close_policy config options to [browser], allowing browser instances to survive across worker lifetimes with tabs, cookies, and login sessions intact.
  • Workers share a single SharedBrowserHandle (Arc<Mutex<BrowserState>>) held in RuntimeConfig when persist_session = true, reconnecting to existing tabs on launch instead of starting a fresh process.
  • ClosePolicy controls what happens on close: close_browser (default, full teardown), close_tabs (close pages, keep Chrome), or detach (disconnect, leave everything running).

Details

Backend (Rust)

New types:

  • ClosePolicy enum with serde support, as_str(), Display (src/config/types.rs)
  • SharedBrowserHandle type alias (Arc<Mutex<BrowserState>>) and new_shared_browser_handle() factory (src/tools/browser.rs)

Config pipeline:

  • TomlBrowserConfig gains persist_session / close_policy fields (src/config/toml_schema.rs)
  • parse_close_policy() helper; both defaults-resolution and per-agent override paths wired (src/config/load.rs)
  • RuntimeConfig.shared_browser conditionally created at construction; hot-reload logs a warning that restart is required (src/config/runtime.rs)

Browser tool:

  • BrowserState::new() constructor, BrowserTool::new_shared() for shared-handle mode
  • reconnect_existing_tabs() discovers open tabs via browser.pages(), populates state.pages, returns tab info to the LLM
  • handle_launch() reconnects instead of erroring when persistent browser is already running
  • handle_close() dispatches on ClosePolicy: Detach clears element refs only, CloseTabs closes pages but keeps browser alive, CloseBrowser does full teardown

Wiring:

  • Both create_worker_tool_server and create_cortex_chat_tool_server use BrowserTool::new_shared() when shared handle is available (src/tools.rs, src/main.rs, src/api/agents.rs)
  • API types BrowserSection / BrowserUpdate include the new fields; TOML writer updated (src/api/config.rs)

Frontend

  • "Persist Session" toggle and "Close Policy" dropdown in the browser config panel (interface/src/routes/AgentConfig.tsx)
  • TypeScript types updated (interface/src/api/client.ts)

Docs

  • config.mdx — browser config example and reference table updated
  • browser.mdx — new "Persistent Sessions" section, close policy table, updated architecture diagram

Config example

[defaults.browser]
headless = false
persist_session = true
close_policy = "detach"

Gate results

  • cargo fmt — clean
  • cargo check — compiles
  • cargo clippy — no warnings
  • cargo test --lib — 321/321 passed
  • cargo test --tests --no-run — integration tests compile
  • bun run build (frontend) — builds successfully

Notes

  • Changing persist_session requires an agent restart (logged as warning on hot-reload).
  • PR Support external browser containers by connecting via CDP #152 (connect_url / external browser via CDP) is a natural complement — when it lands, connect_url + persist_session + close_policy = "detach" gives the full external persistent browser experience.

Add persist_session and close_policy to BrowserConfig, allowing browser
instances to survive across worker lifetimes. When persist_session is
enabled, workers share a single browser handle via Arc<Mutex<BrowserState>>
and reconnect to existing tabs on launch instead of starting fresh.

ClosePolicy controls what happens when a worker finishes:
- close_browser (default): full teardown, same as before
- close_tabs: close all pages but keep browser process alive
- detach: leave browser and tabs running for the next worker

Backend: SharedBrowserHandle type, BrowserTool::new_shared() constructor,
reconnect_existing_tabs() discovery, close policy dispatch in handle_close(),
RuntimeConfig.shared_browser field, API types and TOML support.

Frontend: persist session toggle and close policy dropdown in AgentConfig.

Docs: config.mdx reference table and browser.mdx persistent sessions guide.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8ff1f86-938c-4374-81a7-5bff653d1a16

📥 Commits

Reviewing files that changed from the base of the PR and between 55d80f8 and 58556cf.

📒 Files selected for processing (2)
  • interface/src/api/client.ts
  • src/tools.rs

Walkthrough

This PR adds two new browser configuration options—persist_session (bool) and close_policy (enum: close_browser, close_tabs, detach)—across documentation, API types, UI components, and core Rust modules. It introduces shared browser state handling via SharedBrowserHandle, reconnection logic for persistent sessions, and policy-based close behavior.

Changes

Cohort / File(s) Summary
Documentation
docs/content/docs/(configuration)/config.mdx, docs/content/docs/(features)/browser.mdx
Added persist_session and close_policy to configuration defaults; documented behaviors, reconnection flow, and close policies; updated architecture sections and examples.
API Types
interface/src/api/client.ts
Added persist_session: boolean and close_policy fields to BrowserSection and BrowserUpdate interfaces.
UI Components
interface/src/routes/AgentConfig.tsx
Added browser configuration UI controls for persist_session toggle and close_policy dropdown.
Configuration Types
src/config/types.rs, src/config/toml_schema.rs
Introduced new ClosePolicy enum (CloseBrowser, CloseTabs, Detach) with string conversion; extended BrowserConfig with persist_session and close_policy fields; added TOML schema fields.
Configuration Loading & Runtime
src/config/load.rs, src/config/runtime.rs
Added parse_close_policy helper for config parsing; initialized shared browser handle in RuntimeConfig based on persist_session; added change detection warning for policy persistence.
API/Server
src/api/config.rs
Populated persist_session and close_policy in agent config responses and TOML serialization.
Tools
src/tools.rs, src/tools/browser.rs
Exported SharedBrowserHandle; implemented shared browser state (Arc<Mutex<BrowserState>>), reconnect logic (reconnect_existing_tabs), persistent-session-aware launch path, and policy-based close handling (Detach/CloseTabs/CloseBrowser).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • spacebot#309: Directly modifies the same Rust and frontend files to implement the persist_session and close_policy feature, including SharedBrowserHandle, reconnection logic, and policy-based closing.
  • spacebot#306: Refactored the config subsystem (types.rs, load.rs, toml_schema.rs, runtime.rs) that this PR extends with new persist_session and close_policy fields.
  • spacebot#246: Modifies tool server integration paths to accept and pass runtime_config, relevant to how the shared browser state is propagated to tool construction.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: persistent browser sessions with configurable close policy' directly and clearly summarizes the main feature addition—persistent browser sessions with configurable close behavior.
Description check ✅ Passed The PR description comprehensively explains the changes, providing context on the new config options, implementation approach, backend and frontend modifications, documentation updates, and test results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch persistent-browser

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/config.rs (1)

200-206: ⚠️ Potential issue | 🟠 Major

Validate close_policy before writing to config.toml.

At Line 205 and Lines 684-685, close_policy is a raw String and is persisted without validation. Invalid values can be stored and break later reload/startup behavior.

Suggested fix (typed input + strict serialization)
+#[derive(Deserialize, Debug, Clone, Copy)]
+#[serde(rename_all = "snake_case")]
+pub(super) enum BrowserClosePolicyUpdate {
+    CloseBrowser,
+    CloseTabs,
+    Detach,
+}
+
 #[derive(Deserialize, Debug)]
 pub(super) struct BrowserUpdate {
     enabled: Option<bool>,
     headless: Option<bool>,
     evaluate_enabled: Option<bool>,
     persist_session: Option<bool>,
-    close_policy: Option<String>,
+    close_policy: Option<BrowserClosePolicyUpdate>,
 }
@@
-    if let Some(ref v) = browser.close_policy {
-        table["close_policy"] = toml_edit::value(v.as_str());
+    if let Some(value) = browser.close_policy {
+        let close_policy = match value {
+            BrowserClosePolicyUpdate::CloseBrowser => "close_browser",
+            BrowserClosePolicyUpdate::CloseTabs => "close_tabs",
+            BrowserClosePolicyUpdate::Detach => "detach",
+        };
+        table["close_policy"] = toml_edit::value(close_policy);
     }

As per coding guidelines: "Don't silently discard errors; use let _ = only on channel sends where the receiver may be dropped; handle, log, or propagate all other errors".

Also applies to: 681-686

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/config.rs` around lines 200 - 206, The BrowserUpdate.close_policy
field is stored as a raw String and written to config.toml without validation,
allowing invalid values to be persisted and later break startup; change
BrowserUpdate.close_policy to use a typed enum (e.g., ClosePolicy) with serde
strict deserialization/serialization (deny unknown variants) and
validate/convert any incoming raw strings before writing, returning/logging an
error on invalid values instead of persisting them, and ensure any write errors
are handled (not ignored) when saving the config so failures are propagated or
logged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/`(features)/browser.mdx:
- Around line 166-173: The docs describe shared browser state via
persist_session = true, SharedBrowserHandle, RuntimeConfig and close_policy =
"detach" but later still claims "single browser per worker"; update the later
limitation text to be conditional: when persist_session = false maintain the
"single browser per worker" wording, and otherwise describe the shared-browser
behavior (workers reconnect via launch to the same process, discover tabs and
share state). Locate references to persist_session, SharedBrowserHandle,
RuntimeConfig, launch and close_policy and revise the sentence(s) to mention
both modes (persist_session = true vs false) so the limitation only applies when
persist_session is false.

In `@src/tools/browser.rs`:
- Around line 631-644: The reconnection logic currently only inserts missing
entries into state.pages and leaves stale entries and possibly a stale
state.active_target; change the handler that processes the incoming pages list
(the loop that calls page_target_id(&page) and manipulates state.pages) to
replace the tab map instead of only inserting missing keys: clear or build a new
HashMap from the provided pages (using page_target_id as the key) and assign it
to state.pages so stale entries are removed, and after rebuilding ensure
state.active_target is set to Some(first_existing_id) only if the existing
active_target is missing from the new map (i.e., if
state.active_target.as_ref().map(|id|
!state.pages.contains_key(id)).unwrap_or(true) then set it to the first key);
apply the same replacement logic for the other reconnection block that mirrors
this behavior so active_target never points at a dead page.
- Around line 1145-1147: handle_close is currently swallowing failures from
page.close(), context.close(), and browser.close() by only logging them and
still returning success; change handle_close to propagate these shutdown errors
as a structured Result instead. Locate the calls to page.close().await,
context.close().await, and browser.close().await inside handle_close and replace
the debug-only handling with propagation (use ? or map_err to convert the
underlying error into the function's error type, e.g.,
BrowserError/ToolError/anyhow::Error) so the function returns Err(...) when any
close call fails; retain logging if desired but do not return Ok(()) when a
close failed, and aggregate or wrap multiple errors if more than one close can
fail before returning. Ensure the function signature reflects a Result return
type and update callers to handle the propagated error.

---

Outside diff comments:
In `@src/api/config.rs`:
- Around line 200-206: The BrowserUpdate.close_policy field is stored as a raw
String and written to config.toml without validation, allowing invalid values to
be persisted and later break startup; change BrowserUpdate.close_policy to use a
typed enum (e.g., ClosePolicy) with serde strict deserialization/serialization
(deny unknown variants) and validate/convert any incoming raw strings before
writing, returning/logging an error on invalid values instead of persisting
them, and ensure any write errors are handled (not ignored) when saving the
config so failures are propagated or logged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a3cc5e6b-3cd9-4567-97d8-5d29b34ac07d

📥 Commits

Reviewing files that changed from the base of the PR and between 5d82132 and 80857a2.

📒 Files selected for processing (13)
  • docs/content/docs/(configuration)/config.mdx
  • docs/content/docs/(features)/browser.mdx
  • interface/src/api/client.ts
  • interface/src/routes/AgentConfig.tsx
  • src/api/agents.rs
  • src/api/config.rs
  • src/config/load.rs
  • src/config/runtime.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/main.rs
  • src/tools.rs
  • src/tools/browser.rs

jamiepine and others added 3 commits March 3, 2026 23:54
- P1: Use typed ClosePolicy enum in BrowserUpdate API layer instead of
  raw String, so invalid values get a 400 error on deserialization
  rather than being silently persisted to config.toml.

- P2: Rebuild tab map in reconnect_existing_tabs from browser.pages()
  instead of append-only, pruning stale entries. Validate active_target
  points to a live page after refresh.

- P2: Reduce mutex hold time in handle_close — CloseTabs drains pages
  under the lock then closes them outside it; CloseBrowser takes state
  out under the lock then tears down outside it.

- P3: Preserve active_target on Detach so next worker picks up exactly
  where the previous one left off (element_refs still cleared).

- P3: Gate shared browser mode on both persist_session config flag AND
  shared_browser handle presence, not just handle existence.

- P3: Fix doc inconsistency — 'single browser per worker' limitation
  now conditional on persist_session mode. Tighten ClosePolicy doc
  comment to only mention explicit close action.

- P4: Use async tokio::fs::remove_dir_all in launch race path and
  CloseBrowser teardown to avoid blocking while holding the mutex.

- Propagate close errors as BrowserError instead of swallowing them,
  so the LLM sees failures and can recover.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant