Sessions and lifecycle
Every browser-touching tool accepts an optional session argument (default
"default"). Each session id is a fully isolated browser context: its own
cookie jar and storage, its own ref registry, its own console and network
buffers. Concurrency between agents rests on that isolation. For persistent
sessions the cookie jar is the profile, so concurrent persistent sessions need
distinct profile names (see Session modes below), or use incognito.
The concurrency model
Section titled “The concurrency model”- Many agents, one server. Give each agent its own
sessionid and they cannot stomp each other. There is no server-global “active session”. - One agent, many sessions. Drive several windows or flows in parallel by id.
- Multi-user. Two sessions logged in as different users of the same app do not bleed, because they are different browser contexts with different cookie jars.
Omitting session resolves to a lazily created "default" session, so simple
single-session callers need to know none of this. A session id and a profile
name are each one plain name: letters, digits, ., _ and -, starting with a
letter or digit, at most 128 characters. Anything else is refused with
invalid-name.
Managing sessions
Section titled “Managing sessions”open_session({ session, mode?, engine?, profile?, channel?, backgroundThrottling?, device?, viewport?, har?, hars? })eagerly creates an id. Re-opening a live id is an error.close_session({ session })tears one down.close_sessions({ prefix?, all?, idleMs? })is the bulk reap primitive for multi-agent cleanup. Selectors AND together, and at least one is required so you cannot accidentally close nothing or everything.list_sessions()returns{ sessions: [{ id, mode, engine, url, pages, openedAt }] }.
Session modes
Section titled “Session modes”| Mode | Isolation | Persistence | Use it for |
|---|---|---|---|
persistent | the workspace default profile, or profile | cookies and storage survive across runs | logged-in flows you want to resume |
incognito | own ephemeral context and browser | nothing persisted, all state discarded on close | one-off driving with no profile trace |
attached | the externally launched Chrome (not owned) | that Chrome’s own profile | BYOB, attach to a Chrome you already control |
incognito ids are always isolated contexts. With persistent, a session that
names no profile runs on the workspace’s default profile, whatever its id, and
profile: "name" uses <workspace>/profiles/name. A profile is open in one
browser at a time, so a second live session on the same profile is refused with
profile-in-use, naming the profile and the session that holds it. Give each
session its own profile, or use incognito, when two sessions need two
identities.
The MCP-restart gotcha
Section titled “The MCP-restart gotcha”In persistent and incognito modes, browxai spawns Chrome as a child
process of the MCP server. When the MCP client restarts the server, for a
config edit, a code reload, or any other reason, that Chrome child dies with
it and any live page state is gone. A stored ref that pointed at a now-dead
page resolves to about:blank or a fresh document.
If you need page state to survive server restarts, run Chrome yourself and
attach to it. browxai chrome start opens the installed Chrome on the
workspace’s default profile with a debugging port (9222 unless --port says
otherwise):
browxai chrome startThen point the browxai server at it. The server reads its environment once at
start, so set these in the env of the MCP config entry, with byob-attach
added to the capability list next to the defaults:
{ "mcpServers": { "browxai-attached": { "command": "browxai", "env": { "BROWX_ATTACH_CDP": "http://127.0.0.1:9222", "BROWX_CAPABILITIES": "read,navigation,action,human,byob-attach" } } }}Without byob-attach every attached session is refused, naming the
capability. browxai init <workspace> writes an MCP config with a
browxai-attached entry that already sets both. A profile is open in one Chrome
at a time, so quit any other Chrome on it first, or pass --profile NAME. Stop
the Chrome with browxai chrome stop.
With the default confirmation hooks, byob_action holds every action tool on an
attached browser until a person answers: __browx.confirm(true) in the DevTools
browxai console context of that Chrome. An unanswered prompt times out after
five minutes and the action does not run. For an unattended run, the operator
drops byob_action from BROWX_CONFIRM_REQUIRED, or enables self-approval and
lets the agent call approve_actions. See
Capabilities and safety.
An attached Chrome is not owned by browxai: it is never closed on shutdown and it survives browxai restarts cleanly.
Browser channel and background throttling
Section titled “Browser channel and background throttling”Two launch-time knobs on a chromium session:
- A chromium session with no
channellaunches the installed Google Chrome, without Playwright’s--enable-automationswitch, and falls back to the bundled Chromium with a logged warning when Chrome is not installed.open_session({ channel: "chromium" })pins the bundled build, and any other channel ("msedge","chrome-beta") launches that installed browser. The browser version follows the install. Server-wide default via thechannelconfig key orBROWX_CHANNEL. See Sites behind bot protection. open_session({ backgroundThrottling: "disabled" })launches with Chrome’s three background-lifecycle flags off, so a backgrounded session keeps timers andrequestAnimationFramerunning at full rate. Worth turning on for pooled multi-agent work, where an agent polling a backgrounded tab otherwise stalls to its deadline and the stall looks like a page bug. The default keeps Chrome’s own throttling, because reproducing a background-lifecycle bug needs a genuinely throttled tab.
Both are launch-time, so an attached session ignores them. Pass
--disable-background-throttling to browxai chrome start instead.
Device and viewport
Section titled “Device and viewport”open_session({ device })takes any Playwright device-preset name (for example"iPhone 14","Pixel 7","Desktop Chrome") and applies its viewport, scale factor, touch, and user agent.open_session({ viewport: { width, height } })overrides a preset’s size while keeping its mobile, touch, and UA traits.set_viewport({ session, width, height })resizes mid-session for responsive testing and returns anActionResult, since a re-layout often triggers responsive re-render or lazy loading. Only the size changes live; full device traits are fixed at session creation.
Per-session policies
Section titled “Per-session policies”Beyond isolation, each session carries policies for browser events that would
otherwise deadlock a headless context or silently change app state: dialogs
(alert / confirm / prompt), permissions (camera, geolocation, and more),
the Notification constructor, and File System Access pickers. Each defaults
to a raise posture, meaning the event is handled so the page does not hang,
and the next result reports it. The
tool reference documents every policy mode.